9 动静态库
9.1 为什么需要库文件&什么是库文件
-
假设你是一个大型项目,一个偏向接口编写的维护者,我需要向开发者提供接口,比方说
opencv,DirectX12,它们本身并不是一个大众可用的项目,而是专门编给其他程序员的一套接口,程序员使用这一套接口就可以编写游戏引擎,游戏等等内容 -
我们如果想提供一套接口给别人,我们会做什么?
| 情景 | 形式 | 缺点 |
|---|---|---|
| 情景1 | 提供原始的.c和.h文件 | 大型项目接口众多,文件也相对较多,甚至高达上千个文件,并且直接给源代码给别人,是非常不安全的事情 |
| 情景2 | 提供由.c编译形成的.o文件和.h文件,.o文件是二进制机器码,使得源码不可见 | 但依旧不能解决文件众多的问题,链接时我们需要写的命令太长了,我们需要更加方便的方式 |
| 情景3 | 提供一个库文件和一堆.h打包成的压缩包,库文件由全部.o文件链接而成,并且编译器可以直接使用库文件而不用解包 | 自此解决了以上两个痛点 |
-
库文件是由
.o文件链接形成的,并且编译器可以直接使用库而不用先解包库 -
所以本质上,库文件就是一个
.o的集合,或者说是一堆接口的集合,因此,库文件一般不包含main()函数,如果一个库包含了main(),这个库就不能被使用,因为main()已经存在了,用户此时不能使用main()了 -
而库文件也分为"静态库"和"动态库",分别使用后缀
.a和.so(即achieve文件和shared object文件)具体的使用我们后面还会再谈到
9.2 如何制作一个静态库文件
- 我们来看看库文件的制作方式(这里使用
.c文件制作库)
$ ls
mystdio.c mystdio.h mystring.c mystring.h
$ ar -rc libmyc.a *.c
$ ls
libmyc.a mystdio.c mystdio.h mystring.c mystring.h
$ ll
total 20
-rw-rw-r-- 1 oldking oldking 3658 Mar 13 15:52 libmyc.a
-rw-rw-r-- 1 oldking oldking 3361 Mar 12 20:53 mystdio.c
-rw-rw-r-- 1 oldking oldking 548 Mar 12 20:53 mystdio.h
-rw-rw-r-- 1 oldking oldking 167 Mar 12 22:24 mystring.c
-rw-rw-r-- 1 oldking oldking 47 Mar 12 22:24 mystring.h
-
注意:使用
.c制作库文件可能会导致有些语法错误或者一些兼容性错误无法被检查出来,所以尽量不要这样使用 -
.o文件一样可以制作库,并且使用.o文件也是更加规范的方式
$ gcc -c *.c
$ ls
mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
$ ar -rc libmyc.a *.o
$ ls
libmyc.a mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
-
即命令
ar就是库文件的制作命令,或者说是gru归档工具,代表archive,即"归档" -
选项
-rc代表replace和create,replace的意思是如果源文件有变动,就对库里的变动部分做替换,而create的意思是创建文件,即如果库里没有该文件,此文件就会被添加进库
$ ar [options] [dst file] [src file]
- 关于库文件的命名规范:
- 库文件的命名一定是固定的,是有一定规范的,不能乱更改
- 库文件的命名由 "前缀":
lib"文件名":filename"后缀":.a/.so组成 - 只有中间的文件名我们可以随意更改,前缀和后缀都是不能更改的,否则在使用库文件的时候可能会导致库文件无法被识别
9.3 如何使用一个静态库文件
//用户需要调用我们写的库接口
#include<stdio.h>
#include"mystdio.h"
#include<string.h>
int main()
{
MYFILE* pfile = myFopen("log.txt", "w");
//printf("%p", pfile);
const char* str = "oldking is H\n";
int count = 100;
while(count--)
{
//int sum = myFwrite(str, strlen(str), 1, pfile);
myFwrite(str, strlen(str), 1, pfile);
}
myFflush(pfile);
//printf("%d", sum);
//printf("%d\n", myFclose(pfile));
//printf("%d\n", pfile->fileno);
//printf("")
//printf("%d", (int)myFwrite(str, strlen(str), 1, pfile));
return 0;
}
$ ls
C_file libc log.txt Makefile usercode.c
$ tree libc/
libc/
├── include
│ ├── mystdio.h
│ └── mystring.h
└── lib
└── libmyc.a
2 directories, 3 files
$ gcc usercode.c -I ./libc/include/ -L ./libc/lib/ -l myc -o test
$ ll
total 36
drwxrwxr-x 2 oldking oldking 4096 Mar 13 16:05 C_file
drwxrwxr-x 4 oldking oldking 4096 Mar 13 12:48 libc
-rw-rw-r-- 1 oldking oldking 1300 Mar 13 16:30 log.txt
-rw-rw-r-- 1 oldking oldking 69 Mar 13 12:21 Makefile
-rwxrwxr-x 1 oldking oldking 13152 Mar 13 16:33 test
-rw-rw-r-- 1 oldking oldking 575 Mar 13 16:28 usercode.c
$ ./test
$ ls
C_file libc log.txt Makefile test usercode.c
$ cat log.txt
oldking is H
oldking is H
oldking is H
oldking is H
# ...
- 使用库文件相比正常编译链接,增加了几个选项
gcc usercode.c -I [head file path] -L [lib file path] -l [lib name] -o test
# -I: 表示头文件路径,后面跟头文件路径
# -L: 表示库文件路径,后面跟库文件路径
# -l:表示库文件名,后面跟库文件名
- 如果你的头文件和
usercode.c在同一目录,那么就可以不用加关于头文件目录的选项和路径
9.3.1 库文件安装(拷贝)
-
当然,如果每次编译都要加库的路径和头文件路径啥的,未免有点太麻烦了
-
值得一提的是,我们从很早就学习了C语言,我们也知道我们一定使用了C语言的库文件,所以意味着,系统中一定有某个目录存放这些库文件,并且这个目录会作为
ar默认寻找库文件的目录,以达到每次都不需要加目录的效果 -
头文件也是如此
-
事实上,安装库文件的目录是比较多的,目录设计上为它们分了类
-
该表来自
Deepseek:
| 目录 | 用途 |
|---|---|
/lib | 存放系统启动和基本命令依赖的核心动态库(如glibc) |
/lib64 | 在 64 位系统中存放 64 位核心库(某些发行版如 CentOS/RHEL 使用此目录) |
/usr/lib | 用户安装的非核心动态库和静态库(如第三方应用程序的依赖库) |
/usr/lib64 | 64 位系统的非核心库(某些发行版使用) |
/usr/local/lib | 用户手动编译安装的库(通常通过 make install 默认安装到此目录) |
- 头文件也是如此
- 该表依然来自
Deepseek:
| 目录 | 用途 |
|---|---|
/usr/include | 系统或软件包管理器安装的公共头文件(如标准库,第三方库的头文件) |
/usr/local/include | 用户手动编译安装的软件的头文件(如通过make install默认安装到此目录) |
/usr/include/<子目录> | 某些库的头文件会按名称归类到子目录(例如/usr/include/openssl) |
- 我们可以手动拷贝库进去试试
$ tree libc
libc
├── include
│ ├── mystdio.h
│ └── mystring.h
└── lib
└── libmyc.a
2 directories, 3 files
$ sudo cp ./libc/lib/libmyc.a /usr/local/lib
[sudo] password for oldking:
$ ls
C_file libc log.txt Makefile test usercode.c
$ ls /usr/local/lib
libmyc.a
$ rm test
$ ls /usr
bin etc games include lib lib64 libexec local sbin share src tmp
$ sudo cp libc/include/*.h /usr/include/
$ gcc usercode.c -l myc -o test
$ ls
C_file libc log.txt Makefile test usercode.c
$ rm log.txt
$ ./test
$ ls
C_file libc log.txt Makefile test usercode.c
$ cat log.txt
oldking is H
oldking is H
oldking is H
# ...
- 下一小节我们会尝试写一个
Makefile用于打包库文件,届时,对于用户的使用方面会更加方便一些
9.4 库文件打包
- 我们可以简单写一个
Makefile用于快捷打包库文件
AFILE=libmyc.a
CC=gcc
AR=ar
FLAGO=-o
FLAGC=-c
ARFLAG=-rc
RM=rm -rf
$(AFILE):mystdio.o mystring.o
$(AR) $(ARFLAG) $@ $^
mystdio.o:mystdio.c
$(CC) $(FLAGC) mystdio.c
mystring.o:mystring.c
$(CC) $(FLAGC) mystring.c
.PHONY:output
output:
mkdir -p lib/mylib
mkdir -p lib/include
cp *.h ./lib/include
cp $(AFILE) ./lib/mylib
.PHONY:clean
clean:
$(RM) mystdio.o mystring.o $(SO) $(AFILE) ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
Makefile mystdio.c mystdio.h mystring.c mystring.h
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make
gcc -c mystdio.c
gcc -c mystring.c
ar -rc libmyc.a mystdio.o mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
libmyc.a Makefile mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make output
mkdir -p lib/mylib
mkdir -p lib/include
cp *.h ./lib/include
cp libmyc.a ./lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
lib libmyc.a Makefile mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ tree lib
lib
├── include
│ ├── mystdio.h
│ └── mystring.h
└── mylib
└── libmyc.a
2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make clean
rm -rf mystdio.o mystring.o libmyc.a ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
Makefile mystdio.c mystdio.h mystring.c mystring.h
9.5 动态库文件的制作,打包,使用
9.5.1 动态库文件的制作和打包
- 动态库文件的制作不需要使用其他工具就可以,
gcc就自带这个功能,同时因为涉及到"地址无关码"(或称"位置无关码")的问题,所以我们编译形成.o文件时,需要加一个选项,"地址无关码"我们以后会再提的,现在先记住
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc -fPIC -c mystdio.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc -fPIC -c mystring.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc mystring.o mystdio.o -o libmyc.so -shared
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
libmyc.so Makefile mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
- 当然,我们依旧可以搞一个
Makefile出来以节约时间
libmyc.so:mystring.o mystdio.o
gcc mystring.o mystdio.o -o libmyc.so -shared
mystring.o:mystring.c
gcc -c -fPIC mystring.c
mystdio.o:mystdio.c
gcc -c -fPIC mystdio.c
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp ./*.h ./lib/include
cp ./*.so ./lib/mylib
.PHONY:clean
clean:
rm -rf *.o *.so ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile mystdio.c mystdio.h mystring.c mystring.h
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make
gcc -c -fPIC mystring.c
gcc -c -fPIC mystdio.c
gcc mystring.o mystdio.o -o libmyc.so -shared
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make output
mkdir -p lib/include
mkdir -p lib/mylib
cp ./*.h ./lib/include
cp ./*.so ./lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
lib libmyc.so Makefile mystdio.c mystdio.h mystdio.o mystring.c mystring.h mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ tree lib
lib
├── include
│ ├── mystdio.h
│ └── mystring.h
└── mylib
└── libmyc.so
2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make clean
rm -rf *.o *.so ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile mystdio.c mystdio.h mystring.c mystring.h
9.5.2 动态库文件的使用
- 动态库和
.c的编译指令和静态库一样,就不多赘述
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib lib libc log.txt Makefile shared_object usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ tree lib
lib
├── include
│ ├── mystdio.h
│ └── mystring.h
└── mylib
└── libmyc.so
2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ gcc usercode.c -o test -I ./lib/include/ -L ./lib/mylib/ -l myc
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib lib libc log.txt Makefile shared_object test usercode.c
- 但此时我们会发现一个问题,这个编译的可执行程序怎么样都运行不起来
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
./test: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory
- 用
ldd检查后发现,这个可执行程序找不到对应的库文件,这是为什么呢????
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
linux-vdso.so.1 => (0x00007ffcf63e4000)
libmyc.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f2af26a1000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2af2a6f000)
-
记得吗,静态库本质上是把库的内容和源文件编译成
.o的内容打包在了一起,等到程序运行起来后一并加载到内存 -
动态库的内容其实和静态库没有本质区别,都是打包多个
.o成一个文件,但动态库和进程的内存机制不是这样,动态库是等程序运行之后一并将库分开加载到内存,以保证动态库允许被多个进程使用,而事实上,每个进程都认为这个动态库是自己独占的,其实就是让多个进程的虚拟地址映射到相同的物理地址,所以即便动态库在物理地址中只有一个,每个进程也能认为这个库文件是自己独占的 -
所以其实本质上,动态库和静态库的区别并不是内容的差别,相反,他俩内容上没什么差别,但使用方式,或者说对于系统而言它的使用方式,有很大差别,在物理内存中,动态库仅存在一份,而静态库将可能会存在多个,而对于进程而言,他俩却也没什么差别,因为进程只能使用虚拟地址空间以保证进程独立性
-
所以,一旦我们需要加载某个动态库进内存,首先我们得先找到它,或者说,系统得找到它,我们就需要一个固定的目录,用于系统默认加载动态库的目录
-
根本上,我们需要一个专门的目录保存动态库而不需要保存静态库的原因是:找到一个可执行程序的静态库太简单了(事实上系统不需要找),因为其静态库在编译过程中就已经拷贝在可执行程序中了,已经成为了可执行程序的一部分了,但因为可执行程序不包含动态库的内容,所以我们就需要在打开文件时临时找动态库(在内存还没有加载进当前动态库的时候)
-
或者说,操作系统压根就不知道静态库是什么,也不在乎静态库是啥,在操作系统看来,这里就只有一个可执行程序,静态库是什么??我(操作系统)不知道!!!不在乎!!!
-
所以!对于动态库而言!一定会有安装这个步骤!或者说如果你要使用动态库!一定要安装!!!
-
于是我们把库拷贝到
/usr/lib64中,然后再ldd查看一下可执行程序的库连接情况
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls /usr/lib64 | grep "myc"
libmyc.so
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
linux-vdso.so.1 => (0x00007ffe003fc000)
libmyc.so => /lib64/libmyc.so (0x00007faa056d1000)
libc.so.6 => /lib64/libc.so.6 (0x00007faa05303000)
/lib64/ld-linux-x86-64.so.2 (0x00007faa058d4000)
- 此时该程序就可以正常使用了
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib lib libc log.txt Makefile shared_object test usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt
oldking is H
oldking is H
oldking is H
oldking is H
#...
-
当然,除了安装动态库文件到系统目录中,其实我们还有一些其他的方式
-
使用动态库的几种方式:
- 安装动态库文件到系统目录中
- 在系统目录中创建动态库软链接
- 修改环境变量
LD_LIBRARY_PATH - 修改配置文件
/etc/ld.so.conf以及ldconfig重新加载配置文件
-
通过软链接
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ sudo ln -s /home/oldking/code/lib_file_test/lib/mylib/libmyc.so /usr/lib64
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
linux-vdso.so.1 => (0x00007ffe18654000)
libmyc.so => /lib64/libmyc.so (0x00007f7eb3e30000)
libc.so.6 => /lib64/libc.so.6 (0x00007f7eb3a62000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7eb4033000)
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ll /usr/lib64 | grep "myc"
lrwxrwxrwx 1 root root 52 Mar 20 13:05 libmyc.so -> /home/oldking/code/lib_file_test/lib/mylib/libmyc.so
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib lib libc log.txt Makefile shared_object test usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt
oldking is H
oldking is H
oldking is H
#...
- 通过环境变量
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ echo $LD_LIBRARY_PATH
:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/oldking/code/lib_file_test/lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
linux-vdso.so.1 => (0x00007ffdce388000)
libmyc.so => /home/oldking/code/lib_file_test/lib/mylib/libmyc.so (0x00007f908ae05000)
libc.so.6 => /lib64/libc.so.6 (0x00007f908aa37000)
/lib64/ld-linux-x86-64.so.2 (0x00007f908b008000)
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ echo $LD_LIBRARY_PATH
:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/oldking/code/lib_file_test/lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt
oldking is H
oldking is H
oldking is H
#...
-
至于通过配置文件,这个其实比较复杂,也不太常用,就不多提了,感兴趣的话可以去问问ai
-
当然,既然
gcc可以编译形成库文件,那么其他的编译器是不是也可以形成库文件? -
答案是当然可以,我们Windows端的
vs2022也可以用于形成库文件,几乎所有常见编译器都可以用于形成动静态库
9.6 库原理
9.6.1 库的本质与ELF
- 我们知道,库的本质是一堆
.o的集合,而.o文件其实就是二进制文件,跟可执行程序没有本质区别,所以库也是二进制程序 - 既然,库,
.o文件,可执行程序都是二进制程序,此时我们就可以解释一个问题了,不过我们还得稍等一会,我们得先提一下文件格式的问题 - 我们知道,不同的文件格式,其用途,解析文件的方式等等都不一样,意味着这些文件都不仅仅存储着数据,还有需要有配合解释该文件的程序的某些字段,我们举个例子
- 比方说一个
.jpeg格式的文件,假设你想打开这个文件,应用程序怎么知道你想解析的是一个.jpeg格式的文件,以什么算法解析这个.jpeg文件??所以必然会有多个结构,或者说字段,用于表示该文件的类型,表示解析该文件的算法类型,同时还需要有一个结构用于描述该文件中,哪些部分是用于描述文件的,哪些部分是文件真正的数据部分 - 而
ELF就是.o文件,库文件以及可执行程序的文件类型
| 名称 | 职能 |
|---|---|
ELF头(ELF Header) | 标示文件类型与架构,其他表的位置信息 |
程序头表(Program Header Table) | 其内容帮助系统加载该程序的段的权限,地址,对齐方式,以及如何加载进内存 |
节头表(Section Header Table) | 帮助划分节的区间 |
节(Section) | 包含多种不同类型的节,每个节中包含同种类型的数据,未来用于合并成段 |
-
上头这个表我们看个大概就行,反正等会也会具体聊到
-
这里我们大概了解一下"节"这个概念,"节"就是一个单位,表示一部分数据,意义上和我们之前所说的"块","段"没有区别的,我们后面会谈到的"段"也是同理的(就是我们之前在了解进程地址空间的时候的那个"段",也是我们在学习C/C++的时候可能经常会谈到的"数据段"等等空间的单位)
9.6.2 库文件,.o文件与ELF
-
我们知道,多个
.o文件可以归档合并形成库文件,所以它具体是怎样做到的?? -
其实也不难,因为
.o文件和库文件的类型是一样的!所以归档的本质,其实就是将多个.o文件的相同类型的数据(或者称数据节)合并成一个数据节
9.6.3 段的概念以及打开程序粗谈
-
我们知道,所有
ELF类型的文件包含很多个节,每个节储存着相同类型的数据,并且,一个程序的代码和数据全部存储在这些节之中,所以一个ELF中,除了节的其他的部分,全部都不是一个程序自己的内容,前面我们也解释过了,这些剩余的诸如"程序头表","节头表"之类的内容,最终目的都是用于描述程序自己的内容的空间,也就是代码和数据的空间,也就是这些节的空间 -
我们知道,打开一个程序的过程,本质上就是拷贝一个可执行程序的代码和数据到内存,问题就是操作系统怎么知道一个可执行程序哪些部分是哪些数据??
-
所以我们在
ELF类型文件中,拥有了一堆用于描述节的数据,操作系统只需要读这些描述节的数据,就可以找到一个可执行程序的代码和数据了,跟我们之前谈的什么Super Block,GDT,在意义上都是差不多的内容,都是一段数据,用于描述另一段数据的空间 -
我们知道,一个程序被打开之后会变成进程,用于管理进程的结构叫做
task_struct,用于管理一个进程,我们也知道task_struct中存在一个叫做mm_struct的结构,其中也有用于描述各种段的数据,所以操作系统怎么知道这个mm_struct中用于描述和管理各种段的字段应该填什么? -
答案就是从
ELF文件的各种表里来,所以,本质上,ELF的各种表,其实是用来帮助系统初始化mm_struct的!或者说,是用来帮助系统初始化task_struct的! -
所以,我们称,通过节合并并加载进内存的空间,称作为段
9.6.4 readelf的使用与库中的节
-
这里了解一下查看一个可执行程序的
Section所用的工具readelf的使用方式 -
查看一个可执行程序的
Section Header Table
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -S test
There are 30 section headers, starting at offset 0x19a8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
0000000000000038 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002d0 000002d0
0000000000000120 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003f0 000003f0
0000000000000087 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400478 00000478
0000000000000018 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400490 00000490
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004004b0 000004b0
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004004c8 000004c8
0000000000000090 0000000000000018 AI 5 23 8
[11] .init PROGBITS 0000000000400558 00000558
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400580 00000580
0000000000000070 0000000000000010 AX 0 0 16
[13] .text PROGBITS 00000000004005f0 000005f0
00000000000001d2 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004007c4 000007c4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004007d0 000007d0
0000000000000028 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 00000000004007f8 000007f8
0000000000000034 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400830 00000830
00000000000000f4 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e00 00000e00
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e08 00000e08
0000000000000008 0000000000000008 WA 0 0 8
[20] .jcr PROGBITS 0000000000600e10 00000e10
0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000600e18 00000e18
00000000000001e0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000601000 00001000
0000000000000048 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601048 00001048
0000000000000004 0000000000000000 WA 0 0 1
[25] .bss NOBITS 000000000060104c 0000104c
0000000000000004 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 0000104c
000000000000002d 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001080
0000000000000630 0000000000000018 28 46 8
[28] .strtab STRTAB 0000000000000000 000016b0
00000000000001e9 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 00001899
0000000000000108 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
- 好,我们来详细解释一下该怎么看这张表(我们只抓重要的内容)
- 表头字段
| 字段 | 意义 |
|---|---|
Name | 表示节的名称 |
Type | 表示节的类型 |
Address | 加载进内存之后,该节在虚拟地址空间中的起始位置 |
Offset | 该节在文件中的偏移量 |
Size | 该节的大小 |
Flags | 节的权限,包括读写,只读,只写,执行等等 |
Name相关
| 字段 | 意义 |
|---|---|
.dynsym | 动态符号表,存放全局符号信息,负责符号管理 |
.text | 代码段,存放可执行的指令 |
.rodata | 只读数据,例如字符串常量 |
.data | 属于数据段,用于保存已初始化的数据 |
.bss | 属于数据段,但本身不保存实际数据,以简化形式保存未初始化的数据,例如int 50,就是一共有50个未初始化的int变量,所以我们也可以称它"Better Save Space" |
.symtab | 符号表,包含所有符号,调试会使用 |
-
本质上,查看
Section Header Table就是查看一个ELF类型文件的所有"节"究竟保存了多少内容,一共有多少"节",每个"节"大概都是干啥的 -
不难发现,每一个节前面都有一个编号,所以更更本质上,查看这个
Section Header Table,本质上不就是查看一个数组吗?! -
使用
readelf查看一个ELF类型文件的Program Header Table
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -l test
Elf file type is EXEC (Executable file)
Entry point 0x4005f0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000924 0x0000000000000924 R E 200000
LOAD 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
0x000000000000024c 0x0000000000000250 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e0 0x00000000000001e0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000007f8 0x00000000004007f8 0x00000000004007f8
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
0x0000000000000200 0x0000000000000200 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
-
不难发现,这里存在一个节与段的关系表,
Section to Segment mapping -
所以,段,即
Segment,本质其实是多个相同类型,权限的节构成的,对应的,每个段的职能则写在Section to Segment mapping的上面,也就是Program Headers -
相同的,这里只抓重点聊这些字段
Segment编号 | 类型 | 职能 |
|---|---|---|
00 | PHDR | ELF的头部,存储所有的程序头,即这些表 |
02 | 代码LOAD | 其实就是代码段,存储相关代码的部分 |
03 | 数据LOAD | 其实就是数据段,存储相关数据的部分 |
04 | DYNAMIC | 动态链接表,存储动态链接信息 |
- 使用
readelf查看一个ELF类型文件的ELF Header
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -h test
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4005f0
Start of program headers: 64 (bytes into file)
Start of section headers: 6568 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
- 相关字段的解释:
| 字段 | 意义 |
|---|---|
Magic | 用于标记该文件的类型是ELF格式的文件 |
Data | 存储格式,little endian表示小端存储 |
Type | 描述ELF下细分的各种类型,包括可重定向目标文件,库文件,可执行文件,EXEC表示可执行文件 |
Machine | 机器架构 |
Version | ELF版本号,一般是1 |
Entry point address | 程序的入口地址,一般是_start函数的入口地址 |
Start of program headers | Program Header Table在文件的偏移量 |
Start of section headers | Section Header Table在文件的偏移量 |
Size of this header | ELF Header自己的大小 |
Size of program headers | 单个Program Header的大小 |
Number of program headers | Program Header Table的条目个数 |
Size of section headers | 单个Section Header的大小 |
Number of section headers | Section Header Table的条目个数 |
- 事实上,一个
ELF类型的文件一般是这么存储的
9.6.5 可执行文件的加载
- 本小节会比较绕,比较恶心,比较难,建议多看一看
9.6.5.1 段
-
我们知道,一个可执行文件在被加载的时候会将其中非节的内容用于初始化内存的
task_struct,mm_struct,vm_area_struct等结构 -
同时,多个相同属性或权限的节会被整合成一个段被拷贝进内存
9.6.5.2 反汇编工具的使用
-
我们使用
objdump作为反汇编工具,一般会带上选项-d,表示以简略模式反汇编 -
这里我们简单写一个测试代码和
Makefile,本小节"可执行文件的加载"所有示例的代码都是基于这三个文件进行的
//main.c
#include<stdio.h>
void run();
int main()
{
printf("hello\n");
run();
return 0;
}
//run.c
#include<stdio.h>
void run()
{
printf("run\n");
}
# Makefile
exec:main.o run.o
gcc main.o run.o -o exec
main.o:main.c
gcc main.c -c -g
run.o:run.c
gcc run.c -c -g
# 这里要使用-g选项,可以方便反汇编查看函数名称,否则函数名称显示的可能是一串地址
.PHONY:clean
clean:
rm -rf *.o exec
- 我们
make后可以查一下反汇编
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
main.c Makefile run.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ make
gcc main.c -c
gcc run.c -c
gcc main.o run.o -o exec
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
exec main.c main.o Makefile run.c run.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d main.o > main.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d run.o > run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d exec > exec.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
exec exec.s main.c main.o main.s Makefile run.c run.o run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat main.s
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: e8 00 00 00 00 callq 18 <main+0x18>
18: b8 00 00 00 00 mov $0x0,%eax
1d: 5d pop %rbp
1e: c3 retq
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat run.s
run.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <run>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <run+0xe>
e: 5d pop %rbp
f: c3 retq
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat exec.s
exec: file format elf64-x86-64
Disassembly of section .init:
00000000004003e0 <_init>:
4003e0: 48 83 ec 08 sub $0x8,%rsp
4003e4: 48 8b 05 0d 0c 20 00 mov 0x200c0d(%rip),%rax # 600ff8 <__gmon_start__>
4003eb: 48 85 c0 test %rax,%rax
4003ee: 74 05 je 4003f5 <_init+0x15>
4003f0: e8 3b 00 00 00 callq 400430 <__gmon_start__@plt>
4003f5: 48 83 c4 08 add $0x8,%rsp
4003f9: c3 retq
Disassembly of section .plt:
0000000000400400 <.plt>:
400400: ff 35 02 0c 20 00 pushq 0x200c02(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400406: ff 25 04 0c 20 00 jmpq *0x200c04(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40040c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmpq *0x200c02(%rip) # 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 pushq $0x0
40041b: e9 e0 ff ff ff jmpq 400400 <.plt>
0000000000400420 <__libc_start_main@plt>:
400420: ff 25 fa 0b 20 00 jmpq *0x200bfa(%rip) # 601020 <__libc_start_main@GLIBC_2.2.5>
400426: 68 01 00 00 00 pushq $0x1
40042b: e9 d0 ff ff ff jmpq 400400 <.plt>
0000000000400430 <__gmon_start__@plt>:
400430: ff 25 f2 0b 20 00 jmpq *0x200bf2(%rip) # 601028 <__gmon_start__>
400436: 68 02 00 00 00 pushq $0x2
40043b: e9 c0 ff ff ff jmpq 400400 <.plt>
Disassembly of section .text:
0000000000400440 <_start>:
400440: 31 ed xor %ebp,%ebp
400442: 49 89 d1 mov %rdx,%r9
400445: 5e pop %rsi
400446: 48 89 e2 mov %rsp,%rdx
400449: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40044d: 50 push %rax
40044e: 54 push %rsp
40044f: 49 c7 c0 d0 05 40 00 mov $0x4005d0,%r8
400456: 48 c7 c1 60 05 40 00 mov $0x400560,%rcx
40045d: 48 c7 c7 2d 05 40 00 mov $0x40052d,%rdi
400464: e8 b7 ff ff ff callq 400420 <__libc_start_main@plt>
400469: f4 hlt
40046a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0000000000400470 <deregister_tm_clones>:
400470: b8 3f 10 60 00 mov $0x60103f,%eax
400475: 55 push %rbp
400476: 48 2d 38 10 60 00 sub $0x601038,%rax
40047c: 48 83 f8 0e cmp $0xe,%rax
400480: 48 89 e5 mov %rsp,%rbp
400483: 77 02 ja 400487 <deregister_tm_clones+0x17>
400485: 5d pop %rbp
400486: c3 retq
400487: b8 00 00 00 00 mov $0x0,%eax
40048c: 48 85 c0 test %rax,%rax
40048f: 74 f4 je 400485 <deregister_tm_clones+0x15>
400491: 5d pop %rbp
400492: bf 38 10 60 00 mov $0x601038,%edi
400497: ff e0 jmpq *%rax
400499: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004a0 <register_tm_clones>:
4004a0: b8 38 10 60 00 mov $0x601038,%eax
4004a5: 55 push %rbp
4004a6: 48 2d 38 10 60 00 sub $0x601038,%rax
4004ac: 48 c1 f8 03 sar $0x3,%rax
4004b0: 48 89 e5 mov %rsp,%rbp
4004b3: 48 89 c2 mov %rax,%rdx
4004b6: 48 c1 ea 3f shr $0x3f,%rdx
4004ba: 48 01 d0 add %rdx,%rax
4004bd: 48 d1 f8 sar %rax
4004c0: 75 02 jne 4004c4 <register_tm_clones+0x24>
4004c2: 5d pop %rbp
4004c3: c3 retq
4004c4: ba 00 00 00 00 mov $0x0,%edx
4004c9: 48 85 d2 test %rdx,%rdx
4004cc: 74 f4 je 4004c2 <register_tm_clones+0x22>
4004ce: 5d pop %rbp
4004cf: 48 89 c6 mov %rax,%rsi
4004d2: bf 38 10 60 00 mov $0x601038,%edi
4004d7: ff e2 jmpq *%rdx
4004d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004e0 <__do_global_dtors_aux>:
4004e0: 80 3d 4d 0b 20 00 00 cmpb $0x0,0x200b4d(%rip) # 601034 <_edata>
4004e7: 75 11 jne 4004fa <__do_global_dtors_aux+0x1a>
4004e9: 55 push %rbp
4004ea: 48 89 e5 mov %rsp,%rbp
4004ed: e8 7e ff ff ff callq 400470 <deregister_tm_clones>
4004f2: 5d pop %rbp
4004f3: c6 05 3a 0b 20 00 01 movb $0x1,0x200b3a(%rip) # 601034 <_edata>
4004fa: f3 c3 repz retq
4004fc: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400500 <frame_dummy>:
400500: 48 83 3d 18 09 20 00 cmpq $0x0,0x200918(%rip) # 600e20 <__JCR_END__>
400507: 00
400508: 74 1e je 400528 <frame_dummy+0x28>
40050a: b8 00 00 00 00 mov $0x0,%eax
40050f: 48 85 c0 test %rax,%rax
400512: 74 14 je 400528 <frame_dummy+0x28>
400514: 55 push %rbp
400515: bf 20 0e 60 00 mov $0x600e20,%edi
40051a: 48 89 e5 mov %rsp,%rbp
40051d: ff d0 callq *%rax
40051f: 5d pop %rbp
400520: e9 7b ff ff ff jmpq 4004a0 <register_tm_clones>
400525: 0f 1f 00 nopl (%rax)
400528: e9 73 ff ff ff jmpq 4004a0 <register_tm_clones>
000000000040052d <main>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: bf f0 05 40 00 mov $0x4005f0,%edi
400536: e8 d5 fe ff ff callq 400410 <puts@plt>
40053b: b8 00 00 00 00 mov $0x0,%eax
400540: e8 07 00 00 00 callq 40054c <run>
400545: b8 00 00 00 00 mov $0x0,%eax
40054a: 5d pop %rbp
40054b: c3 retq
000000000040054c <run>:
40054c: 55 push %rbp
40054d: 48 89 e5 mov %rsp,%rbp
400550: bf f6 05 40 00 mov $0x4005f6,%edi
400555: e8 b6 fe ff ff callq 400410 <puts@plt>
40055a: 5d pop %rbp
40055b: c3 retq
40055c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400560 <__libc_csu_init>:
400560: 41 57 push %r15
400562: 41 89 ff mov %edi,%r15d
400565: 41 56 push %r14
400567: 49 89 f6 mov %rsi,%r14
40056a: 41 55 push %r13
40056c: 49 89 d5 mov %rdx,%r13
40056f: 41 54 push %r12
400571: 4c 8d 25 98 08 20 00 lea 0x200898(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
400578: 55 push %rbp
400579: 48 8d 2d 98 08 20 00 lea 0x200898(%rip),%rbp # 600e18 <__init_array_end>
400580: 53 push %rbx
400581: 4c 29 e5 sub %r12,%rbp
400584: 31 db xor %ebx,%ebx
400586: 48 c1 fd 03 sar $0x3,%rbp
40058a: 48 83 ec 08 sub $0x8,%rsp
40058e: e8 4d fe ff ff callq 4003e0 <_init>
400593: 48 85 ed test %rbp,%rbp
400596: 74 1e je 4005b6 <__libc_csu_init+0x56>
400598: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40059f: 00
4005a0: 4c 89 ea mov %r13,%rdx
4005a3: 4c 89 f6 mov %r14,%rsi
4005a6: 44 89 ff mov %r15d,%edi
4005a9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005ad: 48 83 c3 01 add $0x1,%rbx
4005b1: 48 39 eb cmp %rbp,%rbx
4005b4: 75 ea jne 4005a0 <__libc_csu_init+0x40>
4005b6: 48 83 c4 08 add $0x8,%rsp
4005ba: 5b pop %rbx
4005bb: 5d pop %rbp
4005bc: 41 5c pop %r12
4005be: 41 5d pop %r13
4005c0: 41 5e pop %r14
4005c2: 41 5f pop %r15
4005c4: c3 retq
4005c5: 90 nop
4005c6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005cd: 00 00 00
00000000004005d0 <__libc_csu_fini>:
4005d0: f3 c3 repz retq
Disassembly of section .fini:
00000000004005d4 <_fini>:
4005d4: 48 83 ec 08 sub $0x8,%rsp
4005d8: 48 83 c4 08 add $0x8,%rsp
4005dc: c3 retq
9.6.5.3 ELF格式文件的链接
-
上小节我们反汇编了一下
main.o,run.o,exec三个文件,我们可以观察一下这三个文件关于run()函数的汇编代码 -
PS:因为上小节我们使用的
objdump命令生成的汇编文本不包含函数名称,不方便观察,这里我们再带一个选项-S显示一下当前行代码
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d -S run.o > run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat run.s
run.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <run>:
#include<stdio.h>
void run()
{
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
printf("run\n");
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <run+0xe>
}
e: 5d pop %rbp
f: c3 retq
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d -S main.o > main.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat main.s
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
#include<stdio.h>
void run();
int main()
{
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
printf("hello\n");
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
run();
e: b8 00 00 00 00 mov $0x0,%eax
13: e8 00 00 00 00 callq 18 <main+0x18>
return 0;
18: b8 00 00 00 00 mov $0x0,%eax
}
1d: 5d pop %rbp
1e: c3 retq
- 我们再截取一下
exec.s的关于main和run两个函数的相关部分
000000000040052d <main>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: bf f0 05 40 00 mov $0x4005f0,%edi
400536: e8 d5 fe ff ff callq 400410 <puts@plt>
40053b: b8 00 00 00 00 mov $0x0,%eax
400540: e8 07 00 00 00 callq 40054c <run>
400545: b8 00 00 00 00 mov $0x0,%eax
40054a: 5d pop %rbp
40054b: c3 retq
000000000040054c <run>:
40054c: 55 push %rbp
40054d: 48 89 e5 mov %rsp,%rbp
400550: bf f6 05 40 00 mov $0x4005f6,%edi
400555: e8 b6 fe ff ff callq 400410 <puts@plt>
40055a: 5d pop %rbp
40055b: c3 retq
40055c: 0f 1f 40 00 nopl 0x0(%rax)
-
我们来解释一下这部分汇编代码
- 左边部分的一串16进制数字中,前两位是机器码指令,即汇编代码
callq,mov转成机器码的样子 - 剩下的八位内容是地址
- 左边部分的一串16进制数字中,前两位是机器码指令,即汇编代码
-
所以我们也不难发现,实际上
main.c中调用的run()方法,在编译成.o之后,callq跳转命令后面根本就没有正确的跳转地址而是全0 -
而找到地址这个过程实际上是链接的过程需要做的
-
链接器会将两个文件进行对比,
main.o中不存在run()的地址,就会跑到其他.o文件找,如果还是找不到,就往库文件找(这里指的是静态库,动态库的机制不太一样,我们留到后面再说) -
意味着实际上编译器是暂定我们这个函数存在,除非等到链接时找不到这个函数的地址,才会报错
-
所以我们能看到在
exec.s中,run()函数和printf()函数的地址是已经被填好的了
9.6.5.4 逻辑地址与线性地址与虚拟地址和地址映射
- 我们知道,一个
ELF类型的文件,一定会包含很多节,在加载到内存之后相同属性和权限的多个节会被合并成段 - 问:一行代码在
ELF类型文件中,有没有存储地址?? - 答案是肯定的,他一定存储着地址,我们称这个地址为"逻辑地址"
- 有两种逻辑地址的表示机制,我们先聊旧方法
- 我们知道,一个
ELF文件,其实比较容易获取的地址,是所有节的起始地址,而一行代码的地址的表示方式,就可以用"当前代码所在的节的起始地址+该行代码在该节的偏移量"表示,例如.text节的起始地址是0x1000,而函数a()所在该节的偏移量为0x50 - 所以我们就可以以
0x1000 + 0x50来定位函数a()
- 新方法
-
而在当今计算机中,旧方法已经很少见了,取而代之的是新方式(主要是因为引入了页表机制为了适配页表机制而采用的,同时旧方法也会因为多一步计算过程而造成一定性能损耗)
-
在新方式中,一行代码的地址直接以
0号地址作为起点+其偏移量,所以一行代码的地址直接用偏移量表示就行了,就不需要多一步计算过程了 -
同时,使用新方式生成的地址又称作"线性地址",这个新方法又称为"平坦模式编址"
-
而之所以一个存储在硬盘的文件需要逻辑地址,其实是为了方便页表的加载/初始化!!!
-
我们知道,一个可执行文件一旦被打开,就需要加载页表,构建虚拟地址和物理地址的映射关系,这个映射关系是打开文件时现场算出来的吗????
-
当然不是!你不觉得等到现场算太慢了吗?用户不会因为打开软件慢而急吗?!!!
-
所以你以为页表的虚拟地址是怎么来的?一个进程的虚拟地址不就是储存在硬盘中的可执行文件的逻辑地址吗?!操作系统只需要无脑拷贝逻辑地址到页表的虚拟地址栏不就行了吗?!!!
-
所以,逻辑地址,线性地址,虚拟地址的本质是同一个东西!只不过逻辑地址是站在硬盘的可执行文件的视角的,线性地址是逻辑地址的一个分支,是站在"平坦模式编址"的视角的,而虚拟地址是站在进程的视角的
9.6.5.6 可执行文件的加载
-
在真正了解可执行文件的加载之前,我们首先需要了解一些CPU中相关的寄存器:
EIP:这个寄存器会存储进程应该执行的下一条代码的虚拟地址(至于为什么是虚拟地址,我们还会再提到的)CR3:这个寄存器会存储当前进程对应的页表所在的物理地址,注意:是存储页表自己的地址而不是页表中映射的物理地址
-
同时CPU还有一个结构叫做
MMU,在一个进程被加载的过程中,他会负责将物理地址映射给该进程的虚拟地址 -
所以,一个可执行文件被加载,会经历哪些过程?
- 找到该文件
- 通过"文件名+绝对目录"的形式,找到该文件在当前目录的
inode number - 通过
inode number计算找到该文件存储的块组,并找到该块组inode Bitmap的位置,找到inode Table中存储的该文件的属性 - 通过该文件的属性,找到该文件在
data Block的位置
- 通过"文件名+绝对目录"的形式,找到该文件在当前目录的
- 加载器
- 通过文件中的
ELF Header,获得该文件各区域的内存分布,同时获得该进程运行的起始虚拟地址 - 通过
Program Header Table获得该文件相关段属性,空间分配,并以此初始化mm_struct,vm_area_struct - 处理页表问题,初始化页表,通过
MMU构建虚拟地址和物理地址映射关系,将页表的物理地址传递给CR3,拷贝并合并相同属性和权限的节为若干段到映射的物理内存中 - 将进程运行的起始虚拟地址传递给
EIP - 处理库问题(这个我们后面再细讲,这里我们暂时当库文件不存在)
- 将控制权力交给CPU,CPU开始运行程序
- 通过文件中的
- CPU
- 通过
EIP找到页表中映射的物理内存,通过该物理内存对应地址存储的机器码执行指令 - 将
EIP存储的内容改成下一条命令对应的虚拟内存,即下一个虚拟内存(非函数跳转的情况,如果是函数跳转,则会修改EIP为该函数的虚拟地址)
- 通过
-
所以,CPU怎么知道程序的入口在哪里?答案是通过
EIP Header中的一个字段Entry point address来的 -
所以,一个可执行程序在还没有运行的时候,就已经知道该怎样加载自己到内存了,有非常多的内容存在的意义,全部是为了配合加载器加载程序的初始化相关结构而存在的
-
所以,实际上对于CPU来说,他也只能看到一个进程的虚拟地址,所有事情都是围绕虚拟地址来看的!等真的要执行的时候,才会去找物理地址,同样是为了安全!并且CPU不用考虑物理地址实际在什么位置!拿来直接调用对应机器码就行!
-
同时,我们再次体会到了虚拟内存这个东西的好处,我们不需要真的运行程序也能知道程序的起点地址,也知道程序的空间分配,同时在程序真正加载进内存的时候,也不会和其他进程造成地址冲突
9.6.6 动态库实现共享的形式
-
在谈静态库之前,我们先来聊聊静态库
-
所以,静态库和
.o有啥区别呢?实际上没啥区别!反正就那么些个Section,该有的Table,Header啥的都有,哪能有啥区别?多个.o组合起来,本质上不还是.o吗? -
所以因为本质上是
.o,所以静态库的所有代码和数据全部会和.o文件一样在链接的时候存进可执行文件中!但如果多个进程包含一样的静态库,内存中存在很多份重复的静态库代码,不就造成了内存的浪费吗,于是就有了动态库,以防止内存的浪费 -
但动态库不一样,多个进程可以共享动态库的代码,所以即便多个进程使用了动态库的代码,内存中也只需要存在一份动态库代码即可,这点我们很久之前就提过了
-
下面我们通过一张图了解一下动态库的共享机制
- 所以,为什么能做到共享的操作呢?
- 实际上在每个进程自己的角度而言,他们都认为共享库是自己独享
- 这是因为虚拟地址和物理地址的映射关系不同导致的,虽然虚拟地址不一样,但物理地址是否可以一样呢?物理地址一样不久实现共享库了嘛?
9.6.7 动态库链接的机制
- 动态库的链接机制比较复杂,这里我们分为三个阶段来看
- 编译时
- 加载时
- 运行时
9.6.7.1 编译时
- 首先编译器
gcc会把多个.o或者静态库文件合并成一个初始的可执行文件 - 然后链接器
ld会负责检查合并后的文件中缺少的函数地址,并进行填充 - 如果函数地址仍然没有找到,就将该函数所需跳转的地址暂时置空(或是留下符号引用)
- 填写节
.dynamic中的一张叫DT_NEEDED的表,该表中标注了该可执行程序对于动态库的调用情况 - 生成可执行文件
9.6.7.2 加载时
- 加载器会解析该可执行文件的
Program Header Table,并找到该程序的.Dynamic节,并找到DT_NEEDED表结构,通过该表结构,加载器会知道,该可执行文件需要哪些动态库 - 加载器,通过环境变量,配置文件,在对应目录中,查找有没有该可执行文件所需要的动态库
- 如果动态库没有被加载,将动态库加载进内存
- 将动态库的物理地址和该进程的虚拟地址进行映射,以共享动态库
- 修正编译时没有找到跳转地址的函数的跳转地址(其实是通过一个叫
PLT的接口来找函数,同时初始化一张叫做.got的表) - 运行
ELF的DT_INIT段(如果有初始化代码的话,比方说CPP全局构造函数)
9.6.7.3 运行时
- 可执行文件中有一个节叫做
.got,.got是一个表,其中记载了该程序使用的动态库接口在当前进程的虚拟地址,程序在调用函数时,实际上会先从.got中找到该结构的虚拟地址 - 如果是第一次调用该函数,则
PLT接口会负责跳转到解析该函数的虚拟地址的方法,通过该方法解析虚拟地址(准确来说是动态库在当前进程的起始虚拟地址+该函数在动态库中的偏移量),然后存入.got中,后续再调用该函数的时候无需使用PLT接口跳转到解析虚拟地址的方法的功能,而仅需在.got中查找
9.6.7.4 PLT/GOT
-
PLT的本质是一个函数,是一个接口,所有的调用动态库接口的过程,实际上都需要调用一次PLT接口 -
GOT本质上是一个结构,本质上存储的是动态库函数的虚拟地址(也是当前进程中的虚拟地址的一部分) -
所以,宏观来看,
PLT就是一个工具,用于跳转到解析函数的虚拟地址的方法或者直接通过GOT获取已经解析好的函数地址,GOT是一个缓存,用于存储解析出来的函数的虚拟地址,以减少后续因为解析而造成的CPU开销 -
但以我们的了解,加载时不就已经找到动态库函数的地址了吗,为什么后续还要用
PLT和GOT再找一边呢? -
原因是为了实现一个叫按需加载的功能,要知道,一个项目的函数量可能是非常多的,你不能指望一个程序被打开时能找到所有函数的地址还能使程序加载速度比较快
-
所以使用了延迟加载的设计,以实现按需加载的功能
9.6.7.5 查看一个可执行文件的GOT表
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ readelf -r exec
Relocation section '.rela.dyn' at offset 0x380 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x398 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
- 这条命令本质是列出所有的动态链接的函数
9.6.7.6 动态库的加载机制中细节问题的解释
-
注意:不要因为动态库和可执行文件是两个文件就将它俩完全区别开了,他俩实际上是高度契合的!在可执行文件的加载过程中,如果动态库在此之前从来没有被加载过,动态库文件会跟随一起加载,本质上和加载可执行文件中其他各种段的原理是一样的
-
同时,动态库实际上没有自己的虚拟地址的,或者说它的虚拟地址并不固定,动态库也没有自己的页表,动态库的页表是和众多使用了该库的进程一起共享的,或者说动态库就像是一个变量被多个指针指向,是被进程共享的
-
同时,
GOT存储的关于函数的虚拟地址,其实是进程自己的页表的虚拟地址 -
所以你会发现,动态库的设计中的每一项东西,都是为了让进程认为这个库是自己独占的!
9.6.7.7 关于PIC地址无关码
-
所以,以上通过缓存
GOT,以动态库起始地址(动态库基地址)+函数在动态库的偏移量找到一个动态库函数的方式,称作PIC地址无关代码,即PIC= "动态库在当前进程的起始地址" + "函数在动态库的偏移量" -
所以,我们可以通过
PIC地址无关码找到一个函数在当前进程的虚拟地址,并通过页表来获取对应的物理地址,即可实现一个函数的调用! -
所以,通过
PIC地址无关码与页表机制,我们能快速获得并调用当前进程中任意一个动态库文件的任意一个函数,因为PIC地址无关码能找到这个函数在哪个库的什么位置,包括虚拟地址也包括物理地址!!!
9.6.7.8 关于-static
- 如果编译程序时附加了选项
-static,那么与动态库相关的结构(包括共享内存,GOT表,动态链接函数符号表等)就不会存在 - 此时会强制使用静态库进行链接,动态库默认就不会链接进来
- 如有问题或者有想分享的见解欢迎留言讨论