簡體   English   中英

如何掛接 static function?

[英]How can I hook a static function?

我試圖在不修改源代碼的情況下模擬 static function 。 這是因為我們有大量的遺留代碼庫,我們想添加測試代碼而不需要開發人員通過 go 並更改一堆原始代碼。

使用 objcopy,我可以玩 object 文件之間的函數,但我不能影響內部鏈接。 換句話說,在下面的代碼中,我可以讓 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();
}

酒吧.c

#include <stdio.h>

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

主文件

#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

通常的技巧是將它們編譯為共享庫,然后使用 LD_PRELOAD 預加載將替換您所針對的功能的版本。

這是一個不錯的教程:

https://catonmat.net/simple-ld-preload-tutorial

我想你可以試試 gcc 中的 --wrap 標志。 使用標志的示例: 如何正確使用 `--wrap` 選項包裝函數?

如果您願意更改源代碼,long.kl 的答案將有效。 不幸的是,因為我們希望盡可能地保持源代碼的原始狀態,所以這對我們來說是不可用的。

盡管 AndrewHenle 在他的回復中認為,我們可以重寫 object 文件以允許我們覆蓋 static function。 這需要理解和解析 object 文件寫入的 ELF 格式。

主要問題是 object 文件中的函數將使用相對跳轉/分支/調用文本段中的地址。 換句話說,假設我們有以下代碼:

#include <stdio.h>

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

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

在這種情況下,沒有優化(“gcc -c foo.c”),這將生成一個 object 文件 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 之后,字節的 rest 為 0x00。 這是因為在最終編譯到 puts 地址的過程中,它們將被 linker 替換(假設這是一個 static 鏈接)。

現在注意 0x28 處的調用指令正在使用 0xd3 ff ff ff 的地址進行調用。 如果這是一個非靜態 function,我們會在操作碼之后看到相同的 0x00 字節,但在這種情況下,我們會看到 0xd3ffffff。 這是一個 32 位相對調用,對應於 2 的補碼中的 -1(最終地址將在指令指針中變為 0)。 這意味着我們的文本段(代碼)已被硬編碼為使用該地址。

為了解決這個問題,我們將不得不重新編寫 ELF 以更改對 foo() 的調用的處理方式。 有幾個選擇:

  1. 我們將另一個.text.[somename] 部分添加到我們的文件中,其中包含充當蹦床的代碼,即:FakeFoo()。 然后我們重寫 foo() 的第一條指令,立即跳轉到 FakeFoo()。 Hacky,但可能會丟失調試信息。

  2. .rela.text 部分包含 function 重定位。 這些用於告訴 linker 我們需要用最終位置替換調用的字節。 當 linker 看到此部分時,它將用最終二進制中的實際計算地址替換“偏移”字段中的地址。 對於我們的二進制文件,我們看到:

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() function 的位置(注意:編譯器將我們對“printf()”的調用轉換為使用“puts()”)。

因此,我們可以在此處為指令 0x28 處的調用添加另一個條目,並讓 linker 在未聲明的代碼中某處查找另一個名為“foo()”的 function。

這也需要修復 ELF 文件的 .symtab 條目,因為它將包含對本地 function 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

為了使 linker 在此 object 文件之外查找 foo(),我們必須將 foo 的條目更改為“NOTYPE GLOBAL”“UND”類型,因此 Z3175B426046787EECE2Z777 中不存在這個文件。

還有另一個部分,.rela.eh_frame,用於調試,您也需要注意。

最后,此方法要求您通過二進制文件訪問 go,搜索與跳轉/調用/分支對應的操作碼,並創建/修復條目,以便 linker 將在其他 ZA8CFDE6331B4B666AC 文件中查找“foo()”。

所有這些只是為了讓 linker 在不同的文件中查找 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();
}

最終,如果您的開發人員將他們的大部分功能從 static 方法轉移到全局方法,那會更好(並且更容易)。 但是,如果您想在 object 文件創建后重新編寫它,在適當的情況下,可以付出相當大的努力。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM