环境:
- centos7.6
- gcc 4.8.5
1. 从一个test.c
到test.out
这里实验的环境是 linux
,linux
的可执行文件默认后缀名是.out
。
先看下面代码:
test.c
#include <stdio.h>
int main()
{
printf("ok\n");
return 0;
}
我们,首先使用gcc test.c --save-temps -o test.out
将它编译为test.out
,并保留痕迹,如下:
--save-temps
命令可以保留整个编译的过程痕迹。
根据上面的痕迹,我们直接上图解释其过程:
第一步:预处理
预处理器先对test.c
进行预处理,就是将里面#include...
、#ifdef...
等内容处理掉,处理完后,里面将不再有#include...
等内容,最后生成test.i
。关于预处理的内容,可参考《c:预处理指令(#include、#define、#if等)》。
总之,生成的文件内容,大概如下:
第二步:编译
在这一步,gcc就将test.i
进行词法分析,优化,最终转成汇编文件test.s
,注意,test.s
仍然是文本,大概如下图:
第三步:汇编
在这一步,汇编器就将test.s
翻译成了二进制格式的指令,输出为test.o
,它是ELF
格式的二进制文件(后面说ELF文件格式)。
test.o
又称为可冲定位文件
,我们通过file
可观察到:
第四步:链接
在这一步,链接器就将test.o
文件与其引用资源做链接,主要是与其他引用的资源进行整合,重新分配内存地址,最终生成test.out
,使用file
观察到:
2. GCC是一个编译驱动器
在上面4个步骤中,我们分别提到:预处理器
、编译器
、汇编器
、链接器
,这四个有对应专门的程序,如:
- 预处理器:
/usr/bin/cpp
- 编译器:
/usr/bin/cc
或/usr/bin/c++
(可能是这两个) - 汇编器:
/usr/bin/as
- 链接器:
/usr/bin/ld
如果我们是window环境,看的就更清楚了:
既然,gcc是编译驱动器,那么脱离gcc命令,我们自然也能一步步编译,比如:
cpp test.c test.i
:进行预处理
as test.s -o test.o
:进行汇编
为什么没有列举其他两个过程?因为实验的时候遇到各种错误,放弃了。。。
现在,我们知道了编译的四个过程,那么我们能控制在哪一步停下来吗?
当然了,我们还是回归gcc
命令:
- 只进行预处理
test.c
=>test.i
gcc -E test.c -o test.i
- 预处理并编译
test.c
=>test.s
gcc -S test.c -o test.s
- 预处理、编译并汇编
test.c
=>test.o
gcc -c test.c -o test.o
- 整个流程,输出可执行文件
test.c
=>test.out
gcc test.c -o test.out
3. 关于ELF文件
上面我们提到 test.o
和 test.out
都是ELF
格式的二进制文件。那么什么是ELF
文件呢?
直接看百度百科的介绍:
也就是说我们关心的 ELF
可以表示4类文件:
- 目标代码:
test.o
- 可执行文件:
test.out
- 动态库:
test.so
- 核心转储文件(用的较少,一般是辅助调试的)
那么ELF
内部的格式是怎样的呢?
还是看百度百科:
再往深处,我们就不研究了,知道个大概就行。
4. 说说链接
对上面的4个过程,我们疑问最大的应该是 链接
,不知道为什么需要链接。。。
链接其实有两个目的:
-
- 布局
假设,我们有两个文件test.c
和libadd.c
并且test.c
调用了libadd.c
的函数,那么,编译时,编译器先分别生成test.o
和libadd.o
目标文件。因为它们是分别编译,所以test.o
和libadd.o
里面汇编指令涉及到的地址都认为是从0
开始,即,它们之间互不认识。
而链接器的布局就是要把它们的地址空间合起来,防止重叠。
- 布局
-
- 重定位
还是假设上面的两个文件test.c
和libadd.c
。我们知道test.c
调用函数的时候只是调用了int add(int x,int y)
这个声明而已,至于这个函数的具体实现在哪?test.o
里面是没有的,所以test.o
里面涉及到调用的地方是callq 0x0000
,即:不知道这个函数的地址,就先填充0。
所以,链接器就要去帮test.o
去找这个函数的实现,而libadd.o
里恰好有这个函数的声明,那么就把libadd.o
里的地址给test.o
好了。
- 重定位
这是链接器的两大目的,上面我简化着说,实际很复杂。
这里举的例子属于
静态链接
,另外还有动态链接
(比如我们调用printf
等标准函数就是),动态链接
里面存的是库装载器
的地址。
5. 动态库和静态库
上面我们在链接时也提到了静态链接
和动态链接
。所谓静态链接
就是将引用的库也一并拷贝过来,而动态链接就不需要拷贝。所以,动态链接
比静态链接
应用的多很多。
那明白了动态链接
和静态链接
后,我们就应该知道动态库
和静态库
了吧。
现在我们就来实验下:
5.1 生成静态库
所谓的静态库就是将编译好的目标代码(如:
libadd.o
、libsub.o
)打成一个压缩包而已,一般后缀名是*.a
。
首先,准备三个文件:
test.c
#include <stdio.h>
int add(int x,int y);
int sub(int x,int y);
int main()
{
int x=20,y=10;
printf("x+y=%d\n",add(x,y));
printf("x-y=%d\n",sub(x,y));
printf("ok\n");
return 0;
}
libadd.c
int add(int x,int y)
{
return x+y;
}
libsub.c
int sub(int x,int y)
{
return x-y;
}
现在,我们分别编译它们:
现在,让我们把 libadd.o
和 libsub.o
做成静态库:
ar rcs ...
中,r表示replace,c表示create
5.2 调用静态库编译
接上面,我们将test.o
和libaddsub.a
生成 test.out
:
5.3 使用动态库
注意:动态库是一个
ELF
格式的二进制文件,不是压缩包,后缀名是*.so
上面,我们是将libadd.c
和libsub.c
生成了静态库,现在我们让它们分别生成动态库:
# 生成 libadd.so
gcc -shared libadd.c -o libadd.so
# 生成 libsub.so
gcc -shared libsub.c -o libsub.so
现在,让我们使用动态库编译可执行文件:
# 生成 test.out
gcc test.c libadd.so libsub.so -o test.out
但当我们执行test.out
时,却大失所望:
为什么会这样呢?当前目录下不是有libadd.so
吗?
这就要说linux加载动态库的原理了:
linux会根据配置从指定路径下找动态库,而不是当前目录,那么这个配置在哪呢?
在 /etc/ld.so.conf
:
可以看到,这个文件里指定了从 etc/ld.so.conf.d/*
里面找
可以看到,最终只在 /usr/lib64/mysql
下找(注意:还有默认的 /lib64
等没有列出)。
那么,当前已经找到多少动态库呢?
可以通过 ldconfig -p
查看:
另外:
/etc/ld.so.cache
文件是动态库的缓存,运行ldconfig
命令可以强制更新/etc/ld.so.cache
文件。
现在,我们知道了,要么我们把libadd.so
和libsub.so
放到/lib64
等系统目录下,要么将libadd.so
所在的目录配置到/ect/ld.so.conf.d
目录下。这的确是一个解决方法。
但,我们还有另外一个解决办法,那就是使用LD_LIBRARY_PATH
环境变量,如下:
然后,我们可以把这个环境变量的配置放到 /etc/profile
里面。
如果,我们不想在系统上留下什么痕迹,那么我们可以写一个脚本,内容如下:
current_dir=$(cd $(dirname $0); pwd)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:current_dir
./test.out
效果如下:
6. 对c语言编译的一些理解
其实根据上面的编译过程,我们应该认识到,c语言编译整体分成两大步骤:
- 所有单个c文件分别编译成目标代码
*.o
; - 将多个
*.o
、静态库或动态库链接成可执行文件test.out
所以,c语言编译的时候是,先单个编译,然后再整合资源。
所以,c语言中的单个c
文件中可以没有某个api函数的实现,但如果要调用,就必须在调用前头先声明一下(全局变量也是一个意思)。
7. gcc常用编译选项
除了上面的编译命令,我们常用的还有
gcc -Og test.c -o test.out
这里的 -g
是生成调试用的信息(如果我们想调试的话,比如使用gdb
调试);
-O
是优化选项。
8. 补充gcc附带的其他命令
8.1 objdump
8.1.1 显示libaddsub.a
内信息
8.1.2 显示libadd.o
的反汇编信息
8.1.3 显示符号表信息
关于objdump
更多,参考:obdump -v
或 man objdump
8.2 readelf
上面,我们说 test.o
、test.so
、test.out
都是ELF
格式的二进制文件,现在我们就用readelf
去看看:
8.2.1 显示elf的文件头信息
8.2.2 显示程序头表信息