程序员的自我修养——学习笔记2

本文详细介绍了程序编译的全过程,包括预处理、编译、汇编和链接四个阶段。预处理负责宏展开、条件编译等;编译涉及词法、语法、语义分析及代码生成;汇编将汇编代码转化为机器指令;链接则整合各模块,解决符号引用,生成可执行文件。同时,文章解释了为何需要高级语言和编译器,并探讨了编译器的工作原理和目标代码优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

程序编译的过程

#include<stdio.h>
int main(){
	printf("Hello, World!\n");
	return 0;
}
gcc hello.c
./a.out
  1. 如上发生了四个过程:预处理、编译、汇编、链接
预处理
gcc -E hello.c -o hello.i
或者
cpp hello.c->hello.i
  1. 文件后缀:.c==>.i
  2. 处理过程:
    1. 将所有的#define删除并进行宏展开;
    2. 处理所有的条件预编译指令如#ifdef;
    3. #include包含的文件插入到预编译指令的位置;
    4. 删除所有的注释;
    5. 添加行号、文件名标识,编译调试时显示错误信息;
    6. 保留#program编译器指令,因为编译时要用到
编译
gcc -S hello.i -o hello.s
  1. 整个过程包括词法分析、语法分析、语义分析、中间代码生成,目标代码生成及优化
汇编
as hello.s -o hello.o
  1. 将汇编代码转换成机器指令,每一个汇编语句几乎对应一条机器指令。
链接
  1. 将可重定位文件、静态库等链接,生成可执行文件.out

编译详解

  1. 为什么需要编译器:编译器实际上就是个单向翻译器,将高级语言翻译成机器语言。
  2. 为什么不用机器语言直接写程序:首先是用机器指令或者汇编语言写程序效率低下;其次是低级语言依赖于特定计算机,而高级语言关注程序逻辑本身,很少考虑硬件设备,比如字节、内存大小、通信方式、存储方式。这样便于移植,即用高级语言写一次,在不同机器上用它对应的编译器编译就行,不用更改程序代码。
  3. 编译过程:词法分析、语法分析、语义分析、中间代码生成,目标代码生成及优化
  4. 如下对array[index]=(index+4)*(2+6);进行分析
词法分析
  1. 使用扫描器,应用状态机算法,将源程序的字符序列分割成一系列记号。记号可以分为标识符、关键字、字面量、运算符等。将标识符放到符号表,将字面量放到文字表。
  2. Lex程序可以通过自定义词法规则,完成词法分析,这样就不用再编写词法分析器。在这里插入图片描述
语法分析
  1. 判断语句写法是否正确,是否有左括号而没有右括号等等问题。
  2. 使用词法分析得到的记号,利用上下文无关语法的分析手段,生成语法树。
  3. 语法树就是一个一表达式为结点的树,符号和数字是最小的表达式,也就是叶子结点。
  4. 在语法分析阶段会报告语法错误。
  5. yacc和Lex一样,可以通过规则生成语法树,而不必要重新编写一个语法分析器。
    在这里插入图片描述
语义分析
  1. 判断语句含义是否正确。比如两个指针相乘,在语法分析中是正确的,但是语义分析就会报错。
  2. 语义分析分为静态语义分析和动态语义分析。静态语义分析在编译器就可确定,动态语义分析在运行期才能确定。
  3. 静态语义分析包括:声明和类型是否匹配,类型转换包括将浮点型转换成整型。动态语义分析包括把0作为除数。
  4. 语义分析的结果是语法树的表达式都被标识了类型,同时也对符号表的符号类型更新。
    在这里插入图片描述
中间代码生成与优化
  1. 中间代码是语法树的顺序表示,和目标代码相似,但是与目标机器、运行时环境无关,即不包含数据尺寸、变量地址、寄存器名称等。
  2. 常见地中间代码有三地址码和P-代码
t1=2+6
t2=index+4
t3=t1+t2
array[index]=t3
  1. 在这个过程里,2+6可以计算出来,以后直接使用8,从而减少临时变量t1。t3也可以被省掉。
t2=index+4
t2=t2*8
array[index]=t2
  1. 编译器分为前端和后端,前端负责产生与机器无关的中间代码,后端负责将中间代码生成机器指令。这样跨平台编译器只需要一个前端,多个后端。
目标代码生成与优化
  1. 编译器后端=代码生成器+目标代码优化器
  2. 代码生成器依赖于机器的字长、寄存器、整数数据类型、浮点数数据类型等。
movl     index,%ecx					;index的值存到ecx
addl     $4,%ecx					;ecx=ecx+4
mull     $8,%ecx					;ecx=ecx*8
movl     index,%eax					;index的值存到eax
movl     %ecx,array(,eax,4)			;array[index]=ecx
  1. 目标代码优化:选择合适的寻址方式、使用位移代替乘法、删除多余的指令
movl 	index,%edx
leal	32(,%edx,8),%eax			;基址比例变址寻址指令lea完成乘法
movl	%eax,array(,%edx,4)

链接器历史

  1. 最初的程序是纸带打孔,程序确定了绝对地址。比如0001 0100这一条机器指令,前四位代表这是跳转指令,后四位表示绝对地址。
  2. 抛开这样编写程序复杂不说,如果我在中间加了一条指令,那么原本跳转到0100的指令,就要跳转到0101。
  3. 汇编语言使用符号帮助人们记忆,比如说0001跳转指令写成jmp,0100地址写成foo。那么上述的跳转指令就是jmp foo
  4. 这样汇编器在汇编程序的时候,重新计算foo的目标地址就行了,解放了人类手工计算地址的过程。
  5. 随着程序的发展,程序越来越大,这时我们采用模块编写的思想。各个不同功能的代码卸载不同的模块中。最后模块组合成一个程序。
  6. 但是各个模块怎么拼接成一个程序,就要看:链接

静态链接

  1. 各个源代码模块独立编译,然后通过链接组装在一起。
  2. 链接就是处理各个模块互相引用的部分,使各个模块正确衔接。
  3. 它和人工修正地址的过程没有本质区别,就是把一些指令对其他符号地址的引用加以修正。
  4. 链接:地址和空间分配、符号决议、重定位。
    在这里插入图片描述
  5. 比如,我们编写了两个模块,一个模块func.c文件中有foo()。另一个模块main.c文件中我们使用到了foo()。首先我们将各个模块独自编译,main.c中此时不知道foo()的地址,暂时将目标地址搁置。在组装成一个可运行程序时,通过链接器,得到foo()的地址,然后修正所有使用到foo()的地址。这样就完成了链接。

参考资料

  1. 俞甲子. 程序员的自我修养 : 链接、装载与库[M]. 北京 : 电子工业出版社, 2009
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值