在 C 中重写函数调用

为了记录这些调用,我希望重写对各种 API 的某些函数调用,但是我也可能希望在将数据发送到实际函数之前对数据进行操作。

例如,假设我在源代码中数千次使用一个名为 getObjectName的函数。有时候我想要临时覆盖这个函数,因为我想要改变这个函数的行为来看到不同的结果。

我创建了一个新的源文件,如下所示:

#include <apiheader.h>


const char *getObjectName (object *anObject)
{
if (anObject == NULL)
return "(null)";
else
return "name should be here";
}

我像平常一样编译所有其他的源代码,但是在链接到 API 的库之前,我首先将它链接到这个函数。这个函数工作得很好,只是我显然不能调用覆盖函数中的实函数。

有没有更简单的方法来“覆盖”一个函数而不用链接/编译错误/警告?理想情况下,我希望能够通过编译和链接一个或两个额外的文件来覆盖函数,而不是瞎忙于链接选项或修改程序的实际源代码。

114308 次浏览

如果您只想捕获/修改源代码中的调用,那么最简单的解决方案是将一个头文件(intercept.h)与以下内容放在一起:

#ifdef INTERCEPT
#define getObjectName(x) myGetObjectName(x)
#endif

然后按照以下步骤实现函数(在 intercept.c中,没有包括 intercept.h) :

const char *myGetObjectName (object *anObject) {
if (anObject == NULL) return "(null)";
return getObjectName(anObject);

然后确保你想拦截电话的每个源文件的顶部都有以下内容:

#include "intercept.h"

当您使用“ -DINTERCEPT”编译时,所有文件将调用 你的函数而不是实际的函数,而您的函数仍将调用实际的函数。

没有“ -DINTERCEPT”的编译将防止发生拦截。

如果你想拦截所有的调用(不仅仅是来自你的源的调用) ,这有点棘手——这通常可以通过动态加载和解析真正的函数(通过 dlload-dlsym-类型的调用)来完成,但是我不认为这在你的情况下是必要的。

在链接器中还有一种棘手的方法,涉及两个存根库。

库 # 1与宿主库链接,并公开在另一个名称下重新定义的符号。

库 # 2与库 # 1链接,拦截调用并调用库 # 1中重定义的版本。

在这里要非常小心的链接订单,否则它不会工作。

你可以把函数指针定义为一个全局变量。调用方语法不会更改。当你的程序启动时,它可以检查一些命令行标志或环境变量是否设置为启用日志记录,然后保存函数指针的原始值并用日志记录函数替换它。您不需要特殊的“启用日志记录”构建。用户可以“在字段中”启用日志记录。

您将需要能够修改调用方的源代码,但不能修改被调用方(因此在调用第三方库时可以这样做)。

食物 h:

typedef const char* (*GetObjectNameFuncPtr)(object *anObject);
extern GetObjectNameFuncPtr GetObjectName;

返回文章页面

const char* GetObjectName_real(object *anObject)
{
return "object name";
}


const char* GetObjectName_logging(object *anObject)
{
if (anObject == null)
return "(null)";
else
return GetObjectName_real(anObject);
}


GetObjectNameFuncPtr GetObjectName = GetObjectName_real;


void main()
{
GetObjectName(NULL); // calls GetObjectName_real();


if (isLoggingEnabled)
GetObjectName = GetObjectName_logging;


GetObjectName(NULL); // calls GetObjectName_logging();
}

如果你使用 GCC,你可以使你的函数 weak。这些 可以被覆盖通过非弱函数:

Test. c :

#include <stdio.h>


__attribute__((weak)) void test(void) {
printf("not overridden!\n");
}


int main() {
test();
}

它有什么用?

$ gcc test.c
$ ./a.out
not overridden!

Test1.c :

#include <stdio.h>


void test(void) {
printf("overridden!\n");
}

它有什么用?

$ gcc test1.c test.c
$ ./a.out
overridden!

遗憾的是,这对其他编译器不起作用。但是您可以将包含可重写函数的弱声明放在它们自己的文件中,如果您使用 GCC 进行编译,那么只需在 API 实现文件中放入一个 include:

H :

__attribute__((weak)) void test(void);
... other weak function declarations ...

功能 c :

/* for GCC, these will become weak definitions */
#ifdef __GNUC__
#include "weakdecls.h"
#endif


void test(void) {
...
}


... other functions ...

这样做的缺点是,如果不对 api 文件做些什么(需要这三行代码和 softdecl) ,它就无法在 完全相信中工作。但是一旦进行了这种更改,就可以通过在一个文件中编写全局定义并将其链接到。

使用 gcc,在 Linux 下您可以像下面这样使用 --wrap链接器标志:

gcc program.c -Wl,-wrap,getObjectName -o program

并将您的功能定义为:

const char *__wrap_getObjectName (object *anObject)
{
if (anObject == NULL)
return "(null)";
else
return __real_getObjectName( anObject ); // call the real function
}

这将确保所有对 getObjectName()的调用都被重新路由到你的包装函式(在链接时)。然而,在 MacOSX 下的 gcc 中没有这个非常有用的标志。

不过,如果你用 g + + 编译,记得用 extern "C"声明包装函式。

您也可以使用共享库(Unix)或 DLL (Windows)来完成这项工作(这可能会带来一点性能损失)。然后可以更改 DLL/,以便加载(一个版本用于调试,一个版本用于非调试)。

我过去也做过类似的事情(不是为了达到你想要达到的目标,但基本前提是一样的) ,而且效果很好。

[基于 OP 注释编辑]

事实上,我想这么做的原因之一 重写函数是因为我 怀疑他们的行为不同 不同的操作系统。

有两种常见的方法(据我所知)来处理这个问题,共享 lib/dll 方法或者编写不同的实现来进行链接。

对于这两种解决方案(共享库或者不同的链接) ,你可以使用 foo _ linux.c,foo _ osx.c,foo _ win32.c (或者更好的方法是 linux/foo.c,osx/foo.c 和 win32/foo.c) ,然后编译并链接到合适的解决方案。

如果您正在为不同的平台和调试与发布寻找不同的代码,我可能倾向于使用共享的 lib/DLL 解决方案,因为它是最灵活的。

您可以使用 LD_PRELOAD技巧重写函数-请参阅 man ld.so。使用函数编译共享库并启动二进制文件(甚至不需要修改二进制文件!)就像 LD_PRELOAD=mylib.so myprog

在你的函数体中(在共享库中) ,你可以这样写:

const char *getObjectName (object *anObject) {
static char * (*func)();


if(!func)
func = (char *(*)()) dlsym(RTLD_NEXT, "getObjectName");
printf("Overridden!\n");
return(func(anObject));    // call original function
}

您可以覆盖来自共享库的任何函数,甚至来自 stdlib,而不需要修改/重新编译程序,这样您就可以在没有源代码的程序上完成这项工作。不错吧?

以@Johannes Schaub 的回答为基础,提供一个适合您不拥有的代码的解决方案。

别名: 要重写到弱定义函数的函数,然后自己重新实现它。

重写

#define foo(x) __attribute__((weak))foo(x)

Fo.c

function foo() { return 1234; }

重写

function foo() { return 5678; }

在 Makefile 中使用 特定于模式的变量值添加编译器标志 -include override.h

%foo.o: ALL_CFLAGS += -include override.h

旁白: 也许您也可以使用 -D 'foo(x) __attribute__((weak))foo(x)'来定义宏。

编译该文件并将其链接到重新实现(override.c)。

  • 这允许您重写任何源文件中的单个函数,而不必修改代码。

  • 缺点是您必须为要覆盖的每个文件使用单独的头文件。

下面是我的实验。在正文和结尾有4个结论。

简而言之

一般来说,要成功地重写函数,您必须考虑:

  • 弱属性
  • 翻译单位的安排

长篇大论

我有这些源文件。

.
├── decl.h
├── func3.c
├── main.c
├── Makefile1
├── Makefile2
├── override.c
├── test_target.c
└── weak_decl.h

总机

#include <stdio.h>


void main (void)
{
func1();
}

Test _ target. c

#include <stdio.h>


void func3(void);


void func2 (void)
{
printf("in original func2()\n");
}


void func1 (void)
{
printf("in original func1()\n");
func2();
func3();
}

函数3. c

#include <stdio.h>


void func3 (void)
{
printf("in original func3()\n");
}

谢谢

void func1 (void);
void func2 (void);
void func3 (void);

Weak _ decl. h

void func1 (void);


__attribute__((weak))
void func2 (void);


__attribute__((weak))
void func3 (void);

重写

#include <stdio.h>


void func2 (void)
{
printf("in mock func2()\n");
}


void func3 (void)
{
printf("in mock func3()\n");
}

生命档案1:

ALL:
rm -f *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_weak.o -include weak_decl.h
ar cr all_weak.a test_target_weak.o func3.o
gcc main.c all_weak.a override.o -o main -include decl.h

生命档案2:

ALL:
rm -f *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_strong.o -include decl.h # HERE -include differs!!
ar cr all_strong.a test_target_strong.o func3.o
gcc main.c all_strong.a override.o -o main -include decl.h

Makefile1结果的输出:

in original func1()
in mock func2()
in mock func3()

Makefile2的输出:

rm *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_strong.o -include decl.h # -include differs!!
ar cr all_strong.a test_target_strong.o func3.o
gcc main.c all_strong.a override.o -o main -include decl.h
override.o: In function `func2':
override.c:(.text+0x0): multiple definition of `func2'  <===== HERE!!!
all_strong.a(test_target_strong.o):test_target.c:(.text+0x0): first defined here
override.o: In function `func3':
override.c:(.text+0x13): multiple definition of `func3' <===== HERE!!!
all_strong.a(func3.o):func3.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Makefile4:2: recipe for target 'ALL' failed
make: *** [ALL] Error 1

符号表:

All _ weak. a:

test_target_weak.o:
0000000000000013 T func1  <=== 13 is the offset of func1 in test_target_weak.o, see below disassembly
0000000000000000 W func2  <=== func2 is [W]eak symbol with default value assigned
w func3  <=== func3 is [w]eak symbol without default value
U _GLOBAL_OFFSET_TABLE_
U puts


func3.o:
0000000000000000 T func3 <==== func3 is a strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts

全力以赴:

test_target_strong.o:
0000000000000013 T func1
0000000000000000 T func2 <=== func2 is strong symbol
U func3 <=== func3 is undefined symbol, there's no address value on the left-most column because func3 is not defined in test_target_strong.c
U _GLOBAL_OFFSET_TABLE_
U puts


func3.o:
0000000000000000 T func3  <=== func3 is strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts

在这两种情况下,override.o符号:

0000000000000000 T func2  <=== func2 is strong symbol
0000000000000013 T func3  <=== func3 is strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts

拆卸:

test_target_weak.o:     file format elf64-x86-64




Disassembly of section .text:


0000000000000000 <func2>: <===== HERE func2 offset is 0
0:   55                      push   %rbp
1:   48 89 e5                mov    %rsp,%rbp
4:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # b <func2+0xb>
b:   e8 00 00 00 00          callq  10 <func2+0x10>
10:   90                      nop
11:   5d                      pop    %rbp
12:   c3                      retq


0000000000000013 <func1>: <====== HERE func1 offset is 13
13:   55                      push   %rbp
14:   48 89 e5                mov    %rsp,%rbp
17:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 1e <func1+0xb>
1e:   e8 00 00 00 00          callq  23 <func1+0x10>
23:   e8 00 00 00 00          callq  28 <func1+0x15>
28:   e8 00 00 00 00          callq  2d <func1+0x1a>
2d:   90                      nop
2e:   5d                      pop    %rbp
2f:   c3                      retq

所以结论是:

  1. .o文件中定义的函数可以覆盖在 .a文件中定义的相同函数。在上述 马克菲勒1中,override.o中的 func2()func3()覆盖了 all_weak.a中的对应物。我尝试了两个 .o文件,但它不工作。

  2. 对于 海湾合作委员会,不需要像 给你中对 VisualStudio 工具链所说的那样将函数拆分为单独的 .o文件。我们可以在上面的例子中看到,func2()(在与 func1()相同的文件中)和 func3()(在单独的文件中)都可以被覆盖。

  3. 要覆盖函数,在编译其 消费者test_target.c0时,需要将该函数指定为弱函数。它会在 consumer.o中将该函数记录为弱函数。在上面的例子中,当编译使用 func2()func3()test_target.c时,需要添加 -include weak_decl.h,它声明 func2()func3()为弱。func2()也是在 test_target.c中定义的,不过没关系。

一些进一步的实验

仍然保留上面的源文件,但是改变一下 override.c:

重写

#include <stdio.h>


void func2 (void)
{
printf("in mock func2()\n");
}


// void func3 (void)
// {
//     printf("in mock func3()\n");
// }

在这里,我删除了重写版本的 func3().我这样做是因为我想回到 ABC2中最初的 func3()实现。

我仍然使用 Makefile1来构建。构建是可以的。但是运行时错误发生如下:

xxx@xxx-host:~/source/override$ ./main
in original func1()
in mock func2()
Segmentation fault (core dumped)

所以我检查了最终 main的符号:

0000000000000696 T func1
00000000000006b3 T func2
w func3

所以我们可以看到 func3没有有效的地址,这就是为什么会发生段错误。

那么为什么呢? 我没有将 func3.o添加到 all_weak.a归档文件中吗?

一个 功能3测试

我对 func2做了同样的尝试,从 ovrride.c中删除了 func2实现。但这次没有段错误。

重写

#include <stdio.h>


// void func2 (void)
// {
//     printf("in mock func2()\n");
// }


void func3 (void)
{
printf("in mock func3()\n");
}

产出:

xxx@xxx-host:~/source/override$ ./main
in original func1()
in original func2()  <====== the original func2() is invoked as a fall back
in mock func3()

我的猜测是,因为 func2func1是在同一个 文件/翻译单位中定义的。所以 func2总是和 func1一起引入。所以链接器总是可以解析 func2,无论是从 test_target.c还是从 override.c

但是对于 func3,它是在单独的 文件/翻译单位(function 3.c)中定义的。如果它被宣布为弱,消费者 test_target.o仍将记录为弱 func3()。但是 不幸的是,GCC 链接器将不会检查来自同一 ABC4文件的其他 ABC3文件,以查找 func3()的实现。虽然它确实存在。

All _ weak. a:

func3.o:
0000000000000000 T func3 <========= func3 is indeed here!
U _GLOBAL_OFFSET_TABLE_
U puts


test_target_weak.o:
0000000000000013 T func1
0000000000000000 W func2
w func3
U _GLOBAL_OFFSET_TABLE_
U puts

所以我必须在 override.c中提供一个覆盖版本,否则无法解析 func3()

但我还是不明白海湾合作委员会为什么会这样。如果有人能解释一下,请。

(更新于2021年8月8日上午9:01: 这个 线程可以解释这种行为,希望如此。)

所以进一步的结论是:

  1. 如果将某个符号声明为弱函数,则最好提供重写版本的 所有弱函数。否则,原始版本无法解析,除非它位于调用者/使用者的同一 文件/翻译单位中。