编译器是如何运作的

本文探讨了编译器的工作流程,从读取源代码、解析标识符、内存地址分配到生成汇编语言,再到汇编编译器转换为机器语言。编译器通过优化提升程序效率,例如忽略多余操作和调整指令顺序。示例展示了GCC编译器将简单C程序转化为汇编语言的过程。

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

1.1 编译器是如何运作的(1)

大多数程序员在日常编程中很少会直接用到CPU 中的指令(即机器语言)。这主要是因为直接使用机器语言比较繁琐,所以我们选择人类更容易理解的语言来编程,然后再通过编译器将其翻译成机器语言。但是,编译器能否准确地将人类的逻辑思维转换为相应的机器语言呢?在这里,我们先来研究一下编译器到底是如何运作的。

比如,使用GCC按以下步骤将程序编译为目标代码(即汇编语言程序)。

1. 读取源程序并进行解析。将字符分离出来整理成较容易统计的形式,收集参数与函数名等标识符。

2. 对收集到的参数与标识符进行内存地址分配(即后文将提到的寄存器),将内存地址与参数或函数对应起来。

3. 根据逻辑程序生成汇编语言程序。

接下来,汇编编译器将已生成的汇编语言转换成机器语言的目标程序,链接器将目标程序和外部模块连接起来(图1-2)。

 
(点击查看大图)图1-2 从源程序到执行代码的实现过程

近代的编译器实现了在编译过程中,让所生成的程序在更短的时间内得到相同的结果,达到更高的效率。其实现的方法多种多样,比如说,编译器扫描程序后,将多余的操作忽略,修改指令的运行顺序以使CPU 处理得更快等。

优化与程序的调优有着密不可分的关联,在后面的章节中将会提到。

编译后的汇编语言程序

我们来看看由GCC 生成的汇编语言程序。程序1-1 是为检验而编写的小程序。

程序1-1 10 次加1 运算的程序

 
  1. #include <stdio.h> 
  2. int a, b;  
  3. main()  
  4. {  
  5. a = 0;  
  6. do {  
  7. b += a + 1;  
  8. a++;  
  9. } while (a < 11);  

如果在编译此程序时加上-S 选项,如“gcc -S test.c”,就会出现如程序1-2 这样的汇编语言程序(该程序在X86 系列64 位环境下进行编译)。

程序1-2 编译后的汇编语言程序(部分)

 
  1. .text  
  2. .globl main  
  3. .type main, @function  
  4. main:  
  5. .LFB2:  
  6. pushq %rbp  
  7. .LCFI0:  
  8. movq %rsp, %rbp  
  9. .LCFI1:  
  10. movl $0, a(%rip) ……变量a 赋值为0  
  11. .L2:  
  12. movl a(%rip), %eax ……变量a 的值放到寄存器%eax 中  
  13. leal 1(%rax), %edx ……(变量a)+1 的值放入%edx(%rdx 的低32 位) 中  
  14. movl b(%rip), %eax ……将变量b 的值放入%eax(%rax 的低32 位)中  
  15. leal (%rdx,%rax), %eax ……%rdx 和%rax 的和放入%eax 中  
  16. movl %eax, b(%rip) ……%eax 的结果赋值给变量b  
  17. movl a(%rip), %eax ……变量a 的值放入%eax 中  
  18. addl $1, %eax ……对%eax 进行加1 运算  
  19. movl %eax, a(%rip) ……%eax 的结果赋值给变量a  
  20. movl a(%rip), %eax ……变量a 的值放入%eax 中  
  21. cmpl $10, %eax ……将%eax 的值与10 进行比较  
  22. jle .L2 ……小于等于10 的话跳到L2  
  23. leave ……释放栈中的变量  
  24. ret ……程序跳出  
  25. .LFE2:  
  26. .size main, .-main  
  27. .comm a,4,4 ……分配给a 以4 为边界基准的4 字节内存  
  28. .comm b,4,4 ……分配给b 以4 为边界基准的4 字节内存  
  29. … 

1.2 编译器是如何运作的(2)

大致上,左边是标签,中间是CPU 指令,右边是操作目标。以“.”开始的字符串表示指定汇编程序集的函数名。以“:”结束的字符行是标识符的定义。函数名和标识符以外的部分是实际被执行的指令集,与CPU 的机器语言相对应。以“%”开头的是寄存器名,以“$”开头的是常量。

“a(%rip)”表示外部变量a,“b(%rip)”表示外部变量b,外部变量引用以%rip 标识的寄存器(程序计数器标识程序接下来该执行的指令)所指的地址中相对应的位置。

从标识符.L2 到jle 指令是程序的循环部分,相当于C 语言源程序的“do~while(a<11)”。即使没有使用过汇编语言,一看到程序当中的备注也能大致理解其运行原理。

X86 系列CPU 的寄存器

CPU 内部有若干个高速存储单元,也就是常说的寄存器,它可以用来储存数据,还可以作为指示其储存地址的指针来使用。在下面的表格中,我们列举了X86 系列CPU 中常用的寄存器。

一直以来,32 位的CPU 中所使用的寄存器为32 位寄存器,64 位CPU 中则使用扩展到64 位的寄存器,更甚者追加到寄存器r8~r15。扩展后的64 位寄存器可以在64 位模式下使用。

在64 位环境中,通过使用增加的寄存器,可完成程序中函数参数的传递。rax 被用于存放返回值,rdi、rsi、rdx、rcx、r8、r9 分别用于存放第1~6 个整数型参数。

在32 位环境中,将函数写入栈中,这样可以缩短执行时间。另外,对部分寄存器来说,寄存器的一部分也可进行数据存储和计算操作。比如,寄存器rax 的低32 位是eax,eax 的低16 位是ax ;ax 的高8 位是ah,ax 的低8 位是al 等。

x86/x64 系列CPU 的主要寄存器

 

*1 函数变量仅在64 位模式下使用。

*2 寄存器变量在某些时候被分配为操作用寄存器。

 

添加优化选项后的结果

想必即使是不熟悉汇编语言的读者也能看出,前面所生成的代码太过冗余。虽然在计算时将内存上变量a 和b 的内容复制到了寄存器%eax 上,但是如果能将变量a 和b 分别放在不同的寄存器上,然后仅对寄存器进行操作,这样岂不是更合理?

接下来,我们将添加优化选项(-O),执行“gcc -S -O3 example.c”指令后再来编译看看。其结果如程序1-3 所示,可以看得出所生成的代码更为简练。

程序1-3 优化选项的效果

   
  1. .text  
  2. .p2align 4,,15  
  3. .globl main  
  4. .type main, @function  
  5. main:  
  6. .LFB13:  
  7. movl b(%rip), %edx ……变量b 的值放到寄存器%edx 中  
  8. movl $0, a(%rip) ……变量a 赋值为0  
  9. xorl %eax, %eax ……将寄存器%eax 清零  
  10. .p2align 4,,7  
  11. .L2:  
  12. addl $1, %eax ……对寄存器%edx 做加1 运算  
  13. addl %eax, %edx ……寄存器%edx 与%eax 相加  
  14. cmpl $10, %eax ……将寄存器%eax 的值与10 相比  
  15. jle .L2 ……小于等于10 则跳到L2  
  16. movl %edx, b(%rip) ……将加法运算的结果赋值给变量b  
  17. movl $11, a(%rip) ……变量a 赋值为11  
  18. ret  
  19. .LFE13:  
  20. .size main, .-main  
  21. .comm a,4,4  
  22. .comm b,4,4  
  23. … 

前面冗余的代码在循环内对外部变量逐一计算并赋值,但是在优化后的代码中,我们用寄存器代替了外部变量,最后灵活地将变量a 赋值为11,并且在循环代码前面使用了能缩短执行时间的逻辑操作指令xorl(XOR),这也是为了提高效率。

这只是编译器进行优化的一个小小例子,但我们可以从中看出,直接生成的代码和优化后的代码之间存在哪些区别。


学习自:51CTO.com
### 编译器定义及其重要性 编译器是一种特殊的程序,能够将高级编程语言(如C、C++、Java等)编写的源代码转换为目标代码或中间表示形式。对于从事AI算法、软件、编译器开发以及硬件开发等专业的工程技术人员来说,理解编译器的工作机制至关重要[^1]。 ### C/C++编译流程解析 具体到C/C++语言,在整个编译过程中涉及多个阶段: #### 预处理 预处理器会处理所有的宏定义(`#define`)和头文件包含语句(`#include`), 将`.cpp`文件连同它所依赖的所有`.h`文件一起展开形成单个翻译单元(Translation Unit)[^3]。 #### 编译 接着进入真正的编译环节,此时编译器负责将上述得到的大规模文本数据转变为低级的汇编语言或者直接生成机器码(.obj),这些对象文件包含了二进制指令序列,不过它们还不能独立运行,因为缺少入口点(main函数)以及其他必要的支持结构。 #### 汇编与链接 之后通过汇编工具可以进一步把汇编代码组装成更紧凑的目标文件;而链接器的任务则是收集各个模块产生的目标文件并将其组合起来构成完整的可执行文件(.exe/.dll),期间还会加入额外的功能比如启动代码、初始化逻辑等等[^2]。 ```bash gcc demo.c -o demo.elf # 单步完成从源码至最终可执行体的构建过程 ``` 值得注意的是,除了静态链接外还有动态链接这一选项——这意味着某些共享库并不会被打包进应用程序内部,直到实际调用某个特定API时才会加载对应的.so/dll文件[^4]。 ### Java编译特性概述 相比之下,Java采用了一种不同的策略来实现跨平台兼容性。Javac编译器先将.java源文件编译成为字节码(Bytecode),这是一种抽象层面较高的中间表达形式,专门设计用于虚拟机解释执行。由于JVM是以栈为基础架构运作的,因此每条操作均需遵循严格的进出栈规则才能被执行^[5]^。 综上所述,不同类型的编译器虽然有着各自的特点和技术细节上的差异,但核心目的始终围绕着如何高效准确地转化人类易于理解和维护的形式化描述为计算机可以直接识别的动作指南。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值