跳到內容

Linux 應用程式除錯技術/插樁庫

來自華夏公益教科書,開放的書籍,開放的世界

動態連結器允許攔截應用程式對任何共享庫的任何函式呼叫。因此,插樁是一種強大的技術,可以調整效能,收集執行時統計資訊或除錯應用程式,而無需對該應用程式的程式碼進行插樁。

例如,我們可以使用插樁庫來跟蹤呼叫,包括引數值和返回值。

呼叫追蹤

[編輯 | 編輯原始碼]

請注意,以下部分程式碼是 32 位 x86 和 gcc 4.1/4.2 特定的。

程式碼插樁
[編輯 | 編輯原始碼]

在庫中,我們希望解決以下問題

  • 當函式/方法被呼叫和退出時。
  • 當函式被呼叫時,呼叫引數是什麼。
  • 當函式退出時,返回值是什麼。
  • 可選地,函式從哪裡被呼叫。

第一個很容易:如果需要,編譯器將插樁函式和方法,以便當函式/方法被呼叫時,會呼叫一個插樁函式,而當函式退出時,會呼叫一個類似的插樁函式

    void __cyg_profile_func_enter(void *func, void *callsite); 
    void __cyg_profile_func_exit(void *func, void *callsite);

這是透過使用以下標誌編譯程式碼來實現的-finstrument-functions上述兩個函式可以用於收集覆蓋率資料;或用於分析。我們將使用它們來列印函式呼叫的跟蹤。此外,我們可以將這兩個函式和支援程式碼隔離在自己的插樁庫中。這個庫可以在需要的時候載入,從而基本上不改變應用程式程式碼。

現在,當函式被呼叫時,我們可以獲得呼叫的引數

    void __cyg_profile_func_enter( void *func, void *callsite )
    {
        char buf_func[CTRACE_BUF_LEN+1] = {0};
        char buf_file[CTRACE_BUF_LEN+1] = {0}; 
        char buf_args[ARG_BUF_LEN + 1] = {0}; 
        pthread_t self = (pthread_t)0;
        int *frame = NULL;
        int nargs = 0;
    
        self = pthread_self(); 
        frame = (int *)__builtin_frame_address(1); /*of the 'func'*/

        /*Which function*/
        libtrace_resolve (func, buf_func, CTRACE_BUF_LEN, NULL, 0);
        
        /*From where.  KO with optimizations. */
        libtrace_resolve (callsite, NULL, 0, buf_file, CTRACE_BUF_LEN);
        
        nargs = nchr(buf_func, ',') + 1; /*Last arg has no comma after*/ 
        nargs += is_cpp(buf_func);       /*'this'*/
        if (nargs > MAX_ARG_SHOW)
            nargs = MAX_ARG_SHOW;

        printf("T%p: %p %s %s [from %s]\n", 
               self, (int*)func, buf_func, 
               args(buf_args, ARG_BUF_LEN, nargs, frame), 
               buf_file);
    }

當函式退出時,我們得到返回值

    void __cyg_profile_func_exit( void *func, void *callsite )
    {
        long ret = 0L; 
        char buf_func[CTRACE_BUF_LEN+1] = {0};
        char buf_file[CTRACE_BUF_LEN+1] = {0}; 
        pthread_t self = (pthread_t)0;

        GET_EBX(ret); 
        self = pthread_self(); 

        /*Which function*/
        libtrace_resolve (func, buf_func, CTRACE_BUF_LEN, NULL, 0);
        
        printf("T%p: %p %s => %d\n", 
               self, (int*)func, buf_func, 
               ret);

        SET_EBX(ret); 
    }


由於這兩個插樁函式都瞭解地址,而我們實際上希望跟蹤對人類來說可讀,因此我們還需要一種方法將符號地址解析為符號名稱:這就是 libtrace_resolve() 所做的。

Binutils 和 libbfd
[編輯 | 編輯原始碼]

首先,我們必須準備好符號資訊。為了實現這一點,我們使用 -g 標誌編譯應用程式。然後,我們可以將地址對映到符號名稱,這通常需要編寫一些瞭解 ELF 格式的程式碼。

幸運的是,有一個 binutils 包,它包含一個執行此操作的庫:libbfd;以及一個工具:addr2line。addr2line 是一個關於如何使用 libbfd 的很好的例子,我僅僅用它來包裝 libbfd。結果就是 libtrace_resolve() 函式。

由於插樁函式被隔離在獨立的模組中,因此我們透過環境變數(CTRACE_PROGRAM)告訴該模組插樁可執行檔案的名稱,我們在執行程式之前設定它。這對於正確初始化 libbfd 以搜尋符號是必需的。

棧佈局
[編輯 | 編輯原始碼]

要解決第一個問題,這項工作是與體系結構無關的(實際上 libbfd 瞭解體系結構,但這些東西隱藏在它的 API 後面)。但是,要檢索函式引數和返回值,我們必須檢視堆疊,編寫一些特定於體系結構的程式碼並利用一些 gcc 技巧。同樣,我使用的編譯器是 gcc 4.1 和 4.2;以後或以前的版本可能會有所不同。簡而言之

  • x86 規定堆疊向下增長。
  • GCC 規定如何使用堆疊 - 下面描述了一個“典型”的堆疊。
  • 每個函式都有一個由 ebp(基指標)和 esp(堆疊指標)暫存器標記的堆疊幀。
  • 通常,我們希望 eax 暫存器包含返回值
                                \
               +------------+   |
               | arg 2      |   \
               +------------+    >- previous function's stack frame
               | arg 1      |   /
               +------------+   |
               | ret %eip   |   /
               +============+   
               | saved %ebp |   \
        %ebp-> +------------+   |
               |            |   |
               |   local    |   \
               | variables, |    >- current function's stack frame
               |    etc.    |   /
               |            |   |
               |            |   |
        %esp-> +------------+   /

在一個理想的世界裡,編譯器生成的程式碼將確保在插樁函式退出時:設定返回值,然後將 CPU 暫存器壓入堆疊(以確保插樁函式不會影響它們),然後呼叫插樁函式,然後彈出暫存器。此程式碼序列將確保我們始終能夠訪問

return value in the instrumentation function. The code generated by the compiler is a bit different...

此外,在實踐中,許多 gcc 的標誌會影響堆疊佈局和暫存器使用。最明顯的標誌是

  • -fomit-frame-pointer。此標誌會影響找到引數的堆疊偏移。
  • 最佳化標誌:-Ox;這些標誌中的每一個都聚合了許多最佳化。這些標誌不會影響堆疊,而且令人驚訝的是,無論最佳化級別如何,引數始終透過堆疊傳遞給函式。人們會期望一些引數透過暫存器傳遞 - 在這種情況下,獲取這些引數將被證明是困難甚至不可能的。但是,這些標誌確實使恢復返回值變得複雜。但是,在某些體系結構上,這些標誌會吸取 -fomit-frame-pointer 最佳化。
  • 無論如何,請注意:您用來編譯應用程式的其他標誌可能會帶來驚喜。
函式引數
[編輯 | 編輯原始碼]

在我的編譯器測試中,所有引數總是透過堆疊傳遞。因此,這是微不足道的事情,在一定程度上受 -fomit-frame-pointer 標誌的影響 - 此標誌會改變引數開始的偏移量。

函式有多少個引數,堆疊上有多少個引數?推斷引數數量的一種方法是基於它的簽名(對於 C++,注意隱藏的“this”引數),這是 __cyg_profile_func_enter() 中使用的技術。

一旦我們知道了引數在堆疊上的起始偏移量以及它們的數量,我們只需遍歷堆疊即可檢索它們的值

    char *args(char *buf, int len, int nargs, int *frame)
    {
        int i; 
        int offset;

        memset(buf, 0, len); 

        snprintf(buf, len, "("); 
        offset = 1; 
        for (i=0; i < nargs && offset < len; i++) {
            offset += snprintf(buf+offset, len-offset, "%d%s", 
                             *(frame+ARG_OFFET+i), 
                             i==nargs-1 ? " ...)" : ", ");
        }

        return buf; 
    }
函式返回值
[編輯 | 編輯原始碼]

只有在使用 -O0 標誌時,才能獲得返回值。

讓我們看看當這個方法

    class B {
        ...
        virtual int m1(int i, int j) {printf("B::m1()\n"); f1(i); return 20;}
        ...
    };

使用 -O0 插樁時

     080496a2 <_ZN1B2m1Eii>:
     80496a2:    55                       push   %ebp
     80496a3:    89 e5                    mov    %esp,%ebp
     80496a5:    53                       push   %ebx
     80496a6:    83 ec 24                 sub    $0x24,%esp
     80496a9:    8b 45 04                 mov    0x4(%ebp),%eax
     80496ac:    89 44 24 04              mov    %eax,0x4(%esp)
     80496b0:    c7 04 24 a2 96 04 08     movl   $0x80496a2,(%esp)
     80496b7:    e8 b0 f4 ff ff           call   8048b6c <__cyg_profile_func_enter@plt>
     80496bc:    c7 04 24 35 9c 04 08     movl   $0x8049c35,(%esp)
     80496c3:    e8 b4 f4 ff ff           call   8048b7c <puts@plt>
     80496c8:    8b 45 0c                 mov    0xc(%ebp),%eax
     80496cb:    89 04 24                 mov    %eax,(%esp)
     80496ce:    e8 9d f8 ff ff           call   8048f70 <_Z2f1i>

==>  80496d3:    bb 14 00 00 00           mov    $0x14,%ebx
     80496d8:    8b 45 04                 mov    0x4(%ebp),%eax
     80496db:    89 44 24 04              mov    %eax,0x4(%esp)
     80496df:    c7 04 24 a2 96 04 08     movl   $0x80496a2,(%esp)
==>  80496e6:    e8 81 f5 ff ff           call   8048c6c <__cyg_profile_func_exit@plt>
     80496eb:    89 5d f8                 mov    %ebx,0xfffffff8(%ebp)
==>  80496ee:    eb 27                    jmp    8049717 <_ZN1B2m1Eii+0x75>
     80496f0:    89 45 f4                 mov    %eax,0xfffffff4(%ebp)
     80496f3:    8b 5d f4                 mov    0xfffffff4(%ebp),%ebx
     80496f6:    8b 45 04                 mov    0x4(%ebp),%eax
     80496f9:    89 44 24 04              mov    %eax,0x4(%esp)
     80496fd:    c7 04 24 a2 96 04 08     movl   $0x80496a2,(%esp)
     8049704:    e8 63 f5 ff ff           call   8048c6c <__cyg_profile_func_exit@plt>
     8049709:    89 5d f4                 mov    %ebx,0xfffffff4(%ebp)
     804970c:    8b 45 f4                 mov    0xfffffff4(%ebp),%eax
     804970f:    89 04 24                 mov    %eax,(%esp)
     8049712:    e8 15 f5 ff ff           call   8048c2c <_Unwind_Resume@plt>

==>  8049717:    8b 45 f8                 mov    0xfffffff8(%ebp),%eax
     804971a:    83 c4 24                 add    $0x24,%esp
     804971d:    5b                       pop    %ebx
     804971e:    5d                       pop    %ebp
     804971f:    c3                       ret

請注意,返回值是如何被移動到 ebx 暫存器中的 - 這有點出乎意料,因為傳統上,eax 暫存器用於返回值 - 然後呼叫插樁函式。這對於檢索返回值很好,但為了避免 ebx 暫存器在插樁函式中被覆蓋,我們在進入函式時儲存它,並在退出時恢復它。

當編譯使用一定程度的最佳化(-O1...3;這裡顯示的是 -O2)時,程式碼會發生變化

    080498c0 <_ZN1B2m1Eii>:
     80498c0:    55                       push   %ebp
     80498c1:    89 e5                    mov    %esp,%ebp
     80498c3:    53                       push   %ebx
     80498c4:    83 ec 14                 sub    $0x14,%esp
     80498c7:    8b 45 04                 mov    0x4(%ebp),%eax
     80498ca:    c7 04 24 c0 98 04 08     movl   $0x80498c0,(%esp)
     80498d1:    89 44 24 04              mov    %eax,0x4(%esp)
     80498d5:    e8 12 f4 ff ff           call   8048cec <__cyg_profile_func_enter@plt>
     80498da:    c7 04 24 2d 9c 04 08     movl   $0x8049c2d,(%esp)
     80498e1:    e8 16 f4 ff ff           call   8048cfc <puts@plt>

     80498e6:    8b 45 0c                 mov    0xc(%ebp),%eax
     80498e9:    89 04 24                 mov    %eax,(%esp)
     80498ec:    e8 af f7 ff ff           call   80490a0 <_Z2f1i>
     80498f1:    8b 45 04                 mov    0x4(%ebp),%eax
     80498f4:    c7 04 24 c0 98 04 08     movl   $0x80498c0,(%esp)
     80498fb:    89 44 24 04              mov    %eax,0x4(%esp)
==>  80498ff:    e8 88 f3 ff ff           call   8048c8c <__cyg_profile_func_exit@plt>
     8049904:    83 c4 14                 add    $0x14,%esp
==>  8049907:    b8 14 00 00 00           mov    $0x14,%eax
     804990c:    5b                       pop    %ebx
     804990d:    5d                       pop    %ebp
==>  804990e:    c3                       ret    

     804990f:    89 c3                    mov    %eax,%ebx
     8049911:    8b 45 04                 mov    0x4(%ebp),%eax
     8049914:    c7 04 24 c0 98 04 08     movl   $0x80498c0,(%esp)
     804991b:    89 44 24 04              mov    %eax,0x4(%esp)
     804991f:    e8 68 f3 ff ff           call   8048c8c <__cyg_profile_func_exit@plt>
     8049924:    89 1c 24                 mov    %ebx,(%esp)
     8049927:    e8 f0 f3 ff ff           call   8048d1c <_Unwind_Resume@plt>
     804992c:    90                       nop    
     804992d:    90                       nop    
     804992e:    90                       nop    
     804992f:    90                       nop

請注意,插樁函式是如何先被呼叫,然後 eax 暫存器才被返回值設定的。因此,如果我們絕對需要返回值,我們必須使用 -O0 編譯。

示例輸出
[編輯 | 編輯原始碼]

最後,以下是結果。在 shell 提示符處鍵入

$ export CTRACE_PROGRAM=./cpptraced
$ LD_PRELOAD=./libctrace.so ./cpptraced
T0xb7c0f6c0: 0x8048d34 main (0 ...) [from ]
./cpptraced: main(argc=1)
T0xb7c0ebb0: 0x80492d8 thread1(void*) (1 ...) [from ]
T0xb7c0ebb0: 0x80498b2 D (134605416 ...) [from cpptraced.cpp:91]
T0xb7c0ebb0: 0x8049630 B (134605416 ...) [from cpptraced.cpp:66]
B::B()
T0xb7c0ebb0: 0x8049630 B => -1209622540 [from ]
D::D(int=-1210829552)
T0xb7c0ebb0: 0x80498b2 D => -1209622540 [from ]
Hello World! It's me, thread #1!
./cpptraced: done.
T0xb7c0f6c0: 0x8048d34 main => -1212090144 [from ]
T0xb740dbb0: 0x8049000 thread2(void*) (2 ...) [from ]
T0xb740dbb0: 0x80498b2 D (134605432 ...) [from cpptraced.cpp:137]
T0xb740dbb0: 0x8049630 B (134605432 ...) [from cpptraced.cpp:66]
B::B()
T0xb740dbb0: 0x8049630 B => -1209622540 [from ]
D::D(int=-1210829568)
T0xb740dbb0: 0x80498b2 D => -1209622540 [from ]
Hello World! It's me, thread #2!
T#2!
T0xb6c0cbb0: 0x8049166 thread3(void*) (3 ...) [from ]
T0xb6c0cbb0: 0x80498b2 D (134613288 ...) [from cpptraced.cpp:157]
T0xb6c0cbb0: 0x8049630 B (134613288 ...) [from cpptraced.cpp:66]
B::B()
T0xb6c0cbb0: 0x8049630 B => -1209622540 [from ]
D::D(int=0)
T0xb6c0cbb0: 0x80498b2 D => -1209622540 [from ]
Hello World! It's me, thread #3!
T#1!
T0xb7c0ebb0: 0x80490dc wrap_strerror_r (134525680 ...) [from cpptraced.cpp:105]
T0xb7c0ebb0: 0x80490dc wrap_strerror_r => -1210887643 [from ]
T#1+M2 (Success)
T0xb740dbb0: 0x80495a0 D::m1(int, int) (134605432, 3, 4 ...) [from cpptraced.cpp:141]
D::m1()
T0xb740dbb0: 0x8049522 B::m2(int) (134605432, 14 ...) [from cpptraced.cpp:69]
B::m2()
T0xb740dbb0: 0x8048f70 f1 (14 ...) [from cpptraced.cpp:55]
f1 14
T0xb740dbb0: 0x8048ee0 f2(int) (74 ...) [from cpptraced.cpp:44]
f2 74
T0xb740dbb0: 0x8048e5e f3 (144 ...) [from cpptraced.cpp:36]
f3 144
T0xb740dbb0: 0x8048e5e f3 => 80 [from ]
T0xb740dbb0: 0x8048ee0 f2(int) => 70 [from ]
T0xb740dbb0: 0x8048f70 f1 => 60 [from ]
T0xb740dbb0: 0x8049522 B::m2(int) => 21 [from ]
T0xb740dbb0: 0x80495a0 D::m1(int, int) => 30 [from ]
T#2!
T#3!

請注意,當函式被內聯時,libbfd 如何無法解析某些地址。

華夏公益教科書