当前位置: 首页 > news >正文

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代码,现在我们任意新增"库⽂件"
// 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.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
t: 列出静态库中的⽂件
v:verbose 详细信息
2-2 静态库使⽤
任意⽬录下,新建main.c,引⼊库头⽂件
#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_PATH
4. 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");
}
// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o
可以看到,在编译之后会⽣成两个扩展名为 .o 的⽂件,它们被称作⽬标⽂件。要注意的是如果我们修改了⼀个原⽂件,那么只需要单独编译它这⼀个,⽽不需要浪费时间重新编译整个⼯程。⽬标⽂件是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。
5. ELF⽂件
要理解编译链链接的细节,我们不得不了解⼀下ELF⽂件。其实有以下四种⽂件其实都是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,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。
对于 程序头表 和 节头表 ⼜有什么⽤呢,其实 ELF ⽂件提供 2 个不同的视图/视⻆来让我们理解这
两个部分:
链接视图(Linking view) - 对应节头表 Section header table
        1.⽂件结构的粒度更细,将⽂件按功能模块的差异进⾏划分,静态链接分析的时候⼀般关注的是链接视图,能够理解 ELF ⽂件中包含的各个部分的信息。
        2.为了空间布局上的效率,将来在链接⽬标⽂件时,链接器会把很多节(section)合并,规整成可执⾏的段(segment)、可读写的段、只读段等。合并了后,空间利⽤率就⾼了,否则,很⼩的很⼩的⼀段,未来物理内存⻚浪费太⼤(物理内存⻚分配⼀般都是整数倍⼀块给你,⽐如4k),所以,链接器趁着链接就把⼩块们都合并了。
执⾏视图(execution view) - 对应程序头表 Program header table
        1.告诉操作系统,如何加载可执⾏⽂件,完成进程内存的初始化。⼀个可执⾏程序的格式中,⼀定有 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的理解,我们后⾯会说。
从 执⾏视图 来看:
告诉操作系统哪些模块可以被加载进内存。
加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。
我们可以在 ELF头 中找到⽂件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看下hello.o这个可重定位⽂件的主要信息:
// 查看目标文件
$ 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
而对于 ELF HEADER 这部分来说,我们只⽤知道其作⽤即可,它的主要⽬的是定位⽂件的其他部分。
现在我们对ELF文件的四个部分都有了了解,下面让我们深入理解链接
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.o 中的 main 函数不认识 printfrun 函数
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。那这是为什么呢?
其实就是在编译 hello.c 的时候,编译器是完全不知道 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.ocode.o.text被合并了,是main.exe的第16section
怎么证明上⾯的说法?
关于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程序,在没有被加载到内存的时候,本来就有地址,当代计算机⼯作的时候,都采⽤"平坦
模式"进⾏⼯作。所以也要求ELF对⾃⼰的代码和数据进⾏统⼀编址,下⾯是 objdump -S 反汇编
之后的代码:
最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执⾏程序进⾏统⼀编址了

这是对可执行程序完成在磁盘上的编址,平坦模式编址也就是在线性地址上进行编址,对于所有可执行程序,内部都是互相独立的,所以所有函数,变量编址起始偏移量都为0

进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?从ELF各个segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end] 等范围数据,另外在⽤详细地址,填充⻚表
所以:虚拟地址机制,不光光OS要⽀持,编译器也要⽀持
7-2-2 重新理解进程虚拟地址空间
ELF 在被编译好之后,会把⾃⼰未来程序的⼊⼝地址记录在ELF header的Entry字段中:
$ 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)
在C/C++程序中,当程序开始执⾏时,它⾸先并不会直接跳转到 main 函数。实际上,程序的⼊⼝点是 _start ,这是⼀个由C运⾏时库(通常是glibc)或链接器(如ld)提供的特殊函数。
在 _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(或其他共享库)时:

  1. 路径解析:通过 struct path(图中 struct path f_path)解析库的路径(如 /lib64/libc.so),找到对应的 struct dentry(目录项)。

  2. 关联 inodestruct dentry 包含 struct inode *d_inodeinode 是文件的元数据核心,记录文件在磁盘的数据块位置(图中 struct ext2_inode { __le32 i_block[EXT2_N_BLOCKS]; })。

  3. 定位数据块:通过 inode 的 i_block 数组,内核可以找到 libc.so 在磁盘上的物理数据块,完成 “文件 → 磁盘块” 的映射。

步骤 2:库加载(内存分配与映射准备)

  1. 创建 vm_area_struct:内核为共享库在进程的虚拟内存中,创建一个新的 vm_area_struct(虚拟内存区域),标记为共享区(图中 “共享区”)。

  2. 关联文件对象vm_area_struct 中包含 struct file *vm_file,指向 libc.so 的 struct file(文件对象),建立 “虚拟内存区域 → 磁盘文件” 的关联。

步骤 3:映射(虚拟地址 → 物理内存 / 内核缓存)

  1. 页表映射:内核通过页表(图中 “页表”),将共享库的虚拟地址区域(vm_start ~ vm_end)映射到物理内存。

    • 由于共享库是 “只读” 且多进程复用的,Linux 会利用内核缓存(图中 “库内容其实就是在文件内核缓冲区中”),避免重复加载到物理内存。

  2. 延迟加载(按需映射):实际执行时,若访问未加载的页,会触发缺页异常,内核再从磁盘读数据到物理内存,完成映射(这是 “按需加载” 的优化)。

步骤 4:得到库起始虚拟地址(进程视角)

  1. 虚拟地址暴露:进程通过 vm_area_struct 的 vm_start,得到共享库在进程虚拟地址空间的起始地址(图中 “4. 得到库起始虚拟地址”)。

  2. 符号解析:动态链接器(ld-linux-x86-64.so.2)会解析共享库的符号(函数、变量),让进程能通过虚拟地址调用库的代码(如 printf 实际调用 libc.so 的实现)。

我们的程序,怎么进⾏库函数调⽤
库已经被我们映射到了当前进程的地址空间中
库的虚拟起始地址我们也已经知道了
库中每⼀个⽅法的偏移量地址我们也知道
所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅法
⽽且:整个调⽤过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完
全在进程地址空间中进⾏的.

只要起始地址+偏移量,就能得出所有方法的地址

全局偏移量表GOT(global offset table)
也就是说,我们的程序运⾏之前,先把所有库加载并映射,所有库的起始虚拟地址都应该
提前知道
然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置
(这个叫做加载地址重定位)
等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
所以动态链接采⽤的做法是在 .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 来辅助解析地址。

好了,关于动静态库的内容我们就讲到这里了,我们下次再见!

http://www.dtcms.com/a/332723.html

相关文章:

  • Java—注解
  • mysql-条件查询案例
  • zabbix部署问题后常见问题
  • Codeforces 无路可走
  • 分布式系统设计的容错机制
  • AI优质信息源汇总:含X账号,Newsletter,播客,App
  • 如何在 FastAPI 中玩转 APScheduler,让任务定时自动执行?
  • 上下文块嵌入(contextualized-chunk-embeddings)
  • collections:容器数据类型
  • C语言——深入理解指针(四)
  • 完整技术栈分享:基于Hadoop+Spark的在线教育投融资大数据可视化分析系统
  • 使用XXL-SSO实现登录认证以及权限管控
  • 解决 MySQL 查询速度缓慢的问题
  • Filebeat 轻量级日志采集实践:安装、配置、多行合并、JSON 解析与字段处理
  • Java集合Map与Stream流:Map实现类特点、遍历方式、Stream流操作及Collections工具类方法
  • 【软件设计模式】前置知识类图、七大原则(精简笔记版)
  • C++ 调试报错 常量中有换行符
  • 基于桥梁三维模型的无人机检测路径规划系统设计与实现
  • Cursor 分析 bug 记录
  • 3D视觉与空间智能
  • imx6ull-驱动开发篇25——Linux 中断上半部/下半部
  • 智谱开源了最新多模态模型,GLM-4.5V
  • 关系型数据库从入门到精通:MySQL 核心知识全解析
  • 高并发系统性能优化实战:实现5万并发与毫秒级响应
  • Kafka生产者——提高生产者吞吐量
  • LeetCode 面试经典 150_数组/字符串_最长公共前缀(20_14_C++_简单)(暴力破解)(求交集)
  • 简单使用 TypeScript 或 JavaScript 创建并发布 npm 插件
  • 从零到一:发布你的第一个 npm 开源库(2025 终极指南)
  • IT资讯 | VMware ESXi高危漏洞影响国内服务器
  • Day62--图论--97. 小明逛公园(卡码网),127. 骑士的攻击(卡码网)