很多人在第一次接触到C语言的时候是在windows的集成开发环境中学的。把写好的C语言程序放到开发环境中,点一下编译按钮就会帮你生成编译好的可执行程序,这个过程看似非常简单,但是其背后需要经过很多的步骤。其具体步骤可以分为预编译,编译,汇编,链接四步。
下面我们就在Linux中利用gcc编译器来简单的了解一下一个C文件是如何被编译成一个可执行程序的。
首先我们先来写两个简单的C程序
a.c程序
#include <stdio.h>
#define ADD(a,b) (a + b) // 宏定义
extern int globe_B; // 外部声明b.c中的全局变量
int main(void)
{
printf("globe_B = %d\r\n",globe_B); // 打印出globe_B的值
printf("ADD(4,5) = %d\r\n",ADD(4,5)); // 打印出,ADD(4,5)的值
}
b.c程序
int globe_B = 50;
上面两个程序很简单,a.c程序只是简单得打印出globe_B的值和打印出ADD宏定义的值。b.c程序只定义一个globe_B的全局变量。
- 预编译
首先来看预编译,预编译的作用是把程序中的宏定义、头文件全部展开、处理条件编译的真假值、去掉注释等作用,最后预编译得到的是一个纯净的C文件。
在gcc中利用gcc -E xxx.c -o xxx.i得到预编译后的文件。
下面来对a.c文件进行预编译
# 1 "a.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "a.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 367 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 410 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 411 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 368 "/usr/include/features.h" 2 3 4
# 391 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 392 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4
/********************省略***********************/
extern int globe_B;
int main(void)
{
printf("globe_B = %d\r\n",globe_B);
printf("ADD(4,5) = %d\r\n",(4 + 5));
}
因为头文件stdio预编译后会展开变成800多行的代码,所以就省略了大部分程序。可以看到a.c被预编译后,注释已经消失了,在第二个printf的ADD宏定义也被展开了,条件编译大家可以自行进行实验。
- 编译
编译是在第一步预编译的基础上,对C文件进行语法和语义检查,排除掉有错误的语句,最后得到的是汇编文件
在gcc中利用gcc -S xxx.i -o xxx.s得到预编译后的文件。
下面对a.i文件进行编译
.file "a.c"
.section .rodata
.LC0:
.string "globe_B = %d\r\n"
.LC1:
.string "ADD(4,5) = %d\r\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl globe_B(%rip), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $9, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
汇编得到的是跟机器架构相关的汇编语言。
-
汇编
汇编的作用是把第二步生成的汇编文件转化成目标文件,也就是二进制文件。
在gcc中用gcc -c xxx.s -o xxx.o生成
由于生成的文件是二进制文件,所以一般的文本工具不能打开,必须要用一些二进制查看软件才可以,常用的有winhex。
上面是用winhex打开的a.o二进制文件。 -
链接
链接是把第三步中生成的所有目标文件全部链接成一个可执行文件。在链接的时候链接器会把各个文件中的函数,变量一一对应,如果某个变量或函数没有做外部声明就会报链接错误。
在gcc中用gcc xxx.o -o xxx.out生成
最后链接出来的可执行文件运行正常。
在实际开发中常用gcc xxx.c -o xxx.out一步到位直接生成可执行文件。