0xzhang的博客

替换程序中的特定函数

· 0xzhang

问题描述§

修改或替换现有程序中的实现函数是一种非常常见的需求,尤其是,在不能得到源码的情况下应该如何解决这一问题?

这里我们将问题描述为我们有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

示例代码§

对于各种解决方案,提供了示例代码。


构建方法§

通过Makefile可以生成9个可执行程序:

  1. main 未替换函数B的原程序;
  2. main_gcc_wrap --wrap方法通过gcc编译生成的程序;
  3. main_g++_wrap --wrap方法通过g++编译生成的程序;
  4. main_objcopy 使用objcopy设置弱符号的方法生成的程序;
  5. main_extract 通过ar替换目标文件生成的程序;
  6. main_dynamic 通过设置LD_PRELOAD环境变量实现的运行时替换程序,此程序在本地运行需要自行设置LD_PRELOAD环境变量,通过export LD_PRELOAD=""可以恢复环境变量;
  7. main_C 通过定义宏替换调用函数名生成的程序;
  8. main_weaken 通过定义函数B为弱符号,替换函数进行覆盖生成的程序;
  9. main_replace 直接修改函数B,通过ar替换原先的目标文件生成的程序。
cc = gcc
cxx = g++
gcc_wrap := -Wl,--wrap=B
g++_wrap := -Wl,--wrap=_Z1Bv

src := main.c A.c B.c
cc_obj := main.o A.o B.o
cxx_obj := main.oxx A.oxx B.oxx
slib := libabc.a
dlib := libabc.so
target := main main_gcc_wrap main_g++_wrap main_objcopy \
main_extract main_dynamic main_C main_weaken main_replace

all : clean ${target} del

%.o : %.c
	${cc} -c $< -o $@

%.oxx : %.c
	${cxx} -c $< -o $@

# original program
main : ${cc_obj}
	ar cr ${slib} ${cc_obj}
	${cc} ${slib} -o $@
	rm -f ${slib}

# gcc wrap
main_gcc_wrap : C_gcc_wrap.c ${cc_obj}
	ar cr ${slib} ${cc_obj}
	${cc} ${gcc_wrap} $< ${slib} -o $@
	rm -f ${slib}

# g++ wrap
main_g++_wrap : C_g++_wrap.c ${cxx_obj}
	ar cr ${slib} ${cxx_obj}
	${cxx} ${g++_wrap} $< ${slib} -o $@
	rm -f ${slib}

# objcopy
main_objcopy : C_objcopy.c ${cc_obj}
	ar cr ${slib} ${cc_obj}
	objcopy ${slib} --weaken-symbol=B ${slib}
	${cc} $< ${slib} -o $@
	rm -f ${slib}

# extract
main_extract : C_extract.c ${cc_obj}
	ar cr ${slib} ${cc_obj}
	ar d ${slib} B.o
	${cc} $< ${slib} -o $@
	rm -f ${slib}

# run-time
main_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 definition
main_C : C.c ${cc_obj}
	${cc} -c A.c -D"B()=C()"
	ar cr ${slib} ${cc_obj}
	${cc} $< ${slib} -o $@
	${cc} -c A.c
	rm -f ${slib}

# weaken
main_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.c
	rm -f ${slib}

# replace
main_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 *.oxx

clean :
	rm -f *.o *.oxx *.a *.so ${target}

小结§

以上多种方法总体上可归类为四种:

  1. 通过改名实现替换。实现方法包括链接器支持的--wrap选项,需要特别注意C++项目中符号名的变化;也可通过编译时定义替换函数名;
  2. 通过弱符号实现替换。可以使用objcopy设定弱符号属性或在编译中定义将原函数替换为弱符号属性的函数;
  3. 通过环境变量实现运行时替换。通过设置LD_PRELOAD优先调用目标函数;
  4. 通过修改原文件实现替换。

参考资料§

  1. 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.
  2. Wrapping C++ functions with GNU linker - Stack Overflow
  3. linker - GNU gcc_ld - wrapping a call to symbol with caller and callee defined in the same object file - Stack Overflow
  4. Override a function call in C - Stack Overflow
  5. ld - Options - binutils docs
  6. GCC中通过--wrap选项使用包装函数_网络资源是无限的-CSDN博客
  7. 使用ld的wrap选项替换已有库函数_leolinux的专栏-CSDN博客
  8. LD_PRELOAD作用_chen_jianjian的专栏-CSDN博客