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() 所做的。
首先,我們必須準備好符號資訊。為了實現這一點,我們使用 -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 如何無法解析某些地址。