遇到一个问题: 封装SQLite3成静态库,过程中发现SQLite3的源码的shell.c中有main函数:
int SQLITE_CDECL main(int argc, char **argv){ char *zErrMsg = 0; ShellState data; const char *zInitFile = 0; int i; int rc = 0; int warnInmemoryDb = 0; int readStdin = 1; int nCmd = 0; char **azCmd = 0; ...
将其封装成静态库.a文件自然是被使用者调用的,也就是说使用者也要有自己的main函数才行。如此说来,在使用该.a的项目中就有了两个main函数,那应该是一定编译不过的,然而事实并非如此,程序能正常编译且符合设计逻辑运行,这涉及gcc中链接器ld的链接过程,于是通过自行编写测试程序试验一番。
新建如下文件:
addLib.和subLib.将编译为库函数使用,其实现为:
//addLib.h #ifndef __ADDLIB_H__ #define __ADDLIB_H__ int add(int a, int b); #endif /* __ADDLIB_H__ */ //addLib.c #include <stdio.h> #include "addLib.h" int add(int a, int b) { printf("%d + %d = %d\n", a, b, a + b); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
//subLib.h #ifndef __SUBLIB_H__ #define __SUBLIB_H__ #include <stdio.h> void sub(int a, int b); #endif /* __SUBLIB_H__ */ //subLib.c #include "subLib.h" void sub(int a, int b) { printf("%d - %d = %d\n", a, b, a - b); }
而main.c是对该库函数的调用:
#include <stdio.h> #include "addLib.h" #include "subLib.h" int main(void) { add(4, 6); return 0; }
简单写个makefile:
all : addLib.o subLib.o ar -r libcal.a addLib.o subLib.o gcc main.c -L./ -lcal addLib.o : addLib.c gcc -c $< subLib.o : subLib.c gcc -c $< clean: rm *.o *.a -rf
编译通过:
运行正确:
Linux的nm可列出目标文件的符号清单,通过它查看libcal.a:
图中的T表示该符号位于代码段,U表示在当前文件是未定义的,即该符号是定义在别的文件。
在这个例子中,链接的命令为
gcc main.c -L./ -lcal
其中gcc main.c其实是做了编译和链接,所以最后一步的链接操作是
gcc main.o -L./ -lcal
链接器从左向右扫描链接命令行参数中的.o和.a,最终目的是确定“最终.o文件集合”和各个.o文件中的外部符号的定义位置。
以本程序为例,
(1)首先是扫描main.o,main.o会无条件被加入到“最终.o文件集合”中,该文件引用了main符号,它是程序开始执行的符号,会将main放入“已定义符号表”中,接着又引用了外部符号符号add,因此会将add放入“未定义符号表”中。
(2)扫描到libcal.a中addLib.o,“未定义符号表”中存放的add在addLib.o中找到了定义,于是将addLib.o文件加入到“最终.o文件集合”中,且将add符号从“未定义符号表”中转换到“已定义符号表”中。但是在扫描addLib.o中,发现了外部符号printf,于是printf符号被放入“未定义符号表”。
(3)扫描到libcal.a中的subLib.o,此时“未定义符号表”中存放的是printf,并不能在subLib.o中找到定义,直接略过该文件,所以subLib.o并不加入到“最终.o文件集合”中,其中的符号信息也没有被加载“未定义符号表”和“已定义符号表”。
(4)“未定义符号表”里仍然存在printf符号,所以链接器会继续扫描,它往哪里扫描?链接命令上写到-lcal就截止了,其实c程序默认会去链接标准c库的,找到标准c库的定义printf符号的.o文件,并把该文件加入“最终.o文件集合”中,链接操作至此完成。注意链接完成的标志是“未定义符号表”中为空,也就是不能出现未定义的符号。
如上分析,因为main.c程序中并没有调用sub函数,subLib.o并不会被加入到“最终.o文件集合,那么在subLib.c中加上如下代码且不调用,同样是能编译通过咯:
void sub(int a, int b) { printf("%d - %d = %d\n", a, b, a - b); } int main(void) { printf("in lib mian!\n"); return 0; }
果然如此:
main.c的main符号在库函数外部,链接器已经认识这个符号了,会将其放入“已定义的符号表”中,所以不会去扫描cal库内的subLib.o里的main符号。
再做改动,在main.c的main函数中调用subLib.c的sub函数:
int main(void) { add(4, 6); sub(9, 2); return 0; }
再编译就出现重定义错误了:
原因也很简单,因为main函数调用了sub函数导致subLib.c中的sub符号一开始放入到“未定义符号集”,再找到subLib.o后,该符号会被放入到“已定义符号集”,subLib.o也会随之加入“最终.o文件集合”中,问题就出现了:该集合中main.o和subLib.o均包含了main符号,自然就报错了!
综上所述,我们可以推论,链接器对目标文件(.o)和库文件(.a)是区别对待的。我们知道,可执行程序是由一系列的.o文件“合并”而成,以静态链接为例,“最终.o文件集合”中除了包含我们显示提供的由.c编译而来.o文件外,还有从.a库文件提取出来的.o文件,可执行程序对由.c编译而成的.o文件无条件的包含到“最终.o文件集合”中,而对从.a库提取的.o并非全盘提取,而是“按需”提取,“按需”是根据“未定义符号表”中的符号去提取的。这也符合软件设计的思想,尽可能使得可执行文件的size小。
实际开发中,我还遇到这样一个问题: 可执行程序链接了n个静态库和一个动态库,动态库想要调用静态库里的某个函数,运行时报错找不到该符号,通过前面的学习,可以分析原因就是在于,可执行程序虽然链接了这个静态库但并没有使用静态库的某个.o文件,导致.o没有真正被链接到可执行程序,而该.o文件的某个符号又要被动态库使用,这就出现了上面的报错了。解决办法无非就是让可执行程序去调用一下该符号了。