编译和链接
hahahanba Lv1

对于平常的应用程序开发,我们很少需要关注编译与链接过程。因为通常的开发环境都是流行的集成开发环境(IDE),比如Visual Studio等。这样的IDE一般都将编译和链接的过程一步完成,通常这种编译和链接合并到一起的过程为构建(Build)。但是在这样的开发过程中,我们往往会被这些复杂的集成工具所提供的强大功能所迷惑,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。如果能够深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

被隐藏的过程

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("Hello World");
return 0;
}

在Linux下,当我们使用GCC来编译Hello World程序时,只需要最简单的命令我们便可以在屏幕上打印出“Hello World”字样

1
2
3
$ gcc hello.c
$ ./a.out
Hello World

事实上,上述过程可以分解为以下四个过程:

image)

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)

预编译

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

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

我们可是使用如下的命令,将源代码文件hello.c和相关的头文件如stdio.h等被预编译器cpp预编译成一个.i文件。

1
2
3
$ gcc -E hello.c -o hello.i
或者
$ cpp hello.c > hello.i

hello.i文件如下所示:

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
# 1 "hello.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 384 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "hello.c" 2

...

typedef unsigned char __uint8_t;
typedef short __int16_t;
typedef unsigned short __uint16_t;

...

extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 417 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h" 2 3 4
# 5 "hello.c" 2

int main()
{
printf("Hello World\n");
return 0;
}

编译

编译过程就是把预处理完成的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。上面的编译过程相当于如下的命令:

1
2
3
$ gcc -S hello.i -o hello.s
或者
$ gcc -S hello.c -o hello.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
25
26
27
28
	.section	__TEXT,__text,regular,pure_instructions
.build_version macos, 13, 0 sdk_version 13, 3
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World\n"

.subsections_via_symbols

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。上述汇编过程我们可以用一下命令完成:

1
2
3
4
5
$ as hello.s -o hello.o.  // as是汇编器
或者
$ gcc -c hello.s -o hello.o
或者
$ gcc -c hello.c -o hello.o // 经过预编译、编译和汇编直接输出目标文件(Object File)

我们可以使用objdump -d hello.o 命令来查看 .o文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
hello.o:        file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000000000000 <_main>:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
f: 48 8d 3d 0f 00 00 00 leaq 15(%rip), %rdi ## 0x25 <_main+0x25>
16: b0 00 movb $0, %al
18: e8 00 00 00 00 callq 0x1d <_main+0x1d>
1d: 31 c0 xorl %eax, %eax
1f: 48 83 c4 10 addq $16, %rsp
23: 5d popq %rbp
24: c3 retq

链接

通常我们要将一大堆文件链接起来才可以得到a.out文件,即最终的可执行文件。我们可以通过以下命令来获取所有的编译选项:

1
$ gcc hello.c --verbose // 或 gcc -v hello.c

输出的编译信息含义可参考gcc-verbose

参考资料

  • 程序员的自我修养-链接,装载与库
 评论