一、什么是库
1. 概念
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll),所谓静态、动态是指链接。
2. 将一个程序编译成可执行程序的步骤
3. 静态链接方式和动态链接方式
4. 静态库
4.1 概念
之所以称为静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件(.out)中。因此对应的链接方式称为静态链接。试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o 文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件
下面编写一些简单的四则运算C++类,将其编译成静态库给他人用,头文件如下所示:
1 class StaticMath
2 {
3 public:
4 StaticMath(void);
5 ~StaticMath(void);
6
7 static double add(double a, double b);//加
8 static double sub(double a, double b);//减
9 static double mul(double a, double b);//乘
10 static double div(double a, double b);//除
11
12 };
Linux下使用 ar 工具,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。一般创建静态库的步骤如图所示:
4.2 Linux下创建与使用静态库
4.2.1 Linux静态库命名规则
Linux静态库命名规范,必须*是”lib[your_library_name].a”:lib为前缀,中间是静态库名,扩展名为 .a
创建静态库(.a)
通过上面的流程可以知道,Linux创建静态库过程如下:
- (1)首先,将代码文件编译成目标文件.o(StaticMath.o)
1 //这是StaticMath.cpp 文件 2 #include<iostream> 3 #include"myhead.h" 4 using namespace std; 5 double StaticMath::add(double a,double b) 6 { 7 return a+b; 8 } 9 double StaticMath::sub(double a,double b) 10 { 11 return a-b; 12 } 13 double StaticMath::mul(double a,double b) 14 { 15 return a*b; 16 } 17 double StaticMath::div(double a,double b) 18 { 19 return a/b ; 20 }
1 g++ -c StaticMath.cpp
注意带参数-c,将其编译为.o 文件,否则直接编译为可执行文件
- (2)然后,通过 ar 工具将目标文件打包成 .a 静态库文件,最好编写makefile文件(CMake等等工程管理工具)来生成静态库,输入多个命令太麻烦了。格式为:ar rcs + 静态库的名字(libMytest.a) + 生成的所有的.o
1 ar -jcv -f libstaticmath.a StaticMath.o
- (3) 使用静态库
编写使用上面创建的静态库的测试代码:
1 #include "StaticMath.h"
2 #include <iostream>
3 using namespace std;
4
5 int main(int argc, char* argv[])
6 {
7 double a = 10;
8 double b = 2;
9
10 cout << "a + b = " << StaticMath::add(a, b) << endl;
11 cout << "a - b = " << StaticMath::sub(a, b) << endl;
12 cout << "a * b = " << StaticMath::mul(a, b) << endl;
13 cout << "a / b = " << StaticMath::div(a, b) << endl;
14
15 return 0;
16 }
Linux下使用静态库,只需要在编译的时候,指定静态库的搜索路径(-L选项)、指定静态库名(不需要lib前缀和.a后缀,-l选项)。
1 g++ TestStaticLibrary.cpp -L../StaticLibrary -lstaticmath
-L:表示要连接的库所在目录
-l (小写L):指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。
4.3 gcc 参数简介
这里只写了几个关键的,其它的可以在这里GCC 参数详解查找。
-E:只执行到预处理阶段,不生成任何文件
-S:将C代码转换为汇编代码(.s 汇编文件)
-c:仅执行编译操作,不进行连接操作(.o 机器码)
-o:指定生成的输出文件(.out 可执行文件)
-L:告诉gcc去哪里找库文件。 gcc默认会在程序当前目录、/lib、/usr/lib和/usr/local/lib下找对应的库
-l:用来指定具体的静态库、动态库是哪个
-I: 告诉gcc去哪里找头文件
5. 动态库
5.1 为什么还需要动态库?
为什么需要动态库,其实也是静态库的特点导致。
- 空间浪费是静态库的一个问题。
- 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可。
5.2 动态库特点
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。
5.3 Linux下创建与使用动态库
5.3.1 Linux动态库的命名规则
动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为“.so”,ib + 名字 + .so
5.3.2 创建动态库(.so)
编写四则运算动态库代码
1 // myhead.h 文件
2 class DP
3 {
4 public:
5 static void print_111();
6 static void print_222();
7 static void print_333();
8 };
9
10 //print_333 文件(print_111和print_222 文件与print_333 文件类似)
11 #include<iostream>
12 #include"myhead.h"
13 using namespace std;
14 void DP::print_333()
15 {
16 cout << "333333333333333" << endl ;
17 }
首先,生成目标文件,此时要加编译器选项-fpic
1 g++ -fPIC -c print_*.cpp
-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。
然后,生成动态库,此时要加链接器选项 -share
1 g++ -shared -o libtest.so print_*.o
-shared 指定生成动态链接库。
其实上面两个步骤可以合并为一个命令:
1 g++ -fPIC -shared -o libtest.so print_*.cpp
生成libtest.so 动态库
5.3.3 使用动态库
lld命令:用于查看可执行程序依赖的so动态链接库文件
1 [root@localhost ld.so.conf.d]# ldd /usr/local/tengine/sbin/nginx
2 linux-vdso.so.1 => (0x00007ffc9fd66000)
3 libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ff1c5f56000)
4 libdl.so.2 => /lib64/libdl.so.2 (0x00007ff1c5d52000)
5 libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007ff1c5b1a000)
6 libjemalloc.so.2 => /usr/jemalloc/lib/libjemalloc.so.2 (0x00007ff1c58cb000)
7 libc.so.6 => /lib64/libc.so.6 (0x00007ff1c550a000)
8 /lib64/ld-linux-x86-64.so.2 (0x00007ff1c6187000)
9 libfreebl3.so => /lib64/libfreebl3.so (0x00007ff1c5306000)
显示not found的提示说明没有找到该库文件,则程序运行会报错,手动添加就可以了
编写使用动态库的测试代码:
1 // testDP.cpp 文件
2 #include "myhead.h"
3 #include <iostream>
4 using namespace std;
5
6 int main(void)
7 {
8 DP::print_111();
9 DP::print_222();
10 DP::print_333();
11 return 0;
12 }
引用动态库编译成可执行文件(跟静态库方式一样):
1 g++ testDP.cpp -L./ -ltest
然后运行:./a.out,报错如下:
这是由于程序运行时没有找到动态链接库造成的。程序编译时链接动态链接库和运行时使用动态链接库的概念是不同的,在运行时,系统能够知道其所依赖的库的名字,但是还需要知道绝对路径。有几种办法可以解决此种问题:
(1) 因为系统会按照 LD_LIBRARY_PATH 环境变量来查找除了默认路径之外( /lib, /usr/lib, /usr/local/lib)的共享库(动态链接库)的其他路径,就像PATH变量一样!所以我们可以修改该环境变量来解决这个问题。
1 export LD_LIBRARY_PATH=/home/hp/LinuxC:$LD_LIBRARY_PATH
也就是将我们发布的共动态库所在的路径加入系统的查找选项中。动态库的加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。
(2)使用LD_PRELOAD,如果使用了这个变量,系统会优先去这个路径下寻找,如果找到了就返回,不在往下找了
1 LD_PRELOAD=./libtest.so ./a.out
(3)将动态链接库赋值一份到默认路径
1 sudo cp libtest.so /usr/lib
(4)因为系统中的配置文件/etc/ld.so.conf是动态链接库的搜索路径配置文件,在程序运行时会去读取该文件,那么我们就将我们自己编写的库的路径写到该配置文件中去即可。
在最后一行加入 /home/liushengxi/C-/自建库/test,运行ldconfig ,该命令会重建/etc/ld.so.cache文件
二、可执行程序的链接、装载
《程序员的自我修养-链接装载与库》是一本值得推荐的书,主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。
2.1 基础知识
许多IDE和编译器将编译和链接的过程合并在一起,称为构建(Build),使用起来非常方便。但只有深入理解其中的机制,才能看清许多问题的本质,正确解决问题。
一般的编译过程可以分解为4个步骤,预处理,编译,汇编和链接:
- 预编译:处理源代码中的以”#”开始的预编译指令,如”#include”、”#define”等。
- 编译:把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件,是程序构建的核心部分,也是最复杂的部分之一。
- 汇编:将汇编代码根据指令对照表转变成机器可以执行的指令,一个汇编语句一般对应一条机器指令。
- 链接:将多个目标文件综合起来形成一个可执行文件。
而对于第2步,编译由编译器完成器,编译器是将高级语言翻译成机器语言的一个工具,其具体步骤包括:
- 词法分析:将源代码程序输入扫描器,将源代码字符序列分割成一系列记号(Token)。
- 语法分析:对产生的记号使用上下文无关语法进行语法分析,产生语法树。
- 语义分析:进行静态语义分析,通常包括声明和类型的匹配,类型的转换。
- 中间语言生成:使用源代码优化器将语法树转换成中间代码并进行源码级的优化。
- 目标代码生成:使用代码生成器将中间代码转成依赖于具体机器的目标机器代码。
- 目标代码优化:使用目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移替代乘法、删除多余指令等。
如果一个源代码文件中有变量或函数等符号定义在其他模块,那么编译后得到的目标代码中,该符号的地址并没有确定下来,因为编译器不知道到哪里去找这些符号,事实上这些变量和函数的最终地址要在链接的时候才能确定。现代的编译器只是将一个源代码编译成一个未链接的目标文件,最终由链接器将这些目标文件链接起来形成可执行文件。
先以 helloworld.c 程序为例,搞清楚可执行文件是如何生成的:
1 #include <stdio.h>
2 int main(void)
3 {
4 printf("hello, world!
");
5 return 0;
6 }
(1)预处理,处理代码中的宏定义和 include 文件,并做语法检查
1 gcc -E helloworld.c -o helloworld.cpp
(2)编译,生成汇编代码
1 gcc -S helloworld.cpp -o helloworld.s
(3)汇编,生成汇编代码
1 gcc -c helloworld.s -o helloworld.o
(4)链接,生成可执行文件
1 gcc helloworld.o -o helloworld
具体过程可以用下面的图片表示,各种文件格式之间的关系如下:
2.2 ELF 文件格式
编译器编译源代码后生成的文件称为目标文件,事实上,目标文件是按照可执行文件的格式存储的,二者结构只是稍有不同。Linux下的目标文件和可执行文件可以看成一种类型的文件,统称为ELF文件,一般有以下几类:
- 可重定位文件,如:.o 文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这类。
- 可执行文件,如:/bin/bash 文件,包含可直接执行的程序,没有扩展名。
- 共享目标文件,如:.so 文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分。
ELF 文件由 ELF header 和文件数据组成,文件数据包括:
- Program header table, 程序头:描述段信息
- .text, 代码段:保存编译后得到的指令数据
- .data, 数据段:保存已经初始化的全局静态变量和局部静态变量
- Section header table, 节头表:链接与重定位需要的数据
除了这几个常用的段之外,ELF可能包含其他的段,保存与程序相关的信息,如:
1 .comment 编译器版本信息
2 .debug 调试信息
3 .dynamic 动态链接信息
4 .hash 符号哈希表
5 .line 调试时的行号表,源代码行号与编译后指令的对应表
6 .note 额外的比编译器信息
7 .strtab String Table,字符串表,存储用到的各种字符串
8 .symtab Symbol Table,符号表
9 .shstrtab Section String Table,段名表
10 .plt 动态链接跳转表
11 .got 动态链接全局入口表
12 .init 程序初始化代码段
13 .fini 程序终结代码段
1 Section Headers:
2 [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
3 [ 0] NULL 00000000 000000 000000 00 0 0 0
4 [ 1] .group GROUP 00000000 000034 000008 04 12 16 4
5 [ 2] .text PROGBITS 00000000 00003c 000078 00 AX 0 0 1
6 [ 3] .rel.text REL 00000000 000338 000048 08 I 12 2 4
7 [ 4] .data PROGBITS 00000000 0000b4 000008 00 WA 0 0 4
8 [ 5] .bss NOBITS 00000000 0000bc 000004 00 WA 0 0 4
9 [ 6] .rodata PROGBITS 00000000 0000bc 000004 00 A 0 0 1
10 [ 7] .text.__x86.get_p PROGBITS 00000000 0000c0 000004 00 AXG 0 0 1
11 [ 8] .comment PROGBITS 00000000 0000c4 000012 01 MS 0 0 1
12 [ 9] .note.GNU-stack PROGBITS 00000000 0000d6 000000 00 0 0 1
13 [10] .eh_frame PROGBITS 00000000 0000d8 00007c 00 A 0 0 4
14 [11] .rel.eh_frame REL 00000000 000380 000018 08 I 12 10 4
15 [12] .symtab SYMTAB 00000000 000154 000140 10 13 13 4
16 [13] .strtab STRTAB 00000000 000294 0000a2 00 0 0 1
17 [14] .shstrtab STRTAB 00000000 000398 000082 00 0 0 1
18 Key to Flags:
19 W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
20 L (link order), O (extra OS processing required), G (group), T (TLS),
21 C (compressed), x (unknown), o (OS specific), E (exclude),
22 p (processor specific)
ELF文件组成字段分析:
ELF文件头(ELF Header):保存描述整个文件的基本属性,如ELF魔数、文件机器字节长度、数据存储格式等。
段表(Section Header Table):保存各个段的基本属性,是除了文件头之最重要的结构。节选样例内容如下:
[Nr] | Name | Type | Addr | Off | Size | ES | Flg | Lk | Inf | Al |
[1] | .text | PROGBITS | 00000000 | 000034 | 00005b | 00 | AX | 0 | 0 | 4 |
其表示的意义为,下标为1的段是.text段,类型是程序段(PROGBITS包括代码段和数据段),加载地址为0,在文件中的偏移量是0×34,长度为0x5b,项的长度为0(表示该段不包含固定大小的项),标志AX表示该段要分配空间及可以被执行,链接信息的两个0没有意义(不是与链接相关的段),最后的4表示段地址对齐为2^4=16字节。
重定位表:链接器在处理目标文件的时候,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些绝对地址的引用位置,这些重定位信息记录在重定位表里。每个需要重定位的代码段或数据段都会有一个相应的重定位表,如.rel.text是针对”.text”段的重定位表,”.rel.data”是针对”.data”段的重定位表。
字符串表:ELF文件中用到很多字符串,如段名、变量名,因为字符串的长度不固定,用固定的结构来表示它比较困难,一般把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在ELF中以段的形式保存,常见的有.strtab(字符串表,String Table)和.shstrtab(段表字符串表,Section Header String Table),前者保存如符号名字等普通字符串,后者保存如段名等段表中用到的字符串。
符号表:函数和变量统称为符号,其名称称为符号名。链接过程中关键的部分就是符号的管理,每一个目标文件都会有一个相应的符号表,记录了目标文件用到的所有符号,每个符号有一个对应的符号值,一般为符号的地址。一个样例如下:
Num | Value | Size | Type | Bind | Vis | Ndx | Name |
13 | 0000001b | 64 | FUNC | GLOBAL | DEFAULT | 1 | main |
其意义如下:下标为13的符号的符号值为0x1b,大小为64字节,类型为函数,绑定信息为全局符号,VIS可以忽略,Ndx表示其所在段的下标为1(通过上一个样例可知,该段为.text段),符号名称为main。如果Ndx下标一项为UND(undefine),则表示该符号在其他模块定义,以后需要重定位。
调试信息:目标文件里可能保存有调试信息,如在GCC编译时加上”-g”参数,会生成许多以”.debug”开头的段。
2.3 链接
链接,是收集和组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。一般分为两步:1.空间与地址分配,2.符号解析与重定位。一般有两种类型,一是静态链接,二是动态链接。
- 空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,连接器将能获得所有输入如目标文件的 段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
- 符号解析与重定位
使用上面一步中收集的所有信息,读取输入文件中的段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上,这一步是链接过程的核心,特别是重定位过程。
使用静态链接的好处是,依赖的动态链接库较少(这句话有点绕),对动态链接库的版本更新不会很敏感,具有较好的兼容性;不好地方主要是生成的程序比较大,占用资源多。使用动态链接的好处是生成的程序小,占用资源少。动态链接分为可执行程序装载时动态链接和运行时动态链接。
当用户启动一个应用程序时,它们就会调用一个可执行和链接格式映像。Linux 中 ELF 支持两种类型的库:静态库包含在编译时静态绑定到一个程序的函数。动态库则是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的。
2.4 代码分析
sys_execve内部会解析可执行文件格式。代码在内核中/linux-4.15.0/fs/exec.c中。sys_execve调用顺序:do_execve -> do_execve_common -> exec_binprm
do_execve 函数
1 int do_execve(struct filename *filename,
2 const char __user *const __user *__argv,
3 const char __user *const __user *__envp)
4 {
5 struct user_arg_ptr argv = { .ptr.native = __argv };
6 struct user_arg_ptr envp = { .ptr.native = __envp };
7 return do_execve_common(filename, argv, envp);
8 }
do_execve_common 函数
1 /*
2 * sys_execve() executes a new program.
3 */
4 static int do_execveat_common(int fd, struct filename *filename,
5 struct user_arg_ptr argv,
6 struct user_arg_ptr envp,
7 int flags)
8 {
9 char *pathbuf = NULL;
10 struct linux_binprm *bprm;
11 struct file *file;
12 struct files_struct *displaced;
13 int retval;
14
15 if (IS_ERR(filename))
16 return PTR_ERR(filename);
17
18 /*
19 * We move the actual failure in case of RLIMIT_NPROC excess from
20 * set*uid() to execve() because too many poorly written programs
21 * don't check setuid() return code. Here we additionally recheck
22 * whether NPROC limit is still exceeded.
23 */
24 if ((current->flags & PF_NPROC_EXCEEDED) &&
25 atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
26 retval = -EAGAIN;
27 goto out_ret;
28 }
29
30 /* We're below the limit (still or again), so we don't want to make
31 * further execve() calls fail. */
32 current->flags &= ~PF_NPROC_EXCEEDED;
33
34 retval = unshare_files(&displaced);
35 if (retval)
36 goto out_ret;
37
38 retval = -ENOMEM;
39 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
40 if (!bprm)
41 goto out_files;
42
43 retval = prepare_bprm_creds(bprm);
44 if (retval)
45 goto out_free;
46
47 check_unsafe_exec(bprm);
48 current->in_execve = 1;
49
50 file = do_open_execat(fd, filename, flags);
51 retval = PTR_ERR(file);
52 if (IS_ERR(file))
53 goto out_unmark;
54
55 sched_exec();
56
57 bprm->file = file;
58 if (fd == AT_FDCWD || filename->name[0] == '/') {
59 bprm->filename = filename->name;
60 } else {
61 if (filename->name[0] == '