【问题标题】:How can I hook a static function?如何挂钩静态函数?
【发布时间】:2022-01-23 01:48:22
【问题描述】:

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

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

我知道这是因为 foo() 已经在 foo.c 中定义。除了更改源代码之外,我有什么方法可以使用 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

【问题讨论】:

  • static void foo() 即使在同一个库中也不会在 foo.c 之外可见 - 从库外部完全无法访问它。符号名称foo 可能根本不存在。
  • "We can solve any problem by introducing an extra level of indirection." 如果没有间接级别,则无法强制内部调用者使用您的模拟版本。我能想到的不涉及触及内部代码的唯一方法是编写一个代码处理器,该处理器作为构建过程的一部分运行,以创建可编译的实际代码。从那里您可以对其进行调整以替换对foo 的调用。我不知道这是否适合您的用例;最好以某种方式更改遗留代码。
  • @AndrewHenle 静态符号绝对可见。您可以使用“readelf -s foo.o”来查看它,但它是定义为 LOCAL 的,这与您所期望的完全一样。我尝试使用 objcopy 使其全局化,然后重新定义其名称,但它并没有改变结果。
  • @AndyG 谢谢,我可能不得不走那条路,虽然我希望避免它。
  • @Maxthecat 静态符号绝对是可见的。 这次,为了这次编译。尝试更改优化级别,或剥离生成的二进制文件。静态函数并不意味着在单个编译单元之外可见,因此它们甚至不必作为符号存在于最终二进制文件中。有人花时间让它们成为静态的事实意味着它们的名字本来就不应该被看到。鉴于 C 中的所有函数都驻留在一个命名空间中,盲目地更改函数从不意味着可见以使其可见是非常危险的。

标签: c++ c linker ld objcopy


【解决方案1】:

我认为你可以在 gcc 中尝试 --wrap 标志。使用标志的示例: How to wrap functions with the `--wrap` option correctly?

我将 --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.

【讨论】:

  • 谢谢,这是个好建议。不幸的是,这似乎仍然调用了静态函数。
  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
  • @Maxthecat 我刚刚更新了我的评论。
【解决方案2】:

如果您愿意更改源代码,long.kl 的答案将有效。不幸的是,因为我们希望尽可能保持源代码的原始状态,所以这对我们来说是不可用的。

尽管 AndrewHenle 在他的回复中认为,我们可以重写目标文件以允许我们覆盖静态函数。这需要理解和解析目标文件所使用的 ELF 格式。

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

#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    $0x0,%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()。哈克,但可能在调试信息丢失的情况下工作。

  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,用于调试,您也需要注意。

最后,这种方法需要您检查二进制文件,搜索与跳转/调用/分支对应的操作码,并创建/修复条目,以便链接器在其他目标文件中查找“foo()”。

所有这些只是为了让链接器在不同的文件中查找 foo(),以便您可以将原始 foo() 替换为您编写的文件。如果你想在这一切之后调用原始的 foo(),你可能想将 foo() 重命名为其他名称,即:_real_foo(),并设置符号表 (.symtab) 以便你的假 foo( ) 可以执行以下操作:

bar.c:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-28
    • 2011-11-05
    • 1970-01-01
    • 2014-10-13
    • 2013-01-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多