替換程序中的特定函數



問題描述

修改或替換現有程序中的實現函數是一種非常常見的需求,尤其是,在不能得到源碼的情況下應該如何解決這一問題?

這里我們將問題描述為我們有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個可執行程序:

  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博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM