不改动源代码实现替换 C/C++ 程序入口函数

前言

众所周知,C/C++ 程序在启动时被首先调用的函数,其名称固定为 main。在某些情况下,有这样的需求:

  1. 不改动现有源代码(但可以增加新的源代码文件)
  2. 替换(或修饰) main 函数,或将 main 函数命名为其他名称

下面描述了一个利用 gcc (或 g++ ),以及 objcopy 工具,实现上述需求的方案。至于这种方案有何应用,可以参考笔者的另一篇博客《VSCode × OJ —— 优雅地实现自动样例输入》。

方案

  1. 通过附加新的 main 函数,以替换原有 main 函数(实际上进行的操作是:将旧的 main 函数更名为其他名称,如 originalMain ,然后由新的 main 函数调用 originalMain 函数)
  2. 通过修改目标代码文件(仅编译而不未进行链接的文件),进行函数更名(实际上是符号表的改动)
  3. 完成函数更名后,进行链接操作,得到的可执行文件,其 main 函数即完成替换(间接实现了 main 函数的修饰、替换或更名)

可行性

  1. 附加新的 main 函数:在 C / C++ 源程序中是不允许同时出现两个 main 函数定义的,但通过分开编译得到两个单独的目标代码文件可以暂时绕开这一限制,以供完成后续操作。
  2. 函数的替换:在目标代码中,函数调用是未决的,函数之间的调用关系是通过符号来联系的,在完成链接后,函数之间的调用关系才会固定到汇编代码中(如 call 指令的偏移量参数),因此通过修改目标代码中的符号表,可以实现函数的替换。
  3. 目标代码文件的获得:编译器 gccg++ 可以通过 -c 参数进行仅编译不链接,得到目标代码文件。
  4. 符号表的修改:利用 objcopy 工具可以实现目标代码或可执行程序的符号表修改。
  5. 符号名称的获得:符号名称与函数名称的对应关系:特别地,main 函数的符号为 main,其他函数的符号可以利用 objdump 工具查看。
  6. 对调试的影响:调试器使用的调试信息是在编译生成目标代码时通过 -g 参数附加的,其中记录的函数名称、文件名与代码行数均与源代码对应,符号表的修改不影响调试。

步骤

以下假设要被修改 main 函数的源程序如下。

文件:main.cpp

#include <iostream>
int main() {
    std::cout << "Hello in main();" << std::endl;
    return 0;
}

期望替换进去新的 main 函数。这里是在 main 函数之前添加了一行输出。

文件:decorator.cpp

#include <iostream>

int originalMain(); // 后续会将 main.cpp 中的 main() 重命名为 originalMain() 

int mainWrapper() { // mainWrapper() 后续会被重命名为 main()
     std::cout << "Hello in mainWrapper();" << std::endl;
    return originalMain();
}

操作:编译生成中间代码文件

g++ main.cpp -o main.o -c
g++ decorator.cpp -o decorator.o -c

操作:查看符号名称

objdump -t decorator.o

输出

decorator.o:     文件格式 elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*    0000000000000000 decorator.cpp
0000000000000000 l    d  .text    0000000000000000 .text
0000000000000000 l    d  .data    0000000000000000 .data
0000000000000000 l    d  .bss    0000000000000000 .bss
0000000000000000 l    d  .rodata    0000000000000000 .rodata
0000000000000000 l     O .rodata    0000000000000001 _ZStL19piecewise_construct
0000000000000000 l     O .bss    0000000000000001 _ZStL8__ioinit
0000000000000033 l     F .text    0000000000000049 _Z41__static_initialization_and_destruction_0ii
000000000000007c l     F .text    0000000000000015 _GLOBAL__sub_I__Z11mainWrapperv
0000000000000000 l    d  .init_array    0000000000000000 .init_array
0000000000000000 l    d  .note.GNU-stack    0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame    0000000000000000 .eh_frame
0000000000000000 l    d  .comment    0000000000000000 .comment
0000000000000000 g     F .text    0000000000000033 _Z11mainWrapperv
0000000000000000         *UND*    0000000000000000 _ZSt4cout
0000000000000000         *UND*    0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*    0000000000000000 _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000000000         *UND*    0000000000000000 _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000         *UND*    0000000000000000 _ZNSolsEPFRSoS_E
0000000000000000         *UND*    0000000000000000 _Z12originalMainv
0000000000000000         *UND*    0000000000000000 _ZNSt8ios_base4InitC1Ev
0000000000000000         *UND*    0000000000000000 .hidden __dso_handle
0000000000000000         *UND*    0000000000000000 _ZNSt8ios_base4InitD1Ev
0000000000000000         *UND*    0000000000000000 __cxa_atexit

记录:(不同的编译器可能会对相同的函数声明给出不同的符号,但一般都会包含函数的原名称,请根据实际情况记录符号名)

  • mainWrapper() 函数的符号为 _Z11mainWrapperv
  • originalMain() 函数的符号为 _Z12originalMainv
  • 注:main() 函数的符号一般为 main

整理思路

  • 将 main.o 中 main() 的符号重命名为 originalMain() 的符号
  • 将 decorator.o 中 mainWrapper() 的符号重命名为 main() 的符号

操作:重命名符号( --redefine-sym old=new

objcopy --redefine-sym main=_Z12originalMainv main.o
objcopy --redefine-sym _Z11mainWrapperv=main decorator.o

操作:链接两个目标代码文件

g++ main.o decorator.o -o a.out

操作:执行程序验证修改

./a.out

输出:输出结果与预期一致

Hello in mainWrapper();
Hello in main();

脚本

上述流程可以总结为以下脚本。

#!/bin/sh

# parameter
sourceFile="./main.cpp"
decoratorSourceFile="./decorator.cpp"

targetFile="./a.out"

mainSymbolName="main"
wrapperSymbolName="_Z11mainWrapperv"
originalMainSymbolName="_Z12originalMainv"

objFile="${sourceFile%.*}.o"
decoratorObjFile="${decoratorSourceFile%.*}.o"

# compiles source files without linking 
g++ "${sourceFile}" -o "${objFile}" -c
g++ "${decoratorSourceFile}" -o "${decoratorObjFile}" -c

# rename symbols to apply decoration
objcopy --redefine-sym "${mainSymbolName}=${originalMainSymbolName}" "${objFile}"
objcopy --redefine-sym "${wrapperSymbolName}=${mainSymbolName}" "${decoratorObjFile}"

# linking object files
g++ "${objFile}" "${decoratorObjFile}" -o "${targetFile}"

# cleanup
rm "${objFile}" "${decoratorObjFile}"
本文采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
本文作者:KeNorizon
本文链接:https://blog.kenorizon.cn/solution/modify-c-program-entrypoint.html
# C++

评论

暂无

添加新评论