• Linux下库打桩机制分析 function Interposition


    [时间:2017-08] [状态:Open]
    [关键词:linux, libray,打桩,interposition,函数替换,链接器,gcc,malloc,free]

    0 引言

    本文主要参考《深入理解计算机系统》(原书第三版)ch7.13。作为个人知识整理和后续参考使用。

    库打桩(interposition)这个名词比较陌生,这是由Linux链接器所提供的技术,允许用户截获对共享库函数的调用,并执行自己的代码(当然是在普通权限下,管理员权限通常是禁止使用该技术的)。
    使用打桩机制,可以追踪某个特殊库函数的调用次数、验证并追踪其输入输出,甚至把它替换成一个完全不同的实现。

    2 基本原理

    打桩机制的基本实现原理如下:
    给定需要打桩的目标函数,常见一个wrapper函数,其原型和目标函数一致。
    利用特殊的打桩机制,可以实现让系统调用你的wrapper函数而不是目标函数。
    wrapper函数中通常会执行自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。

    打桩可以发生在编译时、链接时或者程序被加载执行的运行时。不同的阶段都有对应的打桩机制,也有其局限性。
    下文将以c标准库中的malloc和free函数的打桩来说明不同打桩机制。基本目标是用打桩来追踪程序运行时对malloc和free的调用。

    3 示例一:编译时打桩

    编译时打桩说白就是将对目标函数的调用替换为对应wrapper的调用。实现方式很简单,通过指定编译指令来实现。
    下面代码实现了一个demo,用于说明如何使用预处理器实现编译时打桩。包装函数实现如下:

    // malloc.h
    #ifndef COMPILE_TIME
    #define malloc(size) mymalloc(size)
    #define free(ptr) myfree(ptr)
    #endif
    
    void * mymalloc(size_t size);
    void myfree(void *ptr);
    
    // mymalloc.cpp
    #ifdef COMPILE_TIME
    #include <stdio.h>
    #include <malloc.h>
    
    // malloc wrapper function
    void * mymalloc(size_t size) {
        void * ptr = ::malloc(size);
        printf("malloc %p size %u
    ", ptr, size);
        return ptr;
    }
    
    // free wrapper function
    void myfree(void *ptr) {
        ::free(ptr);
        printf("free %p
    ", ptr);
    }
    #endif
    

    在wrapper函数中我们调用目标函数,并打印追踪记录。本地的malloc.h头文件用于替换系统的目标函数调用逻辑,将其切换到对应的包装函数中。
    所有源码可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打桩compile目录找到,可以使用下面函数指令编译代码:

    gcc -DCOMPILE_TIME -c mymalloc.cpp
    gcc -I. -o intpos main.cpp mymalloc.o
    

    编译后执行可执行文件,就可以得到将对malloc/free的调用转到我们的wrapper函数中。

    4 示例二:链接时打桩

    Linux静态链接器支持用--wrap f标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成__wrap_f(前缀是两个下划线),还要对符号__real_f的引用解析成f。
    我们的wrap函数实现如下:

    #ifdef LINK_TIME
    #include <stdio.h>
    
    extern "C" {
    void * __real_malloc(size_t size);
    void __real_free(void * ptr);
    
    // malloc wrapper function
    void * __wrap_malloc(size_t size) {
        printf("%s enter %u
    ", __FUNCTION__, size);
        void * ptr = __real_malloc(size);
        printf("malloc %p size %u
    ", ptr, size);
        return ptr;
    }
    
    // free wrapper function
    void __wrap_free(void *ptr) {
        __real_free(ptr);
        printf("free %p
    ", ptr);
    }
    }
    #endif
    

    所有源码可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打桩link目录找到使用下面命令编译:

    gcc -DLINK_TIME -c mymalloc.cpp
    gcc -c main.cpp
    gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intpos main.o mymalloc.o
    

    注意这里是WL(L小写,不是数字1,Linker option)。-Wl,option标志把option传递给链接器。option中的每个逗号都会替换为一个空格。即-Wl,--wrap,malloc就是把--wrap malloc传递给链接器。上面编译必须分开,否则可能会出错。

    通过链接器的命令打桩也可以实现我们的目的,但是有一个缺点,你需要重新连接所有需要监测的模块。

    5 示例三:运行时打桩

    编译时打桩需要访问程序的源代码,连接时打桩需要能够访问程序的可重定位的对象文件。不过运行时打桩仅需要访问可执行目标文件即可,它的基本原理是基于动态链接器的LD_PRELOAD环境变量的。
    如果LD_PRELOAD环境变量被设置为一个共享库路径的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器会先搜做LD_PRELOAD中给定的库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中任意函数打桩,包括libc.so中的malloc和free。

    我们的wrapper函数实现如下

    #ifdef RUNTIME
    #define _GNU_SOURCE
    #include <stdio.h>
    #include <stdlib.h>
    #include <dlfcn.h>
    
    // malloc wrapper function
    void * malloc(size_t size) {
        printf("%s enter %u
    ", __FUNCTION__, size);
        void *(* mallocp)(size_t size);
        char * error;
        
        // get address of libc malloc
        mallocp = dlsym(RTLD_NEXT, "malloc");
        if ((error = dlerror()) != NULL) {
            fputs(error, stderr);
            exit(1);
        }
        void * ptr = mallocp(size);
        printf("malloc %p size %u
    ", ptr, (int)size);
        return ptr;
    }
    
    // free wrapper function
    void free(void *ptr) {
        void (* freep)(void *ptr);
        char * error;
        
        // get address of libc free
        freep = dlsym(RTLD_NEXT, "free");
        if ((error = dlerror()) != NULL) {
            fputs(error, stderr);
            exit(1);
        }
    
        freep(ptr);
        printf("free %p
    ", ptr);
    }
    #endif
    

    所有源码可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打桩 untime目录找到,需要通过下面指令编译:
    gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.cpp -ldl -fpermissive

    主函数不做修改。但运行时需要使用下面指令:
    LD_PRELOAD="./mymalloc.so" ./intpos
    如此就可以达到预期的效果,监测对malloc和free函数的调用。

    6 补充知识:malloc调试变量——__malloc_hook

    如果单纯为了处理malloc/free的调用,可以参考下万能的manual。
    __malloc_hook是glibc提供的malloc调试变量中的一个,详情参考MALLOC_HOOK
    只要在代码中添加__malloc_hook= my_malloc_hook;语句,当前程序中关于的malloc调用都会使用my_malloc_hook函数,简单方便。但是这组调试变量不是线程安全的,很多新的编译器已经将该功能废弃。有兴趣的可以参考下。

    7 总结

    到此,本文开头的问题已经解决。整理此文目的只是为了加深记忆。
    同时了解下基本的内存泄露分析方法。

    8 参考

  • 相关阅读:
    javascript异步编程系列【十】—Jscex+Easeljs制作坦克大战
    博客园分页JQuery打造的分页无刷新的Repeater
    参赛作品
    摄像机、投影、3D旋转、缩放
    javascript异步编程系列【八】Jscex版火拼俄罗斯
    javascript异步编程系列【七】扫盲,我们为什么要用Jscex
    javascript异步编程系列【五】Jscex制作愤怒的小鸟
    javascript异步编程系列【六】Jscex版愤怒的小鸟之冲锋陷阵鸟
    每周优秀代码赏析—Jscex内核【一】
    javascript异步编程系列【一】用Jscex画圆
  • 原文地址:https://www.cnblogs.com/tocy/p/Linux-library-Interposition.html
Copyright © 2020-2023  润新知