通常的开发环境都是流行的集成开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建(Build)。
#include
int main()
{
printf("Hello World\n");
return 0;
}
在Linux下,使用GCC来编译Hello World程序时,只需
# gcc helloworld.c
# ./a.out
Hello World
上述编译代码的过程可以分为四个步骤:
可以使用预编译器cpp或者gcc的-E命令来预编译成一个.i文件
预编译过程主要处理那些源代码文件中以'#'开始的预编译指令。(#include、#define)
# gcc -E helloworld.c -o hello.i
# cpp helloworld.c -o hello.i
编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件
# gcc -S hello.i -o hello.s
汇编器(as)是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令,只需要根据汇编指令和机器指令的对照表一一翻译就可以了。
汇编过程可以调用as完成或者gcc的-c命令
# as hello.s -o hello.o
# gcc -c hello.s -o hello.o
或者使用gcc命令直接从源文件直接输出目标文件(Object File)
# gcc -c helloworld.c -o hello.o
我们把一大推文件链接起来才可以得到a.out,即最终的可执行文件。
链接器(ld)
编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。
以一段c语言代码为例讲述该过程。
array[index]=(index+4)*(2+6)
CompilerExpression.c
首先源代码程序被输入到扫描器(Scanner),扫描器只是简单地进行词法分析,运用一种类似于有限状态机的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)
词法分析产生的记号一般分为:关键词、标识符、字面量(数字、字符串等)和特殊符号(加号、等号)。在识别记号的同时,扫描器也完成了其他工作,将标识符存放到符号表,将数字、字符串常量存放到文字表等,以备后续步骤使用。
有一个lex程序可以实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个记号。(具体如何使用以后慢慢研究)
语法分析器(Grammar Parser)将对扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个过程采用上下文无关语法(Context-free Grammar)的分析手段。语法分析器生成的语法树就是以表达式(Expression)为节点的树。本例产生的语法树如图:
语法分析有一个现成的工具叫做yacc(Yet Another Compiler Compiler),可以根据用户指定的语法规则对输入的记号进行解析,从而构建一棵语法树。
语法分析仅仅对表达式进行语法层面的分析,并不了解这个语句是否真正有意义。编译器可以分析的语义是静态语义,即在编译期间可以确定的语义,与之对应的是动态语义(在运行期才能确定的语义)
静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程需要完成这个步骤。比如将一个浮点型赋值给一个指针时,语义分析程序会发现这个类型不匹配,编译器将会报错。
动态语义一般在运行时出现的语义相关的问题,比如将0作为除数是一个运行时语义错误。
经过语义分析,整个语法树的表达式都被标识了类型。
现代的编译器有很多层次的优化,往往在源代码级别会有一个优化过程。(2+6)这个表达式可以被优化掉,经过优化后的语法树如图:
其实在语法树上作优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示。有很多种类型,比较常见的有:三地址和P-代码。
语法树可以被翻译成的三地址代码是:
t1=2+6
t2=index+4
t3=t2*t1
array[index]=t3
经过优化后的代码如下:
t2=index+4
t2=t2*8
array[index]=t2
中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换为目标机器代码
编译器后端主要包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。
代码生成器将中间代码转换为目标机器代码,这个过程十分依赖目标机器。因为不同的机器有不同的字长、寄存器、整数数据类型和浮点数数据类型等。(使用x86的汇编语言来表示,假设index是int类型,array的类型为int型数组)
movl index,%ecx ;value of index to ecx
addl $4,%ecx ;ecx=ecx+4
mull $8,%ecx ;ecx=ecx*8
movl index,%eax ;value of index to eax
movl %ecx,array(,eax,4) ;array[index]=ecx
目标代码优化器对上述目标代码进行优化。
movl index,%edx
leal 32(,%edx,8),%eax
movl %eax,array(,%edx,4)
经过扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化,源代码被编译成目标代码,但是index和array的地址还没有确定,这需要在最终链接时才能确定。
现代编译器可以将源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。
链接过程主要包括地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。最基本的静态链接过程如图所示:
每个模块的源代码文件经过编译器编译成目标文件(Object File一般扩展名为.o或.obj),目标文件和库一起链接形成最终可执行文件。最常见的库就是运行时库(Runtime Library),它是支持程序运行的基本函数集合。库其实是一组目标文件的包,就是一些常用的代码编译成目标文件后打包存放。
编译源代码后生成的文件叫做目标文件,从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接过程,其中有些符号或有些地址还没有调整。
现在流行的可执行文件格式主要是windows下的PE和linux的ELF。目标文件就是源代码编译后但未进行链接的那些中间文件(windows下的.obj和linux下的.o)
windows下的可执行文件(.exe),动态链接库(.dll),静态链接库(.lib)文件都按照可执行文件格式(PE-COFF)存储。linux下的ELF可执行文件,动态链接库(.so),静态链接库(.a)按照ELF格式存储。静态链接库把很多目标文件捆绑在一起形成一个文件,再加上一些索引,可以理解为一个包含很多目标文件的文件包。
在linux下可以使用file命令查看响应的文件格式
# file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
# file /bin/bash
/bin/bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=1bfc7c24648dd01d470fccd2b90675ecc61ef16e, stripped
# file /lib/libettercap.so.0.0.0
/lib/libettercap.so.0.0.0: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=b403bc1e87178a082dfbbe296d51f51985a1ff78, stripped
目标文件和可执行文件格式和操作系统与编译器密切相关,不同的系统平台下会有不同的格式。
目标文件内容有编译后的机器指令代码、数据。除此还包括了链接时所须的一些信息(符号表、调试信息、字符串等)。一般目标文件将这些信息按照不同的属性,以‘节’(‘段’)的形式存储。代码段、数据段(.data以初始化的全局变量和局部静态变量)、bss段(未初始化的全局变量和局部静态变量)
下面对这个例子进行分析
/*SimpleSection.c*/
int printf(const char* format,...);
int global_init_var=84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n",i);
}
int main(void)
{
static int static_var=85;
static int static_var2;
int a=1;
int b;
func1(static_var + static_var2 +a+b);
return a;
}
使用gcc只编译不链接:
# gcc -c SimpleSection.c
使用binutils的工具objdump查看object内部结构
# objdump -h SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000001e 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000c2 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000c8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
在linux下还可以使用readelf专门针对ELF文件格式进行解析。objdump -x可以打印更多的信息。
有个专门的命令size可以用来查看ELF文件的各段长度
# size SimpleSection.o
text data bss dec hex filename
179 8 4 191 bf SimpleSection.o
使用objdump的-s参数可将所有段的内容以十六进制方式打印出来,-d参数可将所有包含指令的反汇编打印出来。
# objdump -s -d SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 488d3d00 000000b8 00000000 e8000000 H.=.............
0020 0090c9c3 554889e5 4883ec10 c745fc01 ....UH..H....E..
0030 0000008b 15000000 008b0500 00000001 ................
0040 c28b45fc 01c28b45 f801d089 c7e80000 ..E....E........
0050 00008b45 fcc9c3 ...E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202844 65626961 6e20382e .GCC: (Debian 8.
0010 332e302d 31392920 382e332e 3000 3.0-19) 8.3.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 24000000 00410e10 8602430d ....$....A....C.
0030 065f0c07 08000000 1c000000 3c000000 ._..........<...
0040 00000000 33000000 00410e10 8602430d ....3....A....C.
0050 066e0c07 08000000 .n......
Disassembly of section .text:
0000000000000000 :
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 17
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 callq 21
21: 90 nop
22: c9 leaveq
23: c3 retq
0000000000000024 :
24: 55 push %rbp
25: 48 89 e5 mov %rsp,%rbp
28: 48 83 ec 10 sub $0x10,%rsp
2c: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 39
39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f
3f: 01 c2 add %eax,%edx
41: 8b 45 fc mov -0x4(%rbp),%eax
44: 01 c2 add %eax,%edx
46: 8b 45 f8 mov -0x8(%rbp),%eax
49: 01 d0 add %edx,%eax
4b: 89 c7 mov %eax,%edi
4d: e8 00 00 00 00 callq 52
52: 8b 45 fc mov -0x4(%rbp),%eax
55: c9 leaveq
56: c3 retq
.data段保存那些已初始化的全局静态变量和局部静态变量
.rodata存放只读数据,一般是程序的只读变量(const修饰的变量)和字符串常量
.bss段存放未初始化的全局变量和局部静态变量,.bss段不占磁盘空间。
使用readelf命令详细查看ELF文件
# readelf -h SimpleSection.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1096 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
词法分析:
http://kaiyuan.me/2016/04/19/compiler_lex/
https://www.nosuchfield.com/2017/07/16/Talk-about-compilation-principles-1/
https://blog.csdn.net/jzyhywxz/article/details/78285722
语法分析:
https://www.nosuchfield.com/2017/07/30/Talk-about-compilation-principles-2/
语义分析:
https://www.nosuchfield.com/2017/08/20/Talk-about-compilation-principles-3/
Let’s Build A Simple Interpreter.是一个用Python实现了语言解释器的系列文章
中间表示:
https://blog.csdn.net/jzyhywxz/article/details/78720620