如何挂钩静态函数?
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() 调用的处理方式。有几个选项:
我们在文件中添加了另一个 .text.[somename] 部分,其中包含用作蹦床的代码,即:FakeFoo()。然后我们重写 foo() 的第一条指令立即跳转到 FakeFoo()。 Hacky,但可能会丢失调试信息。
.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();
}
最终,如果您的开发人员将他们的大部分功能从静态方法转移到全局方法,情况会好得多(也容易得多)。但是,如果你想在目标文件创建后重写它,在适当的情况下,可以付出相当大的努力。
我想在不修改源代码的情况下模拟一个静态函数。这是因为我们有大量遗留代码库,我们希望添加测试代码而不需要开发人员检查和更改大量原始代码。
使用 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() 调用的处理方式。有几个选项:
我们在文件中添加了另一个 .text.[somename] 部分,其中包含用作蹦床的代码,即:FakeFoo()。然后我们重写 foo() 的第一条指令立即跳转到 FakeFoo()。 Hacky,但可能会丢失调试信息。
.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();
}
最终,如果您的开发人员将他们的大部分功能从静态方法转移到全局方法,情况会好得多(也容易得多)。但是,如果你想在目标文件创建后重写它,在适当的情况下,可以付出相当大的努力。