GNU gcc/ld - 如何在同一目标文件中,用调用者和被调用者定义包装一个符号?

47
为了澄清,我的问题是关于使用GCC编译器和链接器时,在同一编译单元中定义调用者和被调用者的情况下,对一个函数/符号的调用进行包装/拦截的问题。
我有一个类似以下情况的情况:
/* foo.c */
void foo(void)
{
  /* ... some stuff */
  bar();
}

void bar(void)
{
  /* ... some other stuff */
}

我想包装对这些函数的调用,我可以使用ld的--wrap选项(然后实现__wrap_foo和__wrap_bar,它们依次调用__real_foo和__real_bar,正如ld的--wrap选项所期望的那样)来做到这一点。
gcc -Wl,--wrap=foo -Wl,--wrap=bar ...

我遇到的问题是,这仅在来自此编译单元之外(并在链接时解决)对 foo 和 bar 的引用产生影响。也就是说,在 foo.c 中的其他函数中调用 foo 和 bar 不会被包装。

calls from within the compilation unit get resolved before the linker's wrapping

我尝试使用objcopy --redefine-sym,但那只是重命名符号及其引用。
在将*.o文件传递给链接器的--wrap选项之前,我想将对foobar(在foo.o内部)的调用替换为__wrap_foo__wrap_bar(就像链接器的--wrap选项在其他对象文件中解析它们一样),而不必修改foo.c的源代码。
这样,包装/拦截就会发生在对foobar的所有调用上,而不仅仅是在foo.o之外的调用上。
这可能吗?

2
如果你必须对目标文件进行操作,可能需要用一些包装逻辑来覆盖函数的开头,并且这将需要理解特定平台的函数调用、寄存器保存等序列,并希望它们不会改变。仅仅通过查找和替换地址是行不通的,因为它们通常是相对的 - 你可以模式匹配你认为编译器会使用的任何调用指令,找出它们的目标并进行修改,但这会很快变得混乱不堪。 - Chris Stratton
1
如果您可以修改源代码/构建命令以实现您所希望的修复,为什么不能在源代码中的函数名称级别上解决它呢?或者将该函数移动到自己的编译单元中? - Chris Stratton
1
我不确定我看得出自动更改源代码的脚本和更难以证明对象修改的脚本之间的区别。https://dev59.com/GHRB5IYBdhLWcg3wc3A0 提供了一些变化。如果只是为了分析性能,您可以使用断点调试器功能吗? - Chris Stratton
2
这不完全是你所问的,但我来到这里寻找一个稍微不同的问题:如何替换已编译的目标文件中的函数,以便现有目标文件内的调用者引用另一个文件中的新函数?答案是使用 objcopy --weaken-symbol=called_function 并链接一个定义了 called_function() 的新目标文件。 - Vegard
1
有人能使用“--wrap”实现目标,这很有趣,但我没有成功。但是我发现可以使用LD_PRELOAD运行时函数替换技术进行运行时函数包装来实现目标。 - Alexey Yahno
显示剩余8条评论
7个回答

23

你需要使用objcopy弱化和全局化符号。

-W symbolname
--weaken-symbol=symbolname
    Make symbol symbolname weak. This option may be given more than once.
--globalize-symbol=symbolname
    Give symbol symbolname global scoping so that it is visible outside of the file in which it is defined. This option may be given more than once.

这对我有用

bar.c:

#include <stdio.h>
int foo(){
  printf("Wrap-FU\n");
}

foo.c:

#include <stdio.h>

void foo(){
printf("foo\n");
}

int main(){
printf("main\n");
foo();
}

编译它

$ gcc -c foo.c bar.c 

削弱foo符号并将其设为全局变量,以便它可以再次供链接器使用。

$ objcopy foo.o --globalize-symbol=foo --weaken-symbol=foo foo2.o

现在,您可以将新的对象与bar.c中的包裹连接。

$ gcc -o nowrap foo.o #for reference
$ gcc -o wrapme foo2.o bar.o

测试

$ ./nowrap 
main
foo

还有一个被包装的:

$ ./wrapme 
main
Wrap-FU

我在以下情况下尝试了这个技巧: 1- 我有一个嵌入式平台的SDK,其中有一个函数需要用另一个声明替换。2- 编译后,我使用gcc-objcopy从目标库中的对象文件再次将符号设置为弱和全局。问题在于构建过程包括创建一个存档文件(称为core.a),其中包含旧库对象文件。3- 我添加了一步,使用gcc-ar从cora.a删除对象文件并用新的对象文件(带有弱符号)替换它。结果,这个技巧没有成功(多重定义..)。请帮忙? - Yahya Tawil

10
您可以在被调用的函数实现前使用__attribute__((weak))来允许其他人重新实现它,而不会导致GCC报告多个定义的错误。
例如,假设您想在以下hello.c代码单元中模拟world函数。您可以添加该属性以便能够覆盖它。
#include "hello.h"
#include <stdio.h>

__attribute__((weak))
void world(void)
{
    printf("world from lib\n");
}

void hello(void)
{
    printf("hello\n");
    world();
}

你可以在另一个单元文件中覆盖它。非常适用于单元测试/模拟:

#include <stdio.h>
#include "hello.h"

/* overrides */
void world(void)
{
    printf("world from main.c"\n);
}

void main(void)
{
    hello();
    return 0;
}

这是个好主意。下次会用到。不幸的是,在我提出问题的时候,我正在处理无法修改以添加此类属性的软件。然而,这很好,并且将来肯定会成为我的工具箱中的一部分。 - luis.espinal
1
如果您无法修改源代码,那么@PeterHuewe的答案是使用objcpy的解决方案。如果您可以修改源代码,那么这个解决方案似乎更容易设置。 - MicroJoe

7
#include <stdio.h>
#include <stdlib.h>

//gcc -ggdb -o test test.c -Wl,-wrap,malloc
void* __real_malloc(size_t bytes);

int main()
{
   int *p = NULL;
   int i = 0;

   p = malloc(100*sizeof(int));

   for (i=0; i < 100; i++)
       p[i] = i;

   free(p);
   return 0;
}

void* __wrap_malloc(size_t bytes)
{
      return __real_malloc(bytes);
}

然后只需编译此代码并进行调试。 当您调用reall malloc时,将调用__wrap_malloc函数,并且__real_malloc将调用malloc。

我认为这是拦截调用的方法。

基本上是由ld提供的--wrap选项。


8
我知道这个选项,这基本上就是我使用的。但在我提到的情况下它不起作用。请再次查看我的原始问题。 - luis.espinal
1
这个答案中的示例展示了如何使用--wrap,但它并没有展示在同一编译单元中定义了被包装的函数(在本例中为malloc)和调用之间的情况,这正是原问题的核心。因此,它并不是对问题的真正回答,我会给这个答案点个踩。 - Matthijs Kooijman

6

这似乎按照文档所述正常工作:

 --wrap=symbol
       Use a wrapper function for symbol. 
       Any undefined reference to symbol will be resolved to "__wrap_symbol". ...

请注意上面的undefined。 当链接器处理foo.o时,bar()不是未定义的,因此链接器不会包装它。我不确定为什么要这样做,但可能存在需要这样做的用例。

我使用这个来包装跨编译单元的调用(请参见我的原始问题以获取示例)。但是,它无法拦截/包装来自编译单元内部的所有调用(这正是我感兴趣的拦截内容)。显然,在编译单元内部,引用已经被解析。当链接器介入时,使用--wrap链接器选项包装这些调用已经为时过晚了。 - luis.espinal
@luis.espinal “现在已经太晚了” - 不,还没有。链接器可以轻松更改调用目标;它只是不这样做(出于我不知道的原因)。 - Employed Russian
当我说“太晚了”的时候,我是在GNU ld的上下文中这么说的(而不是一般链接器的上下文中)。是的,一个链接器可以很容易地改变那个调用目标。但是问题中的链接器(GNU ld)没有。原因是它限制自己只替换/重写在编译单元内没有解析的引用。正是因为最后一步,我才说对于GN ld来说,链接阶段已经太晚了(尽管对于更聪明的链接器来说还不算太晚)。 - luis.espinal

5
我尝试了solution,来自@PeterHuewe,它可以工作,但是它不允许从包装器中调用原始函数。为了允许这样做,我的解决方案如下:

foo.c


#include <stdio.h>

void foo(){
    printf("This is real foo\n");
}

int main(){
    printf("main\n");
    foo();
}

foo_hook.c

#include <stdio.h>

void real_foo();

int foo(){
  printf("HOOK: BEFORE\n");
  real_foo();
  printf("HOOK: AFTER\n");
}

Makefile

all: link

link: hook
    gcc -o wo_hook foo.o
    gcc -o w_hook foo_hooked.o foo_hook.o

hook: build_o
    objcopy \
    foo.o \
    --add-symbol real_foo=.text:$(shell  objdump -t foo.o | grep foo | grep .text | cut -d' ' -f 1),function,global \
    --globalize-symbol=foo \
    --weaken-symbol=foo \
    foo_hooked.o

build_o:
    gcc -c foo.c foo_hook.c

clean:
    -rm w_hook wo_hook *.o

例子

virtualuser@virtualhost:~/tmp/link_time_hook$ make
gcc -c foo.c foo_hook.c
objcopy foo.o \
--add-symbol real_foo=.text:0000000000000000,function,global \
--globalize-symbol=foo \
--weaken-symbol=foo \
foo_hooked.o
gcc -o wo_hook foo.o
gcc -o w_hook foo_hooked.o foo_hook.o
virtualuser@virtualhost:~/tmp/link_time_hook$ ls
Makefile  foo.c  foo.o  foo_hook.c  foo_hook.o  foo_hooked.o  w_hook  wo_hook
virtualuser@virtualhost:~/tmp/link_time_hook$ ./w_hook
main
HOOK: BEFORE
This is real foo
HOOK: AFTER
virtualuser@virtualhost:~/tmp/link_time_hook$
virtualuser@virtualhost:~/tmp/link_time_hook$ ./wo_hook
main
This is real foo
virtualuser@virtualhost:~/tmp/link_time_hook$

谢谢!我已经很久没有碰过这个问题了 :) - luis.espinal
2
这个脚本有一个错误,因为它只在0函数上进行了测试。也就是说,objcopy将解释该值为十进制,而objdump则给出十六进制,因此必须添加“0x”,例如--add-symbol real_foo=.text:0x$(shell objdump -t foo.o | grep foo | grep .text | cut -d' ' -f 1),function,global和--add-symbol real_foo=.text:0x0000000000000000,function,global将使此函数在这种特殊的零情况之外。 - Gregory Morse

4

如果您在--wrap后使用--undefined,则可以实现您想要的功能。

  -u SYMBOL, --undefined SYMBOL
                              Start with undefined reference to SYMBOL

2
你会把这个选项添加在哪里?可以给一个更完整的例子吗?我试过在连接器命令行中添加 -u bar,再加上 -Wl,--wrap=bar,但好像没有任何变化?或许这会使 foo 在开头变为未定义,但是不会影响 foo.c 中的内容… - Matthijs Kooijman

0

使用链接器

$ /usr/bin/ld --version
GNU ld (GNU Binutils for Ubuntu) 2.30

我能够使用defsym选项解决了这个问题:

--defsym SYMBOL=EXPRESSION  Define a symbol`

不是

gcc -Wl,--wrap=foo -Wl,--wrap=bar ...

尝试

gcc -Wl,--defsym,foo=__wrap_foo -Wl,--defsym,bar=__wrap_bar ...

我也没有尝试定义__real_*符号。


有趣的是,--defsym 似乎只允许覆盖 .o 文件中现有的符号(即 --defsym 和 .o 文件中定义的 foo 不会导致多重定义错误)。--defsym 看起来与链接脚本中的赋值处理方式基本相同,可能会产生相同的行为。然而,我认为这种方法不允许同时定义 __real_* 符号:一旦覆盖了 foo 符号,我认为您将无法访问原始符号... - Matthijs Kooijman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接