$gcc example.c
这个命令马上给我们产生一个“a.out”(如果程序没有错误的话)。
实际上,整个工作至少要分成四个阶段,分别由不同的程序完成:
第一阶段:由预处理程序(unix的预处理程序通常是cpp)执行C源文件的预处理指令;
第二阶段:C编译器把经过预处理的C代码文件编译成汇编代码文件。
第三阶段:汇编编译器(GAS)把汇编代码文件编译成目标代码文件(以.o为后缀)。
第四阶段:链接程序(LD)把所有目标代码链接起来产生可执行文件。
所以,编译(包括第一、二、三阶段)和链接(第四阶段)是两回事。
为什么搞得这么复杂?这是因为C语言是用来进行大型项目开发的系统语言,它必须允许很多人合作开发一个项目,所以,一个C程序可以分开由很多个源代码文件组成,不一定非得把所有代码都放在同一个文件里。这样就要求编译过程必须分成某些步骤,因为既然源代码由很多个文件组成,那就是说每个单独的源文件都不可能包含所有的程序代码,所以根本不可能要求编译器仅仅根据其中某一个C文件就直接产生可执行文件,它肯定要等到所有的代码文件都完成编译才有可能输出最后的执行文件。
事实上我们以前编写的程序就是由多个文件组成的,main函数和其他我们自己定义的函数自然就是由我们自己来编写,其他的一些函数,例如printf,我们一样能使用,但我们从来没有编写过printf。像printf这些函数作为C语言标准库的一部分,已经由其他人(通常是编译器厂商)写好了并编译成库文件,我们的代码编译后只需和这些库进行链接就可以产生可执行文件。
从前面的讨论我们大概明白C代码经过编译会产生目标代码,这个目标代码又是什么呢?还记得我们把一些C代码编译成汇编代码吗?仔细观察汇编文件就不难得出结论,目标代码其实已经是以机器码的形式存在,只不过还有一些符号的地址需要链接时才能解决。举个例子:
/*Example C code*/
#include<stdio.h>
int main(void)
{
extern int a;
printf(“a=%d\n”, a);
return 0;
}
$gcc –S test.c
$cat test.s
.section .rodata
.LC0:
.string “a=%d\n”
.text
.globl main
.type mian, @function
main:
...
movl $.LCO, (%esp)
movl a, %eax
movl %eax, 4(%esp)
call printf
...
编译程序要做的事情之一就是把所有需要确定地址的符号($.LC0、a、printf)记录下来,然后链接程序在找到它们的定义点之后通过计算给与合适的地址。当所有符号都有确定的地址时,链接程序就能够产生可执行文件。如果还有符号不能确定地址(找不到定义或重复定义)链接程序就会报错。例如对上面的C程序:
$gcc test.c
马上就有出错信息:undefined reference to ‘a’。
因为我们根本没有定义变量a,链接程序自然就不可能确定a的地址,于是出错退出,最终的可执行文件也无法生成。如果我们再写一个C文件如下:
/*Example C code 2*/
int a = 1;
执行:
$gcc test.c test2.c
外部变量在test2.c中得到定义,链接程序可以确定a的地址,顺利产生可执行文件a.out。