程序编译过程
程序编译的整体流程
为了在系统上运行一个C程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令,然后这些指令按照一种为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件。
GCC编译器驱动程序读取源程序文件example.c
,并把它翻译成一个可执行目标文件example
。这个翻译过程分为四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了编译系统。
预处理
- 预处理器(cpp)将所有的
#define
删除,并且展开所有的宏定义。 - 处理所有的条件预编译指令,比如
#if
、#ifdef
、#elif
、#else
、#endif
等。 - 处理#include预编译指令,将被包含的文件直接插入到预编译指令的位置。
- 删除所有的注释。
- 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
- 保留所有的
#pragma
编译器指令,因为编译器需要使用它们。 - 使用
gcc -E hello.c -o hello.i
命令来进行预处理, 预处理得到的另一个程序通常是以.i作为文件扩展名。 以一下程序为例:
1
2
3
4
5
6
7
8
#define N 114514
#define M 1919810
int main() {
// 这是注释
int f = N + M;
return 0;
}
使用命令
1
riscv64-unknown-linux-gnu-gcc -E -march=rv32g -mabi=ilp32 -o test.i test.c
将test.c
进行预处理,其中
-E
表示预处理-march=rv32g
指定生成的架构为rv32gmabi=ilp32
指定了数据模型:整型(i)、长整型(l)和指针(p)均为 32 位,这是与前面的指令集相适应的数据模型 得到的test.i
文件内容为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 0 "test.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/opt/riscv64/sysroot/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "test.c"
int main() {
int f = 114514 + 1919810;
return 0;
}
这是预处理的结果。对于本程序,我们并没有显式的调用外部库,如果我们在C程序中加入代码:
1
2
3
4
5
6
7
#include<stdio.h>
int main(){
...
printf("f = %d\n", f);
...
}
那么预处理的结果在main
函数之前就会多出非常多的内容,这是因为预处理过程中将库<stdio.h>
的内容展开插入到了test.c
中。
编译
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理文件(
test.i
)之后的程序转换成特定汇编(test.s
)代码的过程。
仍是对于上面的程序,使用命令:
1
riscv64-unknown-linux-gnu-gcc -S -march=rv32g -mabi=ilp32 -o test.s test.i
将test.i
编译形成汇编语言,-S
表示只输出汇编代码。 编译器(ccl)将预处理完的文本文件test.i进行一系列的词法分析、语法分析、语义分析和优化,翻译成文本文件test.s
,它包含一个汇编语言程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.file "test.c"
.option nopic
.attribute arch, "rv32i2p1_m2p0_a2p1_f2p2_d2p2_zicsr2p0_zifencei2p0"
.attribute unaligned_access, 0
.attribute stack_align, 16
.text
.align 2
.globl main
.type main, @function
main:
addi sp,sp,-32
sw s0,28(sp)
addi s0,sp,32
li a5,2035712
addi a5,a5,-1388
sw a5,-20(s0)
li a5,0
mv a0,a5
lw s0,28(sp)
addi sp,sp,32
jr ra
.size main, .-main
.ident "GCC: (g2ee5e430018) 12.2.0"
.section .note.GNU-stack,"",@progbits
该程序包含函数main
的定义,main
中的每条语句都以一种文本格式描述了一条低级机器语言指令。 汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。 编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
- 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
- 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
- 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
- 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被 分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
- 目标代码生成:代码生成器(Code Generator).
- 目标代码优化:目标代码优化器(Target Code Optimizer)。
.s
和.S
: 两者主要的区别为,扩展名为.S
的汇编文件支持预处理,而扩展名为.s
的汇编文件不支持。一般地,由人工编写的汇编程序使用.S
作为后缀,而由编译器或反汇编器生成的汇编程序使用.s
作为后缀。
汇编
汇编器(Assembler, AS)将汇编文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中。 输出的目标文件是一个二进制文件,可以直接被链接器使用,与其他目标文件、库文件一起链接成可执行文件。 使用命令:
1
riscv64-unknown-linux-gnu-as -march=rv32g -mabi=ilp32 -o test.o test.s
即可将test.s编译为汇编文件test.o
,test.o
是一个二进制文件,可以使用命令
1
riscv64-unknown-linux-gnu-objdump -d test.o
查看内容,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test.o: file format elf32-littleriscv
Disassembly of section .text:
00000000 <main>:
0: fe010113 add sp,sp,-32
4: 00812e23 sw s0,28(sp)
8: 02010413 add s0,sp,32
c: 001f17b7 lui a5,0x1f1
10: a9478793 add a5,a5,-1388 # 1f0a94 <main+0x1f0a94>
14: fef42623 sw a5,-20(s0)
18: 00000793 li a5,0
1c: 00078513 mv a0,a5
20: 01c12403 lw s0,28(sp)
24: 02010113 add sp,sp,32
28: 00008067 ret
链接
简述
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。 riscv-unknown-linux-gnu
提供了链接器,链接器可以将多个目标文件链接为可执行文件,我们使用如下命令:
1
riscv64-unknown-linux-gnu-ld -m elf32lriscv -Ttext 0x80000000 -o test test.o
-m elf32lriscv
指定了目标可执行文件格式为 32 位小端对齐(l)的 RISC-V 可执行可链接格式(elf);-Ttext 0x80000000
选项指定了程序的入口地址为 0x80000000。
但这里产生了一个warning
:
1
riscv64-unknown-linux-gnu-ld: warning: cannot find entry symbol _start; defaulting to 80000000
这是因为 GCC 默认程序要从 _start
开始执行,但是我们程序只有 main
函数。这时候,我们可以通过反汇编看看到底发生了什么:
1
riscv64-unknown-linux-gnu-objdump -d test
我们发现,程序的入口在 main
函数,但链接器告诉我们entry symbol
应该是_start
发生这种情况的原因是,我们并没有链接 GCC 在链接时的默认链接库,这些库十分冗长,并不适合我们在链接器中直接指定。 所幸,我们可以直接使用 riscv64-unknown-linux-gnu-gcc
指令来链接程序,GCC 会自动地链接某些库:
1
riscv64-unknown-linux-gnu-gcc -march=rv32g -mabi=ilp32 -Ttext 0x80000000 -o test test.o
再次通过riscv64-unknown-linux-gnu-objdump -d test
来查看内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
test: file format elf32-littleriscv
Disassembly of section .text:
80000000 <_start>:
80000000: 2015 jal 80000024 <load_gp>
80000002: 87aa mv a5,a0
80000004: 00000517 auipc a0,0x0
80000008: 09850513 add a0,a0,152 # 8000009c <main>
8000000c: 4582 lw a1,0(sp)
8000000e: 0050 add a2,sp,4
80000010: ff017113 and sp,sp,-16
80000014: 4681 li a3,0
80000016: 4701 li a4,0
80000018: 880a mv a6,sp
8000001a: 80010097 auipc ra,0x80010
8000001e: 2b6080e7 jalr 694(ra) # 102d0 <__libc_start_main@plt>
80000022: 9002 ebreak
80000024 <load_gp>:
80000024: 00002197 auipc gp,0x2
80000028: 7dc18193 add gp,gp,2012 # 80002800 <__global_pointer$>
8000002c: 8082 ret
...
80000030 <deregister_tm_clones>:
80000030: 80002537 lui a0,0x80002
80000034: 80002737 lui a4,0x80002
80000038: 00050793 mv a5,a0
8000003c: 00070713 mv a4,a4
80000040: 00f70863 beq a4,a5,80000050 <deregister_tm_clones+0x20>
80000044: 00000793 li a5,0
80000048: c781 beqz a5,80000050 <deregister_tm_clones+0x20>
8000004a: 00050513 mv a0,a0
8000004e: 8782 jr a5
80000050: 8082 ret
80000052 <register_tm_clones>:
80000052: 80002537 lui a0,0x80002
80000056: 00050793 mv a5,a0
8000005a: 80002737 lui a4,0x80002
8000005e: 00070593 mv a1,a4
80000062: 8d9d sub a1,a1,a5
80000064: 4025d793 sra a5,a1,0x2
80000068: 81fd srl a1,a1,0x1f
8000006a: 95be add a1,a1,a5
8000006c: 8585 sra a1,a1,0x1
8000006e: c599 beqz a1,8000007c <register_tm_clones+0x2a>
80000070: 00000793 li a5,0
80000074: c781 beqz a5,8000007c <register_tm_clones+0x2a>
80000076: 00050513 mv a0,a0
8000007a: 8782 jr a5
8000007c: 8082 ret
8000007e <__do_global_dtors_aux>:
8000007e: 1141 add sp,sp,-16
80000080: c422 sw s0,8(sp)
80000082: 8141c783 lbu a5,-2028(gp) # 80002014 <completed.0>
80000086: c606 sw ra,12(sp)
80000088: e789 bnez a5,80000092 <__do_global_dtors_aux+0x14>
8000008a: 375d jal 80000030 <deregister_tm_clones>
8000008c: 4785 li a5,1
8000008e: 80f18a23 sb a5,-2028(gp) # 80002014 <completed.0>
80000092: 40b2 lw ra,12(sp)
80000094: 4422 lw s0,8(sp)
80000096: 0141 add sp,sp,16
80000098: 8082 ret
8000009a <frame_dummy>:
8000009a: bf65 j 80000052 <register_tm_clones>
8000009c <main>:
8000009c: fe010113 add sp,sp,-32
800000a0: 00812e23 sw s0,28(sp)
800000a4: 02010413 add s0,sp,32
800000a8: 001f17b7 lui a5,0x1f1
800000ac: a9478793 add a5,a5,-1388 # 1f0a94 <__libc_start_main@plt+0x1e07c4>
800000b0: fef42623 sw a5,-20(s0)
800000b4: 00000793 li a5,0
800000b8: 00078513 mv a0,a5
800000bc: 01c12403 lw s0,28(sp)
800000c0: 02010113 add sp,sp,32
800000c4: 00008067 ret
Disassembly of section .plt:
000102b0 <_PROCEDURE_LINKAGE_TABLE_>:
102b0: 7fff2397 41c30333 d503ae03 fd430313 .#..3..A......C.
102c0: d5038293 00235313 0042a283 000e0067 .....S#...B.g...
000102d0 <__libc_start_main@plt>:
102d0: 7fff2e17 auipc t3,0x7fff2
102d4: d38e2e03 lw t3,-712(t3) # 80002008 <__libc_start_main@GLIBC_2.34>
102d8: 000e0367 jalr t1,t3
102dc: 00000013 nop
发现入口地址0x80000000
处变成了_start
函数,而 main
函数是在 _start
中被调用的一个函数。 这是因为 GCC 链接时会自动链接一些库,其中就包括了启动库,这个库中包含了 _start
函数。 事实上,任何程序的起始点都是 _start
函数,main
只是显式执行的第一个函数。main
函数的返回值会视为是否成功执行的标志,决定退出时 exit
函数的系统调用行为。
链接的意义
链接使得分离编译(separate compilation)成为可能,我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
动态链接和静态链接
在这里,我们的程序中并没有显式的调用外部库,如果test.c中调用了printf函数,printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的test.o程序中。连接器(ld)就负责处理这种合并。结果就得到了test文件,它是一个可执行目标文件(或者称为可执行文件),可以被加载到内存中,由系统执行。(链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件)。
请参考此处
END