@[TOC]}#静态库
程序的编译
hello.c
#include
void main(void)
{
printf(“Hello World!\n”);
}
程序的编译及运行命令
gcc hello.c
./a.out
程序的编译过程
什么是库
在计算机科学中,库是用于开发软件的子程序集合。
库和可执行文件的区别是,库不是独立程序,他们是向其他程序提供服务的代码。
静态库
概念:
-将所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件。
-在构建可执行文件时只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标文件,并且只把用到的模块从库中拷贝出来。
-在gcc命令行中无需明显指定C标准库libc.a
(默认库)
静态库练习
myproc1.c
#include
void myfunc1()
{
printf("This is myfunc1!\n");
}
myproc2.c
#include
void myfunc2()
{
printf("This is myfunc2!\n");
}
$ gcc –c myproc1.c myproc2.c
$ ar rcs libmyproc.a myproc1.o myproc2.o
静态库练习
main.c
void myfunc1();
int main()
{
myfunc1();
return;
}
$ gcc –static –o main main.c libmyproc.a
静态库-图
#静态链接
程序的编译
链接器的由来
最早的计算机完全是用机器语言进行编程的。
程序员需要在纸质表格上写下符号化程序,然后手工将其汇编为机器码。
最后通过开关、纸带或卡片将其输入到计算机中。
纸带穿孔实现机器代码
汇编语言出现
-用助记符表示操作码
-用符号表示位置
0:0101 0101 add B
1:0010 0101 jmp L0
2:…… ……
3:…… ……
4:…… ……
5:0110 0111 L0: sub C
6:…… ……
汇编语言的出现,软件规模日渐庞大,代码量快速地膨胀。
代码按照功能或者性质分割成不同的功能模块。
模块之间的通信有两种方式:模块间的函数调用和模块间的变量访问,统称为模块间的符号引用。
人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接。
链接过程
主要包括了地址
和空间分配
、符号解析
和重定位
。
链接过程的步骤
确定符号引用关系(符号解析
)
合并相关的.o文件
确定每个符号的地址 (重定位
)
在指令中填入新的地址
目标文件
可执行文件格式
PC平台流行的可执行文件格式主要包含如下两种,它们都是COFF(Common File Format)格式的变种。
Windows下的PE(Portable Executable)
Linux下的ELF(Executable Linkable Format)
ELF文件有三种形式
可重定位目标文件
典型的可重定位目标文件的布局
elf文件头信息
readelf -h main.o
使用objdump –h hello.o命令
代码和数据
源代码中执行语句都编辑成机器代码,保存在.text
;
编译生成的数据的存放:
已初始化的全局变量和局部静态变量都保存在.data
未初始化的全局变量和局部静态变量保存在.bss
程序中的只读数据存放在.rodata
sample.c
节头表
readelf –S sample.o
符号与符号表
在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。
每个目标文件都包含文件中定义的符号信息,以及文件中引用的符号信息。
符号的分类
符号分为三类
文件中定义的全局符号
,可以被其他文件引用
文件中引用的全局符号
,在其他文件中被定义
文件中定义的本地符号
,只在本文件中被引用
本地符号和程序局部变量无关
符号的举例
静态链接——符号解析
本地符号
在一个模块中,每个本地符号只允许有一个定义
本地符号的解析很直接:在符号表中找到与引用同名的符号即可
全局符号
如果该符号不是在当前模块中定义的,编译器假设该符号是在其他模块中被定义,生成一个链接器符号表项目,交给链接器处理:
若最终未找到则输出错误并中止
如果相同的全局符号被多个目标模块定义,此时链接器将根据相应原则进行处理
符号解析的步骤
符号解析(Symbol resolution)
-程序中定义和引用的符号(变量名和函数名)
int main() {
…} /*定义符号main*/
swap() /*引用符号swap*/
swap(&a, &shared) /*引用符号shared*/
-编译器将定义的符号存放在一个符号表中
-编译器将符号的引用存放在重定位表中
-链接器将每个符号的引用都与一个确定的符号定义建立关联
#目标文件中的符号表
符号表记录了目标文件中所用到的所有符号。
ELF文件中.symtab节记录符号表,是一个结构数组Elf32_Sym。
typedef struct {
Elf32_Word st_name; /*符号名*/
Elf32_Addr st_value; /*符号值,一般是函数和变量的地址*/
Elf32_Word st_size; /*符号占用空间大小,字节数*/
unsigned char st_info; /*符号类型和绑定信息*/
unsigned char st_other; /*未使用*/
Elf32_Half st_shndx; /*符号所在的节*/
}Elf32_Sym;
#符号与符号表
readelf -s a.o b.o
强符号和弱符号
在编译时,编译器将每一个全局符号标记为strong和weak两类:
函数和初始化的全局变量被标记为强符号
未初始化的全局变量被标记为弱符号
链接时,链接器对多重定义的全局符号的解析原则如下:
同一个符号不允许有多个strong定义
假如一个符号有一个strong定义和多个weak定义,那么采用该符号的strong定义
假如一个符号有多个weak定义,那么会选择占用空间大的符号定义
#重定位
链接的步骤
符号解析(Symbol resolution)
-编译器将定义的符号存放在一个符号表中
-编译器将符号的引用存放在重定位表中
-链接器将每个符号的引用都与一个确定的符号定义建立关联
重定位
-将多个代码段与数据段分别合并为一个单独的代码段和程序段
-计算每个定义的符号在虚拟地址空间的绝对地址
-将可执行文件中符号引用的地址修改为重定位后的地址信息
空间与地址分配—合并相似节
将相同性质的节合并到一起
空间与地址分配—符号地址的确定
输入a.o、b.o,输出ab。
符号地址确定
objdump –h a.o
objdump –h ab
重定位表
重定位表:ELF文件中专门用来保存与重定位信息有关的表,它在ELF文件中往往是一个或者多个节。
每个要被重定位的ELF节都有一个对应的重定位表,比如:“.data
”对应重定位表叫“.rel.data
”,“.text
”对应的重定位表叫“.rel.text
”。
objdump –r a.o
,查看a.o文件的重定位表
重定位表是一个Elf32_Rel
结构的数组,每个数组元素对应一个重定位入口:
typedef struct {
Elf32_Addr r_offset; /*重定位入口的偏移*/
Elf32_Word r_info; /*重定位入口的类型和符号*/
}Elf32_Rel;
objdump –d a.o
查看a.o文件的代码段内容
readelf –s ab
查看swap、shared符号的虚拟地址
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
……
61: 0804a014 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
62: 080483b4 39 FUNC GLOBAL DEFAULT 13 main
63: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
64: 080483dc 58 FUNC GLOBAL DEFAULT 13 swap
65: 08048294 0 FUNC GLOBAL DEFAULT 11 _init
66: 0804a010 4 OBJECT GLOBAL DEFAULT 24 shared
objdump –d ab 查看可执行文件ab main函数内容
#3-6 共享库
静态库的弊端
系统空间的浪费。如果多个程序链接了同一个库,则每一个生成的可执行文件就会有一个库的副本,必然会浪费系统空间。
一旦发现库中的bug,挽救起来很麻烦,必须把链接该库的程序一一找出来,然后重新编译。
动态链接
基本思想:把程序的模块相互分割开来,形成独立的文件。在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个单独的可执行文件。
动态链接文件
Windows称其为动态链接库(Dynamic Link libraries, .dll文件)
Linux称其为动态共享目标文件(Dynamic Shared Objects, .so文件),简称共享库文件。
从程序中分离出来,磁盘和内存中只有一个库文件备份
静态链接和动态链接的区别
静态链接生成可执行程序时,将库文件的全部内容都合并到可执行文件中,运行时不依赖静态库
。
动态链接生成可执行程序时,仅链接库文件中的符号表、重定位信息,而没有链接代码段和数据段
,程序运行时依赖动态库
。
动态链接工作原理
在程序加载的时候,内核会检查程序使用到的共享库是否已经加载到内存
如果没有被加载到内存,则从系统库路径搜索并且加载相关的共享库
如果共享库已经被加载到内存,程序可以直接使用而无须加载
共享库是linux系统最广泛的一种程序使用方式
共享库工作原理-加载和链接共享库的时机
应用程序自身加载时动态链接和加载共享库
当创建可执行文件时,静态执行一些链接(共享库的重定位和符号表信息,而非代码和数据),然后在应用程序加载时,动态完成链接过程
应用程序运行过程中动态链接和加载共享库
应用程序在运行过程中要求动态链接器加载和链接任意共享库,而无需编译时链接那些库到应用中
共享库练习
myproc1.c
#include
void myfunc1()
{
printf("This is myfunc1!\n");
}
myproc2.c
#include
void myfunc2()
{
printf("This is myfunc2!\n");
}
$ gcc –shared –fPIC –o libmy.so myproc1.c myproc2.c
PIC(Positon-independent Code):地址无关代码
共享库练习
main.c
void myfunc1();
int main()
{
myfunc1();
return;
}
$ gcc –o myproc main.c ./libmy.so
运行时动态链接
通过调用动态链接器接口提供的函数在运行时进行动态链接。
类UNIX系统中的动态链接器接口定义了相应的函数,如dlopen,dlsym,dlerror,dlclose等,其头文件为dlfcn.h。
运行时动态链接
#include
#include
#include
int main()
{
void *handle;
void (*myfunc1)();
char *error;
handle = dlopen("./mylib.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
myfunc1 = dlsym(handle, "myfunc1");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
myfunc1();
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
编译命令:gcc -o myproc main.c -ldl