Linux系统--库制作与原理
大家好,又是几天不见,上一次我们学习了文件,今天我们来学习库。那么话不多说,我们直接开始今天的学习:
目录
库制作与原理
1. 什么是库
2. 静态库
2-1 静态库⽣成
2-2 静态库使⽤
3. 动态库
3-1 动态库⽣成
3-2 动态库使⽤
3-3 库运⾏搜索路径
3-3-1 问题
3-3-2 解决⽅案
4. ⽬标⽂件
5. ELF⽂件
6. ELF从形成到加载轮廓
6-1 ELF形成可执⾏
6-2 ELF可执⾏⽂件加载
7. 理解连接与加载
7-1 静态链接
7-2 ELF加载与进程地址空间
7-2-1 虚拟地址/逻辑地址
7-2-2 重新理解进程虚拟地址空间
7-3 动态链接与动态库加载
7-3-1 进程如何看到动态库
7-3-2 进程间如何共享库的
7-3-3 动态链接
7-3-4 库间依赖
库制作与原理
1. 什么是库
库是写好的现有的,成熟的,可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个⼈的代码都从零开始,因此库的存在意义⾮同寻常。本质上来说库是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。库有两种:静态库 .a[Linux]、.lib[windows]动态库 .so[Linux]、.dll[windows]
// my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{int flag; // 刷新⽅式int fileno; // ⽂件描述符char outbuffer[SIZE];int cap;int size;// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{int fd = -1;if(strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if(strcmp(mode, "w")== 0){fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);}else if(strcmp(mode, "a") == 0){fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);}if(fd < 0) return NULL;mFILE *mf = (mFILE*)malloc(sizeof(mFILE));if(!mf){close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;mf->cap = SIZE;return mf;
}void mfflush(mFILE *stream)
{if(stream->size > 0){// 写到内核⽂件的⽂件缓冲区中!write(stream->fileno, stream->outbuffer, stream->size);// 刷新到外设fsync(stream->fileno);stream->size = 0;}
}int mfwrite(const void *ptr, int num, mFILE *stream)
{// 1. 拷⻉memcpy(stream->outbuffer+stream->size, ptr, num);stream->size += num;// 2. 检测是否要刷新if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1]== '\n'){mfflush(stream);}return num;
}void mfclose(mFILE *stream)
{if(stream->size > 0){mfflush(stream);}close(stream->fileno);
}
// my_string.h
#pragma once
int my_strlen(const char *s);
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{const char *end = s;while(*end != '\0')end++;return end - s;
}
2. 静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执⾏⽂件中,程序运⾏的时候将不再需要静态库。⼀个可执⾏程序可能⽤到许多的库,这些库运⾏有的是静态库,有的是动态库,⽽我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使⽤ gcc的 -static 强转设置链接静态库。
2-1 静态库⽣成
// Makefile
libmystdio.a:my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"
%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done"
.PHONY:clean
clean:@rm -rf *.a *.o stdc*@echo "clean ... done"
ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)
$ ar -tv libmystdio.arw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.orw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.ot: 列出静态库中的⽂件v:verbose 详细信息
2-2 静态库使⽤
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{const char *s = "abcdefg";printf("%s: %d\n", s, my_strlen(s));mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;
}
// 场景1:头⽂件和库⽂件安装到系统路径下$ gcc main.c -lmystdio// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下$ gcc main.c -L. -lmymath// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
-L: 指定库路径-I: 指定头⽂件搜索路径-l: 指定库名测试⽬标⽂件⽣成后,静态库删掉,程序照样可以运⾏库⽂件名称和引⼊库的名称:去掉前缀 lib ,去掉后缀 .so , .a ,如: libc.so -> c
-L是要告诉操作系统去哪里找库,-l是告诉操作系统找什么库,如果我们要链接任何非c/c++标准库,都需要指明-L和-l
另外,我们还可以将静态库打包:
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.a stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
这样将压缩包交给他人进行解包后也可以直接使用
3. 动态库
动态库(.so):程序在运⾏的时候才去链接动态库的代码,多个程序共享使⽤库的代码。⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬标⽂件的整个机器码在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采⽤虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空间。
3-1 动态库⽣成
// Makefile
libmystdio.so:my_stdio.o my_string.ogcc -o $@ $^ -shared%.o:%.cgcc -fPIC -c $<
.PHONY:clean
clean:@rm -rf *.so *.o stdc*@echo "clean ... done"
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.so stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
shared: 表⽰⽣成共享库格式fPIC:产⽣位置⽆关码(position independent code)库名规则:libxxx.so
在生成动态库时,在gcc或g++后面加上-shared,在生成.o文件时,在gcc或g++后面加上-fPIC
动态库(
.so
)的核心特性是:可以被多个程序共享,且加载到内存时的地址不固定(由操作系统动态分配)。
如果不用-fPIC
,编译出的代码会包含绝对地址引用(例如访问全局变量或函数时,直接使用固定内存地址)。当动态库被加载到不同程序的内存空间(地址可能不同)时,这些绝对地址会失效,导致程序崩溃。
这一点我们后面进行详细说明
3-2 动态库使⽤
// 场景1:头⽂件和库⽂件安装到系统路径下$ gcc main.c -lmystdio// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下$ gcc main.c -L. -lmymath // 从左到右搜索-L指定的⽬录// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
这一点和静态库的使用一样
3-3 库运⾏搜索路径
3-3-1 问题
我们可以使用ldd命令来查看库或者可执⾏程序的依赖
$ ldd libmystdio.so
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
但当我们链接动态库生成可执行程序时,
$ ll
total 24
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
$ gcc main.c -L. -lmystdio
$ ll
total 36
-rwxrwxr-x 1 whb whb 8600 Oct 29 14:51 a.out
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
再来查看可执行程序的依赖:
$ ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
结果是并没有查到我们的动态库,这是因为虽然我们将动态库的路径等信息都告诉了gcc/g++编译器,但是操作系统并不知道我们的动态库,所以想解决这个问题,只需要让操作系统也知道就行了。
3-3-2 解决⽅案
1. 拷⻉ .so ⽂件到系统共享库路径下, ⼀般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等2. 向系统共享库路径下建⽴同名软连接3. 更改环境变量: LD_LIBRARY_PATH4. ldconfig⽅案:配置/ etc/ld.so.conf.d/ ,ldconfig更新
那么为什么静态库没有这个问题呢?
1. 静态库的链接方式:“编译时完全嵌入”
静态库(
.a
文件)在程序编译链接阶段,会被完整地复制到可执行文件(a.out
)中。
链接完成后,可执行文件不再依赖原静态库,即使删除静态库,
a.out
也能正常运行。
ldd
命令用于查看可执行文件依赖的动态库,静态库由于已被嵌入,不会出现在ldd
的输出中,自然也不会有 “找不到” 的问题。2. 动态库的链接方式:“运行时依赖”
动态库(
.so
文件)在编译链接时,仅会在可执行文件中记录 “需要使用该动态库” 的信息,不会将库内容复制到a.out
中。
程序运行时,操作系统才会去查找并加载对应的
.so
文件。如果动态库不存在(或路径不正确),
ldd
会显示not found
,程序运行时也会报错(如error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory
)。
还有一个问题,在动静态库中,要不要包含main函数呢?
无论是动态库还是静态库,本质都是源文件对应的.o文件,所以不需要main函数
结论 1:gcc/g++ 默认使用动态库!
非得静态链接只能 - static,一旦 - static,就必须存在对应的静态库
只存在静态库,可执行程序,对于该库,只能静态链接了!结论 2:在 linux 系统下,默认情况安装的大部分库,默认都优先安装的是动态库!
结论 3:库:应用程序 = 1:n
4. ⽬标⽂件
编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们⼀般都是⼀键构建⾮常⽅便,但⼀旦遇到错误的时候呢,尤其是链接相关的错误,很多⼈就束⼿⽆策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这⼀系列操作。
接下来我们深⼊探讨⼀下编译和链接的整个过程,来更好的理解动静态库的使⽤原理。先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运⾏的机器代码。⽐如:在⼀个源⽂件 hello.c ⾥简单输出"hello world!",并且调⽤⼀个run函数,⽽这个函数被定义在另⼀个原⽂件 code.c 中。这⾥我们就可以调⽤ gcc -c 来分别编译这两个原⽂件。
// hello.c
#include<stdio.h>void run();int main() {printf("hello world!\n");run();return 0;
}// code.c
#include<stdio.h>
void run() {printf("running...\n");
}
可以看到,在编译之后会⽣成两个扩展名为 .o 的⽂件,它们被称作⽬标⽂件。要注意的是如果我们修改了⼀个原⽂件,那么只需要单独编译它这⼀个,⽽不需要浪费时间重新编译整个⼯程。⽬标⽂件是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。
5. ELF⽂件
可重定位⽂件(Relocatable File) :即 xxx.o ⽂件。包含适合于与其他⽬标⽂件链接来创建可执⾏⽂件或者共享⽬标⽂件的代码和数据。可执⾏⽂件(Executable File) :即可执⾏程序。共享⽬标⽂件(Shared Object File) :即 xxx.so⽂件。内核转储(core dumps) ,存放当前进程的执⾏上下⽂,⽤于dump信号触发。⼀个ELF⽂件由以下四部分组成:ELF头(ELF header) :描述⽂件的主要特性。其位于⽂件的开始位置,它的主要⽬的是定位⽂件的其他部分。程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表⾥记着每个段的开始的位置和位移(offset)、⻓度,毕竟这些段,都是紧密的放在⼆进制⽂件中,需要段表的描述信息,才能把他们每个段分割开。节头表(Section header table) :包含对节(sections)的描述。节(Section ):ELF⽂件中的基本组成单位,包含了特定类型的数据。ELF⽂件的各种信息和数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。最常⻅的节:代码节(.text):⽤于保存机器指令,是程序的主要执⾏部分。数据节(.data):保存已初始化的全局变量和局部静态变量。
6. ELF从形成到加载轮廓
6-1 ELF形成可执⾏
step-1:将多份 C/C++ 源代码,翻译成为⽬标 .o ⽂件step-2:将多份 .o ⽂件section进⾏合并
6-2 ELF可执⾏⽂件加载
⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等.这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起很显然,这个合并⼯作也已经在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中
我们可以使用readelf -S指令查看可执⾏程序的section:
$ readelf -S a.out
There are 31 section headers, starting at offset 0x19d8:Section Headers:
[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 000002540000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 000002740000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b80000000000000048 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400300 000003000000000000000038 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400338 000003380000000000000006 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400340 000003400000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400360 000003600000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400378 000003780000000000000018 0000000000000018 AI 5 24 8
[11] .init PROGBITS 0000000000400390 00000390
...
同样,我们可以使用readelf -l a.out来查看section合并的segment,这里就不演示了
为什么要将section合并成为segment?
Section合并的主要原因是为了减少⻚⾯碎⽚,提⾼内存使⽤效率。如果不进⾏合并, 假设⻚⾯⼤⼩为4096字节(内存块基本⼤⼩,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占⽤3个⻚⾯,⽽合并后,它们只需2个⻚⾯。此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。
链接视图(Linking view) - 对应节头表 Section header table1.⽂件结构的粒度更细,将⽂件按功能模块的差异进⾏划分,静态链接分析的时候⼀般关注的是链接视图,能够理解 ELF ⽂件中包含的各个部分的信息。2.为了空间布局上的效率,将来在链接⽬标⽂件时,链接器会把很多节(section)合并,规整成可执⾏的段(segment)、可读写的段、只读段等。合并了后,空间利⽤率就⾼了,否则,很⼩的很⼩的⼀段,未来物理内存⻚浪费太⼤(物理内存⻚分配⼀般都是整数倍⼀块给你,⽐如4k),所以,链接器趁着链接就把⼩块们都合并了。执⾏视图(execution view) - 对应程序头表 Program header table1.告诉操作系统,如何加载可执⾏⽂件,完成进程内存的初始化。⼀个可执⾏程序的格式中,⼀定有 program header table 。说⽩了就是:⼀个在链接时作⽤,⼀个在运⾏加载时作⽤。
从 链接视图 来看:命令 readelf -S hello.o 可以帮助查看ELF⽂件的节头表。.text节 :是保存了程序代码指令的代码节。.data节 :保存了初始化的全局变量和局部静态变量等数据。.rodata节 :保存了只读的数据,如⼀⾏C语⾔代码中的字符串。由于.rodata节是只读的,所以只能存在于⼀个可执⾏⽂件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。.BSS节 :为未初始化的全局变量和局部静态变量预留位置.symtab节 : Symbol Table 符号表,就是源码⾥⾯那些函数名、变量名和代码的对应关系。.got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导⼊的共享库函数的访问⼊⼝,由动态链接器在运⾏时进⾏修改。对于GOT的理解,我们后⾯会说。从 执⾏视图 来看:告诉操作系统哪些模块可以被加载进内存。加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。
// 查看目标文件
$ readelf -h hello.o
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 VABI Version: 0Type: REL (Relocatable file) # 指出ELF文件的类型Machine: Advanced Micro Devices X86-64 # 该程序需要的体系结构Version: 0x1Entry point address: 0x0 # 系统第一个传输控制的虚拟地址,在那启动进程。假如文件没有关联入口点,该成员就保持为0。Start of program headers: 0 (bytes into file)Start of section headers: 728 (bytes into file)Flags: 0x0Size of this header: 64 (bytes) # 保存着ELF头大小(以字节计数)Size of program headers: 0 (bytes) # 保存着在文件的程序头表(program header table)中一个入口的大小Number of program headers: 0 # 保存着在程序头表中入口的个数。因此,e_phentsize和e_phnum的乘积就是表的大小(以字节计数).假如没有程序头表,变量为0。Size of section headers: 64 (bytes) # 保存着section头的大小(以字节计数)。一个section头是在section头表的一个入口Number of section headers: 13 # 保存着在section header table中的入口数目。因此,e_shentsize和e_shnum的乘积就是section头表的大小(以字节计数)。假如文件没有section头表,值为0。Section header string table index: 12 # 保存着跟section名字字符表相关入口的section头表(section header table)索引。// 查看可执行程序
$ gcc *.o
$ readelf -h a.out
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Shared object file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060Start of program headers: 64 (bytes into file)Start of section headers: 14768 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 13Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
7. 理解连接与加载
7-1 静态链接
⽆论是⾃⼰的.o, 还是静态库中的.o,本质都是把.o⽂件进⾏连接的过程
所以:研究静态链接,本质就是研究.o是如何链接的
$ ll
-rw-rw-r-- 1 user user 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 user user 103 Oct 31 15:36 hello.c
user@bite:~/test/test/test$ gcc -c *.c
user@bite:~/test/test/test$ gcc *.o -o main.exe
$ ll
-rw-rw-r-- 1 user user 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 user user 1672 Oct 31 15:46 code.o
-rw-rw-r-- 1 user user 103 Oct 31 15:36 hello.c
-rw-rw-r-- 1 user user 1744 Oct 31 15:46 hello.o
-rwxrwxr-x 1 user user 16752 Oct 31 15:46 main.exe*
下面我们使用objdump -d 命令:将代码段(.text)进⾏反汇编查看
$ objdump -d code.ocode.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <run>:0: f3 0f 1e fa endbr64 4: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <run+0xf>f: e8 00 00 00 00 callq 14 <run+0x14>14: 90 nop15: 5d pop %rbp16: c3 retq $ objdump -d hello.ohello.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: f3 0f 1e fa endbr64 4: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <main+0xf>f: e8 00 00 00 00 callq 14 <main+0x14>14: b8 00 00 00 00 mov $0x0,%eax19: e8 00 00 00 00 callq 1e <main+0x1e>1e: 5d pop %rbp1f: c3 retq
hello.c#include<stdio.h>void run();int main()
{printf("hello world!\n");run();return 0;
}
code.c#include<stdio.h>void run()
{printf("running...\n");
}
我们可以看到这⾥的call指令,它们分别对应之前调⽤的printf和run函数,但是你会发现他们的跳转地址都被设成了0。那这是为什么呢?
这个地址会在哪个时候被修正?
链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表⾥记录的地址将其修正。
# 查看code.o的代码段
$ objdump -d code.ocode.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <run>:0: f3 0f 1e fa endbr64 4: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <run+0xf>
<run+0xf>:f: e8 00 00 00 00 callq 14 <run+0x14>14: 90 nop15: 5d pop %rbp16: c3 retq
# 查看hello.o的代码段
$ objdump -d hello.ohello.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: f3 0f 1e fa endbr64 4: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <main+0xf>
<main+0xf>:f: e8 00 00 00 00 callq 14 <main+0x14>14: b8 00 00 00 00 mov $0x0,%eax19: e8 00 00 00 00 callq 1e <main+0x1e>1e: 5d pop %rbp1f: c3 retq
至此就是之前的结论: 多个.o彼此不知道对方
# 读取code.o符合表
$ readelf -s code.oSymbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 9 9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 run
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
puts: 就是printf的实现
UND就是: undefine,表示未定义说白了就是本.o文件找不到
# 读取hello.o符合表
user@ite:~/test/test/test$ readelf -s hello.oSymbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 9 9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 37 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run
puts: 就是printf的实现, run就是我们自己的方法在hello.o中未定义(因为在code.o中)
UND就是: undefine,表示未定义说白了就是本.o文件找不到
# 读取 main.exe 符号表
$ readelf -s main.exeSymbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE BIND DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable2: 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_registerTMCloneTable6: 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 Name0: 0000000000000000 0 NOTYPE BIND 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: 0000000000000520 0 SECTION LOCAL DEFAULT 10
10: 00000000000005e0 0 SECTION LOCAL DEFAULT 11
11: 0000000000000100 0 SECTION LOCAL DEFAULT 12
12: 0000000000000100 0 SECTION LOCAL DEFAULT 13
13: 0000000000000120 0 SECTION LOCAL DEFAULT 14
14: 0000000000000140 0 SECTION LOCAL DEFAULT 15
15: 0000000000000150 0 SECTION LOCAL DEFAULT 16
16: 0000000000000160 0 SECTION LOCAL DEFAULT 17
17: 0000000000000180 0 SECTION LOCAL DEFAULT 18
18: 0000000000000190 0 SECTION LOCAL DEFAULT 19
19: 0000000000000200 0 SECTION LOCAL DEFAULT 20
20: 0000000000000210 0 SECTION LOCAL DEFAULT 21
21: 0000000000000220 0 SECTION LOCAL DEFAULT 22
22: 0000000000000230 0 SECTION LOCAL DEFAULT 23
23: 0000000000000240 0 SECTION LOCAL DEFAULT 24
24: 0000000000000250 0 SECTION LOCAL DEFAULT 25
25: 0000000000000260 0 SECTION LOCAL DEFAULT 26
26: 0000000000000270 0 SECTION LOCAL DEFAULT 27
27: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
28: 0000000000000100 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
29: 0000000000000110 0 FUNC LOCAL DEFAULT 16 register_tm_clones
30: 0000000000000120 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
31: 0000000000000410 1 OBJECT LOCAL DEFAULT 26 completed.8061
32: 00000000000003c0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin
33: 0000000000000140 0 FUNC LOCAL DEFAULT 16 frame_dummy
34: 00000000000003b8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_entry
35: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
38: 0000000000000200 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
39: 0000000000000000 0 NOTYPE LOCAL DEFAULT ABS _edata
40: 00000000000003d0 0 NOTYPE LOCAL DEFAULT ABS __bss_start
41: 00000000000003d8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
42: 00000000000002b0 0 NOTYPE LOCAL DEFAULT 21 __init_array_start
43: 00000000000003b8 0 OBJECT LOCAL DEFAULT 24 __GNU_EH_FRAME_HDR
44: 0000000000000100 0 FUNC LOCAL DEFAULT 12 _init
45: 0000000000000190 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini
46: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
47: 0000000000000000 0 NOTYPE WEAK DEFAULT UND data_start
48: 0000000000000000 0 FUNC WEAK DEFAULT UND puts@GLIBC_2.2.5
49: 0000000000000410 0 NOTYPE GLOBAL DEFAULT 25 _edata
50: 0000000000000114 23 FUNC GLOBAL HIDDEN 16 run
51: 0000000000000190 0 FUNC GLOBAL DEFAULT 16 _fini
52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (3)
53: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
54: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
55: 00000000000003d0 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
56: 0000000000000130 37 FUNC GLOBAL HIDDEN 16 main
57: 0000000000000418 0 NOTYPE GLOBAL DEFAULT ABS _end
58: 0000000000000160 44 FUNC GLOBAL DEFAULT 16 __libc_csu_init
59: 0000000000000100 0 FUNC GLOBAL DEFAULT 12 _init
# 查看可执行程序的所有section清单
$ readelf -S a.outThere are 31 section headers, starting at offset 0x39d0:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .interp PROGBITS 0000000000000318 00000318000000000000001c 0000000000000000 A 0 0 1[ 2] .note.gnu.property NOTE 0000000000000338 000003380000000000000020 0000000000000000 A 0 0 8[ 3] .note.gnu.build-id NOTE 0000000000000358 000003580000000000000024 0000000000000000 A 0 0 4[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c0000000000000020 0000000000000000 A 0 0 4[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a00000000000000030 0000000000000000 A 6 0 8[ 6] .dynsym DYNSYM 00000000000003d0 000003d000000000000000d8 0000000000000018 A 7 1 8[ 7] .dynstr STRTAB 00000000000004a8 000004a8000000000000008a 0000000000000000 A 0 0 1[ 8] .gnu.version VERSYM 0000000000000532 00000532000000000000000e 0000000000000002 A 6 0 2[ 9] .gnu.version_r VERNEED 0000000000000540 000005400000000000000040 0000000000000000 A 7 1 8[10] .rela.dyn RELA 0000000000000580 0000058000000000000000c0 0000000000000018 A 6 0 8[11] .rela.plt RELA 0000000000000640 000006400000000000000030 0000000000000018 AI 6 24 8[12] .init PROGBITS 0000000000001000 000010000000000000000017 0000000000000000 AX 0 0 4[13] .plt PROGBITS 0000000000001020 000010200000000000000040 0000000000000010 AX 0 0 16[14] .plt.got PROGBITS 0000000000001060 000010600000000000000010 0000000000000010 AX 0 0 16[15] .plt.sec PROGBITS 0000000000001070 000010700000000000000020 0000000000000010 AX 0 0 16[16] .text PROGBITS 0000000000001090 000010900000000000000125 0000000000000000 AX 0 0 16[17] .fini PROGBITS 00000000000011b5 000011b50000000000000009 0000000000000000 AX 0 0 4[18] .rodata PROGBITS 0000000000002000 000020000000000000000014 0000000000000000 A 0 0 4[19] .eh_frame_hdr PROGBITS 0000000000002014 00002014000000000000003c 0000000000000000 A 0 0 4[20] .eh_frame PROGBITS 0000000000002050 000020500000000000000118 0000000000000000 A 0 0 8[21] .init_array INIT_ARRAY 0000000000003db8 00002db80000000000000008 0000000000000008 WA 0 0 8[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc00000000000000008 0000000000000008 WA 0 0 8[23] .dynamic DYNAMIC 0000000000003dc8 00002dc800000000000001f0 0000000000000010 WA 7 0 8[24] .got PROGBITS 0000000000003fb8 00002fb80000000000000048 0000000000000008 WA 0 0 8[25] .data PROGBITS 0000000000004000 000030000000000000000010 0000000000000000 WA 0 0 8[26] .bss NOBITS 0000000000004010 000030100000000000000008 0000000000000000 WA 0 0 1[27] .comment PROGBITS 0000000000000000 00003010000000000000002a 0000000000000001 MS 0 0 1[28] .gnu.build.attributes NOTE 0000000000000000 0000303a0000000000000030 0000000000000000 0 0 4[29] .symtab SYMTAB 0000000000000000 0000306a00000000000009f0 0000000000000018 30 55 8[30] .strtab STRTAB 0000000000000000 00003a5a0000000000000536 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)
hello.o和code.o的.text被合并了,是main.exe的第16个section
关于hello.o或者code.o call后⾯的00 00 00 00有没有被修改成为具体的最终函数地址呢?
$ objdump -d main.exe # 反汇编main.exe只查看代码段信息,包含源代码main.exe: file format elf64-x86-64Disassembly of section .init:0000000000001000 <_init>:1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__>100f: 48 85 c0 test %rax,%rax1012: 74 02 je 1016 <_init+0x16>1014: ff d0 callq *%rax1016: 48 83 c4 08 add $0x8,%rsp101a: c3 retq Disassembly of section .plt:0000000000001020 <.plt>:1020: ff 35 9a 2f 00 00 pushq 0x2f9a(%rip) # 3fb0 <_GLOBAL_OFFSET_TABLE_+0x10>1026: ff 25 9c 2f 00 00 jmpq *0x2f9c(%rip) # 3fb8 <_GLOBAL_OFFSET_TABLE_+0x18>102c: 0f 1f 40 00 nopl 0x0(%rax)Disassembly of section .plt.got:0000000000001040 <__cxa_finalize@plt>:1040: f3 0f 1e fa endbr64 1044: ff 25 ff 2f 00 00 jmpq *0x2fff(%rip) # 4044 <__cxa_finalize@GLIBC_2.2.5>104a: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .plt.sec:0000000000001050 <puts@plt>:1050: f3 0f 1e fa endbr64 1054: ff 25 75 2f 00 00 jmpq *0x2f75(%rip) # 40d0 <puts@GLIBC_2.2.5>105a: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .text:0000000000001060 <_start>:1060: f3 0f 1e fa endbr64 1064: 31 ed xor %ebp,%ebp1066: 49 89 d1 mov %rdx,%r91069: 5e pop %rsi106a: 48 89 e2 mov %rsp,%rdx106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp1071: 50 push %rax1072: 54 push %rsp1073: 48 8d 05 86 01 00 00 lea 0x186(%rip),%rax # 1200 <__libc_csu_fini>107a: 48 8d 0d 8f 01 00 00 lea 0x18f(%rip),%rcx # 1210 <__libc_csu_init>1081: 48 8d 3d 38 00 00 00 lea 0x38(%rip),%rdi # 10c0 <main>1088: ff 15 52 2f 00 00 callq *0x2f52(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>108e: f4 hlt 108f: 90 nop 0000000000001090 <deregister_tm_clones>:1090: 48 8d 3d 79 2f 00 00 lea 0x2f79(%rip),%rdi # 4010 <__TMC_END__>1097: 48 8d 72 2f 00 00 lea 0x2f2f(%rip),%rsi # 402c <__TMC_END__+0x1c>109e: 48 39 f8 cmp %rdi,%rax10a1: 74 15 je 10b8 <deregister_tm_clones+0x28>10a3: 48 8b 05 2e 2f 00 00 mov 0x2f2e(%rip),%rax # 3fd8 <__ITM_deregisterTMCloneTable>10aa: 48 85 c0 test %rax,%rax10ad: 74 0b je 10ba <deregister_tm_clones+0x2a>10af: ff e0 jmpq *%rax10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)10b8: c3 retq 10b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)00000000000010c0 <register_tm_clones>:10c0: 48 8d 3d 49 2f 00 00 lea 0x2f49(%rip),%rdi # 4010 <__TMC_END__>10c7: 48 8d 35 42 2f 00 00 lea 0x2f42(%rip),%rsi # 4010 <__TMC_END__>10ce: 48 29 fe sub %rdi,%rsi10d1: 48 89 d1 mov %rdx,%rcx10d4: 48 c1 e3 03 shr $0x3,%rbx10d8: 48 c1 f8 03 sar $0x3,%rax10dc: 4c 89 f2 mov %r14,%rdx10df: 4c 89 ee mov %r13,%rsi10e2: 44 89 df mov %r15d,%edi10e5: 41 ff 14 bf callq *(%r15,%rdi,8)10e9: 48 83 c3 01 add $0x1,%rbx10ed: 48 39 dd cmp %rbx,%rbp10f0: 75 ea jne 10dc <register_tm_clones+0x1c>10f2: 48 83 c4 08 add $0x8,%rsp10f6: 5b pop %rbx10f7: 5d pop %rbp10f8: 41 5c pop %r1210fa: 41 5d pop %r1310fc: 41 5e pop %r1410fe: 41 5f pop %r151100: c3 retq 1101: 66 66 2e 0f 1f 84 00 data16 data16 nopw 0x0(%rax,%rax,1)1108: 00 00 00 0000000000001110 <__do_global_dtors_aux>:1110: f3 0f 1e fa endbr64 1114: 48 83 3d 85 2e 00 00 cmpq $0x0,0x2e85(%rip) # 4000 <__dso_handle>111b: 75 2b jne 1148 <__do_global_dtors_aux+0x38>111d: 55 push %rbp111e: 48 89 e5 mov %rsp,%rbp1121: 48 83 3d 66 2e 00 00 cmpq $0x0,0x2e66(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>1128: 74 07 je 1131 <__do_global_dtors_aux+0x21>112a: e8 11 ff ff ff callq 1040 <__cxa_finalize@plt>112f: e8 6c ff ff ff callq 10a0 <deregister_tm_clones>1134: c6 05 dd 2e 00 00 01 movb $0x1,0x2edd(%rip) # 4018 <__TMC_END__+0x18>113b: 5d pop %rbp113c: c3 retq 113d: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)1144: 0f 1f 40 00 nopl 0x0(%rax)1148: c3 retq 0000000000001150 <frame_dummy>:1150: f3 0f 1e fa endbr64 1154: e9 77 ff ff ff jmpq 10d0 <register_tm_clones>0000000000001160 <run>:1160: f3 0f 1e fa endbr64 1164: 55 push %rbp1165: 48 89 e5 mov %rsp,%rbp1168: 48 8d 3d e0 0e 00 00 lea 0xee0(%rip),%rdi # 204f <__IO_stdin_used+0x4b>116f: e8 7c fe ff ff callq 1050 <puts@plt>1174: 90 nop1175: 5d pop %rbp1176: c3 retq 0000000000001180 <main>:1180: f3 0f 1e fa endbr64 1184: 55 push %rbp1185: 48 89 e5 mov %rsp,%rbp1188: 48 8d 3d e0 0e 00 00 lea 0xee0(%rip),%rdi # 206f <__IO_stdin_used+0x6b>118f: e8 7c fe ff ff callq 1050 <puts@plt>1194: b8 00 00 00 00 mov $0x0,%eax1199: e8 c2 fd ff ff callq 1160 <run>119e: 89 c2 mov %eax,%edx11a0: b8 00 00 00 00 mov $0x0,%eax11a5: 5d pop %rbp11a6: c3 retq 11a7: 66 66 2e 0f 1f 84 00 data16 data16 nopw 0x0(%rax,%rax,1)11ae: 00 00 00 00 00000000000011b0 <__libc_csu_init>:11b0: f3 0f 1e fa endbr64 11b4: 41 57 push %r1511b6: 41 56 push %r1411b8: 49 8d 3d 1b 2e 00 00 lea 0x2e1b(%rip),%r15 # 401a <__frame_dummy_init_array_entry+0x4>11bf: 41 55 push %r1311c1: 41 54 push %r1211c3: 55 push %rbp11c4: 53 push %rbx11c5: 48 8d 2d 1c 2e 00 00 lea 0x2e1c(%rip),%rbp # 4028 <__do_global_dtors_aux_fini_array_entry>11cc: 31 db xor %ebx,%ebx11ce: 4c 29 fd sub %r15,%rbp11d1: 48 83 ec 08 sub $0x8,%rsp11d5: e8 81 fe ff ff callq 105c <_init>11da: 48 c1 fd 03 sar $0x3,%rbp11de: 74 18 je 11f8 <__libc_csu_init+0x48>11e0: 0f 1f 84 00 00 00 00 nopl 0x0(%rax)11d0: 4c 89 f2 mov %r14,%rdx11d3: 4c 89 ee mov %r13,%rsi11d6: 44 89 e7 mov %r12d,%edi11d9: 41 ff 14 df callq *(%r15,%rbx,8)11dd: 48 83 c3 01 add $0x1,%rbx11e1: 48 39 dd cmp %rbx,%rbp11e4: 75 ea jne 11d0 <__libc_csu_init+0x40>
<__libc_csu_init+0x40>:11e6: 48 83 c4 08 add $0x8,%rsp11ea: 5b pop %rbx11eb: 5d pop %rbp11ec: 41 5c pop %r1211ee: 41 5d pop %r1311f0: 41 5e pop %r1411f2: 41 5f pop %r1511f4: c3 retq 11f5: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)11fc: 00 00 00 00 0000000000001200 <__libc_csu_fini>:1200: f3 0f 1e fa endbr64 1204: c3 retq Disassembly of section .fini:0000000000001208 <_fini>:1208: f3 0f 1e fa endbr64 120c: 48 83 ec 08 sub $0x8,%rsp1210: 48 83 c4 08 add $0x8,%rsp1214: c3 retq
最终:1. 两个.o的代码段合并到了⼀起,并进⾏了统⼀的编址2. 链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进⾏相关call地址,完成代码调⽤
静态链接就是把库中的.o进⾏合并,和上述过程⼀样所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴的可执⾏⽂件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这其实就是静态链接的过程。
所以,链接过程中会涉及到对.o中外部符号进⾏地址重定位。
7-2 ELF加载与进程地址空间
7-2-1 虚拟地址/逻辑地址
⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执⾏程序进⾏统⼀编址了
这是对可执行程序完成在磁盘上的编址,平坦模式编址也就是在线性地址上进行编址,对于所有可执行程序,内部都是互相独立的,所以所有函数,变量编址起始偏移量都为0
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?从ELF各个segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end] 等范围数据,另外在⽤详细地址,填充⻚表
7-2-2 重新理解进程虚拟地址空间
$ gcc *.o
$ readelf -h a.out
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Shared object file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060Start of program headers: 64 (bytes into file)Start of section headers: 14768 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 13Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
在磁盘中的可执行程序,加载到内存中时,不需要再重新编址,只需要与页表建立关系就行了,这样就能执行每条指令了
那么CPU是怎么知道的可执行程序的起始地址是多少呢?也就是说,CPU是怎么知道的该从哪里开始执行的呢?
我们上面说到了,mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据要从ELF文件中获取数据,其中就有程序的入口地址
程序加载时的关键步骤:
当你执行
./a.out
(可执行程序)时,Linux 会:(1)创建进程:
task_struct
+mm_struct
内核为新进程分配
task_struct
,记录进程基本信息。为进程创建
mm_struct
,初始化虚拟内存空间。(2)解析 ELF 文件:找入口地址
内核通过文件系统读取磁盘中的
a.out
(ELF 格式),解析其ELF 头,提取Entry Point Address
(程序入口地址,如0x1060
)。这个入口地址是虚拟地址,属于代码段(
.text
)的vm_area_struct
。(3)建立虚拟内存映射
内核为程序的各个段(代码段、数据段、BSS 等)创建
vm_area_struct
,并挂载到mm_struct
的链表中。代码段的
vm_area_struct
会被标记为可执行(VM_EXEC
),其虚拟地址范围包含Entry Point Address
。(4)关联页表,映射物理内存
mm_struct
包含页表基址(通过CR3
寄存器关联 CPU)。内核会为虚拟地址(如入口地址
0x1060
)建立页表映射,将虚拟地址对应到物理内存(实际加载的指令)。(5)设置 CPU 上下文:让 CPU 找到入口
内核在
task_struct
中保存进程上下文,包括:
指令指针(
RIP
):设置为 ELF 头中的Entry Point Address
(如0x1060
)。页表基址(
CR3
):指向mm_struct
关联的页表,让 CPU 能通过虚拟地址找到物理内存。
7-3 动态链接与动态库加载
7-3-1 进程如何看到动态库
7-3-2 进程间如何共享库的
7-3-3 动态链接
动态链接其实远⽐静态链接要常⽤得多。⽐如我们查看下 hello 这个可执⾏程序依赖的动态库,会发现它就⽤到了⼀个c动态链接库:
$ ldd hello # ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。linux-vdso.so.1 => (0x00007fffeb1ab000)libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)
这⾥的 libc.so是C语⾔的运⾏时库,⾥⾯提供了常⽤的标准输⼊输出⽂件字符串处理等等这些功能。那为什么编译器默认不使⽤静态链接呢?静态链接会将编译产⽣的所有⽬标⽂件,连同⽤到的各种库,合并形成⼀个独⽴的可执⾏⽂件,它不需要额外的依赖就可以运⾏。照理来说应该更加⽅便才对是吧?静态链接最⼤的问题在于⽣成的⽂件体积⼤,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费⼤量的硬盘空间。这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享。
⾸先要交代⼀个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
$ ldd /usr/bin/lslinux-vdso.so.1 (0x00007fffdd85f000)libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1(0x00007f42c025a000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f42c0068000)libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0(0x00007f42bffd7000)libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f42bffd1000)/lib64/ld-linux-x86-64.so.2 (0x00007f42c02b6000)libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0(0x00007f42bffae000)
$ ldd main.exelinux-vdso.so.1 (0x00007fff231d6000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f197ec3b000)/lib64/ld-linux-x86-64.so.2 (0x00007f197ee3e000)
在 _start 函数中,会执⾏⼀系列初始化操作,这些操作包括:1. 设置堆栈:为程序创建⼀个初始的堆栈环境。2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。3. 动态链接:这是关键的⼀步, _start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。动态链接器:1.动态链接器(如ld-linux.so)负责在程序运⾏时加载动态库。2.当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。环境变量和配置⽂件:1.Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置⽂件(如/etc/ld.so.conf及其⼦配置⽂件)来指定动态库的搜索路径。2.这些路径会被动态链接器在加载动态库时搜索。缓存⽂件:1.为了提⾼动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存⽂件。该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先搜索这个缓存⽂件。4. 调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。5. 调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执⾏控制权才正式交给⽤⼾编写的代码。6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调⽤ _exit 函数来终⽌程序
因为有动态链接器,这也对应了我们前面讲到的把动态库告诉操作系统的四种方法
上述过程描述了C/C++程序在 main 函数之前执⾏的⼀系列操作,但这些操作对于⼤多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,⽽不需要关⼼底层的初始化过程。然⽽,了解这些底层细节有助于更好地理解程序的执⾏流程和调试问题
动态库为了随时进⾏加载,为了⽀持并映射到任意进程的任意位置,对动态库中的⽅法,统⼀编址,采⽤相对编址的⽅案进⾏编制的(其实可执⾏程序也⼀样,都要遵守平坦模式,只不过exe是直接加载的)
1. 动态库也是⼀个⽂件,要访问也是要被先加载,要加载也是要被打开的2. 让我们的进程找到动态库的本质:也是⽂件操作,不过我们访问库函数,通过虚拟地址进⾏跳转访问的,所以需要把动态库映射到进程的地址空间中
步骤 1:找到磁盘文件的数据块(文件系统层)
当进程需要加载
libc.so
(或其他共享库)时:
路径解析:通过
struct path
(图中struct path f_path
)解析库的路径(如/lib64/libc.so
),找到对应的struct dentry
(目录项)。关联 inode:
struct dentry
包含struct inode *d_inode
,inode
是文件的元数据核心,记录文件在磁盘的数据块位置(图中struct ext2_inode { __le32 i_block[EXT2_N_BLOCKS]; }
)。定位数据块:通过
inode
的i_block
数组,内核可以找到libc.so
在磁盘上的物理数据块,完成 “文件 → 磁盘块” 的映射。步骤 2:库加载(内存分配与映射准备)
创建
vm_area_struct
:内核为共享库在进程的虚拟内存中,创建一个新的vm_area_struct
(虚拟内存区域),标记为共享区(图中 “共享区”)。关联文件对象:
vm_area_struct
中包含struct file *vm_file
,指向libc.so
的struct file
(文件对象),建立 “虚拟内存区域 → 磁盘文件” 的关联。步骤 3:映射(虚拟地址 → 物理内存 / 内核缓存)
页表映射:内核通过页表(图中 “页表”),将共享库的虚拟地址区域(
vm_start
~vm_end
)映射到物理内存。
由于共享库是 “只读” 且多进程复用的,Linux 会利用内核缓存(图中 “库内容其实就是在文件内核缓冲区中”),避免重复加载到物理内存。
延迟加载(按需映射):实际执行时,若访问未加载的页,会触发缺页异常,内核再从磁盘读数据到物理内存,完成映射(这是 “按需加载” 的优化)。
步骤 4:得到库起始虚拟地址(进程视角)
虚拟地址暴露:进程通过
vm_area_struct
的vm_start
,得到共享库在进程虚拟地址空间的起始地址(图中 “4. 得到库起始虚拟地址”)。符号解析:动态链接器(
ld-linux-x86-64.so.2
)会解析共享库的符号(函数、变量),让进程能通过虚拟地址调用库的代码(如printf
实际调用libc.so
的实现)。
库已经被我们映射到了当前进程的地址空间中库的虚拟起始地址我们也已经知道了库中每⼀个⽅法的偏移量地址我们也知道所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅法⽽且:整个调⽤过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完全在进程地址空间中进⾏的.
只要起始地址+偏移量,就能得出所有方法的地址
也就是说,我们的程序运⾏之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置(这个叫做加载地址重定位)等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
所以动态链接采⽤的做法是在 .data (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数的地址。
1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找到GOT表。3. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。4. 这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT。
在 Linux 系统中,不同进程可以同时使用相同的动态链接库,为了避免地址冲突,操作系统采用地址空间布局随机化(ASLR)技术,使得动态库每次被加载到内存中的虚拟地址是不确定的。GOT 为程序提供了一种间接访问动态库函数和变量的方式,屏蔽了动态库加载地址变化的影响。程序在编译和链接时,只需要知道 GOT 表项的相对位置,而不需要关心动态库中函数和变量的具体地址。在运行时,动态链接器根据实际加载的动态库地址,更新 GOT 表项中的值,使得程序通过 GOT 能够正确访问到动态库中的函数和变量。
7-3-4 库间依赖
1.不仅仅有可执⾏程序调⽤库2.库也会调⽤其他库!!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢??3.库中也有.GOT,和可执⾏⼀样!这也就是为什么⼤家为什么都是ELF的格式!
由于动态链接在程序加载的时候需要对⼤量函数进⾏重定位,这⼀步显然是⾮常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,⽐如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在程序⼀开始就对所有函数进⾏重定位,不如将这个过程推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运⾏期间⼀次都不会被使⽤到。
至于在静态链接过程中,所有的代码和数据都被合并到可执行文件中,函数和变量的地址在链接阶段就已经确定下来。比如,在链接时,链接器知道某个函数在最终可执行文件中的具体偏移地址,程序在运行时可以直接使用这个固定的地址来调用函数,不需要像动态链接那样,在运行时再去解析和查找函数的地址,因此也就不需要 GOT 来辅助解析地址。
好了,关于动静态库的内容我们就讲到这里了,我们下次再见!