链接(extern、static关键词\头文件\静态库\共享库)
原文链接:http://www.orlion.ga/781/
一、 多目标文件的链接
假设有两个文件:stack.c:
/* stack.c */ char stack[512]; int top = -1; void push(char c) { stack[++top] = c; } char pop(void) { return stack[top--]; } int is_empty(void) { return top == -1; }
上边这个文件实现了栈。main.c:
/* main.c */ #include <stdio.h> int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
这个文件是使用了栈,编译:gcc main.c stack.c -o main,也可以分步编译:
gcc -c main.c gcc -c stack.c gcc main.o stack.o -o main
用nm命令查看目标文件的符号表,会发现main.o中有未定义的符号push、pop、is_empty、putchar:
前三个符号在stack.o中实现了,链接生成可执行文件main时可以做符号解析,而putchar是libc的库函数,在可执行文件main中
仍然是未定义的,要在程序运行时做动态链接。
{
nm [option(s)] [file(s)]
有用的options:
- -A 在每个符号信息的前面打印所在对象文件名称;
- -C 输出demangle过了的符号名称;
- -D 打印动态符号;
- -l 使用对象文件中的调试信息打印出所在源文件及行号;
- -n 按照地址/符号值来排序;
- -u 打印出那些未定义的符号;
常见的符号类型:
- A 该符号的值在今后的链接中将不再改变;
- B 该符号放在BSS段中,通常是那些未初始化的全局变量;
- D 该符号放在普通的数据段中,通常是那些已经初始化的全局变量;
- T 该符号放在代码段中,通常是那些全局非静态函数;
- U 该符号未定义过,需要自其他对象文件中链接进来;
- W 未明确指定的弱链接符号;同链接的其他对象文件中有它的定义就用上,否则就用一个系统特别指定的默认值。
注意几点:
- -C 总是适用于c++编译出来的对象文件。还记得c++中有重载么?为了区分重载函数,c++编译器会将函数返回值/参数等信息附加到函数名称中去形成一个mangle过的符号,那用这个选项列出符号的时候,做一个逆操作,输出那些原始的、我们可理解的符号名称。
- 使用 -l 时,必须保证你的对象文件中带有符号调式信息,这一般要求你在编译的时候指定一个 -g 选项,见 Linux:Gcc。
- 使用nm前,最好先用Linux:File查看对象文件所属处理器架构,然后再用相应交叉版本的nm工具。
}
我们通过readelf -a main命令可以看到,main的.bss段合并了main.o和stack.o的.bss段,其中包含了变量a和stack,main的.data段也合并了main.o和stack.o的.data段,其中包含了变量b和top,main的.text段合并了main.o和stack.o的.text段,包含了各函数的定义。如下图所示:
如果在编译时把stack.o放到main.o前面,即:gcc stack.o main.o -o main,可执行文件main的每个段中来自main.o的变量或函数都排到后面去了。
实际上链接的过程是由一个链接脚本(Linker Script)控制的,链接脚本决定了给每个段分配什么地址,如何对齐,哪个段在前,哪个段在后,哪些段合并到同一个Segment,另外链接脚本还要插入一些符号到最终生成的文件中,例如 __bss_start 、 _edata 、 _end 等。如果用 ld 做链接时没有用 -T 选项指定链接脚本,则使用 ld 的默认链接脚本,默认链接脚本可以用 ld --verbose 命令查看。【结果很长直接接书上的截图,实际输出内容与下图并不一致】:
ENTRY(_start)说明_start是整个程序的入口点,因此_start是入口点并不是规定,是可以改用其他函数做入口点的。
PROVIDE(__executable_start = 0x8048000); . = 0x9048000 + SIZEOF_HEADERS;是Text Segment的起始地址,这个Segment包含后面列出的那些段, .plt、.text、.rodata等等。每个段的描述格式都是"段名:{组成}",例如.plt : { *(.plt)) },左边表示最终生成的文件的.plt段,右边表示所有目标文件的.plt段,意思是最终生成的文件的.plt段由各目标文件的.plt段组成。
. = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));是Data Segment的起始地址,要做一系列的对齐操作,这个Segment包含后面列出的那些段,.got、.data、.bss等等。
二、定义和声明
1、extern和static关键字
在上面编译stack.c与main.c文件时其实有一点小问题,(-Wall选项可以看到)由于编译器在处理函数调用代码时没有找打函数原型,只好根据函数调用代码做隐式声明,把三个函数声明为
int push(char); int pop(void); int is_empty(void);
现在完善一中的代码:
/* main.c */ #include <stdio.h> extern void push(char); extern char pop(void); extern int is_empty(void); int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
这样编译器就不会报警报了,extern关键字表示这个标识符具有External Linkage【关于External Linkage参看《[汇编与C语言关系]3. 变量的存储布局》】(“extern关键字表示这个标识符具有External Linkage”其实是不准确的,准确地说应该是Previous Linkage。Previous Linkage的定义是:这次声明的标识符具有什么样的Linkage取决于前一次声明,这前一次声明具有相同的标识符名,而且必须是文件作用域的声明,如果在程序文件中找不到前一次声明(这次声明是第一次声明),那么这个标识符具有External Linkage),push这个标识符具有External Linkage指的是:如果把main.c和stack.c链接在一起,如果push在main.c和stack.c中都有声明(在stack.c中既是声明又是定义)那么这些声明指的是同一个函数,链接之后是同一个GLOBAL符号,代表同一个地址。
函数声明中的extern也可以省略不写,不写extern仍然表示这个函数名具有External Linkage。C语言不允许嵌套定义函数,但如果只是声明而不定义,这种声明时允许写在函数体里面的,这样声明的标识符具有块作用域,例如上面的main.c可以写成:
/* main.c */ #include <stdio.h> int main(void) { void push(char); char pop(void); int is_empty(void); push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
如果用static关键字修饰一个函数声明,则表示该标识符具有Internal Linkage,例如有一下两个程序文件:
/* foo.c */ static void foo(void) {}
/* main.c */ void foo(void); int main(void) { foo(); return 0; }
编译链接在一起会出错:
虽然在foo.c中定义了函数foo,但这个函数只具有Internal Linkage,只有在foo.c中多次声明才表示同一个函数,而在main.c中声明就不表示它了。如果把foo.c编译成目标文件,函数名foo在其中是一个LOCAL的符号,不参与链接过程,所以在链接时,main.c中用到一个External Linkage的foo函数,链接器却找不到它的定义在哪,无法确定它的地址,也就无法做符号解析,只能报错。凡是被多次声明的变量或函数,必须有且只有一个声明是定义,如果有多个定义,或者一个定义都没有,链接器就无法完成链接。
对于变量如果在main.c中的main()函数中用extern int top;来声明top变量可以访问到stack.c中的top变量。(变量的extern声明不能省略,而且不能定义如:extern int top = -1;这样写是不对的)。如果不想让main.c访问到top变量可以把top用static声明,在stack.c中static int top = -1;
2、头文件
(一)中的stack.c模块封装了top和stack两个变量导出了push\pop\is_empty三个函数接口,但是使用这个模块的每个程序文件都要写三个函数声明,假设又有一个文件也使用了这个模块,那么就要写三个函数声明。这时候可以写一个头文件stack.h来简化代码:
/* stack.h */ #ifndef STACK_H #define STACK_H extern void push(char); extern char pop(void); extern int is_empty(void); #endif
在main.c中只需要包含就可以了:
/* main.c */ #include <stdio.h> #include "stack.h" int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
对于用尖括号包含的头文件(#include <stdio.h>)gcc会首先查找-I选项指定的目录,然后查看系统的头文件目录(通常是/usr/include),而对于引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。在#include预处理中可以使用相对路劲如:#include "header/stack.h"
#idndef STACK_H和#endif是如果STACK_H这个宏没有定义过,那么从#ifndef到#endif之间的代码就包含在预处理的输出结果中,否则这一段代码就不出现在预处理的输出结果中。stack.h这个头文件的内容整个被#ifndef和#endif包含起来了,如果在包含这个头文件时STACK_H这个宏已经定义过了,则相当于这个文件中什么都没有。
还有一个问题,既然要#include头文件,那我不如直接在main.c中#include "stack.c"得了。这样把stack.c和main.c合并为同一个程序文件。虽然这样也能编译通过,但是在一个规模较大的项目中不能这么做,假如又有一个foo.c也要使用stack.c这个模块怎么办呢?如果在foo.c里面也#include "stack.c",就相当于push、pop、is_empty这三个函数在main.c和foo.c中都有定义,那么main.c和foo.c就不能链接在一起了。如果采用包含头文件的办法,那么这三个函数只在stack.c中定义了一次,最后可
以把main.c、stack.c、foo.c链接在一起。
三、静态库
有时候要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(如printf)也会用到libc中的变量(比如environ)
把(一)中的stack.c拆成四个文件:
/* stack.c */ char stack[512]; int top = -1;
/* push.c */ extern char stack[512]; extern int top; void push(char c) { stack[++top] = c; }
/* pop.c */ extern char stack[512]; extern int top; char pop(void) { return stack[top--]; }
/* is_empty.c */ extern int top; int is_empty(void) { return top == -1; }
/* stack.h */ #ifndef STACK_H #define STACK_H extern void push(char); extern char pop(void); extern int is_empty(void); #endif
/* main.c */ #include <stdio.h> #include "stack.h" int main(void) { push('a'); return 0; }
目录结构为:
|–main.c
|–stack
|–is_empty.c
|–pop.c
|–push.c
|–stack.c
|–stack.h
把stack.c、push.c、pop.c、is_empty.c编译成目标文件:
gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
然后打包成一个静态库libstack.a:
ar rs libstack.a stack.o push.o pop.o is_empty.o
库文件都是以lib开头的,静态库以.a作为后缀,表示Archive。。ar命令类似于tar命令,起一个打包的作用,但是把目标文件打包成静态库只能用ar命令而不能用tar命令。选项r表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。ranlib命令也可以为静态库创建索引,以上命令等价于:
ar r libstack.a stack.o push.o pop.o is_empty.o ranlib libstack.a
然后把libstack.a和main.c编译连接在一起:
gcc main.c -L. -l stack -I stack -o main
-L选项告诉编译器去哪里找需要的库文件,-L.表示在当前目录找。-lstack告诉编译器要链接libstack库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs选项查看。
编译器会首先找有没有共享库libstack.so,如果有就链接它,如果没有就找有没有静态库libstack.a,如果有就链接它。所以编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。
在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件main中调用的libc库函数仍然是未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。
四、共享库
1、编译、链接、运行
组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项:
// 原文是:gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c, 但是这样写不能生成.so文件
gcc -o libstack.so -O2 -fPIC -shared stack/stack.c stack/push.c stack/pop.c stack/is_empty.c stack/stack.h
-f后边跟编译选项,PIC是其中一种,表示生成位置无关代码。一般的目标文件称为Relocatable,在链接时可以把目标文件中各段的地址做重定位,重定位时需要修改指令。
现在把main.c和共享库编译链接在一起,然后运行:
$ gcc main.c -g -L. -lstack -Istack -o main $ ./main ./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory
(TODO:这里本机实验时有问题)
编译的时候没问题,由于指定了-L.选项,编译器可以在当前目录下找到libstack.so,而运行时却说找不到libstack.so。
可以用ldd命令查看可执行文件依赖哪些共享库:
$ ldd main linux-gate.so.1 => (0xb7f5c000) libstack.so => not found libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000) /lib/ld-linux.so.2 (0xb7f42000)
ldd模拟运行一遍main,在运行过程中做动态链接,从而得知这个可执行文件依赖于哪些共享库,每个共享库都在什么路径下,加载到进程地址空间的什么地址。/lib/ld-linux.so.2是动态链接器,它的路径是在编译链接时指定的,gcc在做链接时用-dynamic-linker指定动态链接器的路径,它也像其它共享库一样加载到进程的地址空间中。libc.so.6的路径/lib/tls/i686/cmov/libc.so.6是由动态链接器ld-linux.so.2在做动态链接时搜索到的,而libstack.so的路径没有找到。linux-gate.so.1这个共享库其实并不存在于文件系统中,它是由内核虚拟出来的共享库,所以它没有对应的路径,它负责处理系统调用。总之,共享库的搜索路径由动态链接器决定,从ld.so(8)Man Page可以查到共享库路径的搜索顺序:
1. 首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。
2. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文
件/etc/ld.so.conf之后生成,稍后详细解释。
3. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib。
解决:可以把libstack.so所在的目录的绝对路径添加到/etc/ld.so.conf中,然后运行ldconfig
2、共享库的命名惯例
按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。例如上面的libcap.so.1.10、libc-2.8.90.so等。soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。例如上面的libcap.so.1和libcap.so.2是两个主版本号不同的libcap,有些应用程序
依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。注意libc的版本编号有一点特殊,libc-2.8.90.so的主版本号是6而不是2或2.8。
linker name仅在编译链接时使用,gcc的-L选项应该指定linker name所在的目录。有的linker
name是库文件的一个符号链接,有的linker name是一段链接脚本