【Linux】动/静态库
目录
1. 整体学习思维导图
2. 库是什么
2.1 库的概念
2.2 库的后缀区分
3. 库的制作
3.1 静态库
3.1.1 使用.o文件连接生成.exe文件
3.1.2 生成一个静态库
3.1.3 使用一个静态库
3.2 动态库
3.2.1 生成一个动态库
3.2.2 使用一个动态库
总结:
4. ELF文件
4.1 什么是ELF文件
4.1.1 以下文件格式为ELF:
4.1.1 什么是ELF文件:
4.2 静态链接,研究.o文件如何链接
4.3 ELF文件的加载,ELF -> 进程
4.3.1 我们知道只有当一个可执行程序执行时才会加载到内存,如果加载到内存他是否存在地址?
4.3.2 进程与ELF的联系
4.4 动态库如何与我们进程关联
4.5 动态库如何加载
4.5.1 全局偏移量表GOT(globaloffsettable)
4.5.2 库与库之间的调用
4.5.3 PLT
1. 整体学习思维导图
2. 库是什么
2.1 库的概念
库是一些函数实现的二进制代码,他们是可以直接执行,库的内容是一些我们经常需要使用的一些方法如:printf/scanf这类函数,我们要加快开发速率不可能再去实现一遍,我们一般都是使用c库封装好的,包上头文件直接使用!
2.2 库的后缀区分
-
对于Linux系统
-
静态库
.a
-
动态库
.so
-
-
对于Windows系统
-
静态库
.lib
-
动态库
.dll
-
3. 库的制作
总体概念(无论动静态库)
-
动/静态库不要实现
main
函数 -
头文件
.h
是对源文件实现方法的说明书 -
所有的库(动/静)都是源文件的,以
.o
后缀结尾
3.1 静态库
-
静态库的本质就是对
.o
文件进行一个打包,静态库.a
->归档文件->使用时不需要进行解包->gcc/g++直接使用即可!
3.1.1 使用.o文件
连接生成.exe文件
场景一:ouyang同学实现了MyFile/MyStrlen
,niuma同学不想实现想要直接使用,ouyang同学给他传来.o
文件,他连接自己实现的main
函数调用即可!
-
ouyang同学生成
.o
文件拷贝给niuma同学,使用说明书.h
文件也需要拷贝过去
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ cd ../niuma/
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 20
-rw-rw-r-- 1 ouyang ouyang 517 Mar 3 16:11 main.c
-rw-rw-r-- 1 ouyang ouyang 602 Mar 3 16:14 MyFile.h
-rw-rw-r-- 1 ouyang ouyang 3400 Mar 3 16:14 MyFile.o
-rw-rw-r-- 1 ouyang ouyang 49 Mar 3 16:14 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang 1272 Mar 3 16:14 MyStrlen.o
-
niuma同学生成自己的
main.o
文件进行连接生成可执行程序.exe
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc *.o
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 44
-rwxrwxr-x 1 ouyang ouyang 13352 Mar 3 16:30 a.out
-rw-rw-r-- 1 ouyang ouyang 514 Mar 3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang 514 Mar 3 16:22 main_cp.c
-rw-rw-r-- 1 ouyang ouyang 2248 Mar 3 16:30 main.o
-rw-rw-r-- 1 ouyang ouyang 602 Mar 3 16:14 MyFile.h
-rw-rw-r-- 1 ouyang ouyang 3400 Mar 3 16:14 MyFile.o
-rw-rw-r-- 1 ouyang ouyang 49 Mar 3 16:14 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang 1272 Mar 3 16:14 MyStrlen.o
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./a.out
len = 14
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat log.txt
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
3.1.2 生成一个静态库
ar -rc mylibc.a *.o
# rc --> replace and create 归档所有.o文件,如果存在就覆盖,不存在就创建
# mylibc.a 静态库的名称 使用时需要去掉lib和.a
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 12
-rw-rw-r-- 1 ouyang ouyang 514 Mar 3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang 2009 Mar 3 16:45 stdc.tgz
3.1.3 使用一个静态库
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main
len = 14
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
log.txt main main.c makefile stdc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat log.txt
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
我们会发现我们在使用gcc编译时,带上了两个选项和路径,这是为什么呢?
-
-I
,-L
-lmylibc
我们试一试不带这两个选项会发生什么?
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c
main.c:4:20: fatal error: MyFile.h: No such file or directory
#include "MyFile.h"
^
compilation terminated.
我们发现main.c文件找不到对应的头文件,这是因为头文件和main.c不在同一路径中!而我们系统默认去自己的头文件库中找不存在就会报错,因此-I
是告诉gcc(默认在系统和当前目录下找寻)去哪个路径下找寻头文件。
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/
/tmp/ccCjcIhR.o: In function `main':
main.c:(.text+0x13): undefined reference to `Myfopen'
main.c:(.text+0x69): undefined reference to `Myfwrite'
main.c:(.text+0x75): undefined reference to `Myflush'
main.c:(.text+0x8e): undefined reference to `Myfclose'
main.c:(.text+0x9a): undefined reference to `MyStrlen'
collect2: error: ld returned 1 exit status
我们现在的问题是找不到头文件对应实现的方法了,我们同样的需要告诉gcc我们要使用哪个库进行连接。
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -lmylibc
/usr/bin/ld: cannot find -lmylibc
collect2: error: ld returned 1 exit status
我们告诉了gcc找寻mylibc库进行连接,但是系统默认是在/user/bin/ld
寻找,同样的我们的库并没实现在其中我们需要告诉我们库对应的位置,使用-L
+路径。
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
linux-vdso.so.1 => (0x00007fff5d559000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa970340000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa97070e000)
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
main main.c makefile stdc
我们通过以上过程可以发现,库需要安装到系统中才方便使用,而库安装-->本质就是拷贝对应的系统之中。
我们也发现ldd
我们的可执行程序并没有我们的静态库的链接,这是因为我们的静态库已经拷贝至可执行程序中了,这也是为什么静态链接的可执行程序空间大的原因,包括即使我们删除了之前的静态库,程序依旧可以执行的原因!
3.2 动态库
3.2.1 生成一个动态库
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ make
gcc -fPIC -c MyFile.c
gcc -fPIC -c MyStrlen.c
gcc -o libmylibc.so MyFile.o MyStrlen.o -shared
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ ll
total 48
-rwxrwxr-x 1 ouyang ouyang 12864 Mar 3 19:33 libmylibc.so
-rw-rw-r-- 1 ouyang ouyang 259 Mar 3 19:33 makefile
-rw-rw-r-- 1 ouyang ouyang 245 Mar 3 17:22 makefile_static
-rw-rw-r-- 1 ouyang ouyang 2084 Mar 3 15:56 MyFile.c
-rw-rw-r-- 1 ouyang ouyang 602 Mar 3 15:56 MyFile.h
-rw-rw-r-- 1 ouyang ouyang 3456 Mar 3 19:33 MyFile.o
-rw-rw-r-- 1 ouyang ouyang 129 Mar 3 16:00 MyStrlen.c
-rw-rw-r-- 1 ouyang ouyang 49 Mar 3 16:00 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang 1272 Mar 3 19:33 MyStrlen.o
3.2.2 使用一个动态库
我们按照使用静态库的方式生成一个可执行程序
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 24
-rwxrwxr-x 1 ouyang ouyang 8800 Mar 3 19:40 main
-rw-rw-r-- 1 ouyang ouyang 514 Mar 3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang 94 Mar 3 19:39 makefile
drwxrwxr-x 4 ouyang ouyang 4096 Mar 3 19:34 stdc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main
./main: error while loading shared libraries: libmylibc.so: cannot open shared object file: No such file or directory
我们按照我们之前的方式成功生成可执行程序,但是却不可以执行,查看main发现没有找到我们自己封装的动态库,这是为什么呢?因为我们前面的指令都是告诉gcc我们的库在什么位置,我们现在的命令是执行可执行程序,我们并没有告诉我们的可执行程序我们的动态库位置,因此导致了动态库找不到的问题!
-
解决方案:
-
拷贝至系统:我们发现我们并没有告诉main可执行程序我们c标准库的位置,它依然找到,说明默认会去系统查找,我们只需要拷贝至系统就可以链接了
-
拷贝 .so 文件到系统共享库路径下,⼀般指 /usr/lib、/usr/local/lib、/lib64
-
-
建立软连接:向系统共享库路径下建立同名软连接,连接指向我们实现的动态库
-
更改环境变量:
LD_LIBRARY_PATH
(没有可以自己创建一个)
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ echo $LD_LIBRARY_PATH
:/home/ouyang/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ pwd
/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ echo $LD_LIBRARY_PATH
:/home/ouyang/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ cd ..
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ stdc]$ cd ..
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 24
-rwxrwxr-x 1 ouyang ouyang 8800 Mar 3 19:40 main
-rw-rw-r-- 1 ouyang ouyang 514 Mar 3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang 94 Mar 3 19:39 makefile
drwxrwxr-x 4 ouyang ouyang 4096 Mar 3 19:34 stdc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
linux-vdso.so.1 => (0x00007fff30141000)
libmylibc.so => /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib/libmylibc.so (0x00007fa667a86000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa6676b8000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa667c89000)
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main
len = 14
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
log.txt main main.c makefile stdc
当然我们重启之后这个临时配置的环境变量就会消失,要永久保存需要系统配置。
4. ldconfig方案:配置/etc/ld.so.conf.d/
, ldconfig
更新生效。
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls /etc/ld.so.conf.d/
kernel-3.10.0-957.21.3.el7.x86_64.conf kernel-3.10.0-957.el7.x86_64.conf mysql-x86_64.conf
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ sudo touch /etc/ld.so.conf.d/mylibso.conf
[sudo] password for ouyang:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls /etc/ld.so.conf.d/
kernel-3.10.0-957.21.3.el7.x86_64.conf kernel-3.10.0-957.el7.x86_64.conf mylibso.conf mysql-x86_64.conf
这个操作需要管路员才能操作!
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat /etc/ld.so.conf.d/mylibso.conf
/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
linux-vdso.so.1 => (0x00007fff6c2e8000)
libmylibc.so => /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib/libmylibc.so (0x00007f3bb5efe000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3bb5b30000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3bb6101000)
总结:
-
gcc/g++默认会去使用动态库(动静态库同时存在时)
-
如果动静态库同时存在非要静态链接需要带上
-static
选项 -
如只存在静态库,无论加不加
-static
,都使用静态链接
4. ELF文件
4.1 什么是ELF文件
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file main.exe
main.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=f509ef97646a4c0b361e15ac85eda3db9048a110, not stripped
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file code.o
code.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ file libmylibc.so
libmylibc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=6c459975831b4db7227b252daf3b14bdb33d70e2, not stripped
通过以上代码我们会发现一个问题,可执行文件.exe
和重定位目标文件.o
文件类型都是ELF
类型的文件,那么哪些文件是ELF
文件,什么是ELF
文件?
4.1.1 以下文件格式为ELF
:
-
可重定向目标文件:以
.o
为结尾的文件 -
可执行程序文件:可以执行的文件
-
共享目标文件:以
.so
结尾的文件,即动态库文件
4.1.1 什么是ELF
文件:
我们先了解ELF
文件的组成部分:
我们之前所认知的可执行文件.exe
我们简单的说是由代码+数据
组成!其实可执行文件作为ELF格式文件,是由多个.o
文件链接形成的,由于文件类型的相同其中少不了把多个文件合并成一个文件的过程!
-
多个section(节)会合并成segment(段)
-
那么将节合并成段需要一个合并规则,规范合并哪些,怎么合并需要有一个准则标准,这个合并的原则存在于
Program Header Table
之中
我们拿两段简单代码来观察合并之前的准备,其中我们需要用到以下命令和代码:
-
代码:
/* main.c */ #include <stdio.h> extern void Run(); int main() { Run(); printf("I am main.c\n"); return 0; }
/* code.c */ #include <stdio.h> void Run() { printf("Runing......!\n"); }
-
readelf -S _ELF文件_
-- 读取section Header Table
(这是一个数组,其中包含对section的描述) -
readelf -l _ELF文件_
-- 读取Program Headers
section Header Table
中描述了各个节的信息。
红框中的描述是权限R/W/E。
从图中我们可以得知那些数据节(section)需要合并在一起成为一个段(segment)。
3. 为什么要将section
合并成一个segment
?
这个问题我们需要先引进文件的加载,我们知道在Ext文件系统之中
我们为了效率会一次去访问八个扇区也就是一个块(4KB大小), 而内存的每一次申请空间为了能够方便交互也是4KB大小,也就是说我们存储一个数据会出现碎片化,存储不足4KB的情况,这个空间我们叫做页面,合并可以减少页面的碎片化,如.text
部分4097字节这就需要2个页面存储,此时.init
部分的大小为256字节需要一个页面,一旦合并只需要2个页面即可,提高了工具利用率。
4. section Header Table
(这是一个数组,其中包含对section的描述)有什么用?
这个节表头看起来像节的管理者,他可以清楚的告诉我们节的信息。
-
链接视角:我们在链接
.o
文件需要合并section
为一个segment
,那么那些需要合并在一起,那些不合并在一起,section Header Table
就是告诉静态链接节的信息位置,让链接去区分合并! -
加载视角:我们观察第二张图可以发现有描述权限的内容,这一点很关键,我们知道我们平时操作文件时需要对应的权限,这个权限是加载到内存的,但是系统怎么知道那些内容可读还是可写?这就
section Header Table
的作用了,他会告诉操作系统哪些该加载到什么地方,完成文件的初始化。
5. Program Header Table
的作用?
在了解作用之前我们需要知道使用什么命令去查看这个表头:
readelf -h _ELF文件_ # 查看 Program Header Table
-
Entry point address
:描述程序的入口地址,这个有什么用呢? -
Start of program headers
:program headers
的起始位置 -
Start of section headers
:section headers
的起始位置
我们发现program headers Table
保存的是整个ELF文件各个部分的分布信息位置,它的主要目的是定位文件的其他部分。
4.2 静态链接,研究.o文件如何链接
研究链接之前我们需要得到得到链接前文件中的内容有什么:
# 将目标文件进行反汇编
objdump -d XXX.o > XXX.s
以上我们发现.o
文件中反汇编后callq
去调用的函数都没有地址?地址为什么都是0?这是因为在编译时,编译器并不知道对应的函数地址在哪,只能先填充0来表示,等到后面链接时在进行地址重定位!这就是为什么我们称.o
文件是可重定位目标文件
。
readelf -s XXX.o # 简略读取
ouyang@iZ2ze0j6dd76e0o9qypo2rZ:~/linux_-git_-warehouse/dir_2025_3_6$ readelf -s main.exe
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000318 0 SECTION LOCAL DEFAULT 1
2: 0000000000000338 0 SECTION LOCAL DEFAULT 2
3: 0000000000000358 0 SECTION LOCAL DEFAULT 3
4: 000000000000037c 0 SECTION LOCAL DEFAULT 4
5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5
6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6
7: 0000000000000470 0 SECTION LOCAL DEFAULT 7
8: 00000000000004f2 0 SECTION LOCAL DEFAULT 8
9: 0000000000000500 0 SECTION LOCAL DEFAULT 9
10: 0000000000000520 0 SECTION LOCAL DEFAULT 10
11: 00000000000005e0 0 SECTION LOCAL DEFAULT 11
12: 0000000000001000 0 SECTION LOCAL DEFAULT 12
13: 0000000000001020 0 SECTION LOCAL DEFAULT 13
14: 0000000000001040 0 SECTION LOCAL DEFAULT 14
15: 0000000000001050 0 SECTION LOCAL DEFAULT 15
16: 0000000000001060 0 SECTION LOCAL DEFAULT 16
17: 0000000000001208 0 SECTION LOCAL DEFAULT 17
18: 0000000000002000 0 SECTION LOCAL DEFAULT 18
19: 0000000000002020 0 SECTION LOCAL DEFAULT 19
20: 0000000000002070 0 SECTION LOCAL DEFAULT 20
21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21
22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22
23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23
24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24
25: 0000000000004000 0 SECTION LOCAL DEFAULT 25
26: 0000000000004010 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
29: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
30: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones
31: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
32: 0000000000004010 1 OBJECT LOCAL DEFAULT 26 completed.8061
33: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin
34: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy
35: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
39: 0000000000002194 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS
41: 0000000000003dc0 0 NOTYPE LOCAL DEFAULT 21 __init_array_end
42: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
43: 0000000000003db8 0 NOTYPE LOCAL DEFAULT 21 __init_array_start
44: 0000000000002020 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
45: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
46: 0000000000001000 0 FUNC LOCAL DEFAULT 12 _init
47: 0000000000001200 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini
48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
49: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start
50: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
51: 000000000000116e 23 FUNC GLOBAL DEFAULT 16 Run
52: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 25 _edata
53: 0000000000001208 0 FUNC GLOBAL HIDDEN 17 _fini
54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
55: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
56: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
57: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
58: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
59: 0000000000001190 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init
60: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
61: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start
62: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
63: 0000000000001149 37 FUNC GLOBAL DEFAULT 16 main
64: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
66: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
其中UND(表示未定义),我们看图会发现两个.o
对于对于调用的函数都是未定义,但是我们查看main.exe
会发现对于缺失的函数地址已经进行重定位了,静态链接->.o合并,地址修正!
4.3 ELF文件的加载,ELF -> 进程
4.3.1 我们知道只有当一个可执行程序执行时才会加载到内存,如果加载到内存他是否存在地址?
-
加载模式
过去我们对于一个可执行程序编址偏向于逻辑地址,访问时需要起始地址加偏移量,当代计算机工作的时候,都采用"平坦模式"进行工作。所以也要求ELF对自己的代码和数据进行统⼀编址,从反汇编的文件中我们可以看出这一点:
main.exe: file format elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub $0x8,%rsp
1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__>
100f: 48 85 c0 test %rax,%rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 callq *%rax
1016: 48 83 c4 08 add $0x8,%rsp
101a: c3 retq
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 9a 2f 00 00 pushq 0x2f9a(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 9b 2f 00 00 bnd jmpq *0x2f9b(%rip) # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 pushq $0x0
1039: f2 e9 e1 ff ff ff bnd jmpq 1020 <.plt>
103f: 90 nop
.............................................
4.3.2 进程与ELF的联系
-
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]等范围数据,另外在用详细地址,填充页表
-
CPU会通过
Entry point address
字段获取程序入口,进入CPU的地址是虚拟地址,EIP
获取入口后得到虚拟地址,将虚拟地址交给CR3
,通过查看页表的映射关系得到物理地址进行访问!
所以:虚拟地址机制,不光光OS要支持,编译器也要支持!
4.4 动态库如何与我们进程关联
-
动态库需要被进程看见:动态库需要映射到进程的地址空间上!
-
被进程调用:在进程的地址空间调转!
整个过程:动态库加载-->物理内存-->页表映射(不会重复加载,多个进程也可以使用)-->共享库
4.5 动态库如何加载
-
动态链接不同于静态链接,他将链接的整个过程推迟到了程序加载时候进行!
我们的可执行程序编译时:调用动态库中的函数方法时会先填充一个0地址,等待动态库加载到内存中,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
-
在我们的程序开始执行时我们第一个入口并不是main函数,程序的入口点是
_start
,这是⼀个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。
_start
的作用:
-
设置堆栈:创建一个初始的堆栈环境
-
初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段
-
动态链接:这是关键的⼀步,
_start
函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(sharedlibraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
动态链接器:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
linux-vdso.so.1 => (0x00007fff5d559000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa970340000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa97070e000)
这个动态链接器会帮我们在程序运行时加载动态库,查找动态库会去对应的环境变量(LD_LIBRARY_PATH
)或者配置文件(/etc/ld.so.conf.d/
)中查找,但是每次查找都会消耗一定的资源时间,所以其内部还存在一个缓存文件用于保存已知的动态库相关信息(名称+路径等等),动态链接器会优先搜寻这个缓存文件!
动态库也是一个ELF
文件,加载到内存时也是平坦模式,然后通过页表映射关系映射到对应进程的共享区部分,但是我们知道此时进程的函数代码都保存在代码区,而代码区只是可读权限,那么怎么去修正我们调用函数的地址呢?
4.5.1 全局偏移量表GOT(globaloffsettable)
为了解决上面代码区不能修改的问题,我们在代码区预留了一个用来存放函数的跳转地址,它也被叫做全局偏移表GOT。
-
.got: 加载重定向表,
GOT
表对于每个进程的映射部分都是不一样的,因此每个进程都拥有一份独立的GOT
表,进程之间的GOT
表不可以共享! -
每次调用函数时,都会先进行查表跳转到对应的函数地址!
-
这种方式实现的动态链接就被叫做
PIC
地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC = 相对编址 + GOT
。
4.5.2 库与库之间的调用
我们平时不止有进程对库进行调用,有时候还会出现库和库之间的调用,那么怎么理解库与库之间的关联呢?其实库中也存在.got
,和可执行程序一样!这也是为什么可执行和库文件都是ELF文件!
4.5.3 PLT
-
由于动态链接在程序加载
(每次加载动态库都会加载到内存的不同地址位置)
的时候需要对大量函数进行重定位,这⼀步显然是非常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,比如延迟绑定,或者也叫PLT(Procedure Linkage Table)->过程链接表
。与其在程序⼀开始就对所有函数进行重定位,不如将这个过程推迟到函数第⼀次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间⼀次都不会被使用到。 -
思路是:GOT中的跳转地址默认会指向⼀段辅助代码,它也被叫做
桩代码/stup
。在我们第⼀次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。