替换程序中的特定函数
问题描述
修改或替换现有程序中的实现函数是一种非常常见的需求,尤其是,在不能得到源码的情况下应该如何解决这一问题?
这里我们将问题描述为我们有main程序代码,会调用文件A中的函数A,函数A会调用文件B中的函数B(两文件中都不一定只有一个函数)。我们需要替换函数B为一个指定的函数C,如何实现?
以下以C/C++语言为例,提供不同条件下的解决方案,所有测试在类UNIX平台下使用GCC编译器完成。
解决方案
当无法获得X源码时,称为没有X,根据条件或许需要X所在的静态库文件来实现替换方案。
在一篇文章【1】中针对Windows,Unix,OS X三个系统平台分别提出了实用的解决方案,这里只关注Unix平台下的方案。文中提供的方法为链接时通过链接器支持的--wrap选项进行函数替换和运行时通过LD_PRELOAD环境变量预先加载修改过的函数生成的动态库。这篇文章提供的方法最大的优点就是在没有A且没有B的条件下完全可用,并且两种方法分别在链接时或运行时作用。
需要注意的是,使用--wrap选项的方法,对于C++使用会有一定的限制,要求函数B必须基于C实现,因此C++中需要使用extern "C"
定义替换函数。而且因为C++使用了名字修饰(name mangling)技术用来处理命名空间,在使用g++编译B.c后,函数名B作为符号名会发生变化,可以通过nm <program name>
查看原函数修饰后的名字,在此处示例中得到函数B的名字为_Z1Bv,这样还需要正确修改替换函数的函数名以及链接选项--wrap的值【2】。具体内容可见示例中main_g++_wrap的构建过程。
还有一个很好的方法,就是将目标(.o)文件或静态库文件(.a)中函数B对应的符号定义为弱符号【3】,同样可以在没有A且没有B的条件下实现。
很多相关的方法在这个问题【4】下有很好的讨论,还有一些参考资料很有帮助【6】【7】【8】。以下对几种方法进行总结,所有的方法实现均提供了示例程序。
为了确保运行时替换方法中可执行程序main_dynamic能够加载当前目录下动态库,需要设置库路径环境变量。export LD_LIBRARY_PATH = "/your/current/path"
已知条件 | 附加条件 | 方法描述 | 可执行程序 |
---|---|---|---|
没有A,没有B | 链接时替换,需要函数B的静态库 | ld支持的--wrap=symbol选项,支持对系统函数进行封装替换【5】 加选项后实际调用为带__wrap_前缀的目标函数,通过__real_前缀可以调用原函数 通过 -Wl,--wrap=B 或-Xlinker --wrap=B 参数生成可执行文件对于C++的代码有不同的要求,详见②的构建过程 |
①main_gcc_wrap ②main_g++_wrap |
链接时替换,需要函数B的静态库 | 通过objcopy --weaken-symbol=B 可以设置原函数B为弱符号 |
③main_objcopy | |
链接时替换,需要函数B的静态库,文件B中只有函数B | 通过ar 删去静态库中函数B所在的目标文件,使用目标函数的文件重新编译生成。有限制,由于没有B源码,最好文件B中只有函数B |
④main_extract | |
运行时替换,通过动态库调用函数B | 通过设置LD_PRELOAD 环境变量以更高优先级调用同名函数 |
⑤main_dynamic | |
有A,没有B | 编译时替换,替换函数B为函数C | 编译文件A时加上-D"B()=C()" 编译选项,替换函数名 |
⑥main_C |
没有A,有B | 编译时替换,将原函数B定义为弱符号 | 编译文件B时加上-D"B()=__attribute__((weak))B()" 编译选项,修改原函数B为弱符号,优先加载同名的目标函数B |
⑦main_weaken |
链接时替换,需要函数B的静态库 | 通过ar 删去静态库中函数B所在的目标文件,修改函数B,重新编译生成 |
⑧main_replace |
示例代码
对于各种解决方案,提供了示例代码。
wrap_test.zip
wrap_test/
Makefile
README.md
main.c
main.h
A.c
B.c
C.c
C_gcc_wrap.c
C_g++_wrap.c
C_objcopy.c
C_extract.c
C_dynamic.c
C_weaken.c
C_replace.c
构建方法
通过Makefile可以生成9个可执行程序:
main未替换函数B的原程序;
main_gcc_wrap--wrap方法通过gcc编译生成的程序;
main_g++_wrap--wrap方法通过g++编译生成的程序;
main_objcopy使用objcopy设置弱符号的方法生成的程序;
main_extract通过ar替换目标文件生成的程序;
main_dynamic通过设置LD_PRELOAD环境变量实现的运行时替换程序,此程序在本地运行需要自行设置LD_PRELOAD环境变量,通过
export LD_PRELOAD=""
可以恢复环境变量;main_C通过定义宏替换调用函数名生成的程序;
main_weaken通过定义函数B为弱符号,替换函数进行覆盖生成的程序;
main_replace直接修改函数B,通过ar替换原先的目标文件生成的程序。
cc = gcccxx = g++gcc_wrap := -Wl,--wrap=Bg++_wrap := -Wl,--wrap=_Z1Bvsrc := main.c A.c B.ccc_obj := main.o A.o B.ocxx_obj := main.oxx A.oxx B.oxxslib := libabc.adlib := libabc.sotarget := main main_gcc_wrap main_g++_wrap main_objcopy main_extract main_dynamic main_C main_weaken main_replaceall : clean ${target} del%.o : %.c${cc} -c $< -o $@%.oxx : %.c${cxx} -c $< -o $@# original programmain : ${cc_obj}ar cr ${slib} ${cc_obj}${cc} ${slib} -o $@rm -f ${slib}# gcc wrapmain_gcc_wrap : C_gcc_wrap.c ${cc_obj}ar cr ${slib} ${cc_obj}${cc} ${gcc_wrap} $< ${slib} -o $@rm -f ${slib}# g++ wrapmain_g++_wrap : C_g++_wrap.c ${cxx_obj}ar cr ${slib} ${cxx_obj}${cxx} ${g++_wrap} $< ${slib} -o $@rm -f ${slib}# objcopymain_objcopy : C_objcopy.c ${cc_obj}ar cr ${slib} ${cc_obj}objcopy ${slib} --weaken-symbol=B ${slib}${cc} $< ${slib} -o $@rm -f ${slib}# extractmain_extract : C_extract.c ${cc_obj}ar cr ${slib} ${cc_obj}ar d ${slib} B.o${cc} $< ${slib} -o $@rm -f ${slib}# run-timemain_dynamic : C_dynamic.c ${src}${cc} ${src} -fPIC -shared -o ${dlib}${cc} $< -fPIC -shared -o libC.so${cc} -L. -labc -o $@# 在脚本中设置的环境变量不能作用于当前shell# 需要在外部设置环境变量后,运行# export LD_PRELOAD="./libC.so"; # ./main_dynamic;# macro definitionmain_C : C.c ${cc_obj}${cc} -c A.c -D"B()=C()"ar cr ${slib} ${cc_obj}${cc} $< ${slib} -o $@${cc} -c A.crm -f ${slib}# weakenmain_weaken : C_weaken.c ${cc_obj}${cc} -c B.c -D"B()=__attribute__((weak))B()"ar cr ${slib} ${cc_obj}${cc} $< ${slib} -o $@${cc} -c B.crm -f ${slib}# replacemain_replace : C_replace.c ${cc_obj}ar cr ${slib} ${cc_obj}ar d ${slib} B.o${cc} $< ${slib} -o $@rm -f ${slib}del : rm -f *.o *.oxxclean :rm -f *.o *.oxx *.a *.so ${target}
小结
以上多种方法总体上可归类为四种:
通过改名实现替换。实现方法包括链接器支持的--wrap选项,需要特别注意C++项目中符号名的变化;也可通过编译时定义替换函数名;
通过弱符号实现替换。可以使用objcopy设定弱符号属性或在编译中定义将原函数替换为弱符号属性的函数;
通过环境变量实现运行时替换。通过设置
LD_PRELOAD
优先调用目标函数;通过修改原文件实现替换。
参考资料
Myers, D. S., & Bazinet, A. L. (2004). Intercepting arbitrary functions on Windows, UNIX, and Macintosh OS X platforms. Center for Bioinformatics and Computational Biology, Institute for Advanced Computer Studies, University of Maryland, Tech. Rep.