开发C程序有四个步骤:预处理、编译、汇编和链接。任何一个体系结构处理器上都可以使用C语言程序,只要该体系结构处理器有相应的C语言编译器和库,那么C源代码就可以编译并连接到目标二进制文件上运行。
我们创建一个test.c为例来讲解程序编译的过程
test.c:
#include <stdio.h>
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
int main()
{
int a = 88;
int b = 66;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
int dif = sub(a, b);
printf("a=%d, b=%d, a-b=%d\n", a, b, dif);
}
一:预处理
使用预处理器把源文件test.c
经过预处理生成test.i
文件,预处理用于将所有的#include头文件以及宏定义替换成其真正的内容。预处理主要工作如下:
- 处理所有的注释,以空格代替
- 将所有的 #define 删除,并且展开所有的宏定义
- 处理条件编译指令
#if, #ifdef, #elif,#else,#endif
- 处理 #include,展开被包含的文件
- 保留编译器需要使用的 #pragma 指令
预编译命令:
gcc -E test.c -o test.i
上述命令中-E
是让编译器在预处理之后就退出,不进行后续编译过程;-o
是指定输出文件名。预处理之后得到的仍然是文本文件。-I
指定头文件目录,这里指定的是我们自定义的头文件目录。
test.i文件的部分截图如下:
二:编译
对预处理文件进行词法分析,语法分析和语义分析
- 词法分析:分析关键字,标示符,立即数等是否合法
- 语法分析:分析表达式是否遵循语法规则
- 语义分析:在语法分析的基础上进一步分析表达式是否合法
分析结束后进行代码优化生成相应的汇编代码文件
编译命令:
gcc -S test.c -o test.s
生成的汇编文件如下:
[root@localhost testEg]# cat test.s
.file "test.c"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.globl sub
.type sub, @function
sub:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
subl %eax, %edx
movl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size sub, .-sub
.section .rodata
.LC0:
.string "a=%d, b=%d, a+b=%d\n"
.LC1:
.string "a=%d, b=%d, a-b=%d\n"
.text
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $88, -4(%rbp)
movl $66, -8(%rbp)
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %edx, %esi
movl %eax, %edi
call add
movl %eax, -12(%rbp)
movl -12(%rbp), %ecx
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %edx, %esi
movl %eax, %edi
call sub
movl %eax, -16(%rbp)
movl -16(%rbp), %ecx
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
三:汇编
- 汇编器将汇编代码转变为机器的可以执行指令
- 每条汇编语句几乎都对应一条机器指令
这一步产生的文件叫做目标文件,是二进制格式
gcc
汇编过程通过as
命令完成:
as test.s -o test.o
等价于:
gcc -c test.s -o test.o
四:链接
链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。
链接命令如下:
gcc test.o -o test
最后运行生成的可执行文件:
[root@localhost testEg]# ./test
a=88, b=66, a+b=154
a=88, b=66, a-b=22
总结:
编译过程分为预处理,编译,汇编和链接四个阶段
- 预处理:处理注释,宏以及已经以 # 开头的符号
- 编译:进行词法分析,语法分析和语义分析等
- 汇编:将汇编代码翻译为机器指令的目标文件
- 链接:链接到一起生成可执行程序
常用编译命令选项
假设源程序文件名为test.c。
1. 无选项编译链接
用法:#gcc test.c
作用:将test.c预处理、汇编、编译并链接形成可执行文件。这里未指定输出文件,默认输出为a.out。
2. 选项 -o
用法:#gcc test.c -o test
作用:将test.c预处理、汇编、编译并链接形成可执行文件test。-o选项用来指定输出文件的文件名。
3. 选项 -E
用法:#gcc -E test.c -o test.i
作用:将test.c预处理输出test.i文件。
4. 选项 -S
用法:#gcc -S test.i
作用:将预处理输出文件test.i汇编成test.s文件。
5. 选项 -c
用法:#gcc -c test.s
作用:将汇编输出文件test.s编译输出test.o文件。
6. 无选项链接
用法:#gcc test.o -o test
作用:将编译输出文件test.o链接成最终可执行文件test。
7. 选项-O
用法:#gcc -O1 test.c -o test
作用:使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。