第 7 章 链接
# 第 7 章 链接
笔记
- 链接是由叫做链接器程序自动执行的
- 链接 (linking) 是将各种代码和数据片段收集并组合为一个单一文件的过程
- 这个文件可被加载到内存并执行
- 使用分离编译成为可能
- 链接执行的时机
- 编译时
- 加载时
- 操作系统中的加载器 (loader) 函数将可执行文件中的代码和数据复制到内存,然后将控制转移到这个程序的开头
- 运行时
- 由应用程序来执行
# 7.1 编译器驱动程序
编译器驱动程序 (complier driver)
- 代表用户在需要时调用语言预处理器、编译器、汇编器和链接器
静态链接的过程
-
cpp
为 C 预处理器- 由
main.c
翻译为main.i
- 由
cc1
为 C 编译器- 由
main.i
翻译成一个汇编语言文件main.s
- 由
as
为汇编器,将main.s
翻译为一个可重定位的目标文件 (relocatable object file)main.o
ld
为链接器程序,将main.o
和sum.o
以及一些必要的系统目标文件组合起来,创建一个可执行的目标文件 (executable object file)
gcc -v
可以查看编译的具体的过程
# 7.2 静态链接
链接器的两个主要任务
- 符号解析 (symbol resolution)
- 符号解析的目的是将每个符号引用正好和每个符号定义关联起来
- 重定位 (relocation)
- 编译器和汇编器生成从地址 0 开始的代码和数据节
- 链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节
- 然后修改所有这些符号的引用,使得它们指向符号定义的内存位置
链接器使用汇编器产生的重定位条目(relocation entry)的详细指令,不加甄别地执行这样的重定位。
# 7.3 目标文件
三种形式的目标文件
- 可重定位目标文件
- 包含二进制代码和数据,
- 其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
- 可执行目标文件
- 包含二进制代码和数据
- 其形式可以被直接复制到内存并执行
- 包含二进制代码和数据
- 共享目标文件
- 一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接
目标文件的格式
- 现代 x86-64 Linux 和 Unix 系统使用可执行可链接格式(Executable and Linkable Format,ELF)
# 7.4 可重定位目标文件
ELF 头与节头部表
- ELF 头(ELF header)以一个 16 字节的序列开始
- 这个序列描述了生成该文件的系统的字的大小和字节顺序
- ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括
- ELF 头的大小
- 目标文件的类型(如可重定位、可执行或者共享的)
- 机器类型(如 X86-64)
- 节头部表(section header table)的文件偏移
- 以及节头部表中条目的大小和数量
- 不同节的位置和大小是由节头部表描述的
- 目标文件中每个节在节头部表中都有一个固定大小的条目(entry)。
典型的 ELF 可重定位目标文件
节 (segment)
- 一个典型的 ELF 可重定位目标文件包含下面几个节
.text
- 已编译程序的机器代码
.rodata
- 只读数据
- 比如
printf
语句中的格式串和开关语句的跳转表。
- 比如
- 只读数据
.data
- 已初始化的全局和静态 C 变量
- 局部 C 变量在运行时被保存在栈中
.bss
- 未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量
- 在目标文件中这个节不占据实际的空间,它仅仅是一个占位符
bss 可以理解为 better save space
目标文件格式区分已初始化和未初始化变量是为了空间效率
- 在目标文件中,未初始化变量不需要占据任何实际的磁盘空间
- 运行时,在内存中分配这些变量,初始值为 0
.symtab
- 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
- 每个可重定位目标文件在
.symtab
中都有一张符号表- 除非程序员特意用 STRIP 命令去掉它
- 和编译器中的符号表不同,
.symtab
符号表不包含局部变量的条目
.rel.text
- 一个
.text
节中位置的列表 - 当链接器把这个目标文件和其他文件组合时,需要修改这些位置
- 一般而言,任何调用外部函数或者引用全局变量的指令都需要修改
- 另一方面,调用本地函数的指令则不需要修改
可执行目标文件中并不需要重定位信息,因此通常省略
- 除非用户显式地指示链接器包含这些信息
- 一个
.rel.data
- 被模块引用或定义的所有全局变量的重定位信息
- 一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- 被模块引用或定义的所有全局变量的重定位信息
.debug
- 一个调试符号表,其条目是
- 程序中定义的局部变量和类型定义
- 程序中定义和引用的全局变量
- 原始的 C 源文件
- 只有以
-g
选项调用编译器驱动程序时,才会得到这张表
- 一个调试符号表,其条目是
.line
- 原始 C 源程序中的行号和 .text 节中机器指令之间的映射
- 只有以
-g
选项调用编译器驱动程序时,才会得到这张表。
.strtab
- 一个字符串表,其内容包括
.symtab
和.debug
节中的符号表- 节头部中的节名字
- 字符串表就是以
null
结尾的字符串的序列
- 一个字符串表,其内容包括
# 7.5 符号和符号表
每个可重定位目标模块 m 都有一个符号表 .symtab
,它包含 m 定义和引用的符号的信息
在链接器的上下文中,有三种不同的符号
- 由模块 m 定义并能被其他模块引用的全局符号
- 全局链接器符号对应于非静态的 C 函数和全局变量
- 由其他模块定义并被模块 m 引用的全局符号
- 这些符号称为外部符号
- 对应于在其他模块中定义的非静态 C 函数和全局变量
- 只被模块 m 定义和引用的局部符号
- 它们对应于带
static
属性的 C 函数和全局变量 - 这些符号在模块 m 中任何位置都可见,但是不能被其他模块引用
- 它们对应于带
.symtab
节包含 ELF 符号表
这张符号表包含一个条目的数组,每个条目的格式如下
typedef struct { int name; /* String table offset */ char type:4, /* Function or data (4 bits) */ binding:4; /* Local or global (4 bits) */ char reserved; /* Unused */ short section; /* Section header index */ long value; /* Section offset or absolute address */ long size; /* Object size in bytes */ } Elf64_Symbol;
1
2
3
4
5
6
7
8
9name
是字符串表 (.strtab
) 中的字节偏移每个符号都被分配到目标文件的某个节,由
section
字段表示- 该字段也是一个到节头部表的索引
- 有三个特殊的伪节(pseudosection),它们在节头部表中是没有条目的
ABS
代表不该被重定位的符号UNDEF
代表未定义的符号- 也就是在本目标模块中引用,但是却在其他地方定义的符号
COMMON
表示还未被分配位置的未初始化的数据目标- 对于
COMMON
符号value
字段给出对齐要求- 而
size
给出最小的大小
- 对于
只有可重定位目标文件中才有这些伪节,可执行目标文件中是没有的
COMMON
和.bss
的区别COMMON
- 未初始化的全局变量
.bss
- 未初始化的静态变量,以及初始化为 0 的全局或静态变量
可以使用 readelf
程序来查看目标文件的内容
# 7.6 符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来
引用和定义在相同模块中的局部符号的解析
- 编译器只允许每个模块中每个局部符号有一个定义
- 静态局部变量也会有本地链接器符号
- 但编译器需要确保它们拥有唯一的名字
全局符号的解析
- 当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时
- 编译器会假设该符号是在其他某个模块中定义的
- 其会生成一个链接器符号表条目,并把它交给链接器处理
- 如果链接器在它的任何输入模块中都找不到这个被引用符号的定义
- 链接器就输出一条(通常很难阅读的)错误信息并终止
- 多个目标文件可能会定义相同名字的全局符号
- 这种情况下,链接器可以标志一个错误
- gcc 中使用
-fno-common
的标志告诉链接器- 在遇到多重定义的全局符号时,触发一个错误
最新版的 gcc 会直接报错
- 可以使用
-fcommon
来达到书中的效果
- 可以使用
- gcc 中使用
- 也可以以某种方法选出一个定义并抛弃其他定义
- 这种情况下,链接器可以标志一个错误
# 7.6.1 链接器如何解析多重定义的全局符号
强符号与弱符号
- 在编译时,编译器向汇编器输岀每个全局符号,或者是强(strong)或者是弱(weak
- 汇编器把这个信息隐含地编码在可重定位目标文件的符号表里
- 函数和已初始化的全局变量是强符号
- 未初始化的全局变量是弱符号
Linux 链接器处理多重定义的符号名的规则
- 规则 1:不允许有多个同名的强符号
- 规则 2:如果有一个强符号和多个弱符号同名,那么选择强符号
- 规则 3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个
# 7.6.2 与静态库链接
静态库
- 将所有相关的目标模块打包成为一个单独的文件,称为静态库
- 可以用做链接器的输入
- 当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块
- 可以用做链接器的输入
- 在 Linux 系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中
- 存档文件是一组连接起来的可重定位目标文件的集合
- 有一个头部用来描述每个成员目标文件的大小和位置
- 存档文件名由后缀 .a 标识。
- 存档文件是一组连接起来的可重定位目标文件的集合
与静态库链接的示例
创建静态库
linux> gcc -c addvec.c multvec.c linux> ar rcs libvector.a addvec.o multvec.o
1
2编译时使用静态库
linux>gcc -c main2.c linux>gcc -static -o prog2c main2.o ./libvector.a
1
2- 或等价的使用
linux>gcc -c main2.c linux>gcc -static -o prog2c main2.o -L. -lvector
1
2-static
参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件-lvector
参数是libvector.a
的缩写L.
告诉链接器首先在当前目录下查找libvector.a
与静态库链接的过程
# 7.6.3 链接器如何使用静态库来解析引用
命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败
- 其逻辑是从左到右扫描命令行,逐个处理指定的库,处理完该库后其会丢掉该库中当前未被引用的内容
# 7.7 重定位
符号解析的作用
- 链接器完成了符号解析后,就将代码中的每个符号引用正好和一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来
- 此时,链接器也知道了它的输入目标模块中的代码节和数据节的确切大小
- 现在,就可以开始重定位步骤
重定位的两个步骤
- 重定位节和符号定义
- 在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节
- 例如,来自所有输入模块的
.data
节被全部合并成一个节,这个节成为输出的可执行目标文件的.data
节
- 例如,来自所有输入模块的
- 然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号
- 当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了
- 在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节
- 重定位节中的符号引用
- 在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址
- 要执行这一步,链接器依赖于可重定位目标模块中称为 重定位条目(relocation entry)的数据结构
# 7.7.1 重定位条目
无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目
- 这个条目会告诉链接器在将目标文件合并成可执行文件时如何修改这个引用
- 代码的重定位条目放在
.rel.text
中。已初始化数据的重定位条目放在.rel.data
中
ELF 重定位条目
typedef struct {
long offset; /* Offset of the reference to relocate */
long type:32, /* Relocation type */
symbol:32; /* Symbol table index */
long addend; /* Constant part of relocation expression */
} Elf64_Rela;
2
3
4
5
6
7
ELF 重定位类型
- ELF 定义了 32 种不同的重定位类型,其中有两种最基本的重定位类型
R_X86_64_PC32
- 重定位一个使用 32 位 PC 相对地址的引用
- 一个 PC 相对地址就是距程序计数器(PC)的当前运行时值的偏移量
- 当 CPU 执行一条使用 PC 相对寻址的指令时
- 它就将在指令中编码的 32 位值加上 PC 的当前运行时值,得到有效地址(如 call 指令的目标)
- PC 值通常是下一条指令在内存中的地址
- 参考 跳转指令的编码
- 重定位一个使用 32 位 PC 相对地址的引用
R_X86_64_32
- 重定位一个使用 32 位绝对地址的引用
- 通过绝对寻址,CPU 直接使用在指令中编码的 32 位值作为有效地址,不需要进一步修改
这两种重定位类型支持 x86-64 小型代码模型(small code model
- 该模型假设可执行目标文件中的代码和数据的总体大小小于 2GB
- 因此在运行时可以用 32 位 PC 相对地址来访问
- GCC 默认使用小型代码模型
- 大于 2GB 的程序可以用
-mcmodel=medium
(中型代码模型)和-mcmodel=large
(大型代码模型)标志来编译
- 该模型假设可执行目标文件中的代码和数据的总体大小小于 2GB
# 7.7.2 重定位符号引用
重定位算法
foreach section s {
foreach relocation entry r {
refptr = s + r.offset; /* ptr to reference to be relocated */
/* Relocate a PC-relative reference */
if (r.type == R_X86_64_PC32){
refaddr = ADDR(s) + r.offset; /* ref's run-time address */
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
}
/* Relocate an absolute reference */
if (r.type ==R_X86_64_32)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 本质上是填充指令中的地址字节
- 当使用相对寻址时,
addend
相当于地址所占用的字节数,从而计算相对于下一条指令的偏移值
# 7.8 可执行目标文件
典型的 ELF 可执行目标文件
-
- ELF 头描述文件的总体格式
- 它包括程序的入口点(entry point
- 即程序运行时要执行的第一条指令的地址
- 它包括程序的入口点(entry point
.text
、.rodata
和.data
节与可重定位目标文件中的节是相似的- 除了这些节已经被重定位到它们最终的运行时内存地址以外
.init
节定义了一个小函数,叫做_init
- 程序的初始化代码会调用它
- 因为可执行文件是完全链接的(已被重定位),所以它不再需要
.rel
节
- ELF 头描述文件的总体格式
# 7.9 加载可执行目标文件
Linux x86-64 运行时内存映像
-
- 代码段总是从地址
0x400000
处开始 - 后面是数据段
- 运行时堆在数据段之后,通过调用 malloc 库往上增长
- 堆后面的区域是为共享库保留的
- 用户栈总是从最大的合法用户地址(
)开始,向较小内存地址增长 - 栈上的区域,从地址(
)开始,是为内核(kernel)中的代码和数据保留的 - 所谓内核就是操作系统驻留在内存的部分
- 代码段总是从地址
# 7.10 动态链接共享库
静态库的缺点
- 静态库更新后,程序员必须显式地将他们的程序与更新了的库重新链接
- 另一个问题是几乎每个 C 程序都使用标准 I/O 函数,比如 printf 和 scanf
- 在运行时,这些函数的代码会被复制到每个运行进程的文本段中
- 在一个运行上百个进程的典型系统上,这将是对稀缺的内存系统资源的极大浪费
共享库与动态链接
- 共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物
- 共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来
- 这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的
- 共享库也称为共享目标(shared object)
- 在 Linux 系统中通常用
.so
后缀来表示 - 微软的操作系统大量地使用了共享库,它们称为
DLL
(动态链接库)
- 在 Linux 系统中通常用
共享库的优点
- 在任何给定的文件系统中,对于一个库只有一个
.so
文件 - 所有引用该库的可执行目标文件共享这个
.so
文件中的代码和数据- 而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中
- 如此,可以节约磁盘空间
- 在内存中,一个共享库的
.text
节的一个副本可以被不同的正在运行的进程共享- 主要通过动态链接和虚拟内存来实现
- 在任何给定的文件系统中,对于一个库只有一个
生成与使用共享库
生成共享库
linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c
1- -fpic 选项指示编译器生成与位置无关的代码
编译时使用共享库
linux> gcc -o prog2l main2.c ./libvector.so
1- 此时生成的
prog2l
包含一个.interp
节- 这一节包含动态链接器的路径名
- 动态链接器本身就是一个共享目标(如在 Linux 系统上的
ld-linux.so
)
- 此时生成的
动态链接的过程
- 加载器加载部分链接的可执行文件 prog2l
- 接着,它注意到 prog2l 包含一个
.interp 节
- 这一节包含动态链接器的路径名
- 加载器不会像它通常所做地那样将控制传递给应用,而是加载和运行动态链接器
- 然后,动态链接器通过执行下面的重定位完成链接任务
- 重定位
libc.so
的文本和数据到某个内存段 - 重定位
libvector.so
的文本和数据到另一个内存段 - 重定位 prog2l 中所有对由
libc.so
和libvector.so
定义的符号的引用
- 重定位
- 最后,动态链接器将控制传递给应用程序
- 从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变
# 7.11 从应用程序中加载和链接共享库
Linux 系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库
- 使用此类函数时,用 gcc 编译时,需要加上
-ldl
参数
dlopen
函数
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
// 返回:若成功则为指向句柄的指针,若出错则为 NULL。
2
3
4
5
dlopen
函数加载和链接共享库 filename- 用已用带
RTLD_GLOBAL
选项打开了的库解析filename
中的外部符号 - 如果当前可执行文件是带
-rdynamic
选项编译的- 那么对符号解析而言,它的全局符号也是可用的
- flag 参数
- 必须要么包括
RTLD_NOW
- 该标志告诉链接器立即解析对外部符号的引用
- 要么包括
RTLD_LAZY
标志- 该标志指示链接器推迟符号解析直到执行来自库中的代码
- 这两个值中的任意一个都可以和
RTLD_GLOBAL
标志取或
- 必须要么包括
- 用已用带
dlsym
函数
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);
// 返回:若成功则为指向符号的指针,若出错则为 NULL。
2
3
4
5
dlsym
函数的输入是一个指向前面已经打开了的共享库的句柄和一个symbol
名字- 如果该符号存在,就返回符号的地址,否则返回
NULL
- 如果该符号存在,就返回符号的地址,否则返回
dlclose
#include <dlfcn.h>
int dlclose (void *handle);
// 返回:若成功则为0,若出错则为-1.
2
3
4
5
- 如果没有其他共享库还在使用这个共享库,
dlclose
函数就卸载该共享库
dlerror
函数
include <dlfcn.h>
const char *dlerror(void);
// 返回:如果前面对 dlopen、dlsym 或 dlclose 的调用失败,
// 则为错误消息,如果前面的调用成功,则为 NULL。
2
3
4
5
6
dlerror
函数返回一个字符串- 它描述的是调用
dlopen
、dlsym
或者dlclose
函数时发生的最近的错误- 如果没有错误发生,就返回
NULL
- 如果没有错误发生,就返回
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* Dynamically load the shared library containing addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
/* Get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
/* Now we can call addvec() just like any other function */
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
/* Unload the shared library */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
编译命令
linux> gcc -rdynamic -o prog2r dll.c -ldl
1
# 7.12 位置无关代码
位置无关代码
- 可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC
- 用户对 GCC 使用
-fpic
选项指示 GNU 编译系统生成 PIC 代码 - 共享库的编译必须总是使用该选项
- 用户对 GCC 使用
- 这种方法使得共享库可以加载到内存的任何位置而无需链接器进行修改
- 从而可以允许无限多个进程可以共享一个共享模块的代码段的单一副本
- 当然,每个进程仍然会有它自己独有读/写数据块
- 从而可以允许无限多个进程可以共享一个共享模块的代码段的单一副本
PIC 数据引用
只是数据的引用,不包括对函数的引用
- 编译器通过运用以下事实来生成对全局变量的 PIC 引用
- 无论在内存中的何处加载一个目标模块(包括共享目标模块)
- 数据段与代码段的距离总是保持不变
- 因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量
- 这与代码段和数据段的绝对内存位置是无关的
- 无论在内存中的何处加载一个目标模块(包括共享目标模块)
- 编译器在数据段开始的地方创建了一个全局偏移量表(Global Offset Table,GOT)
- 在 GOT 中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个 8 字节条目
- 编译器还为 GOT 中每个条目生成一个重定位记录
- 在加载时,动态链接器会重定位 GOT 中的每个条目,使得它包含目标的正确的绝对地址
- 每个引用全局目标的目标模块都有自己的 GOT
- 编译器使用 GOT 进行所有的全局变量引用
在程序运行前,即完成了 GOT 中全局变量(不含函数) 条目的更新
示例
PIC 函数调用
- GNU 编译系统中 PIC 的函数调用使用了 延迟绑定 (lazy binding) 的技术
- 将过程地址的绑定推迟到第一次调用该过程时
- 把函数地址的解析推迟到它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要的重定位
- 第一次调用过程的运行时开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用
- 延迟绑定通过 GOT 和过程链接表 (Procedure Linkage Table, PLT) 这两个数据结构的交互来实现
# 7.13 库打桩机制
库打桩技术
- 库打桩(library interpositioning),允许截获对共享库函数的调用,取而代之执行自定义的代码
- 使用打桩机制
- 可以追踪对某个特殊库函数的调用次数
- 验证和追踪它的输入和输出值
- 或者甚至把它替换成一个完全不同的实现
- 使用打桩机制
- 基本思想
- 给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样
- 使用某种特殊的打桩机制,欺骗系统调用包装函数而不是目标函数
- 包装函数
- 通常会执行它自己的逻辑
- 然后调用目标函数
- 再将目标函数的返回值传递给调用者
# 7.13.1 编译时打桩
通过宏定义将目标函数替换为包装函数
- 编译时使用
-I.
参数让 C 预处理器在搜索通常的系统目录之前- 先在当前目录中查找
malloc.h
- 从而将源文件中定义的目标函数替换为包装函数
- 先在当前目录中查找
# 7.13.2 链接时打桩
静态链接器使用 --wrap f
标志进行链接时打桩
- 其会把对符号
f
的引用解析为__wrap_f
- 还把对符号
__real_f
的引用解析为f
- gcc 中可以使用
-Wl,option
标志把option
传递给链接器option
中的每个逗号都要替换为一个空格所以
-Wl,--wrap,malloc
就把--wrap malloc
传递给链接器示例
linux> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
1
# 7.13.3 运行时打桩
只要能够访问可执行目标文件,就可以进行打桩
- 主要借助动态链接器的
LD_PRELOAD
环境变量来实现相关的功能示例
LD_PRELOAD="./mymalloc.so" /usr/bin/uptime
1
书中的代码在 gcc (GCC) 11.1.0 版本下会报错
- 问题原因是,
printf
会使用用malloc
分配内存,原代码会造成循环调用 - 可以在自定义的
malloc
和free
函数中使用fprintf(stderr, "")
来打印相关的消息stderr
不会使用缓冲区,不涉及内存分配