[轉]瞭解 C/C++ 程式行為的技巧
多數情況下我們會使用別人的程式,或是參與別人開發已久的程式,比較少是自己重頭撰寫所有程式。由此可知,瞭解程式行為的技巧相當重要,但是很少看到有書籍討論這部份的事,或許是因為很難理出一個主題吧。
下面以瞭解 C/C++ 程式為主,列出自己一些零散心得。
動態分析
在程式碼裡填入觀察碼
加入程式 log function call 是直接有效的作法,而且做起來不如想像的麻煩,詳見 《trace C/C++ function call 的方法》。
或是找看看前人是否有了下除錯的 flag。有經驗的程式設計師在開發時必定有一套除錯方式,只是在正式使用時關掉了這些除錯功能,詳見《C/C++ 檢查和打開 debug 功能的小技巧》。
附帶一提,自己開發程式時,除了留下除錯程式之外,記得要考慮使用 gdb 的影響,讓除錯程式更易於使用,詳見 《除錯小技巧: 在程式中直接中斷及偵測是否被 gdb 監控中》。
觀察使用的函式庫
程式碼通常會用到第三方函式庫,有時我們可以從中得知一些線索,《python BaseHTTPServer 速度緩慢的原因》是一個簡短的例子,說明如何使用 ltrace 找出關鍵的第三方函式,繼而找出效率瓶頸。使用 ltrace 無須重編程式或加入 debug symbol。不過有可能讓程式變得不穩定,這時可使用 -l filename 只觀察部份函式庫,執行的時候會比較穩定一些。
其它兩則和 ltrace 相關的例子:
- 《用 strace 和 ltrace 找出用到的 system call 和 library call》
- 《以使用 libsqlite 為例說明如何找到程式的進入點》
觀察使用的 system call
system call 是程式和 kernel 溝通的途徑,介面數量相對少,容易集中觀察。有時藉由觀察 system call 夾帶的參數,可以提供一些線索。比方說,不論上層函式如何包裝,開啟檔案最後會用到 open(),可以從 open() 的參數 找出程式目前使用的設定檔或 log 檔位置。或是像《用 strace 找出 Ubuntu 如何提示未安裝的指令》,使用 strace 觀察 system call 直接解答疑惑。
和 ltrace 一樣,無須重編程式也不用 debug symbol。更棒的是,使用 strace() 執行程式還滿穩定的。
其它 strace 相關的例子:
- 《熟悉系統工具好處多多》
- 《善用 strace、debugger 從執行期間找出問題根源》
使用 gdb
debugger 的重要性無須多言,《gdb 初步心得》條列我常用到的指令。另外值得一提的是,有 core dump 的時候,core dump 裡一定會記錄產生 segmentation fault 的 thread,不用擔心找錯 thread,原因見《linux thread 與 signal》。
編譯上線的程式時,多數會加上 -O2 最佳化效能,讓程式實際執行的狀況和程式碼有些出入。雖然如此,仍然可以加上 -g 觀察程式的 core dump,但要留意觀察的結果不見得是對的,詳見《debug info 和 optimization》。平時觀察程式行為還是用 -O0 -g 編譯較適當。
以下是其它和 gdb 相關的小技巧:
- 《加速 gdb 載入 symbol 時間》
- 《在 x86-64 上對 system call 使用 conditional break》
- 《gdb 顯示 STL container 的方法》
- 《用 python gdb 客製化 backtrace 的結果》
從 kernel 切入
有些情況下我們沒辦法從程式內部或 gdb 取得資訊,比方說程式莫明奇妙地收到 SIGKILL 而結束。由於程式無法攔截 SIGKILL,不方便查出凶手是誰。雖然可以在相關程式內直接對 system call kill() 設中斷點,但若凶手是外部程式,就沒輒了。
這種時候可用 SystemTap 直接從 kernel 內觀察是什麼程式請求使用 kill 送出 SIGKILL。我自己還沒有第一手經驗,這個例子是從同事那邊學來的,在這裡備忘觀察程式時,還可以用 SystemTap 觀察更底層的行為。
靜態分析
從執行檔找出關鍵字
《配合 c++filt 讀程式》說明如何從執行檔中找關鍵字,可藉此找出 UI 相關字串或是可能的函式名稱,有時比直接從程式碼下手容易。
找出 symbol 的定義或使用到 symbol 的程式碼
C 的情況比較單純,相關工具比較正確,也有工具可以產生精美的 call graph。但 C++ 的情況複雜許多,最後我決定無視 C++ 語法,直接找出所有和目標 symbol 有關的程式。《閱讀 C/C++ 原始碼的好幫手》有整理相關工具, 《查 C/C++ symbol 定義的方法》有一點關於我使用 gj 的方法。
使用 doxygen 產生 class 階層關係圖
大型專案會依功能切成數個模組,模組本身亦有一套自己使用 class 的方法。直接看程式碼容易陷入見樹不見林的困境。這時可用 class 階層關係圖協助瞭解整體架構。產生階層圖的方式見《用 doxygen 產生 class hierarchy diagram》。再輔以 gdb 產生 backtrace 觀察類別、模組之間的使用關係,比較容易明白整體架構。
善用編輯器或 IDE
目前是逐步學習如何強化用編輯器或 IDE 讀程式,加快在相關程式碼中探索的時間,還沒整理出一套完整的流程。
比方說用 gj 跳到某個使用函式 foo() 的檔案後,再來我會想知道目前在那個函式裡,《vim 顯示目前函式的名稱》就是針對此情境在 vim 內加了快速鍵做這這件事。或是像《使用 vim script 自動開啟 C/C++ 程式的標頭檔或程式碼》說明如何在 .h 檔裡按 F4 快速開啟 .c 或 .cpp 檔,或是反過來在 .c 或 .cpp 裡開啟 .h。
預防勝於治療
理解到瞭解程式是如此不容易後,行有餘力時,別忘了加強測試碼,讓日後使用相關程式的人更快進入狀況。良好的 unit tests 也是不錯的使用範例,有助於瞭解模組的行為。
核心分析
2014-01-05 更新。
現在來看,這段的概念才是理解大型專案時最重要的技巧。經驗愈多,愈覺得如此。說來容易做來難,需要累積經驗後才能體會。
想像自己會如何進行開發
有經驗的軟體工程師,做事的方式十之八九也差不多。特別是需求愈嚴苛,最後的解法也不會差太多。先自己想像可能的脈絡,之後比較容易縮小觀察目標,專注驗證自己的假設,並從驗證結果獲得新的契機製定下一步。
尋找原有專案的除錯工具
經年累月的專案必定有自己一套除錯工具,不然很難長期維護。有經驗的工程師應該也會在開發過程中研發出負責專案所需的除錯工具。所以,最有效觀察程式的方式,就是用原專案專用的工具。
對目標專案有一定瞭解後,可以自己想像,若自己進行同樣類型的專案開發,可能會做什麼輔助工具呢?有機會從中猜中切入點。