覆盖C中的函数调用


71

为了记录调用,我想覆盖对各种API的某些函数调用,但是我也想在将数据发送到实际函数之前对其进行操作。

例如,假设我getObjectName在源代码中使用了一个被称为数千次的函数。有时我想暂时重写此功能,因为我想更改此功能的行为以查看不同的结果。

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

我会像往常一样编译所有其他源代码,但是在与API库链接之前,我先将其与该函数链接。这工作正常,除非我显然无法在覆盖函数中调用真实函数。

有没有一种更简单的方法来“重写”一个函数而又不会得到链接/编译错误/警告?理想情况下,我希望能够仅通过编译和链接一个或两个额外的文件来覆盖该功能,而不是随意使用链接选项或更改程序的实际源代码。


@dreamlax,我们现在正在从一般(C)到特定(gcc / linux)解决方案-弄清楚您正在运行的内容是一个好主意,以便更好地定位答案。
paxdiablo

1
我正在Linux上进行开发,但目标是Mac OS,Linux和Windows。实际上,我想覆盖功能的原因之一是因为我怀疑它们在不同的操作系统上的行为会有所不同。
dreamlax

Answers:


75

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

并实现以下功能(intercept.c其中包含intercept.h):

然后确保要拦截呼叫的每个源文件都有:

在顶部。

然后,当您使用“ -DINTERCEPT”进行编译时,所有文件都将调用您的函数,而不是实际的文件,并且您的函数仍可以调用实际的文件。

不带“ -DINTERCEPT”的编译将防止发生拦截。

如果要拦截所有调用(不仅是源调用),这会有些棘手-通常可以通过动态加载和解析实函数(使用dlload-dlsym-类型调用)来完成,但是我认为这对您没有必要案件。


1
使用编译时标志控制拦截(请参阅更新的答案)。同样,这也可以在运行时完成,您只需要在myGetObjectName()中检测到它,并在设置了运行时标志时始终调用getObjectName(即,仍然拦截但更改行为)。
paxdiablo

12
您可以使用选项“ -include file”使GCC自动包含该文件。您甚至都无需触摸任何文件:)
Johannes Schaub-litb

1
尽管自动包含可能会在包含原始API文件时引起问题。它的声明也将被替换。不太容易:)
Johannes Schaub-litb

1
我喜欢此解决方案,因为它可以在任何C编译器上使用,因此是所有建议中最好的解决方案。这也是数十年来使用的久经考验的真实方法。很简单
克雷格S

2
这对于单元测试套件(例如使用CGreen)非常有用,因为您需要在函数中添加一些依赖项。就我的目的而言,这是一个很好的答案。
nrjohnstone

84

使用gcc,在Linux下,您可以使用--wrap链接器标志,如下所示:

并将您的函数定义为:

这将确保对的所有调用getObjectName()都重新路由到您的包装器函数(在链接时)。但是,在Mac OS X下的gcc中没有这个非常有用的标志。

extern "C"如果使用g ++进行编译,请记住使用声明包装函数。


3
那是个好方法。不知道那一个。但是,如果我正在正确阅读联机帮助页,则应为“ __real_getObjectName(anObject);”。链接器将其路由到getObjectName。否则,您将再次递归调用__wrap_getObjectName。还是我错过了什么?
Johannes Schaub-litb

没错,它必须是__real_getObjectName,谢谢。我应该在手册页中仔细检查一下:)
codelogic

1
我很失望Mac OS X上的ld不支持该--wrap标志。
Daryl Spitzer

3
顺便说一下,编译器不提供__real *符号的声明。您必须使用进行声明extern char *__real_getObjectName(object *anObject)
布伦丹

37

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

在函数的主体(在共享库中)中,您应这样编写:

您可以覆盖共享库中的任何功能,甚至可以覆盖stdlib中的任何功能,而无需修改/重新编译该程序,因此您可以在没有源代码的程序上解决问题。好不好


2
不,您只能以这种方式覆盖共享库提供的功能。
克里斯·斯特拉顿

2
@ChrisStratton是的,不能以这种方式覆盖syscall,我已经编辑了答案。
qrdl 2012年

26

如果您使用GCC,则可以使您的功能weak。这些可以被非弱函数覆盖

test.c

它有什么作用?

test1.c

它有什么作用?

可悲的是,这不适用于其他编译器。但是,您可以在其自己的文件中包含包含可重写函数的弱声明,如果使用GCC进行编译,则仅在API实现文件中包含一个include:

weakdecls.h

functions.c

不利的一面是,如果不对api文件做任何事情(需要那三行代码和weakdecls),它就无法完全工作。但是,一旦进行了更改,就可以通过在一个文件中编写全局定义并将其链接的方式轻松地覆盖函数。


1
那将需要修改API,不是吗?
dreamlax

您的函数名称将相同。也不会以任何方式更改ABI或API。只需在链接时包含覆盖文件,就会调用非弱函数。libc / pthread可以做到这一点:当链接了pthread时,将使用其线程安全函数代替libc的弱函数
Johannes Schaub-litb

我添加了一个链接。我不知道它是否适合您的目标(例如,您是否可以与该GCC一起生活。如果msvc具有类似的功能,则可以#deweak it体弱)。但是如果在linux上,我会用那个(也许还有更好的方法。我也不知道。也研究版本控制)。
Johannes Schaub-litb

11

通常需要通过包装或替换函数来修改现有代码库的行为。当编辑这些功能的源代码是一个可行的选择时,这可能是一个简单的过程。当功能的来源无法编辑时(例如,如果功能由系统C库提供),则需要替代技术。在这里,我们介绍了用于UNIX,Windows和Macintosh OS X平台的此类技术。

这是一个很棒的PDF,涵盖了如何在OS X,Linux和Windows上完成此操作。

它没有此处未记录的任何惊人技巧(顺便说一句,这是一组令人惊奇的答案)……但这是一本不错的书。

Daniel S. Myers和Adam L. Bazinet在Windows,UNIX和Macintosh OS X平台(2004年)上拦截任意功能

您可以直接从备用位置下载PDF(以实现冗余)

最后,如果前两个来源不知何故,这是Google的搜索结果


9

您可以将函数指针定义为全局变量。调用者的语法不会改变。程序启动时,它可以检查是否已将某些命令行标志或环境变量设置为启用日志记录,然后保存功能指针的原始值,并将其替换为日志记录功能。您不需要特殊的“启用日志记录”版本。用户可以在“现场”启用日志记录。

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

foo.h:

foo.cpp:


1
我已经考虑过这种方法,但是它确实需要修改源代码,除非我必须这样做,否则我并不是真正想做的事情。尽管这具有在运行时进行切换的附加好处。
dreamlax

4

在@Johannes Schaub的答案的基础上,构建适合您不拥有的代码的解决方案。

将要覆盖的函数别名为弱定义函数,然后自己重​​新实现。

覆盖

foo.c

覆盖

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

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

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

  • 这使您可以从任何源文件中覆盖单个功能,而无需修改代码。

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


3

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

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

库#2与库#1链接,侦听该调用并调用库#1中的重新定义的版本。

请非常注意此处的链接顺序,否则将不起作用。


听起来很棘手,但确实避免了修改源。非常好的建议。
dreamlax

我认为您可以强制使用getObjectName而不使用dlopen / dlsym技巧来进入特定的库。
paxdiablo

拖到主机库中的任何链接时操作都将导致一个多重定义的符号。
paxdiablo

示例:链接到lib2,链接器需要l1getObj(重新定义的名称)。但是l1getObj需要getObj(已经在l2中,因此链接程序不会引入宿主对象)-这导致无限递归。
paxdiablo

1
当库是动态的而不是静态的时,这种行为就特别奇怪了。我是偶然发现的。
约书亚州

0

您也可以使用共享库(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),然后编译并与适当的链接。

如果您正在寻找用于不同平台和调试-vs-版本的两种不同代码,那么我可能会倾向于使用共享的lib / DLL解决方案,因为它是最灵活的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.