任何一个 C 程序代码到生成一个可执行文件都需要四步,分别是预处理 Pre-processing
,编译 Compiling
,汇编 Assembling
和链接 Link
,这里借助 Gcc 工具来探究这四步分别做了什么事,起到什么样的作用。本文使用的测试代码是经典入门程序 “Hello World!"。
测试环境
为探究预处理,编译,汇编和链接的功能,我们在 Ubuntu 系统中使用 Gcc 编译器( version=4.8.4 ),用简单的也是最经典的入门程序 “Hello World!” 作为测试代码。源文件 hello.c 代码如下:
1// filename: hello.c
2# include <stdio.h>
3
4int main(void){
5 printf("Hello World!");
6 return 0;
7}
正常情况我们都会执行命令 gcc hello.c -o hello.out
来生成二进制可执行程序 hello.out。
预处理
C 预处理器是用在编译器处理程序之前,它预扫描源代码完成包含头文件,宏扩展,条件编译,行控制等功能。对于测试代码中,预处理器只对头文件进行了处理。获取预处理器输出的结果使用该命令 gcc -E hello.c -o hello.i
。
由于
hello.i 文件内容比较多,这里截取部分进行说明。
1// filename: hello.i
2# 1 "hello.c"
3# 1 "<built-in>"
4# 1 "<command-line>"
5# 1 "/usr/include/stdc-predef.h" 1 3 4
6# 1 "<command-line>" 2
7# 1 "hello.c"
8
9...
10# 1 "/usr/lib/gcc/x86_64-linux-gnu/4.8/include/stddef.h" 1 3 4
11# 212 "/usr/lib/gcc/x86_64-linux-gnu/4.8/include/stddef.h" 3 4
12typedef long unsigned int size_t;
13
14...
15# 5 "hello.c" 2
16
17int main(){
18 printf("Hello World!");
19 return 0;
20}
Tips:
hello.i 中有很多这样的格式# line filename flags
,它表示下面行是由文件 filename 的第 line 行生成的。其中 flags 有 1,2,3,4 四种取值
- 1 代表新文件的开始
- 2 代表返回一个文件
- 3 代表下面的文本来自系统头文件,所以某些警告可以过滤掉
- 4 代表下面的文本应该包含在extern C块中 按照提示 stddef.h 文件中第 212 行有 size_t 的宏定义。
编译
编译的过程是将某种编程语言写的源代码(这里特指 C 语言)转换成另一种编程语言(这里特指汇编语言)。前面我们将
hello.c 预处理成了
hello.i 文件,现在就要将
hello.i 文件编译成汇编文件 hello.s 。获取编译器输出的结果使用命令 gcc -S hello.i -o hello.s
。汇编结果见
hello.s 。
1 .file "hello.c"
2 .section .rodata
3 .LC0:
4 .string "Hello World!"
5 .text
6 .globl main
7 .type main, @function
8 main:
9 .LFB0:
10 .cfi_startproc
11 pushq %rbp
12 .cfi_def_cfa_offset 16
13 .cfi_offset 6, -16
14 movq %rsp, %rbp
15 .cfi_def_cfa_register 6
16 movl $.LC0, %edi
17 movl $0, %eax
18 call printf
19 movl $0, %eax
20 popq %rbp
21 .cfi_def_cfa 7, 8
22 ret
23 .cfi_endproc
24 .LFE0:
25 .size main, .-main
26 .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
27 .section .note.GNU-stack,"",@progbits
汇编
汇编的过程是将汇编语言编写的源码转换成可执行的机器代码,通常目标文件中包含至少两个段:代码段和数据段。其中代码段包含程序的指令,一般可读和可执行,不可写;数据段用来存放程序中所用到的各种全局变量或静态数据,一般可读,可写,可执行。获取汇编器输出的结果使用该命令 gcc -o hello.o -c hello.c
,由于 hello.o 是二进制文件,是无法阅读的。这里我们通过命令 objdump
来对二进制文件进行反汇编,查看里面内容。
1// objdump -d hello.o 查看hello.o中代码段信息
2hello.o: 文件格式 elf64-x86-64
3
4Disassembly of section .text:
5
60000000000000000 <main>:
7 0: 55 push %rbp
8 1: 48 89 e5 mov %rsp,%rbp
9 4: bf 00 00 00 00 mov $0x0,%edi
10 9: b8 00 00 00 00 mov $0x0,%eax
11 e: e8 00 00 00 00 callq 13 <main+0x13>
12 13: b8 00 00 00 00 mov $0x0,%eax
13 18: 5d pop %rbp
14 19: c3 retq
hello.o中各段信息如下:
1// objdump -h hello.o 显示hello.o中各个段的头部信息
2hello.o: 文件格式 elf64-x86-64
3
4节:
5Idx Name Size VMA LMA File off Algn
6 0 .text 0000001a 0000000000000000 0000000000000000 00000040 2**0
7 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
8 1 .data 00000000 0000000000000000 0000000000000000 0000005a 2**0
9 CONTENTS, ALLOC, LOAD, DATA
10 2 .bss 00000000 0000000000000000 0000000000000000 0000005a 2**0
11 ALLOC
12 3 .rodata 0000000d 0000000000000000 0000000000000000 0000005a 2**0
13 CONTENTS, ALLOC, LOAD, READONLY, DATA
14 4 .comment 0000002c 0000000000000000 0000000000000000 00000067 2**0
15 CONTENTS, READONLY
16 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000093 2**0
17 CONTENTS, READONLY
18 6 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
19 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
链接
链接的过程是将一个或多个由编译器或汇编器生成的目标文件链接库(静态库或动态库)形成可执行文件。其中静态库会和汇编生成的目标文件一起链接打包到可执行文件中【静态链接】,它对函数库的链接是放在编译时期完成的。而动态库在程序编译时不会被链接到可执行文件中,而是在程序运行时才会被载入【动态链接】。不同的应用程序如果调用相同的库,那么在内存中只需要一份该共享库实例。获取链接器链接后的可执行文件使用命令 gcc hello.o -o hello
。如果想看该可执行文件依赖的库,可以使用命令 ldd hello
。
1 # ldd hello 显示hello依赖的库
2 linux-vdso.so.1 => (0x00007ffc85980000)
3 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f06c7a53000)
4 /lib64/ld-linux-x86-64.so.2 (0x000055ad7be9e000)