一、从源代码到可执行程序
现有一个test.c文件,内容如下:
$ cat test.c
#include <stdio.h>
#define N 10
int main()
{
// 这是一段注释,预处理过后,就看不到了
printf("N = %d\n", N);
printf("hello world\n");
return 0;
}
从源文件到可执行程序一共需要经历预处理、编译、汇编、链接四个步骤。
1.预处理
预处理阶段,主要会做以下工作:
头文件展开,例如:上述代码中#include <stdio.h>在预处理之后会被展开为stdio.h中的内容;
宏替换,例如:上述代码中#define N 10定义的宏会替换代码中的N为10;
去注释,删除所有注释。
在Linux下,可以使用gcc编译器得到预处理之后的文件:
$ gcc -E test.c -o test.i
-E表示让test.c文件经过预处理之后就停下来,
-o表示生成的目标文件是test.i;
通过vim查看,我们写的test.c代码变成了800多行,其中837行之上的都是头文件stdio.h中的内容,在test.c文件中展开,可以注意到,定义的宏N也消失了,代码中的N被替换成了10,注释也消失了。
2.编译
$ gcc -S test.i -o test.s
使用如上的命令,可以将test.i文件编译完就停止,并生成目标文件test.s。
编译之后,如果代码有写函数,编译器会生成一个符号表,里面存放函数的地址,在之后代码的函数调用中,就会去符号表中找对应函数的地址。
编译完成后得到的文件如上,这时代码变成了汇编语言,但是计算机仍然不能识别,计算机只能识别二进制语言,因此,还必须要执行汇编。
3.汇编
$ gcc -c test.s -o test.o
在命令行执行上述命令,会让gcc编译器把test.c进行完汇编就停止,生成test.o文件。
汇编会将汇编语言转化为二进制语言,这时查看代码,会发现是一堆乱码:
可以使用hexdump来查看二进制文件,
这时候只差最后一步。
4.链接
在最初的代码中调用了printf函数,就会去和标准库进行链接,让我们的代码可以执行printf函数。
$ gcc test.o -o test.exe
此时生成的文件就可以执行了
需要注意的是,可以直接gcc test.c -o test.exe生成可执行程序。
二、动态链接与静态链接
在上述的代码中,调用了printf()函数,但是函数本身并没有被自己实现,而头文件stdio.h中,也仅放了函数的声明,依然没有函数的实现,那么printf()这个函数究竟在哪里呢?答案是库。为了使用库中的代码,必须和库进行链接。
1.动态链接
动态链接,就是在调用库函数的时候,生成一个和库函数的链接,执行可执行程序时,执行到对应命令就会跳转到库中去执行,默认情况下,编译器生成的可执行程序都是动态链接。可以使用file命令查看可执行程序是静态链接还是动态链接。
如图所示,使用file命令查看可执行程序test.exe,可以看到以下的信息,其中,executable表明其是一个可执行程序,dynamically linked表明其使用的是动态链接。
2.静态链接
静态链接,就是把库中的代码拷贝的自己的代码中生成可执行程序,在编译时使用-static选项可以让生成的可执行程序进行静态链接。
如图所示,生成的test_static.exe文件是静态链接。
3.优缺点
由于动态链接生成的文件需要经常跳转到库中去执行,所有动态链接生成的文件在执行时会比静态链接生成的文件要慢一些;
同时,由于静态链接是将库中的代码放到自己这个可执行程序中,因此静态链接生成的文件非常大,上述两个文件几乎差了100倍的大小!
三、Makefile
Makefile是一个文件(也可以写成makefile),其中存放的是文件的依赖关系与依赖方法。如图所示:
如果当前目录下test.exe不存在,就会寻找test.exe的依赖关系,即test.exe是由test.o生成的,执行test.exe的依赖方法:gcc test.o -o test.exe.
如果目录下没有test.o,就会继续寻找test.o的依赖关系,再去文件中找test.o的依赖方法:gcc -c test.s -o test.o,重复上述内容,直到发现test.c在目录下存在。
make
在命令行中直接输入make命令, make会在当前目录下寻找makefile文件, 找到的话就会执行其中的内容, 默认情况下, make只会执行makefile中第一个命令, 在上述的文件中即生成test.exe文件, 若依赖关系中的文件并不在目录下, 就会往下执行, 直到第一个文件被生成.
2.PHONY
我们再在makefile中加入一条clean命令, 移除我们生成的文件:
但是执行之后, 会出现以下信息:
make: `test.exe' is up to date.
而且clean并没有被执行, 首先要注意到, 因为clean并不是第一条指令, 并且并没有文件依赖它,因此它不会被执行, 因此需要输入make clean来执行.
那么现在来解决第一个问题,即make: `test.exe' is up to date.
出现这个信息就表明我们以及生成过test.exe, 并且test.c并没有被更改,也就意味着test.o不会被更改,也就是test.exe不必重新生成了, 如果使用了.PHONY就表明,不管依赖关系的文件有没有做修改,都执行一遍.
如图所示, 如果被.PHNOY修饰了,即便目录下存在test.exe并且test.o没有被更改过,也会执行对应的依赖方法.