【问题标题】:Override a function call in C覆盖 C 中的函数调用
【发布时间】:2010-10-11 16:18:53
【问题描述】:

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

例如,假设我在源代码中使用了数千次名为getObjectName 的函数。有时我想临时覆盖此函数,因为我想更改此函数的行为以查看不同的结果。

我像这样创建一个新的源文件:

#include <apiheader.h>    

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

我像往常一样编译我的所有其他源代码,但在与 API 库链接之前,我先将其链接到此函数。这工作正常,但我显然不能在我的覆盖函数中调用真正的函数。

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

【问题讨论】:

  • @dreamlax,我们现在正在从通用 (C) 解决方案转向特定 (gcc/linux) 解决方案 - 澄清您正在运行的内容以便更好地定位是个好主意答案。
  • 嗯,我正在 Linux 上开发,但目标是 Mac OS、Linux 和 Windows。事实上,我想要覆盖函数的原因之一是因为我怀疑它们在不同的操作系统上表现不同。

标签: c function linker overriding


【解决方案1】:

使用 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() 的调用都重新路由到您的包装函数(在链接时)。然而,在 Mac OS X 下的 gcc 中没有这个非常有用的标志。

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

【讨论】:

  • 这是一个不错的方法。不知道那个。但如果我正在阅读手册页,它应该是“__real_getObjectName(anObject);”它由链接器路由到 getObjectName。否则,您将再次递归调用 __wrap_getObjectName。还是我错过了什么?
  • 你说得对,它必须是 __real_getObjectName,谢谢。我应该仔细检查一下手册页:)
  • 我很失望 Mac OS X 上的 ld 不支持 --wrap 标志。
  • 顺便说一下,编译器不提供__real*符号的声明。您必须使用extern char *__real_getObjectName(object *anObject) 自己声明。
【解决方案2】:

如果您只想捕获/修改调用,则最简单的解决方案是将头文件 (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-type 调用)但是我认为你的情况没有必要。

【讨论】:

  • 使用编译时标志来控制拦截(请参阅更新的答案)。同样,这也可以在运行时完成,您只需要在 myGetObjectName() 中检测它,如果设置了运行时标志(即,仍然拦截但改变行为),则始终调用 getObjectName。
  • 您可以使用“-include file”选项让GCC自动包含文件。你甚至不必触摸任何文件:)
  • 虽然在包含原始 API 文件时自动包含可能会导致问题。它的声明也将被替换。不太容易:)
  • 我最喜欢这个解决方案,因为它适用于任何 C 编译器。这也是几十年来使用的久经考验的真实方法。很简单。
  • 这非常适用于单元测试套件(例如使用 CGreen),您希望在函数中存根某些依赖项。对我来说是一个很好的答案。
【解决方案3】:

您可以使用 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 中的任何函数,而无需修改/重新编译程序,因此您可以对没有源代码的程序执行此操作。不是很好吗?

【讨论】:

  • 不行,您只能通过这种方式覆盖共享库提供的函数。
  • @ChrisStratton 你是对的,系统调用不能以这种方式被覆盖,我已经编辑了我的答案。
【解决方案4】:

如果你使用 GCC,你可以将你的函数设为weak。那些can be overridden的非弱函数:

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 实现文件中添加一个包含:

weakdecls.h

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

functions.c

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

void test(void) { 
    ...
}

... other functions ...

这样做的缺点是,如果不对 api 文件做一些事情(需要这三行和 weakdecls),它就不能完全工作。但是,一旦您进行了更改,就可以通过在一个文件中编写全局定义并将其链接到其中来轻松覆盖函数。

【讨论】:

  • 那需要修改 API,不是吗?
  • 您的函数名称将相同。也不会以任何方式更改 ABI 或 API。链接时只需包含覆盖文件,并且将对非弱函数进行调用。 libc/pthread 做到了这一点:当 pthread 被链接进来时,它的线程安全函数被使用而不是 libc 的弱函数
  • 我添加了一个链接。我不知道它是否适合你的目标(即你是否可以忍受那个 GCC 的东西。如果 msvc 有类似的东西,你可以#define WEAK thoigh)。但如果在 linux 上,我会使用那个(也许还有更好的方法。我不知道。也考虑版本控制)。
【解决方案5】:

通常需要修改 现有代码库的行为 包装或替换功能。什么时候 编辑那些的源代码 功能是一个可行的选择,这可以 是一个直截了当的过程。什么时候 函数的来源不能 编辑(例如,如果函数是 由系统 C 库提供), 那么替代技术是 必需的。在这里,我们提出这样的 适用于 UNIX、Windows 和 Macintosh OS X 平台。

这是一份很棒的 PDF,介绍了如何在 OS X、Linux 和 Windows 上完成此操作。

它没有任何惊人的技巧,这里没有记录(顺便说一句,这是一组惊人的响应)......但它是一个很好的阅读。

Intercepting arbitrary functions on Windows, UNIX, and Macintosh OS X platforms (2004), by Daniel S. Myers and Adam L. Bazinet.

你可以download the PDF directly from an alternate location (for redundancy)

最后,如果前两个来源以某种方式被烧毁,here's a Google search result for it

【讨论】:

    【解决方案6】:

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

    您需要能够修改调用者的源代码,但不能修改被调用者(因此这在调用第三方库时会起作用)。

    foo.h:

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

    foo.cpp:

    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();
    }
    

    【讨论】:

    • 我已经考虑过这种方法,但它确实需要修改源代码,除非我必须这样做,否则我并不想这样做。尽管这具有在运行时切换的额外好处。
    【解决方案7】:

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

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

    覆盖.h

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

    foo.c

    function foo() { return 1234; }
    

    override.c

    function foo() { return 5678; }
    

    在 Makefile 中使用 pattern-specific variable values 添加编译器标志 -include override.h

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

    另外:也许你也可以使用-D 'foo(x) __attribute__((weak))foo(x)' 来定义你的宏。

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

    • 这允许您覆盖任何源文件中的单个函数,而无需修改代码。

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

    【讨论】:

    • 告诉我这个:发生致命错误:在 0x3f000120 加载的段与在 0x3f000020 加载的段位于相同的 64KB 闪存映射中。无法生成二进制文件。建议更改链接描述文件或 ELF 以合并部分。
    • 这个方法不能处理静态函数。
    • 我认为重要的部分是,首先包含override.h而不修改原始源。其次,foo.coverride.h 一起编译,其中原始foo() 被修饰为weak 符号。而 override.c 编译时没有 override.h 以提供 foo() 的非弱符号。
    • 如果foo()函数在定义的地方也被使用,gcc给出error: expected declaration specifiers or ‘...’ before numeric constant
    • @vaughan 请看我的回答。谢谢。
    【解决方案8】:

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

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

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

    这里的链接顺序要非常小心,否则它将不起作用。

    【讨论】:

    • 听起来很棘手,但它确实避免了修改源代码。非常好的建议。
    • 我不认为你可以强制 getObjectName 去一个特定的库而不用 dlopen/dlsym 诡计。
    • 任何在宿主库中拖拽的链接时操作都会产生一个多重定义的符号。
    • 示例:您链接到 lib2,链接器需要 l1getObj(重新定义的名称)。但是 l1getObj 需要 getObj(它已经在 l2 中,所以链接器不会引入宿主对象)——这会导致无限递归。
    • 当库是动态的而不是静态的时,这种行为会特别奇怪。偶然发现的。
    【解决方案9】:

    您也可以使用共享库 (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 解决方案,因为它是最灵活的。

    【讨论】:

      【解决方案10】:

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

      短版

      一般来说,要成功覆盖一个函数,你必须考虑:

      • 弱属性
      • 翻译单元排列

      加长版

      我有这些源文件。

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

      main.c

      #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();
      }
      

      func3.c

      #include <stdio.h>
      
      void func3 (void)
      {
          printf("in original func3()\n");
      }
      

      decl.h

      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);
      

      覆盖.c

      #include <stdio.h>
      
      void func2 (void)
      {
          printf("in mock func2()\n");
      }
      
      void func3 (void)
      {
          printf("in mock func3()\n");
      }
      

      Makefile1:

      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 
      

      Makefile2:

      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
      

      all_strong.a:

      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 文件中定义的相同函数。在上面的 Makefile1 中,override.o 中的 func2()func3() 覆盖了 all_weak.a 中的对应项。我尝试了两个 .o 文件,但它不起作用。

      2. 对于 GCC,您不需要像 Visual Studio 工具链here 中所述将函数拆分为单独的 .o 文件。我们可以在上面的例子中看到,func2()(与func1()在同一个文件中)和func3()(在一个单独的文件中)都可以被覆盖。

      3. 要覆盖一个函数,在编译它的consumertranslation unit时,你需要将该函数指定为weak。这将在consumer.o 中将该功能记录为弱。在上面的例子中,当编译test_target.c,它消耗func2()func3(),你需要添加-include weak_decl.h,它声明func2()func3()是弱的。 func2() 也在 test_target.c 中定义,但没关系。

      一些进一步的实验

      还是上面的源文件。但是稍微改变一下override.c

      覆盖.c

      #include <stdio.h>
      
      void func2 (void)
      {
          printf("in mock func2()\n");
      }
      
      // void func3 (void)
      // {
      //     printf("in mock func3()\n");
      // }
      

      这里我删除了func3() 的覆盖版本。 我这样做是因为我想回退到 func3.c 中的原始 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 存档文件中了吗?

      ar cr all_weak.a func3.o test_target_weak.o

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

      覆盖.c

      #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,它是在一个单独的文件/翻译单元 (func3.c) 中定义的。如果声明为弱,消费者test_target.o 仍会将func3() 记录为弱。但不幸的是,GCC 链接器不会检查同一 .a 文件中的其他 .o 文件以查找 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() 无法解析。

      但我仍然不知道为什么 GCC 会这样。如果有人能解释一下,请。

      (2021 年 8 月 8 日上午 9:01 更新: this 线程可能会解释这种行为,希望。)

      所以进一步的结论是:

      1. 如果您将某个符号声明为weak,则最好提供allweak 函数的覆盖版本。否则,原始版本无法解析,除非它位于调用者/消费者的同一文件/翻译单元中。

      【讨论】:

      • 这没有提供问题的答案。要批评或要求作者澄清,请在他们的帖子下方留下评论。 - From Review
      • @RichardChambers 是的,帖子还没有完成。而且太长了,不适合发表评论。
      猜你喜欢
      • 2016-02-29
      • 2010-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-11
      相关资源
      最近更新 更多