0x06

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时、加载时、运行时。

链接器使得分离编译成为可能。当改变模块时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

编译器驱动程序

GCC驱动程序命令:

linux> gcc -Og -o prog main.c sum.c

驱动程序首先运行C预处理器(cpp),它将C的源程序main.c翻译成一个ASCII码的中间文件main.i:

cpp [other arguments] main.c /tmp/main.i

接下来,驱动程序运行C编译器(ccl),它将main.i翻译成一个ASCII汇编语言文件main.s:

ccl /tmp/main.i -Og [other auguments] -o /tmp/main.s

然后,驱动程序运行汇编器(as),它将main.s翻译成一个可重定位目标文件main.o:

as [other arguments] -o /tmp/main.o /tmp/main.s

驱动程序经过相同的过程生成sum.o。最后,它运行链接器程序ld,将main.o和sum.o以及一些必要的系统目标文件组合其阿里,创建一个可执行目标文件prog:

ld -o prog [system object files and args] /tmp/main.o /tmp/sum.o

要运行可执行文件prog,在shell的命令行上输入它的名字:

linux> ./prog

shell调用操作系统中一个叫做加载器的函数,它将可执行文件prog中的代码和数据复制到内存,然后将控制转移到这个程序的开头。

静态链接

像ld程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成,每一节都是一个连续的字节序列。指令在一节中,初始化了的全局变量在另一节中,而未初始化的变量又在另外一节中。

为了构造可执行文件,链接器必须完成两个主要任务:

  • 符号解析。目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
  • 重定位。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得他们指向这个内存位置。

目标文件是字节块的集合。这些块中,有些包含程序代码,有些包含程序数据,而其他的则包含引导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。

目标文件

目标文件有三种形式:

  • 可重定位目标文件。包含二进制代码和数据。其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  • 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  • 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

现代x86-64Linux和Unix系统使用可执行可链接格式(Executable and Linkable Format, ELF)。

可重定位目标文件

图中展示了一个典型的ELF可重定位目标文件的格式。ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含以下几个节:

  • .text:已编译程序的机器代码。
  • .rodata:只读数据,如printf语句中的格式串和开关语句的跳转表。
  • .data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中。
  • .bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量。
  • .symtab:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
  • .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
  • .rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
  • .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项编译时,才会得到这张表。
  • .line:原始C源程序和.text节中机器指令之间的映射。-g选项编译时,才会得到这张表。
  • .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表是以null结尾的字符串的序列。

符号和符号表

每个可重定位目标模块m都有一个符号表,包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

  • 由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。
  • 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。
  • 只被模块m定义和引用的局部符号。它们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。

.symtab中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理,链接器对此类符号不感兴趣。

带有C static属性的本地过程变量是不在栈中管理的。相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.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 int bytes */
} Elf64_Symbol;

name是字符串表.strtab中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小,以字节为单位。type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。

每个符号都被分配到目标文件的某个节,由section字段表示,该字段也是一个到节头部表的索引。

readelf main.o -a
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 648 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001f 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001e0
0000000000000030 0000000000000018 I 9 1 8
[ 3] .data PROGBITS 0000000000000000 00000060
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000068
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 00000068
0000000000000019 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000081
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000088
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000210
0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000c0
0000000000000108 0000000000000018 10 8 8
[10] .strtab STRTAB 0000000000000000 000001c8
0000000000000017 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 00000228
0000000000000059 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)
There are no section groups in this file.
There are no program headers in this file.
There is no dynamic section in this file.
Relocation section '.rela.text' at offset 0x1e0 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000e 00080000000a R_X86_64_32 0000000000000000 array + 0
000000000013 000a00000002 R_X86_64_PC32 0000000000000000 sum - 4
Relocation section '.rela.eh_frame' at offset 0x210 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 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 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
9: 0000000000000000 31 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
No version information found in this file.

开始的8个条目是链接器内部使用的局部符号。readelf用一个证书索引来标识每个节,Ndx标识name处于哪个节中,如Ndx=1表示.text节,而Ndx=3表示.data节。

符号解析

链接器解析符号引用(其他模块的全局变量)的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。编译器只允许每个模块中每个局部符号(带static的全局变量和函数)有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。

解析多重定义的全局符号

链接器的输入是一组可重定位目标模块。每个模块定义一组符号,有些是局部的(只对定义该符号的模块可见),有些是全局的(对其他模块也可见)。Linux系统对于多个模块定义同名全局符号的处理如下:

在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化地全局变量是弱符号。

  • 不允许有多个同名的强符号。
  • 如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 如果有多个弱符号同名,那么从弱符号中任意选择一个。

注意,链接器通常不会表明它检测到多个x的定义。严重情况下,如果重复的符号有不同的类型:

#include <stdio.h>
void f(void);
int y = 15212;
int x = 15213;
int main() {
f();
printf("x = 0x%x y = 0x%x \n", x, y);
return 0;
}
// -----------------------------------------------------------------------------
double x;
void f() {
x = -0.0;
}

在一台x86-64/Linux机器上,double类型是8个字节,而int类型是4个字节。赋值x = -0.0将用负零的双精度浮点表示覆盖内存中x和y的位置。当怀疑有此类错误时,用像GCC-fno-common标志这样的选项调用链接器,会告诉链接器,在遇到多重定义的全局符号时,触发一个错误。或者使用-Werror选项,会把所有警告都变为错误。

与静态库链接

所有的编译系统都提供一种机制,将所有相关的目标模块打包称为一个单独的文件,称为静态库,它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。

链接器使用静态库来解析引用

在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时,E、U和D均为空。

  • 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器会把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。
  • 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中的所有成员目标文件都依次进行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。
  • 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件。

这种算法会导致一些链接时错误,因为命令行上的库和目标文件的顺序非常重要。如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。

关于库的一般准则是将它们放在命令行的末尾,如果库不是互相独立的,就必须对它们排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的。

重定位

一旦链接器完成了符号解析,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。重定位时,将合并输入模块,并为每个模块分配运行时地址,由两步组成:

  • 重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
  • 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

重定位条目

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置,所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

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;

offset是需要被修改的引用的偏移。symbol标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改的值做偏移调整。

ELF定义了两种最基本的重定位类型:

  • R_X86_64_PC32。重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。
  • R_X86_64_32。重定位一个使用32位绝对地址的引用。

动态链接共享库

静态库有一些明显的缺点,和所有的软件一样,需要定期维护和更新。且几乎每个C程序都使用标准I/O函数,在运行时,这些函数的代码会被复制到每个运行进程的文本段中。

共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。共享库也称共享目标,在Linux系统中通常用.so后缀表示,微软的操作系统大量地使用了共享库,称为DLL。

共享库以两种不同的方式来共享。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

1648092758909

为了构造图中的示例向量例程的共享库libvector.so,调用编译器驱动程序,给编译器和链接器如下指令:

linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c

一旦创建了这个库,随后就要将它连接到图中的示例程序中:

linux> gcc -o prog21 main2.c ./libvector.so

这样就创建了一个可执行目标文件prog21,而此文件的形式使得它在运行时可以和libvector.so链接。基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。

从应用程序中加载和链接共享库

应用程序还可能在它运行时要求动态编译器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

动态链接的例子:

  • 分发软件。生成一个共享库的新版本,然后用户可以下载,并用它替代当前版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
  • 构建高性能Web服务器。许多Web服务器生成动态内容,如个性化的Web页面、账户余额和广告语。早期的Web服务器通过使用fork和execve创建一个子进程,并在该子进程的上下文中运行CGI程序来动态生成内容。然而现代高性能的Web服务器可以使用基于动态链接的方法来生成动态内容。其思路是将每个生成动态内容的函数打包在共享库中。当一个来自Web浏览器的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它。

Linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。

#include <dlfcn.h>
void *dlopen(const char *filename, int flag);

dlopen函数加载和链接共享库filename,使用已经带RTLD_GLOBAL选项打开了的库解析filename中的外部符号。如果当前可执行文件是带-rdynamic选项编译的,那么对符号解析而言,它的全局符号也是可用的。flag参数必须要么包括RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包括RTLD_LAZY标志,该标志指示链接器推迟符号解析直到执行来自库中的代码。这两个值中的任意一个都可以和RTLD_GLOBAL标志取或。

#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);

dlsym函数的输入是一个指向前面已经打开了的共享库的句柄和一个symbol名字,如果该符号存在,就返回符号的地址,否则返回NULL。

#include <dlfcn.h>
int dlclose(void *handle);

如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库。

#include <dlfch.h>
const char *dlerror(void);

dlerror函数返回一个字符串,它描述的是调用dlopen、dlsym或者dlclose函数时发生的错误,如果没有错误发生,就返回NULL。

示例:如何利用接口动态链接libvector.so共享库,然后调用它的addvec例程。

linux> gcc -rdynamic -o prog2r dll.c -ldl
// dll.c
#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;
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}

位置无关代码

共享库的一个主要目的就是允许多个正在运行的进程共享内存中相同的库代码,因而节约宝贵的内存资源。现代系统使得共享模块的代码段可以加载到内存的任何位置而无需链接器修改。使用这种方法,无限多个进程可以共享一个共享模块的代码段的单一副本。

可以加载而无需重定位的代码称为位置无关代码(PIC)。用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。共享库的编译必须总是使用该选项。

库打桩机制

库打桩允许你截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,可以追踪对某个特殊库函数的调用次数,验证和追踪它的输入和输出值,或者甚至把他替换成一个完全不同的实现。

基本思想:给定一个需要打桩的目标函数,创建一个包装函数,原型与目标函数完全一样。使用某种特殊的打桩机制,就可以欺骗系统调用包装函数而不是目标函数。包装函数通常会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。

打桩可以发生在编译时、链接时或当程序被加载和执行的运行时。

编译时打桩

// int.c
#include <stdio.h>
#include <malloc.h>
int main() {
int *p = malloc(32);
free(p);
return 0;
}
// malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
// mymalloc.c
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>
void *mymalloc(size_t size) {
void *ptr = malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}
void myfree(void *ptr) {
free(ptr);
printf("fre(%p)\n", ptr);
}
#endif

上述代码展示了如何使用C预处理器在编译时打桩。像这样编译和链接程序:

linux> gcc -DCOMPILETIME -c mymalloc.c
linux> gcc -I. -o intc int.c mymalloc.o

由于有-I.参数,所以会进行打桩,它告诉C预处理器在搜索通常的系统目录之前,先在当前目录中查找malloc.h。注意,mymalloc.c中的包装函数是使用标准malloc.h头文件编译的。

链接时打桩

// mymalloc.c
#ifdef LINKTIME
#include <stdio.h>
void *__real_malloc(size_t size);
void __real_free(void *ptr);
void *__wrap_malloc(size_t size) {
void (ptr) = __real_malloc(size);
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void __wrap_free(void *ptr) {
__real_free(ptr);
printf("free(%p)\n", ptr);
}
#endif

Linux静态链接器支持用--wrap f标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成__wrap_f,还要把对符号__real_f的引用解析为f。

linux> gcc -DLINKTIME -c mymalloc.c
linux> gcc -c int.c
linux> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o

-Wl,option标志把option传递给链接器。option中的每个逗号都要替换为一个空格。所以-Wl,--wrap,malloc就把--wrap malloc传递给链接器。

运行时打桩

编译时打桩需要能够访问程序的源代码,链接时打桩需要能够访问程序的可重定位对象文件。运行时打桩只需要能够访问可执行目标文件,这个机制基于动态链接器的LD_PRELOAD环境变量。

如果LD_PRELOAD环境变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当加载和执行一个程序,需要解析未定义的引用时,动态链接器(LD-LINUX.SO)会先搜索LD_PRELOAD库,然后才搜索任何其他的库。有了这个机制,当加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括libc.so。

// mymalloc.c
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
void *malloc(size_t size) {
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void free(void *ptr) {
void (*freep)(void *) = NULL;
char *error;
if (!ptr)
return;
freep = dlsym(RTLD_NEXT, "free");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr);
printf("free(%p)\n", ptr);
}
#endif
posted @   Pannnn  阅读(214)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
-->
点击右上角即可分享
微信分享提示