問題描述
修改或替換現有程序中的實現函數是一種非常常見的需求,尤其是,在不能得到源碼的情況下應該如何解決這一問題?
這里我們將問題描述為我們有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 = 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}
小結
以上多種方法總體上可歸類為四種:
- 通過改名實現替換。實現方法包括鏈接器支持的--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.
- Wrapping C++ functions with GNU linker - Stack Overflow
- linker - GNU gcc_ld - wrapping a call to symbol with caller and callee defined in the same object file - Stack Overflow
- Override a function call in C - Stack Overflow
- ld - Options - binutils docs
- GCC中通過--wrap選項使用包裝函數_網絡資源是無限的-CSDN博客
- 使用ld的wrap選項替換已有庫函數_leolinux的專欄-CSDN博客
- LD_PRELOAD作用_chen_jianjian的專欄-CSDN博客