从源文件到可执行文件,主要经历了预处理、编译、汇编和链接四个过程。以下面程序为例:
/* hello.c */
#include
int main()
{
printf("Hello World\n");
return 0;
}
预处理将源代码文件 .c 或 .cpp 预编译为 .i 或 .ii 文件,使用 gcc / g++ 只进行预编译:
gcc -E hello.c -o hello.i
g++ -E hello.c -o hello.ii
预编译主要是做一些文本替换的工作,主要包括以下几个方面:
得到的 hello.i 文件中,最后几行为源代码,前面部分为 #inlcude
编译就是把预处理得到的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件,具体流程见下一节。使用 gcc 编译源文件:
gcc -S hello.c -o hello.s
上述源代码 hello.c 被编译为如下代码:
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
汇编器将汇编代码转变成机器可以执行的指令,每一条汇编代码几乎都对应于一条机器指令。使用 gcc 从源文件得到目标文件:
gcc -c hello.c -o hello.o
如一段常见汇编代码机器对应的机器指令:
机器指令 | 汇编代码 |
---|---|
B82301 | mov ax,0123h |
BB5604 | mov bx,0456h |
03C3 | add ax,bx |
03C0 | add ax,ax |
B8004C | mov ax,4c00h |
CD21 | int 21h |
各机器指令以二进制的形式存放在 .o 文件中。
链接器链接各目标文件后生成可执行文件,后文会详细介绍静态链接和动态链接的内容。gcc 的 --verbose 打印的链接的详细信息:
/usr/lib/gcc/x86_64-linux-gnu/7/collect2
-plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so
-plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
-plugin-opt=-fresolution=/tmp/ccIZXkkG.res
-plugin-opt=-pass-through=-lgcc
-plugin-opt=-pass-through=-lgcc_s
-plugin-opt=-pass-through=-lc
-plugin-opt=-pass-through=-lgcc
-plugin-opt=-pass-through=-lgcc_s
--build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed
-dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o hello
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
-L/usr/lib/gcc/x86_64-linux-gnu/7
-L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu
-L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib
-L/lib/x86_64-linux-gnu -L/lib/../lib
-L/usr/lib/x86_64-linux-gnu
-L/usr/lib/../lib
-L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. /tmp/ccrk6Z5i.o
-lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
词法分析器以源代码程序为输入,产生一系列单词符号,并将其归类:关键字、标识符、字面量或特殊符号。
语法分析器将对由词法分析器产生的单词符号进行语法分析,从而产生语法树。
语法信息仅完成对表达式语法的分析,不了解语句的真正含义。编译器能分析的语义是静态语义,如表达式类型;动态语义在运行期才能确定。
编译器分为前端和后端,前端负责产生机器无关的中间代码,后端将中间代码转换成目标机器代码。
模块化是当前程序设计的主流方法,它不仅可以将复杂系统分解为小系统,还能加快各模块的生成。把每个源代码模块独立编译,然后将它们组装起来,这个过程称为链接。链接的主要任务就是把各个模块间的引用处理好,使得各模块正确地衔接。链接主要包括地址和空间分配、符号决议和重定位等步骤。
本文简要介绍了编译和链接的基础知识,详细内容可查看编译原理相关书籍。