如何挂钩静态函数?

How can I hook a static function?

我想在不修改源代码的情况下模拟一个静态函数。这是因为我们有大量遗留代码库,我们希望添加测试代码而不需要开发人员检查和更改大量原始代码。

使用 objcopy,我可以在目标文件之间使用函数,但不能影响内部链接。换句话说,在下面的代码中,我可以让 main.cpp 从 bar.c 调用模拟的 foo(),但我无法让 UsesFoo() 从 [=] 调用模拟的 foo() 32=].

我明白这是因为 foo.c 中已经定义了 foo()。除了更改源代码之外,有什么方法可以使用 ld 或其他工具删除 foo() 以便最终链接将其从我的 bar.c?

中提取出来

foo.c

#include <stdio.h>

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

void UsesFoo()
{
    printf("UsesFoo(). Calling foo()\n");
    foo();
}

bar.c

#include <stdio.h>

void foo()
{
    printf("I am the foo from bar.c\n");
}

main.cpp

#include <iostream>

extern "C" void UsesFoo();
extern "C" void foo();

using namespace std;

int main()
{
    cout << "Calling UsesFoo()\n\n";
    UsesFoo();
    cout << "Calling foo() directly\n";
    foo();
    return 0;
}

编译:

gcc -c foo.c
gcc -c bar.c
g++ -c main.c
(Below simulates how we consume code in the final output)
ar cr libfoo.a foo.o
ar cr libbar.a bar.o
g++ -o prog main.o -L. -lbar -lfoo
This works because the foo() from libbar.a gets included first, but doesn't affect the internal foo() in foo.o

我也试过:

gcc -c foo.c
gcc -c bar.c
g++ -c main.c
(Below simulates how we consume code in the final output)
ar cr libfoo.a foo.o
ar cr libbar.a bar.o
objcopy --redefine-sym foo=_redefinedFoo libfoo.a libfoo-mine.a
g++ -o prog main.o -L. -lbar -lfoo-mine
This produces the same effect. main will call foo() from bar, but UsesFoo() still calls foo() from within foo.o

我想你可以试试 gcc 中的 --wrap 标志。使用标志的示例:

我将 --wrap 标志与我看到的静态函数一起使用它仍然有效,只是我无法调用原始 __real_foo() 函数。如果你接受这个限制,你可以试试这个。

main.c

  #include <stdio.h>
    
    //extern int __real_foo();
    extern int foo();
    int __wrap_foo() {
        printf("wrap foo\n");
        //__real_foo();
        return 0;
    }
    
    int main () {
        printf("foo:");foo();
        printf("wrapfoo:");__wrap_foo();
    
        return 0;
    }

foo.c

#include <stdio.h>
static int foo() {
    printf("foo\n");
    return 0;
}

终端输出:

└─[0] <> gcc main.c foo.c -Wl,--wrap=foo -o main && ./main 
foo:wrap foo
wrapfoo:wrap foo
┌─[longkl@VN] - [~/test] - [2021-12-22 10:13:54]
└─[0] <> gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
如果您愿意更改源代码,

long.kl 的答案有效。不幸的是,因为我们希望尽可能保持源代码的原始状态,所以这对我们没有用。

不管 AndrewHenle 在他的回答中是怎么想的,我们可以重写目标文件以允许我们覆盖静态函数。这需要理解和解析写入目标文件的ELF格式。

主要问题是目标文件中的函数将使用相对 jumps/branches/calls 到文本段中的地址。换句话说,假设我们有以下代码:

#include <stdio.h>

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

void UsesFoo()
{
    printf("UsesFoo(). Calling foo()\n");
    foo();
}

在这种情况下,没有优化(“gcc -c foo.c”),这会生成一个目标文件,foo.o,它具有以下反汇编:

objdump -d foo.o

foo.o:     file format elf64-x86-64


Disassembly of section .text:

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

0000000000000013 <UsesFoo>:
  13:   55                      push   %rbp
  14:   48 89 e5                mov    %rsp,%rbp
  17:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 1e <UsesFoo+0xb>
  1e:   e8 00 00 00 00          callq  23 <UsesFoo+0x10>
  23:   b8 00 00 00 00          mov    [=11=]x0,%eax
  28:   e8 d3 ff ff ff          callq  0 <foo>
  2d:   90                      nop
  2e:   5d                      pop    %rbp
  2f:   c3                      retq   

看看指令 0xb 和 0x1e。这些是 c 代码中的 printf() 被翻译成的调用。您会注意到在操作码 0xe8 之后,其余字节为 0x00。这是因为它们会在最终编译时被链接器替换到puts的地址(假设这是一个静态链接)。

现在注意 0x28 处的调用指令正在使用 0xd3 ff ff ff 的地址进行调用。如果这是一个非静态函数,我们会在操作码后看到相同的 0x00 字节,但在这种情况下我们会看到 0xd3ffffff。这是一个32位的相对调用,对应于2的补码中的-1(最终地址在指令指针中会变为0)。这意味着我们的文本段(代码)已被硬编码为使用该地址。

为了解决这个问题,我们将不得不重新编写 ELF 以更改对 foo() 调用的处理方式。有几个选项:

  1. 我们在文件中添加了另一个 .text.[somename] 部分,其中包含用作蹦床的代码,即:FakeFoo()。然后我们重写 foo() 的第一条指令立即跳转到 FakeFoo()。 Hacky,但可能会丢失调试信息。

  2. .rela.text 部分包含函数重定位。这些用于告诉链接器我们需要用最终位置替换调用的字节。当链接器看到此部分时,它会将“偏移”字段中的地址替换为最终二进制文件中的实际计算地址。对于我们的二进制文件,我们看到:

readelf -r foo.o

Relocation section '.rela.text' at offset 0x280 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000007  000500000002 R_X86_64_PC32     0000000000000000 .rodata - 4
00000000000c  000b00000004 R_X86_64_PLT32    0000000000000000 puts - 4
00000000001a  000500000002 R_X86_64_PC32     0000000000000000 .rodata + 7
00000000001f  000b00000004 R_X86_64_PLT32    0000000000000000 puts - 4

偏移量 0xc 和 0x14 是 foo() 和 UsesFoo() 中的调用指令寻找 puts() 函数的位置(注意:编译器将我们对“printf()”的调用转换为使用“puts( )").

因此,我们可以在此处为指令 0x28 处的调用添加另一个条目,并让链接器在代码中某处查找另一个名为“foo()”的函数,该函数未声明为静态。

这还需要修复 ELF 文件的 .symtab 条目,因为它将包含对本地函数 foo() 的引用:

readelf -s foo.o

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000    19 FUNC    LOCAL  DEFAULT    1 foo
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
    12: 0000000000000013    29 FUNC    GLOBAL DEFAULT    1 UsesFoo

为了让链接器在目标文件之外查找 foo(),我们必须将 foo 的条目更改为“NOTYPE GLOBAL”“UND”类型,这样链接器就不会认为它存在于这个文件中。

还有一个部分,.rela.eh_frame,用于调试,您也需要注意。

最后,这种方法要求您遍历二进制文件,搜索对应于 jumps/calls/branches 和 create/fix 条目的操作码,以便链接器在其他文件中查找“foo()”目标文件。

所有这一切只是为了让链接器在不同的文件中寻找 foo(),这样您就可以用您编写的 foo() 替换原来的 foo()。如果你想在所有这些之后调用原始的 foo(),你可能想将 foo() 重命名为其他名称,即:_real_foo(),并设置符号 table (. symtab) 以便你的假 foo() 可以做类似的事情:

bar.c:

void foo()
{
  printf("I am the fake foo! Calling the real foo!\n");
  __real_foo();
}

最终,如果您的开发人员将他们的大部分功能从静态方法转移到全局方法,情况会好得多(也容易得多)。但是,如果你想在目标文件创建后重写它,在适当的情况下,可以付出相当大的努力。