程序的编译(3/13)

news2024/11/18 11:37:31

经过预处理后的源文件,褪去一切包装,注释被删除,预处理命令也基本上被处理掉,剩下的就是 C 代码了。接下来的第二步,就进入到编译阶段。编译阶段主要分为两步:第一步,编译器调用一系列解析工具分析这些 C 代码,将 C 源文件编译为汇编文件;第二步,通过汇编器将汇编文件汇编成可重定位的目标文件。

文章目录

    • 从 C 文件到汇编文件
      • 词法分析
      • 语法分析
      • 语义分析
      • 生成中间代码
    • 汇编过程
    • 符号表与重定位表
      • 符号表
      • 重定位表

从 C 文件到汇编文件

一个汇编文件是以段为单位来组织程序的:代码段、数据段、BSS 段等,各个段之间相互独立,并且其和二进制目标文件的组织结构已经很接近了,因为汇编指令是二进制指令的助记符,只不过汇编语言的程序结构需要使用各种伪操作来组织,汇编文件经过汇编器汇编后,处理掉各种伪操作命令,就是二进制目标文件了。

从 C 源文件到汇编文件的转换,起始就是将 C 文件中的程序代码块、函数转换为汇编程序中的代码段,将 C 程序中的全局变量、静态变量、常量转换为汇编程序中的数据段、只读数据段。总体来讲,编译过程可以分为以下 6 步:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 中间代码生成
  5. 汇编代码生成
  6. 目标代码生成

词法分析

主要用来解析 C 程序语句。词法分析一般会通过词法扫描器从左到右,一个字符一个字符地读入源程序,通过有限状态机解析并识别这些字符流,将源程序分解为一系列不能再分解的记号单元 —— token。

token 是字符流解析过程中有意义的最小记号单元,常见的 token 如下:

  • C 语言的各种关键字:int、float、for、while、break 等。
  • 用户定义的各种标识符:函数名、变量名、标号等。
  • 字面量:数字、字符串等。
  • 运算符:C 语言标准定义的 40 多个运算符。
  • 分隔符:程序结束符分号、for循环中的逗号等。

语法分析

语法分析是对前一阶段产生的 token 序列进行解析,看是否能构建成一个语法上正确的语法短语(程序、语句、表达式等)。语法短语用语法树表示,是一种树型结构,不再是线性序列。语法分析工具在对 token 序列分析过程中,如果发现不能构建语法上正确的语句或表达式,就会报语法错误。

语义分析

语法分析仅仅对程序做语法检查,对程序、语句的真正意义不了解,而语义分析主要对语法分析输出的各种表达式、语句进行检查,看看有没有错误。如果你传递给函数的实参与函数声明的形参类型不匹配,或者你使用了一个未声明的变量,或者除数为零了,break 在循环语句或 switch 语句之外出现了,或者在循环语句之外发现了 continue 语句,一般都会报语义上的错误或警告。

生成中间代码

在语法分析阶段输出的表达式或程序语句,还是以语法树的形式存储,我们需要将其转换为中间代码。中间代码是编译过程中的一种临时代码,常见的有三地址码P-代码

中间代码和语法树相比,有很多优点:中间代码是一维线性结构,类型伪代码,编译器很容易将中间代码翻译成目标代码。

jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.c
int main(void)
{
	int sum = 0;
	int a = 2;
	int b = 1;
	int c = 1;
	sum = a + b / c;
	return 0;
}
jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-gcc -fdump-tree-gimple main2.c
jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.c.005t.gimple # 自动生成同名文件
main ()
{
  int D.4205;

  {
    int sum;
    int a;
    int b;
    int c;

    sum = 0;
    a = 2;
    b = 1;
    c = 1;
    _1 = b / c;
    sum = a + _1;
    D.4205 = 0;
    return D.4205;
  }
  D.4205 = 0;
  return D.4205;
}

C 程序语句 sum=a+b/c; 编译为三地址码后,就变成了上面所示的类似伪代码的语句。中间码一般和平台是无关的,如果你想将 C 程序编译为 X平台下的可执行文件,那么最后一步就是根据 X86 指令集,将中间代码翻译为 X86 汇编程序;如果你想编译成 ARM 平台上运行的可执行文件,那么就要参考 ARM 指令集,根据 ATPCS 规则分配寄存器,将中间代码翻译成 ARM 汇编程序。

jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-gcc -S main2.c
jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.s # 自动生成该后缀文件
	.arch armv5t
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 2
	.eabi_attribute 30, 6
	.eabi_attribute 34, 0
	.eabi_attribute 18, 4
	.file	"main2.c"
	.text
	.global	__aeabi_idiv
	.align	2
	.global	main
	.syntax unified
	.arm
	.fpu softvfp
	.type	main, %function
main:
	@ args = 0, pretend = 0, frame = 16
	@ frame_needed = 1, uses_anonymous_args = 0
	push	{fp, lr}
	add	fp, sp, #4
	sub	sp, sp, #16
	mov	r3, #0
	str	r3, [fp, #-20]
	mov	r3, #2
	str	r3, [fp, #-16]
	mov	r3, #1
	str	r3, [fp, #-12]
	mov	r3, #1
	str	r3, [fp, #-8]
	ldr	r1, [fp, #-8]
	ldr	r0, [fp, #-12]
	bl	__aeabi_idiv
	mov	r3, r0
	mov	r2, r3
	ldr	r3, [fp, #-16]
	add	r3, r3, r2
	str	r3, [fp, #-20]
	mov	r3, #0
	mov	r0, r3
	sub	sp, fp, #4
	@ sp needed
	pop	{fp, pc}
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
	.section	.note.GNU-stack,"",%progbits

汇编过程

汇编过程是使用汇编器将前一阶段生成的汇编文件翻译成目标文件。汇编器的主要工作就是参考 ISA 指令集,将汇编代码翻译成对应的二进制指令,同时生成一些必要的信息,以 section 的形式组装到目标文件中,后面的链接过程会用到这些信息,汇编的流程主要包括词法分析、语法分析、指令生成等过程。

编译器在编译一个项目时,是以 C 源文件为单位进行编译的,每一个源文件经过编译,生成一个对应的目标文件(main.c --> main.o)。 main.o 目标文件是不可执行的,属于可重定位的目标文件,要经过链接器重定位、链接后才能组装成一个可执行的目标文件 a.out。
在这里插入图片描述
通过编译生成的可重定位目标文件,都是以零地址为链接起始地址进行链接的。编译器在将源文件翻译成可重定位目标文件的过程中,将不同的函数编译成二进制指令后,是从零地址开始依次将每一个函数的指令序列存放到代码段中,每个函数的入口地址也就从零地址开始依次往后偏移。

仅仅汇编的命令:arm-linux-gnueabi-gcc -c main.c

使用 readelf 命令查看 main.o 目标文件:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -S main.o 
There are 12 section headers, starting at offset 0x354:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 00005c 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0002c0 000030 08   I  9   1  4
  [ 3] .data             PROGBITS        00000000 000090 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000098 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 000098 000010 00   A  0   0  4
  [ 6] .comment          PROGBITS        00000000 0000a8 00002c 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 0000d4 000000 00      0   0  1
  [ 8] .ARM.attributes   ARM_ATTRIBUTES  00000000 0000d4 00002a 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 000100 000160 10     10  16  4
  [10] .strtab           STRTAB          00000000 000260 00005d 00      0   0  1
  [11] .shstrtab         STRTAB          00000000 0002f0 000061 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  y (purecode), p (processor specific)

main.o 目标文件在编译时,都是以零地址为基地址进行代码段的组装。在每个可重定位目标文件中,函数或变量的地址起始就是它们在文件中相对于零地址的偏移。在后面的链接过程中,链接器在将各个目标文件组装在一起时,各个目标文件的参考地址就发生了变化,那么这个目标文件内的函数或变量的地址也要随之更新,否则就无法通过函数名去引用函数,无法通过变量名去引用变量。

链接器将各个目标文件组装在一起后,需要重新修改各个目标文件中的变量或函数的地址,这个过程一般称为重定位。将需要重定位的符号(函数名/变量名)收集生成重定位表,以 section 的形式保存到每个可重定位目标文件中就可以了。

main.o 中的 main 函数引用了 sub.o 中的 add 和 sub 函数。在链接器组装的过程中,add 和 sub 函数的地址发生了变化;在链接器组装之后,需要重新计算和更新 add 和 sub 函数的新地址,这个过程就是重定位。

符号表与重定位表

符号表和重定位表是非常重要的两个表,这两个表为链接过程提供各种必要的信息。

符号表

在汇编阶段,汇编器会分析汇编语言中各个 section 的信息,收集各种符号,生成符号表,将各个符号在 section 内的偏移地址也填充到符号表内。符号表主要用来保存源程序中的各种符号的信息,包括符号的地址、类型、占用空间的大小等。这些信息一方面可以辅助编译器作语义检查,看源程序是否有语义错误;另一方面也可以辅助编译器编译代码的生成,包括地址与空间的分配、符号决议、重定位等。

查看符号表:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -s main.o

Symbol table '.symtab' contains 22 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000     0 NOTYPE  LOCAL  DEFAULT    3 $d
     6: 00000000     0 SECTION LOCAL  DEFAULT    5 
     7: 00000000     0 NOTYPE  LOCAL  DEFAULT    5 $d
     8: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
     9: 00000054     0 NOTYPE  LOCAL  DEFAULT    1 $d
    10: 00000000     4 OBJECT  LOCAL  DEFAULT    4 uninit_local_val.4612
    11: 00000000     0 NOTYPE  LOCAL  DEFAULT    4 $d
    12: 00000004     4 OBJECT  LOCAL  DEFAULT    3 local_val.4611
    13: 00000000     0 SECTION LOCAL  DEFAULT    7 
    14: 00000000     0 SECTION LOCAL  DEFAULT    6 
    15: 00000000     0 SECTION LOCAL  DEFAULT    8 
    16: 00000000     4 OBJECT  GLOBAL DEFAULT    3 global_val
    17: 00000004     4 OBJECT  GLOBAL DEFAULT  COM uninit_val
    18: 00000000    92 FUNC    GLOBAL DEFAULT    1 main
    19: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND add
    20: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND sub
    21: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

符号表中的每一个符号,都有符号值和类型。符号值本质上是一个地址,可以是绝对地址,一般出现在可执行目标文件中;也可以是一个相对地址,一般出现在可重定位目标文件中。符号的类型主要有以下几种:

  • OBJECT:对象类型,一般用来表示我们在程序中定义的变量。
  • FUNC:关联的是函数名或其他可引用的可执行代码。
  • FILE:该符号关联的是当前目标文件的名称。
  • SECTION:表明该符号关联的是一个 section,主要用来重定位。
  • COMMON:表明该符号是一个公用块数据对象,是一个全局弱符号,在当前文件中未分配空间。
  • TLS:表明该符号对应的变量存储在线程局部存储中。
  • NOTYPE:未指定类型,或者目前还不知道该符号类型。

如果在一个 C 源文件中,我们引用了在其他文件中定义的函数或全局变量,编译器也不会报错,只需要在调用之前声明即可,编译器就会认为你引用的这个全局变量或函数可能在其他文件、库中定义,在编译阶段暂时不会报错。在后面的链接过程中,链接器会尝试在其他文件或库中查找你引用的这个符号的定义,如果真的找不到才会报错。此时的错误类型是链接错误。

重定位表

编译器在给每个目标文件生成符号表的过程中,如果在当前文件中没有找到符号的定义,也会将这些符号收集在一起并保存到一个单独的符号表中,以待后续填充,这个符号表就是重定位符号表。如在 main.o 的符号表(.symtab)中看到的:

    19: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND add
    20: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND sub
    21: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

add 和 sub 这两个符号的信息处于未定义状态(NOTYPE),需要后续填充。在 main.o 中会使用一个重定位表 .rel.text 记录这些需要重定位的符号:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -r main.o

Relocation section '.rel.text' at offset 0x2c0 contains 6 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000014  0000131c R_ARM_CALL        00000000   add
00000024  0000141c R_ARM_CALL        00000000   sub
00000034  0000151c R_ARM_CALL        00000000   printf
00000040  0000151c R_ARM_CALL        00000000   printf
00000054  00000602 R_ARM_ABS32       00000000   .rodata
00000058  00000602 R_ARM_ABS32       00000000   .rodata

在 .rel.text 中,我们可以看到需要重定位的符号 add、sub 以及库函数 printf,重定位表中的这些符号所关联的地址,在后面的链接过程中经过重定位,会更新为新的实际地址。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/816342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode 面试题 08.05.递归乘法

⭐️ 题目描述 🌟 leetcode链接:面试题 08.05.递归乘法 思路: A 3 , B 4 ,3 * 4 等价于 3 3 3 3。 代码: int multiply(int A, int B){if (!B) {return 0;}return A multiply(A , B - 1); }

23款奔驰AMG GLE53加装原厂HUD抬头显示系统,增加您的行车安全

HUD是平视显示器的简称,它原先是运用在航空器上的飞行辅助仪器。指飞行员不需要低头,就能够看到他需要的重要资讯。由于HUD的方便性以及能够提高飞行安全,这项技术后来也发展到汽车行业。汽车搭载的HUD抬头数字显示功能,是利用光学…

Antd 日期范围选择框选择一个日期后,面板收起

需求 初始状态只有开始时间,如下图⬇️ 产品需要优化体验: 点击结束日期选择日期下拉面板直接收起,而不是需要选择起始时间 思路 需要一段逻辑的open属性控制后续操作还是由组件内部控制 逻辑 第一次打开,open控制选择了&am…

史上最全,商业智能BI干货满满

商业智能BI是什么? 商业智能BI可以实现业务流程和业务数据的规范化、流程化、标准化,打通ERP、OA、CRM等不同业务信息系统,商业智能BI整合归纳企业数据,商业智能BI利用数据可视化满足企业不同人群对数据查询、分析和探索的需求&a…

AOP自定义注解保存到MongoDB数据库

如果你想用mongoDB做全局日志记录,比如记录controller方法执行相关数据,比如像这样。 整个流程:先写自定义注解。定义一个日志对象,字段属性设置好。 写aop 规范自定义注解如何使用,在其中通过切点和反射把相关参数存到…

机器学习笔记之优化算法(五)线搜索方法(步长角度;非精确搜索;Armijo Condition)

机器学习笔记之优化算法——线搜索方法[步长角度,非精确搜索,Armijo Condition] 引言回顾:关于 f ( x k 1 ) ϕ ( α ) f(x_{k1}) \phi(\alpha) f(xk1​)ϕ(α)的一些特性非精确搜索近似求解最优步长的条件 Armijo Condition \text{Armijo…

行业追踪,2023-07-31,板块多数都是指向消费

自动复盘 2023-07-31 凡所有相,皆是虚妄。若见诸相非相,即见如来。 k 线图是最好的老师,每天持续发布板块的rps排名,追踪板块,板块来开仓,板块去清仓,丢弃自以为是的想法,板块去留让…

【数据挖掘竞赛】——科大讯飞:锂离子电池生产参数调控及生产温度预测挑战赛

🤵‍♂️ 个人主页:@Lingxw_w的个人主页 ✍🏻作者简介:计算机科学与技术研究生在读 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞👍🏻 收藏 📂加关注+ ​ 【科大讯飞】报名链接:https://challenge.xfyun.cn?invitaC…

JAVASE---类和对象

1. 面向对象的初步认知 1.1 什么是面向对象 Java是一门纯面向对象的语言(Object Oriented Program,简称OOP),在面向对象的世界里,一切皆为对象。面向对象是解决问题的一种思想,主要依靠对象之间的交互完成一件事情。用面向对象的…

Mysql 查询统计最近12个月的数据

包括当月: SELECTt1.yf AS month,count( t2.uuid ) AS total FROM(SELECTDATE_FORMAT(( CURDATE()), %Y-%m ) AS yf UNIONSELECTDATE_FORMAT(( CURDATE() - INTERVAL 1 MONTH ), %Y-%m ) AS yf UNIONSELECTDATE_FORMAT(( CURDATE() - INTERVAL 2 MONTH ), %Y-%m ) AS yf UNION…

Microsoft Visual C++ 14.0 is required.

Microsoft Visual C 14.0 is required. Get it with “Microsoft Visual C Build Tools 当我们安装绝大部分python包的时候可以通过pip install 或者 conda install解决,但是任然有些包是安装不了的,比如我的就会报Microsoft Visual C 14.0 is required…

gcc编译方法、静态链接库、动态链接库用法

可执行程序的编译过程 第一步:预处理(将宏展开) gcc -E main.c -o main.i 第二步:编译成汇编指令 gcc -S main.i -o main.s 第三步:编译成目标文件,准备链接工作 gcc -c main.s -o main.o 第四步&am…

对话CSDN副总裁-邹欣:先行动的才是赢家,践行长期主义的价值创造者终将收获价值 | COC上海城市开发者社区

文章目录 ⭐️ COC上海城市开发者社区的首次集结契机⭐️ 关于 "技术人如何应对35岁中年危机"🌟 30岁了没转管理,应该焦虑么?🌟 30岁没转管理,是否还有其他选择? ⭐️ 践行长期主义的价值创造者终…

力扣SQL之路:窗口函数应用

文章目录 1.引言2.力扣SQL题目3. 解题策略4.代码实现5.总结 1.引言 窗口函数是 SQL 中一种强大的分析函数,它可以在结果集中创建一个窗口,并对窗口内的数据进行计算和分析。在力扣(LeetCode)的 SQL 题目中,窗口函数经…

这三件事没理顺,你过不了软考

下午好,我的网工朋友 上周软考成绩出来了,大家都过了没? 我看好多人都说早上的题目稳过,下午的好多都挂了。 软考每年这个通过率,确实是一言难尽。 到底怎么样才能过,自学、培训,各种诀窍&am…

最新多模态3D目标检测论文汇总(PDF+代码)

目前在自动驾驶领域,多模态3D目标检测是一个非常重要的研究热点。由于引入了其他传感器数据,多模态3D目标检测在性能上明显优于纯视觉的方案,可以同时预测周围物体的类别、位置和大小,因此对于自动驾驶领域的同学来说,…

【gitlib】linux系统rpm安装gitlib最新版本

目录 下载gitlib安装包 安装需要的依赖 设置开机启动 安装邮件服务器并设置开机启动 rpm执行安装gitlib 修改gitlib.rb文件的属性 修改完毕后执行更新配置 查看gitlib运行 查看gitlib初始化root密码 gitlib入口访问地址 下载gitlib安装包 Index of /gitlab-ce/yum/el7/…

龙蜥社区用户案例征集开始啦,欢迎投稿!

征集倡议 龙蜥社区在 2022 年首次发布了《2022 龙蜥操作系统生态用户实践精选》,为龙蜥广大用户提供了成熟实践样板。随着社区的迅速发展,龙蜥生态和用户的规模迅速壮大,为了更好的服务于广大龙蜥用户,现面向各行业征集龙蜥用户案…

Mybatis源码解析(三)------SqlSession

Mybatis源码解析(三)------SqlSession 序言SqlSession接口SqlSession的实现类DefaultSqlSessionSelect获取Statement查询 序言 Mybatis里面的核心就是SqlSession这个接口,前面我们已经研究了Mybatis的配置过程和Mapper的注册过程&#xff0c…

jsp实现打印功能

1.先实现列表页查询 2.做一个打印按钮 function plprint(){var rows $(#whYcfTzList).datagrid(getData);var ORGCODE$(input[nameORGCODE]).val();var ISCONTAIN$(input[nameISCONTAIN]).val();var RECCODE$(input[nameRECCODE]).val();var CUSTOMERNAME$(input[nameCUSTOM…