linux链接、目标文件全解析

链接

链接的定义: 链接(linking)是将各种代码和数据片段收集并组合为一个单一文件的过程。链接既可被执行于编译时(静态链接),也可被执行于加载时(load time),还可以被执行于运行时(动态链接)。

​ ——《深入理解计算机系统》

1. 静态链接

定义: 静态链接以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。

链接
可重定位目标文件
可执行目标文件

链接器主要完成两个任务:

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

2. 目标文件

定义: 目标文件是编译器生成的中间文件,包含编译后的机器代码和数据,还没有被链接为最终的可执行文件。

目标文件有三种形式:

  • 可重定位目标文件。不能直接运行,必须与其它可重定位目标文件合并起来才能执行。
  • 可执行目标文件。可以被直接复制到内存中运行。
  • 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
静态链接
加载
动态链接
可重定位目标文件
可执行目标文件
运行
共享目标文件

目标文件地格式:

  • Linux: ELF格式 (Executable and Linkable File)
  • Windows: PE格式(Portable Executable)
  • MAC OS-X: Mach-O格式

3. 可重定位目标文件

image-20241012213836939
  • ELF头: 包含生成该目标文件的系统的字的大小和字节序,以及ELF头大小、目标文件类型、机器类型(如x86-64)、节头部表地文件偏移、节头部表中条目地数量和大小。夹在ELF头和节头部表之间的都是节:
  • .text: 已编译程序的机器代码。
  • .rodata: 只读数据。例如字符串字面值。
  • .data:已初始化的全局和静态变量。
  • .bss: 未初始化的全局和静态变量,以及所有被初始化为0的全局或者静态变量。该节的存在主要是为了节省磁盘空间 ,提高加载速度,因为未初始化的变量只需要描述其类型和大小即可,不需要预留存储空间。
  • .symtab: 符号表,存放在程序中定义和引用的函数全局变量信息。
  • .rel.text: 存访.text节中引用或定义的符号的重定位信息。对外部函数全局变量的调用的位置都需要进行修改,对本地函数的调用则不需要修改位置。
  • .rel.data: 存放.data节中引用或定义的所有全局变量的重定位信息。
  • .debug: 调试符号表,里面保存了程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会生成这张表。
  • .line: 原始C源文件中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会生成这张表。
  • .strtab: 一个字符串表,其内容包括.systab和.debug节中的符号表,以及节头部中的节名字。

4. 符号和符号表

每个可重定位目标模块都有一个符号表,其中有三种不同的符号:

  • 在本目标文件中定义并能够被其它模块引用的全局符号。
  • 有其它模块定义,并在本目标文件中引用的全局符号。
  • 只被本目标文件定义和引用的局部符号,即局部静态变量。
image-20241012233138820

上图是符号表中条目的结构。其中需要着重解释两个字段:

  1. section: 表示该符号所在的节,其值为其在节头部表中的索引。除了[3]中图中显示的节外,还有三个特殊的伪节,在节头部表中没有条目,分别是ABS(不该被重定位的符号),UNDEF(被引用但并未被定义的符号),COMMON(还未被分配位置的未初始化的数据条目)。
  2. value: 表示该符号在所在节中的偏移。

5. 符号解析

定义:链接器解析符号引用的方法是将每个符号引用与输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。

对于本地局部的符号(static函数、static全局变量、局部静态变量),编译器会确保它们有且只有一个定义。但是对于全局符号,编译器无法确定它们在别的目标文件中是否有定义,这个工作就交给了链接器。

当链接器无法找到一个符号的定义时,会直接抛出错误信息,如"undefined reference xxx"。那当链接器找到了一个符号的多个定义时,会发生什么呢?

5.1 链接器如何解析多重定义的符号

在编译时,编译器向汇编器输出每个全局符号,要么是强,要么是弱,汇编器会把强弱信息隐含在符号表中。所谓强符号,是指函数和已初始化的全局符号,而弱符号就是未初始化的全局变量。请牢记定义。

根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:

  • 规则1:不允许有多个同名的强符号。
  • 规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。

例子:

// foo1.c
#include <stdio.h>

int x = 12;     // 强符号
int y = 12;     // 强符号

void f(void);
int main()
{
        f();
        printf("x: 0llx%x, y: 0llx%x\n", (long long)&x, (long long)&y);
        printf("x = 0x%x, 0x%x\n", x, y);
        return 0;
}

// foo2.c
double x;	// 弱符号
void f()
{
	x = 0.0;
}

上面的代码会引起什么错误?

显然,x这个符号存在多个定义,一强一弱的情况下链接器会使用强符号,但是在main函数中,由于首先调用了f函数,执行"x = 0.0",而在foo.c中x是double类型,占64位,而链接后x实际是int类型,占32位。当x和y位置相邻且y在x的后面时,x和y的值就都会被改变,引起非常难以定位的错误。

幸运的是,当我们怀疑有此类错误时,可以用GCC的 -fno-common标志这样的选项调用链接器,这样在遇到多重定义时会触发错误,或者-Werror,它会把所有警告都变为错误。

5.2 与静态库链接

静态库:将所有相关的目标模块打包位一个单独的文件,成为静态库(static library)。它可以用作链接器的输入。

在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存访在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。

linux平台GNU C编译器在编译时会自动链接libc.a这个C语言标准库。

使用ar工具创建静态库

> gcc -c addvec.c mulvec.c
> ar rcs libaddvec.a addvec.o mulvec.o

将main函数文件与库文件链接创建一个可执行文件:

> gcc -static -o prog2c main2.c -L. -laddrvec
# 或者
> gcc -static -o prog2c main2.c ./libaddrvec.a
image-20241013123751147
5.3 链接器如何使用静态库来解析引用

符合解析阶段,链接器从左到右按照在命令行中出现的顺序来扫描可重定位目标文件存档文件。在扫描过程中链接器维护着一个可重定位目标文件的集合E,一个未解析的符号的集合U, 一个在前面的输入文件中已经定义的符号集合D。初始时E、U、D均为空。

链接器处理扫描对象的规则如下:

  • 判断文件f是目标文件还是静态库文件(存档文件)
  • 如果f是目标文件,则将其加入E,同时修改U和D反应f中的符号定义和引用。
  • 如果f是归档文件,链接器会尝试匹配U中未解析的符号和由归档文件成员定义的符号,如果匹配成功,就将对应目标文件加入E,并修改U和D。最后未被加入的目标文件被简单丢弃。
  • 最后,链接器完成对输入文件的扫描后,如果U是非空的,链接器会输出错误并终止。否则它会合并和重定位E中的目标文件,构建输出的可执行文件。

根据这个规则可知:命令行中目标文件和静态库文件的顺序非常重要,如果定义一个符号的归档文件出现在引用这个符号的目标文件或归档文件之前,那么该引用就无法被解析

> gcc -static ./libvector.a main2.c
> /tmp/cc9XH6Rp.o: In function 'main': /tmp/cc9XH6Rp.o(.text+0xl8): undefined reference to 'addvec

6. 重定位

完成符号解析之后,链接器就知道了哪些目标文件会被加入到输出文件,以及每个符号对应的定义的位置,于是可以开始进行符号的重定位了。重定位就是为每个符号分配运行时地址。重定位由两步组成:

  • 重定位节和符号定义。在这一步,链接器将所有相同的节合并在一起,并且赋予它们运行时地址,然后赋予所有符号运行时地址。
  • 重定位节中的符号引用。在这一步,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标文件中的重定位条目。
6.1 重定位条目

编译器在生成目标代码时并不知道静态变量和函数最终会被放在什么位置,更不知道在外部定义的符号的最终位置,因此编译器每遇到一个对最终位置未知的目标引用,都需要生成一个重定位条目,来告诉链接器将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,数据的重定位条目放在.rel.data中。

image-20241013160205023

一起看下上图的重定位条目的结构:

  • offset: 需要被修改的引用的节内偏移。
  • type: 重定位类型,告知链接器如何修改引用。
  • symbol: 指向被引用的符号,在符号表中的索引。
  • addend: 一些类型的重定位需要使用它对被修改的引用值做偏移调整。

ELF定义了32中不同的重定位类型,我们只关心最基本的两种:

  • R_X86_64_PC32: 重定位一个使用32位PC(程序计数器)相对地址的引用。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC(程序计数器)的当前运行时值,得到目标地址。
  • R_X86_64_32, 重定位一个使用32位绝对地址的引用。

当程序规模较大,例如数据段与代码段的偏移超过32地址范围,那么就需要在编译时设置更大的代码模型

-mcmodle medium|large
6.2 重定位符号引用

看一下链接器重定位算法的伪代码:

image-20241013162501336

其中refptr表示引用在节内的位置,ADDR(s)表示节s的运行时地址,ADDR(r.symbol)表示重定位条目r表示的符号的运行时地址。

6.2.1 重定位PC相对引用

从第7,8行可以看到,在重定位相对地址的引用时,链接器需要首先算出引用本身的运行时地址refaddr, 然后用符号定义的运行时地址ADDR(r.symbol),加上偏移调整,然后减去refaddr

6.2.2 重定位绝对引用

从第 13行可以看到,重定位绝对引用只需要,在符号定义的运行时地址上加上偏移调整即可。

7. 可执行目标文件

下图是一个典型的ELF可执行目标文件的结构:

image-20241013165649651

可以看到,它于ELF格式的可重定位目标文件结构类似又有所不同。其中.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件时完全链接的,所以不再需要.rel节。

段(segment)与节(section)的关系

节section是编译时的概念,编译器将代码、数据等划分为不同的节。

段segment是执行时的概念,操作系统的加载器会根据段头部表将文件中的多个节合并到同一个段中加载到内存中。

可执行文件的连续的的片/段(chunk)会被映射到连续的内存区域。程序头部表描述了这种映射关系:

image-20241013194712551

8. 加载可执行目标文件

当在shell中运行可执行目标文件时:

> ./prog

shell进程会通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。加载器将可执行目标文件中的代码和数据从磁盘复制到内存,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载

每个linux程序都有一个运行时内存映像,请看下图:

image-20241015130730474

实际上,由于虚拟内存和内存映射的存在,加载器并不是简单的把程序的各个段复制到内存中。实际上,除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射到虚拟页的内存时才会进行复制。

9. 动态链接共享库

定义共享库是一个目标模块,在运行或加载时,加载到进程的地址空间,并和可执行程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。

共享库的优点:

  1. 使程序模块化,方便升级和维护,还能支持热更新,共享库与可执行文件解耦,当更新共享库时,不需要重新编译和链接整个程序。
  2. 节约内存,所有进程共享一个共享库的代码。

创建一个共享库:

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

链接一个共享库

# 在加载时链接共享库
> gcc -o prog21 main2.c ./libvector.so

这里libvector.so会在加载时进行链接。

当加载器加载prog21这个可执行目标文件时,它注意到里面包含一个**.interp节,这一节包含动态链接器**的路径名,动态链接器本身就是一个共享目标,然后加载器会加载和运行这个动态链接器。动态链接器通过执行下面的重定位完成链接任务:

  • 重定位libc.so的文本和数据到某个内存段。
  • 重定位libvector.so的文本和数据到某个内存段。
  • 重定位prog21中所有对libc.so和libvector.so定义的符号的引用。

最后,动态链接器将控制转交给应用程序。

10. 运行时从应用程序中加载和链接共享库

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

#include <dlfcn.h>
// 返回值:若成功则为指向句柄的指针,若失败则为NULL
void *dlopen(const char *filename, int flag);

// dlsym用来获取一个符号的地址
// 如果该符号不存在,就返回NULL
void *dlsym(void *handle, char *symbol);

// 如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库
void dlclose(void *handle);

// 返回前面的函数最近出现的错误,如果没有错误发生,就返回NULL
const char *dlerror(void);

下面给出一个使用这些函数在运行时连接一个共享库的例子:

// dll.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfnc.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);
    
    if (dlclose(handle) < 0)
    {
        fprintf(stderr, "%s\n", dlerror());
    	exit(1);
    }
    return 0;
}

dlopen

#include <dlfcn.h>

void *dlopen(const char *filename, int flag);
// 返回:如果成功返回句柄指针,否则返回NULL

dlopen函数加载和链接共享库filename。用RTLD_GLOBAL选项打开了的共享库库可以被用来解析filename中的外部符号。如果当前可执行文件使用了-rdynamic选项来编译,那么对于符号解析而言,它的全局符号也是可用的。flag参数要么包括RTLD_NOW,要么包括RTLD_LAZY。这两个值中的任意一个都可以和RTLD_GLOBAL取或共用。

如果可执行文件使用了-rdynamic选项来编译,那么其定义的符号可以用来解析共享库中的符号引用。

编译这个程序

gcc -rdynamic -o prog2r dll.c -ldl	# 链接dl库

-rdynamic参数用于控制在生成可执行文件时,在动态符号表中保留全局符号,而非只保留需要在外部解析的符号。默认情况下,可执行文件不保存静态符号表。

11. 位置无关代码

定义:可以加载到任意位置而无需重定位的代码段的代码称为位置无关代码(Position-Independent Code, PIC)。共享库的编译必须总是使用该选项。

共享库的一个主要目的是允许多个进程共享内存中相同的代码,从而节省内存资源。那么这是如何实现的呢?

在一个x86-64系统中,对同一个目标模块中符号的引用是不需要特殊处理使之成为PIC。可以用PC相对寻址来编译这些引用,构造共享目标文件时由静态链接器重定位。然而,对共享模块中定义的外部过程和对全局变量的引用需要一些特殊技巧。

对于共享模块中定义的符号的引用,需要通过GOT和PLT来实现,从而不需要重定位代码段的引用就能够实现链接。要问为啥一定得保证代码段不变?答曰:代码段经常被多个进程共享。

11.1 PIC数据引用

编译器在数据段开始的地方创建了一个表,叫做全局偏移量(Global Offset Table, GOT)。在GOT中,每个被这个目标模块引用的全局符号都有一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。

image-20241015183418147

这里的关键是,最GOT[3]的PC相对引用中的偏移量是一个常数:0x2008b9。这是因为这样一个关键的事实:无论我们在内存中的何处加载一个目标模块(包括共享目标模块 ),数据段与代码段的距离总是保持不变。

这里指的是虚拟地址的上的距离,而不是物理内存上的距离,后者实际上是随机的。

如果addcnt是由 libvector.so 模块定义的 ,编译器可以利用代码段和数据段之间不变的距离,产生对addcnt的直接PC相对引用,并增加一个重定位,让链接器在构造这个共享模块时解析它。如果addcnt是由另一个共享模块定义的,那么就需要通过GOT进行间接访问。在这里,编译器选择采用最通用的解决方案,未所有的引用(不管时定义在模块内还是模块外),都使用GOT。

11.2 PIC函数调用

编译器没有办法预测共享库中定义的函数的运行时地址,那么就需要在运行时进行某种重定位。一种做法是为该引用生成一条重定位记录,然后动态链接器在程序加载时解析它。但是,这种方法并不是PIC,因为它需要链接器修改调用模块的代码段。GNU编译器系统使用间接引用延迟绑定来解决这个问题。

延迟绑定通过两个数据结构来实现,分别是GOT和过程链接表(Procedure Linkage Table, PLT)。如果一个模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,而PLT是代码段的一部分。

linux的动态链接器默认采用延迟绑定机制,就是在函数首次被使用时才进行解析。但是它仍会在启动时就尝试加载所有共享库,以确保在需要时能够找到这些库并进行符号解析。

那么让我们看一下这两个表的内容:

  • 过程链接表(PLT):PLT是一个数组,其中每个条目是16字节。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。
  • 全局偏移量表(GOT):和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余每个条目对应一个被调用的函数,其地址需要在运行时被解析。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
image-20241015191714102

上图展示了,GOT和PLT如何协同完成工作:

第一次调用addvec:

  1. 不直接调用addvec,程序进入PLT[2]。
  2. 间接跳转到GOT[4]所指的地址,即PLT[2]的下一条指令。
  3. 把addvec的ID(0x1)压入栈,跳转到PLT[0]。
  4. PLT[0]把GOT[1]压入栈(即把动态链接器的一个参数压入栈),然后跳转到GOT[2]。动态链接利用两个参数确定addvec的运行时位置,用这个地址重写GOT[4],再把控制传递给addvec。

后续调用addvec:

  1. 程序进入PLT[2]
  2. 此时GOT[4]已被填入addvec的运行时地址,因此直接执行addvec

11.3 动态符号表中的变量在加载时绑定

对于共享库中的全局变量的访问,不会进行延迟绑定。与函数调用不同,全局变量的地址在程序加载时就已确定,程序在运行时直接通过 GOT 获取变量的值。

12. 库打桩机制

库打桩(library interpositioning)机制允许你截获对共享库函数的调用,取而代之执行自己的代码。

其基本思想为:给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。

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

12.1 编译时打桩

使用C预处理器在编译时打桩。

原理:利用预处理器的宏替换,将共享函数的调用替换为包装函数的调用。需要注意的是,包装函数往往需要调用真正的目标函数,因此一般在编译包装函数模块时不使用宏替换。

int.c

#include <stdio.h>
#include <malloc.h>

// 被打桩的程序
int main()
{
    int *p = malloc(32);
    free(p);
    return 0;
}

mymalloc.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("free(%p)\n", ptr);
}

#endif

编译命令

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

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

12.2 链接时打桩

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

原理:使用链接器的参数–wrap <f>来把对f的引用解析为__wrap_f,把对__real_f的引用解析为f。

#ifdef LINKTIME

#include <stdio.h>

void *__real_malloc(size_t size);
void __real_free(void *ptr);

// 包装函数wrapper functions
void *__wrap_malloc(size_t size)
{
    void *ptr = __real_malloc(size);    /* Call libc malloc */
    printf("malloc(%d)= %p\n", (int)size, ptr);
    return ptr;
}

void __wrap_free(void *ptr)
{
    __real_free(ptr);   /* Call libc free */
    printf("free(%p)\n", ptr);
}

#endif

编译命令

gcc -DLINKTIME -Wl,--wrap,malloc -Wl,--wrap,free -o intc int.c mymalloc.c

-Wl参数用于向链接器传递参数,格式为-Wl,option,其中option中的逗号会被替换为空格,然后传递给链接器。

12.3 运行时打桩

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

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

原理:通过LD_PRELOAD环境变量设置动态链接器寻找共享库的路径,从而使其在运行时进行动态链接时链接到我们提供的包装函数,这个包装函数可以使用dlsym动态符号解析函数获取到目标函数。

#ifdef RUNTIME  // 运行时打桩

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>

// 注意不要不要在包装函数中出现无限递归的情况

// 包装函数wrapper functions
void *malloc(size_t size)   
{
    void *(*mallocp)(size_t size);
    char *error;

    mallocp = dlsym(RTLD_NEXT, "malloc"); /* Get the address of libc malloc */
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);
        exit(1);
    }

    char *ptr = mallocp(size);  /* Call libc malloc */

    char buf[30];
    // 牢记:snprintf的返回值意思是假如忽略buf的size,应该输入buf的字符数,不计终止字符
    int should_written = snprintf(buf, sizeof buf, "malloc(%d) = %p\n", (int)size, ptr);
    write(1, buf, should_written);

    return ptr;
}

void free(void *ptr)
{
    void (*freep)(void *) = NULL;
    char *error;

    if (!ptr)
        return;

    freep = dlsym(RTLD_NEXT, "free");   /* Get the address of libc free */
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);   
        exit(1);
    }

    freep(ptr); /* Call libc free */
    
    char buf[30];
    // 牢记:snprintf的返回值意思是假如忽略buf的size,应该输入buf的字符数,不计终止字符
    int should_written = snprintf(buf, sizeof buf, "free(%p)\n", ptr);
    write(1, buf, should_written);
}

#endif

编译命令

编译包装函数共享库

gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl

编译主程序

gcc -o intr int.c

运行程序

LD_PRELOAD="./wrapper.so" ./intr

这样一来,当我们向给一个可执行文件进行库打桩的时,只需要设置器运行时环境变量LD_PRELOAD即可。

13. 处理目标文件的工具

  • ar:创建静态库,插入、删除、列出和提取成员

  • strip: 从目标文件中删除符号表信息

  • nm: 列出一个目标文件的符号表中定义的符号

  • size: 列出目标文件中节的名字和大小

  • readelf: 显示一个目标文件的完整结构

  • objdump: 所有二进制工具之母。能够显示一个二进制文件的所有信息。

  • LDD: 列出一个可执行文件在运行时所需要的共享库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HilariousDog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值