Linux 程序开发 之 库打桩机制

前言

  Linux 链接器支持一个很强大的技术,称为库打桩(library interpositioning),它允许你截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,你可以追踪对某个特殊库函数的调用次数,验证和追踪它的输入和输出值,或者甚至把它替换成一个完全不同的实现。

一、库打桩定义

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

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

  要研究这些不同的机制,我们通过下面内容中的示例程序作为运行例子。它调用C标准库(libc.so)中的malloc和 free函数。对malloc的调用从堆中分配一个32字节的块,并返回指向该块的指针。对free的调用把块还回到堆,供后续的malloc调用使用。我们的目标是用打桩来追踪程序运行时对malloc和 free的调用

二、编译时打桩

  如果学习过C++的朋友阅读本篇文章后,会发现这个打桩机制有些类似于C++中的函数重载多态性

  下面几个实例程序展示了如何使用C预处理器在编译时打桩。mymalloc.c中的包装函数调用目标函数,打印追踪记录,并返回。本地的malloc.h头文件指示预处理器用对相应包装函数的调用替换掉对目标函数的调用。像下面这样编译和链接这个程序:

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

	由于有-I.参数,所以会进行打桩,它告诉C预处理器在搜索通常的系统目录之前,先在
当前目录中查找malloc.h。注意,mymalloc.c中的包装函数是使用标准malloc.h头文件编
译的。
	运行这个程序会得到如下图的追踪信息:
	./intc

在这里插入图片描述

//int.c文件
#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[])
{
    
    
    int *p = malloc(32);
    free(p);
    
    return 0;
}
//malloc.h文件
#define malloc(size) mymalloc(size)
#define free(size) myfree(size)

void *mymalloc(size_t size);
void myfree(void *ptr);
//mymalloc.c文件,包装函数

#ifdef COMPILETIME

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

/* malloc wrapper function*/
void *mymalloc(size_t size)
{
    
    
    void *ptr = malloc(size);
    printf ("malloc(%d)=%p\n" ,(int)size, ptr);
    
    return ptr;
}

/* free wrapper function */
void myfree(void *ptr)
{
    
    
    free(ptr);
    printf ("free(%p)\n", ptr) ;
}

#endif

三、链接时打桩

  Linux静态链接器支持用__wrap_f标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成_wrap_f(前缀是两个下划线),还要把对符号__real_f(前缀是两个下划线)的引用解析为f

  链接时打桩使用的 int.c 和编译时打桩相同,mymalloc.c不同。此外不再需要我们自己写的malloc.h文件

用下述方法把这些源文件编译成可重定位目标文件:
	gcc -DLINKTIME -c mymalloc.c
	gcc -c int.c
然后把目标文件链接成可执行文件 (注意命令中的w的大小写,有的大写有的小写):
	gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl main.o mymalloc.o
	
	-Wl,option标志把 option 传递给链接器。option中的每个逗号都要替换为一个
空格。所以-Wl,--wrap,malloc就把--wrap malloc传递给链接器,以类似的方式传递
-Wl,--wrap,free。

	执行该程序:./intl 
	打印信息如下所示

在这里插入图片描述

//int.c文件

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

int main(int argc, char *argv[])
{
    
    
    int *p = malloc(32);
    free(p);
    
    return 0;
}
//mymalloc.c文件,包装函数

#ifdef LINKTIME
#include <stdio.h>

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

/* malloc wrapper function */
void *__wrap_malloc(size_t size)
{
    
    

    void *ptr = __real_malloc(size);/* Call libc malloc */
    
    printf("malloc(%d)= %p\n" ,(int)size, ptr);
    
    return ptr;
}

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

知识补充:

(1)重定位:可执行文件中代码以及数据的运行时内存地址是链接器指定的,确定程序运行
时地址的过程就是重定位(Relocation)。

	操作系统将逻辑地址转变为物理地址的过程,也就是对目标程序中的指令和数据进行
修改的过程叫重定位。

参考学习:https://copyfuture.com/blogs-details/20211201161126175P

(2)位置无关码:CPU取指时用相对地址取指令(比如pc +4),只要其相对地址没有变,
都能够取指并运行。即该段代码无论放在内存的哪个地址,都能正确运行。究其原因,是
因为代码里没有使用绝对地址,都是相对地址。

参考学习:https://www.ofweek.com/ai/2021-01/ART-201721-11000-30480569_3.html

四、运行时打桩

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

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

  下面提供了malloc和 free的包装函数。每个包装函数中,对dlsym的调用返回指向目标libc函数的指针。然后包装函数调用目标函数,打印追踪记录,再返回。

下面是如何构建包含这些包装函数的共享库的方法:
gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl

这是如何编译主程序:
gcc -o intr int.c

使用命令查阅自己的Linux系统使用的是那种shell:printenv SHELL

下面是如何从bash shell 中运行这个程序:
LD_PRELOAD="./mymalloc.so" ./intr
注:部分版本版本测试可能会报段错误,我在Ubuntu 12.04 和 14.04测试通过了,20.04
报了段错误,百度后有人说glibc版本过高导致的。

下面是如何在csh或tcsh中运行这个程序:
(setenv LD_PRELOAD "./mymalloc.so"; ./intr; unsetenv LD_PRELOAD)

知识补充:

	dlsym是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的
地址,不但可以获取函数地址,也可以获取变量地址。

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

	handle:可以是dlopen函数返回的handle值,也可以是RTLD_DEFAULT或RTLD_NEXT
	RTLD_DEFAULT表示按默认的顺序搜索共享库中符号symbol第一次出现的地址
	RTLD_NEXT表示在当前库以后按默认的顺序搜索共享库中符号symbol第一次出现的地址
	
	symbol:要求获取的函数或全局变量的名称。

返回值:
void* 指向函数的地址,供调用使用。

在这里插入图片描述

//int.c文件

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

int main(int argc, char *argv[])
{
    
    
    int *p = malloc(32);
    free(p);
    
    return 0;
}
//mymalloc.c文件,包装函数
#ifdef RUNTIME
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

/*malloc wrapper function */
void *malloc(size_t size)
{
    
    
    void *(*mallocp)(size_t size);
    char *error;

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

    char *ptr = mallocp(size); /* Call libc malloc */ 
    printf("malloc(%d)= %p\n", (int)size, ptr);
    return ptr;
}

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

    printf("4444\n");
    if (!ptr)
        return;

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

    freep(ptr); /* Call libc free */
    printf("free(%p)\n", ptr);
}
#endif

五、处理目标文件的工具

  在 Linux 系统中有大量可用的工具可以帮助你 理解 和 处理 目标文件。特别地,GNU binutils包尤其有帮助,而且可以运行在每个Linux平台上。

●AR:创建静态库,插入、删除、列出和提取成员.

●STRINGS:列出一个目标文件中所有可打印的字符串。

●STRIP:从目标文件中删除符号表信息。

●NM:列出一个目标文件的符号表中定义的符号。

●SIZE:查看目标文件、库或可执行文件中各段及其总和的大小,是 GNU 二进制工具集 GNU
Binutils 的一员

●READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含SIZE 和 
NM的功能。

●OBJDUMP:所有二进制工具之母。能够显示一个目标文件中所有的信息。它最大的作用是
反汇编.text段中的二进制指令。

Linux系统为操作共享库还提供了LDD程序:
●LDD:列出一个可执行文件在运行时所需要的共享库。

本篇文章核心内容参考自《深入理解计算机操作系统》第三版,如有侵权,联系删除。欢迎各位在评论区交流讨论。

猜你喜欢

转载自blog.csdn.net/weixin_45842280/article/details/128274001