1. 编译器把c语言编程成可执行的机器码之间发生了什么?
  2. 编译出来的可执行文件里是什么?
  3. 程序是怎么运行起来的?

真正了不起的程序员对自己的程序的每一个字节都了如指掌。

当我们将一段代码编译成可执行文件时,可分解为四个步骤:预处理(Prepressing), 编译(Compilation), 汇编(Assembly), 链接(Linking)。

预处理阶段

1
2
3
gcc -E hello.c -o hello.i

cpp hello.c > hello.i

预处理过程主要处理那些源代码文件中的以”#”开始的预编译指令。比如: #include, #define等,主要处理规则如下:

  1. 将所有的#define删除,并且展开所有的宏定义。
  2. 处理所有条件预编译指令,比如#if, #ifdef, #elif, #else, #endif
  3. 处理#include预编译指令, 将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  4. 删除所有的注释”//“和”/**/“。
  5. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  6. 保留所有的#pragma编译器指令,因为编译器需要使用它们。

经过编译后的.i文件不包含任何宏定义,并且包含的文件已经被插入到.i文件中。所以 当我们无法判断宏定义是否正确或头文件是否正确时,可以查看预编译后的文件来确定问题。

编译

编译过程就是编译器把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件。

编译过程一般可分为6步: 扫描(词法分析), 语法分析, 语义分析, 源代码优化, 代码生成目标代码优化

1
2
3
$gcc -S hello.c -o hello.s

$/usr/lib/gcc/xxx-linux-gnu/7.xx/ccl hello.c

词法分析

词法分析运用一种类似有限状态机(Finite State Machine)的算法将源代码的字符序列分割成一系列的记号(Token)。

词法分析器flexlex的现代版本,它可以按照描述好的词法规则将输入的字符分割成一个个记号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 识别出用于计算机的记号并把它们输出symbol.l */
%{
#include "count.tab.h"
%}
%%
"+" { return ADD; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
"|" { return ABS; }
[0-9]+ { yylval = atoi(yytext); return NUMBER; }
\n { return EOL; }
[ \t] { /* 忽略空白字符 */ }
"(" { return OP; }
")" { return CP; }
"//".* { printf("Mystery character %s\n", yytext); }
%%

语法分析

语法分析器(Grammar Parser)将对由词法分析器(扫描器)产生的记号进行上下文无关语法(Context-free Grammar)分析的手段生成语法树(Syntax Tree), 该语法树就是以表达式为节点的树。

语法分析器Bisonyacc的现代版本,可以根据描述的语法规则对输入的记号序列进行解析。

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
/*计算器的最简单版本count.y */
%{
#include <stdio.h>
%}
/* declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL
%%
calclist: /* 空规则 */
| calclist exp EOL { printf("= %d\n", $2); }
;
exp: factor
| exp ADD factor { $ = $1 + $3; }
| exp SUB factor { $ = $1 - $3; }
;
factor: term
| factor MUL term { $ = $1 * $3; }
| factor DIV term { $ = $1 / $3; }
;
term: NUMBER
| ABS term { $ = $2 >= 0 ? $2 :- $2; }
| OP exp CP { $ = $2; }
;
%%
main(int argc, char **argv)
{
yyparse();
}
yyerror(char *s)
{
fprintf(stderr, "error: %s\n", s);
}
1
2
3
4
count: symbol.l count.y
bison -d count.y
flex symbol.l
cc -o $@ count.tab.c lex.yy.c -lfl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@atticus yacc]# make
bison -d count.y
flex symbol.l
cc -o count count.tab.c lex.yy.c -lfl
[root@atticus yacc]# ls
count count.tab.c count.tab.h count.y lex.yy.c makefile symbol.l
[root@atticus yacc]# ./count
123+567
= 690
2 + 3 * 4
= 14
20/4-2
= 3
[root@atticus yacc]#

语义分析

语义分析分为静态语义分析动态语义分析, 编译器所能进行的是静态语义分析, 动态语义分析是指在运行时才能确定的语义。

语法分析仅完成对表达式语法层面的分析,但并不了解这个语句是否真正有意义。静态语义分析通常包括: 声明和类型的匹配, 类型转换过程。经过语义分析阶段后的语法树的结点都加入了类型,如果有些类型需要隐式类型转换,就插入相应的转换结点。

中间语言生成

中间代码使得编译器可以被分为前端后端,编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。这样可以使编译器针对不同的平台使用同一个前端和针对不同机器平台的多个后端。

源码级优化器(Source Code Optimizer)会在源代码级别进行优化,例如:(2+6)这个表达式可以被优化掉,它的值可以在编译期基本确定。但是直接在语法树上做优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示。

目标代码生成与优化

源代码优化器产生中间代码标志着其后的过程都属于编译器后端,主要包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。

代码生成器中间代码转换成目标机器代码(汇编代码),这个过程非常依赖于目标机器,因为不同的机器有着不同的字长,寄存器,整数数据类型和浮点数数据类型等。

目标代码优化器对目标代码进行优化,包括: 选择合适的寻址方式,使用位移来替代乘法操作,删除多余的指令等。

汇编

代码生成器生成汇编代码(目标机器代码), 汇编器as 将汇编代码(.s)转变成机器可以执行的(0和1组成)指令,并把它们打包输出成一个目标文件(relocatable object program),每个汇编语句几乎都对应一条机器指令。汇编过程基本上是根据汇编指令和机器指令的对照表进行”翻译”。

1
2
3
4
5
$as hello.s -o hello.o

$gcc -c hello.s -o hello.o

$gcc -c hello.c -o hello.o

链接(静态链接)

把每个源代码模块独立的编译成目标文件, 然后按照需要将它们”组装”起来,这个组装模块的过程就是链接

链接过程主要包括:地址和空间分配,符号决议,重定位等步骤。

空间与地址分配

符号解析与重定位

静态库链接

可执行文件的装载&虚拟地址空间

装载方式

进程虚拟地址空间布局

动态链接

地址无关

延迟绑定(PLT)

动态链接步骤

显示运行时链接

共享库

创建&安装共享库

环境变量与查找过程

程序的内存布局

栈调用惯例

堆与内存管理

运行库

glibc

系统调用&API

系统调用原理

目标文件&ELF文件结构

Linux平台下的可执行文件(Executable)主要是COFF(Common file format)格式的变种ELF(Executable Linkable Format)格式。目标文件是源代码编译后但未进行链接的那些中间文件(Linux下的.o)与可执行文件结构相似,采用相同的存储格式。

不仅可执行文件,动态链接库(DDL,Dynamic Linking Library)(.so文件), 静态链接库(Static Linking Library)(.a文件)文件以及核心转储文件(Core Dump File)(core dump文件)都是按照可执行文件格式存储。

可以通过file命令查看文件类型(rust与c的可执行文件):

1
2
3
4
5
6
7
8
root@VM-0-6-ubuntu:~# file rust_projects/ownership/target/debug/ownership
rust_projects/ownership/target/debug/ownership: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dyna
mically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=862ef596138c40abd3900d95100613e76c5
4316b, with debug_info, not stripped
root@VM-0-6-ubuntu:~# file /home/atticus/ctest/smallpt
/home/atticus/ctest/smallpt: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, int
erpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=80ab6932c857c3dd44bd36a27f93135dfac61ced, not stripped
root@VM-0-6-ubuntu:~#
1
2
3
file hello_world.o
file /bin/bash
file /lib/ld-2.6.1.so

编写一个测试文件test.c, gcc -c test.c -o test.o(-c参数只编译不链接)来研究目标文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

我们通过objdump这个命令来查看一下可执行文件的内部结构,-h参数把ELF文件的各个段的基本信息打印出来,也可以使用objdump -x打印更多信息。size表示段的长度,File off表示段的所在位置, 每个段中第二行的CONTENTS表示该段在文件中存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@VM-0-6-ubuntu:~/cunit# objdump -h test.o 

test.o: file format elf64-x86-64

Sections:
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 0000002c 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d0 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
root@VM-0-6-ubuntu:~/cunit#

命令size, 也可以用来查看ELF文件的代码段,数据段和bss段的长度。

1
2
3
root@VM-0-6-ubuntu:~/cunit# size test.o 
text data bss dec hex filename
179 8 4 191 bf test.o

-s可以将所有段的内容以十六进制打印出来,-d参数可以将所有包含指令的段反汇编。

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
root@VM-0-6-ubuntu:~/cunit# objdump -s -d test.o 

test.o: file format 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 c745f801 ....UH..H....E..
0030 0000008b 15000000 008b0500 00000001 ................
0040 c28b45f8 01c28b45 fc01d089 c7e80000 ..E....E........
0050 00008b45 f8c9c3 ...E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 342e302d 31756275 6e747531 7e31382e 4.0-1ubuntu1~18.
0020 30342e31 2920372e 342e3000 04.1) 7.4.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 <func1>:
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 <func1+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 callq 21 <func1+0x21>
21: 90 nop
22: c9 leaveq
23: c3 retq

0000000000000024 <main>:
24: 55 push %rbp
25: 48 89 e5 mov %rsp,%rbp
28: 48 83 ec 10 sub $0x10,%rsp
2c: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 39 <main+0x15>
39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f <main+0x1b>
3f: 01 c2 add %eax,%edx
41: 8b 45 f8 mov -0x8(%rbp),%eax
44: 01 c2 add %eax,%edx
46: 8b 45 fc mov -0x4(%rbp),%eax
49: 01 d0 add %edx,%eax
4b: 89 c7 mov %eax,%edi
4d: e8 00 00 00 00 callq 52 <main+0x2e>
52: 8b 45 f8 mov -0x8(%rbp),%eax
55: c9 leaveq
56: c3 retq
root@VM-0-6-ubuntu:~/cunit#
常用段名 说明
.comment 编译器的版本信息等
.bss 未初始化的全局变量和局部静态变量
.data 已经初始化的全局变量与局部静态变量
.rodata 只读数据段
.txt 执行语句代码段

ELF文件头

readelf -h test.o查看ELF的文件头结构,ELF文件头结构及相关常熟被定义在/usr/include/elf.h文件里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@VM-0-6-ubuntu:~/cunit# readelf -h test.o 
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1096 (bytes into file)
Flags: 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
root@VM-0-6-ubuntu:~/cunit#

ELF段表

readelf -S test.o查看ELF文件段表的内容。

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
root@VM-0-6-ubuntu:~/cunit# readelf -S test.o 
There are 13 section headers, starting at offset 0x448:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000057 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000338
0000000000000078 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000098
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a4
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000d0
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003b0
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000128
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002c0
0000000000000073 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003e0
0000000000000061 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
root@VM-0-6-ubuntu:~/cunit#

重定位表(.rel.text)

连接器在处理目标 文件时, 须要对目标文件中某些部位进行重定位, 即代码段和数据段中那些绝对地址的引用的位置,这些重定位的信息都记录在ELF文件的重定位表里,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。例如: .rel.text就死针对.text表的重定位表。

**字符串表(.strtab)段表字符串(.shstrtab)**保存定义过的变量名,函数名,段表里的段名等字符串。

符号表(.symtab)

nm查看test.o的符号表:

1
2
3
4
5
6
7
8
9
10
root@VM-0-6-ubuntu:~/cunit# nm test.o 
0000000000000000 T func1
0000000000000000 D global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000004 C global_uninit_var
0000000000000024 T main
U printf
0000000000000004 d static_var.1802
0000000000000000 b static_var2.1803
root@VM-0-6-ubuntu:~/cunit#

readelf -s test.o查看符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@VM-0-6-ubuntu:~/cunit# readelf -s test.o 

Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1802
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1803
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main
root@VM-0-6-ubuntu:~/cunit#

每个目标文件都有一个符号表(Symbol Table), 这个表中记录了目标文件中所用到的所有符号, 每个符号都有一个对应的值, 叫做符号值(Symbol Value), 对于变量和函数来说,符号值就是它们的地址。