http://arthurchiao.art/blog/how-does-ltrace-work-zh/
strace
是一個系統呼叫,也是一個訊號跟蹤器(signal tracer),
- 主要用於跟蹤系統呼叫,列印系統呼叫的引數、返回值、時間戳等很多資訊。
- 也可以跟蹤和列印程序收到的訊號。
- 在前一篇文章strace 是如何工作的 中介紹過,
strace
內部基於 ptrace 系統呼叫。
ltrace
是一個函式庫呼叫跟蹤器(library call tracer)
- 顧名思義,主要用於跟蹤程式的函式庫呼叫資訊。
- 它也可以像
strace
一樣跟蹤系統呼叫和訊號。 - 它的命令列引數和
strace
很相似。 ltrace
也是基於 ptrace。
雖然 strace
和 ltrace
底層都是基於 ptrace 系統呼叫, 但跟蹤庫函式和跟蹤系統呼叫還是有很大差別的,這就是為什麼會有 ltrace
的原因。
#重要概念
共享庫可以被載入到任意地址。這意味著,共享庫內的函式地址只有在執行時載入以後才能確定。 即使重複執行同一程式,載入同一動態庫,庫內的函式地址也是不同的。那麼,程式是如何呼叫地址未知的函式的呢?
簡短版的回答是:二進位制格式、作業系統,以及載入器。在 Linux 上,這是一 支程式和動態載入器之間的曼妙舞蹈
下面是詳細版的回答。
Linux 程式使用 ELF binary format,它提供了 許多特性。出於本文目的,我們這裡只介紹兩個:
- 過程連結表(Procedure Linkage Table,
PLT
) - 全域性偏移表(Global Offset Table,
GOT
)
庫函式在 PLT 裡都有一組對應的彙編指令,通常稱作 trampoline
,在函式被呼叫的時候執行。
PLT trampoline 程式碼
PLT trampoline 都遵循類似的格式,下面是一個例子:
PLT1: jmp *name1@GOTPCREL(%rip)
pushq $index1
jmp .PLT0
- 第一行程式碼跳轉到一個地址,這個地址的值儲存在 GOT 中。
- GOT 儲存了絕對地址。這些地址在程式啟動時初始化,指向 PLT
pushq
指令所在的地址(第二行程式碼)。 - 第三行
pushq $index1
為動態聯結器準備一些資料,然後透過jmp .PLT0
跳轉到另一段程式碼,後者會進而呼叫動態連結器。
動態連結器透過 $index1
和其他一些資料來判斷程式想呼叫的是哪個庫函式,然後定位 到函式地址,並將其寫入 GOT,覆蓋之前初始化時的預設值。
當後面再次呼叫到這個函式時,就會直接找到函式地址,而不需再經過以上的動態連結器查 找過程。
流程總結
總結起來:
- 程式載入到記憶體時,程式和每個動態共享庫(例如 DSO)透過 PLT 和 GOT 對映到記憶體
- 程式開始執行時,動態共享庫裡的函式的記憶體地址是未知的,因為動態庫可以被載入到程式地址空間的任意地址
- 首次執行到一個函式的時候,執行過程轉到函式的 PLT,裡面是一些彙編程式碼(trampoline)
- trampoline 組織資料,然後呼叫動態連結器
- 動態連結器透過 PLT 準備的資料找到函式地址
- 將地址寫入 GOT 表,然後執行轉到該函式
- 後面再次呼叫到這個函式時,不再經過動態連結器,因為 GOT 裡已經儲存了函式地址,PLT 可以直接呼叫