嚴琦 畢業於中國科技大學和美國倫斯勒理工學院。 畢業後近三十年裡曾先後任職於五家軟件技術公司從事技算機編程工作,從初級程序員成長為一個中等規模的軟件公司的首席架構師,涉及包括嵌入式系統,有限元分析,計算機輔助設計,商業智能系統等領域,致力於微處理器,編譯器,服務器,系統內核等技術。作者有多項開源項目在實際應用中受到公司內外的肯定和感謝,在工作中申請並獲批一項軟件專利,並有另一項專利在審批中。
盧憲廷 本科畢業於天津大學,研究生東南大學。目前在微策略軟件擔任高級軟件工程師,負責設計和開發優化企業全域搜索引擎;專注於高穩定/事件驅動異步架構/C++ /Rust編程。
《高效C/C++ 調試》是一本精心編寫的實用指南,為軟件開發工程師提供了寶貴的調試技巧和知識。作者通過多年的一線經驗,深入講解了如何高效地調試軟件內存故障、理解C ++物件模型、閱讀匯編代碼等重要內容。書中還介紹了調試器插件和工具的開發,拓展了開發者的視野。無論是初學者還是有經驗的開發人員,都能從本書中獲得實際的指導和啟發。豐富的實戰例子和代碼片段讓讀者更好地理解和應用所學知識。如果你想提升調試能力、掌握C/C ++高級內容,並成為實戰資質的中高級開發人員,那麼這本書絕對是你的不二選擇。
序一
這是一本關於調試的書。作為一名程序員,在多年的寫代碼和調試代碼的過程中,我一次又一次地經歷了過山車般的情緒變化:困惑,沮喪,興奮,周而復始,特別是在處理看上去永無止境的程序錯誤(bug)時尤其如此。隨著時間的推移,我掌握了更多的調試技能,對要支持的產品和架構有了更多的了解,大部分問題變得容易解決。然而,偶爾也會出現一些棘手的問題,試圖縮小範圍並解決一個真正困難的問題可能需要數小時甚至數天的時間。
記得有一次,我花了幾個月的時間嘗試修復一個問題,這個問題的奇怪之處在於它只在每個星期二在客戶的服務器上發生(我將在稍後的內存損壞一章中講述這個實戰故事)。我相信這不僅僅是我的故事,很多軟件工程師都曾有過同樣的經歷。因為計算機已經深入我們的生活幾十年,軟件行業積累了大量的遺留代碼。因此,我們中的許多人不得不花費大量時間來維護和完善現有程序。即使你為全新的項目編寫代碼,遲早也要對它進行調試。不管喜歡與否,調試bug是不可避免的,它已經成為軟件開發工程師日常工作的一部分。
另一方面,調試也可以有很多樂趣。在經歷了許多挫折和無聊的時刻後,我學到了許多探索和尋找bug的技巧,並開始感到興奮和滿足。每當我解決了案子中具有挑戰性的問題時,我都會獲得同事們的感謝與贊許。這讓我覺得自己像一個能解決問題的真正的偵探。在現實世界的程序中有很多看似很困難的bug,我常常聽到類似的抱怨和借口—“這是我見過最奇怪的事情”,“這段代碼存在了這麼多年,如果它有bug,早該失敗了”,或者“我已經審閱我的代碼好多遍了,這是不可能發生的”。隨著在實戰中積累的經驗的增加,我更加相信通過正確的解決方案和基本技能,都可以有效地揭示並解決bug。無論表面上看起來多麼神秘或不可能的問題,當我們最終找到根本原因時,一切都說得通了,畢竟計算機程序是那麼虔誠地完全地照著我們編寫的方式運行,即使那是錯誤的。
本書討論調試方法論。盡管關於這一主題已經有很多優秀的書籍,但我相信通過總結我個人的實戰經驗,可以為讀者提供更多實用的觀察方法和技巧。從學校畢業以後,我閱讀了各種關於編程和調試的書籍,曾以為已經完全理解並對解決任何問題都充滿信心。然而,實際問題往往比書中的例子更為複雜。我經常在工作中找不到任何線索,無法將書本知識應用於實際問題。
回想起那些初出茅廬的歲月,一方面是我沒有完全理解書中的內容,另一方面是大部分書籍都是從設計和編程的角度出發的。它們可能充滿了使用調試器命令的技巧,但當問題類型和維度迷霧重重時,它們缺乏如何起步、如何從最基礎去分解問題,以及如何選擇不同的調試策略和有效利用調試器的各種功能的介紹。我看到許多年輕的工程師在沒有明確計劃的情況下就急切地啟動調試器。對於一些人來說,調試程序就是使用調試器而已。在本書中,我將通過深入挖掘一些內部數據結構,展示許多調試過程的實戰例子,並提出可操作的實用建議,以縮小理論知識和可用技術的溝壑。
本書的示例包含了大量的代碼片段和實際案例。在編寫過程中,我盡可能地運用真實發生的例子,除非在某些情況下,理論性例子的簡明性和清晰度優於實戰例子。此外,本書還專門介紹了調試器插件和實用工具的開發。這些工具能夠增強現有的調試器,拓寬我們的視野,要麼提供新的角度審視問題,要麼幫助我們更深入地研究問題。盡管本書主要探討的是C/C++ ,但書中所介紹的方法和策略是通用的,獨立於特定語言。
通常,教材並不覆蓋特定調試器、內存管理庫或者編譯器的內部實現,許多軟件開發人員也不熟悉這些知識,因為在設計和編程階段通常並不需要關注這些內容,而且常規的調試工作也不需要。有些人可能認為,除了軟件的開發者之外,其他人沒有必要去學習這些知識。然而,這些知識對於我們對可能會觀察到的情況以及在錯誤發生時可能會錯過的細節具有深遠的影響。
如果你在軟件行業待了足夠長的時間,就會遇到需要深入理解程序行為的情況。例如,由於代碼優化或者缺少足夠調試符號,調試器可能無法正確地顯示局部變量;如果棧損壞極為嚴重,調試器無法正確打印調用棧,因為它依賴保存在棧上的特定數據結構;程序也可能在看起來不可能崩潰(crash)的地方崩潰了。在這些情況下,我們必須比普通程序員挖掘得更深:可能需要梳理編譯器布局的棧空間,或者內存管理庫的堆數據結構,甚至需要手動重新生成調用棧和數據物件。
在本書中,我嘗試鋪就調試符號、調試器內部實現、內存管理器的內部結構、分析優化後的程序和C++ 物件模型等基礎知識。這些知識肯定可以幫助你突破學習瓶頸,進一步提高調試技能,從而更上一層樓。
許多非法操作的行為,如常見的內存溢出、重復釋放內存塊、訪問釋放後的物件、使用未初始化的變量等,根據編程語言的標準和文檔都是未定義行為。這基本上意味著這些違規行為的實際結果完全是隨機的或取決於具體實現;它們可能在一個環境無害,但是在另一個環境就是災難性的。一個經典的例子是:同樣有bug的代碼在一個平臺上沒有發生任何問題,可以正常運行,但在另一個平臺上,程序就會崩潰。最糟糕的情況是一個bug在初始階段沒有任何錯誤的跡象,在它完成了某些惡意操作很久以後,才出現奇怪和意料之外的行為。
從調試的角度看,理解特定實現中的“未定義”行為是必要的。這與我們不知道也不應該假設任何關於“未定義”行為的設計和編程實踐相違背。一種實現的內部數據結構不同於另一種實現。因此,有些人可能選擇忽略這些“未定義”行為。但是,當我們面臨由未定義行為引起的未知問題時,對這些內部數據結構的理解可以帶領我們走出迷霧,找到最終的解決方案。因此,在我看來,了解程序如何因這些“未定義”行為而失敗對於調試許多棘手問題至關重要。我的工作經歷也證明了這一點。本書中的許多示例將展示如何利用這些知識更有效地進行調試。
本書假設讀者具有基本的計算機科學和軟件開發學習經歷。讀者至少具有一年的實際編程經驗,並且知道怎麼使用調試器解決較為複雜的問題。在整本書中,我致力於關注書的主題—更高效的調試。為了避免偏離主題,一些相關的概念和術語被簡要描述或者以跳躍性方式串聯在一起。對於核心知識,我盡量以實際操作為主(可能不完全準確或者不具有學術性)來解釋。我們的目的是幫助讀者掌握基本的概念,並能夠快速將這些知識應用到調試實踐中。
通過互聯網,可以方便地獲取幾乎所有事物的權威性定義。如果讀者對書中提及的內容不太熟悉,或者需要更詳細的解釋,可以通過網上搜索來解決疑惑。本書末尾的引用也可以為讀者提供線索。希望本書沒有重復很多讀者已經知曉的內容,或者一些可以輕鬆獲取的信息,比如如何使用某個工具的命令,通常都可以在它的手冊中找到清晰的解釋。
本書的許多章節是獨立的,讀者可以跳到任何感興趣或適合當前工作的章節;跳過熟悉或者不感興趣的章節也沒有問題。一些章節會介紹調試器、運行時或者語言的底層細節,也許這些知識並非必需,但它確實能夠幫助你應對更複雜的問題。本書的許多例子都使用Linux/x86_64平臺,但是底層方法通過微小的調整就可以應用到其他平臺上。
附錄提供了其他平臺的豐富的示例,鼓勵讀者使用本書提供的源文件和鏈接生成對應的項目,並加以應用。這些實戰的示例可以進一步幫助讀者理解書中討論的話題,也可以作為開發自己項目的起點。事實上,一些程序是我在工作中開發的,從那時起它們就成為不可或缺的工具。其中大部分源代碼都是跨平臺的,如果碰巧你使用其中某個平臺,它可能會立即引起你的興趣;如果不碰巧,那麼當你理解這些設計背後的思路後,自己編寫工具也並非難事。
根據我的個人經驗,許多程序bug,特別是用C/C ++編寫的程序,都與內存相關。從各個角度理解內存怎麼分配和使用非常必要。本書的大部分內容聚焦於應用程序、編譯器、內存管理器、系統加載器/連接器和內核虛擬內存,以及如何從微觀到宏觀看待一塊內存。
內存是動態資源,會在程序執行的各個階段發生變化。在本書中,讀者將了解內存管理器如何分配內存,編譯器如何在分配的內存塊中布局應用程序的數據結構,以及棧是如何被局部變量和函數參數使用的。此外,讀者還將了解系統鏈接器和加載器如何跟系統虛擬內存管理器合作,創建進程的虛擬地址空間。應用程序以源文件聲明的形式看待數據物件:它們要麼是原始的數據類型,要麼是其他類型的聚合。編譯器會添加更多隱藏的數據成員,例如指向虛函數表的指針,並在必要時為了對齊而進行填充。為了滿足對齊要求和其自身的隱藏標籤,內存管理器會插入額外的字節。系統內核負責使用由頁構成的段來記錄進程的內存。
當研究一個有疑問的數據物件時,有經驗的工程師可以理解以上組件的各個視角:從編譯器的角度來看,該數據物件的大小和結構定義是怎樣的;從內存管理器的角度看,該數據物件的內存塊被釋放了還是在使用中;從鏈接器和加載器的角度看,該數據物件是在代碼段、全局數據段、堆數據還是棧段;從內核虛擬內存管理器的角度看,該數據物件是不是被某些權限保護著。所有這些信息可以作為創建一個理論的基石,驗證或證偽程序錯誤原因的假設。毋庸置疑,當調試與內存相關的問題時,這些知識是無價的。
在許多情況下,調試是一個試錯的過程。一個特定的問題有各種可能的原因,工程師通常通過分析問題的症狀來開始調研,接著根據觀察和推理提出一個可能的原因假設,然後證明這個假設,並給出一種修復方案,最後測試和驗證修復方案。如果理論無法解釋現象或者修復方案不行,該該參數需要重復上面的步驟。調試同一個問題有多種方法,每個人也有自己偏好的方法和風格。本書展示的例子和技巧是我在實踐中積累的,旨在與讀者分享其中的方法。當一種方法看上去沒有出路時,另一種使用其他工具的方法可能就是你所需要的。同樣地,非常歡迎讀者跟我分享自己的經驗和調試方法。
嚴琦
序二
在編程的道路上,每一個程序員都不可避免會遇到調試的挑戰。我仍然記得那些難忘的調試經歷:大學時期,我和朋友共同調試機器人的程序;進入職場後,我又開始鉆研數百萬行的C++ 代碼。從初入編程世界時的探索與迷茫,到如今的穩健與沉穩,這背後蘊含著無數次的學習與實踐。更為關鍵的是,我們站在諸多行業前輩的肩膀上。本書的第一作者嚴琦,正是其中一位令人尊敬的巨人。幸運的是,我在美國工作期間得到了他的直接指導和悉心幫助。
當清華大學出版社的編輯詢問我是否有興趣出版書籍時,我想到了從學生時代到職場的點滴經驗。我常常與同學或者同事分享自己的體會,也在知乎帳號(CrackingOysters)上發表相關文章,但要整理成一本完整的書籍,仍有不少工作要做。這時,我想到了嚴琦以及他那份關於高效調試的英文書稿。於是,我建議基於這份書稿共同打造一本新的書籍。因此,本書中絕大部分的內容都深受他的經驗和智能的啟發。同時我在他的書稿的基礎上增添了關於Google Address Sanitzer和逆向調試的內容、以及編寫了第9章和第12~18章的內容。
希望這本書能為編程愛好者提供實用的知識和啟示。如果讀者在書中發現了錯誤,歡迎指正。我樂於分享我的學習體會,因為總有熱心的朋友願意糾正我的錯誤。另一方面,讀者所認為的“錯誤”可能只是對知識理解的不同,在討論中可以加深或者修正理解。
盧憲廷
配書資源
為方便讀者使用本書,本書提供了源代碼文件,需要使用微信掃描下面的二維碼獲取。如果閱讀中發現問題或有疑問,請通過booksaga@126.com與我們聯繫,郵件主題請寫“高效C/C ++調試”。
目 錄
第1章 調試符號和調試器 1
1.1 調試符號 1
1.1.1 調試符號概覽 2
1.1.2 DWARF格式 3
1.2 實戰故事1:數據類型的不一致 14
1.3 調試器的內部結構 16
1.3.1 用戶界面 16
1.3.2 符號管理模塊 16
1.3.3 目標管理模塊 17
1.4 技巧和注意事項 21
1.4.1 特殊的調試符號 21
1.4.2 改變執行及其副作用 24
1.4.3 符號匹配的自動化 25
1.4.4 後期分析 26
1.4.5 內存保護 27
1.4.6 斷點不工作 27
1.5 本章小結 28
第2章 堆數據結構 29
2.1 理解內存管理器 30
2.1.1 ptmalloc 31
2.1.2 TCMalloc 34
2.1.3 多個堆 38
2.2 利用堆元數據 39
2.3 本章小結 42
第3章 內存損壞 43
3.1 內存是怎麼損壞的 44
3.1.1 內存溢出與下溢 44
3.1.2 訪問釋放的內存 45
3.1.3 使用未初始化的值 46
3.2 調試內存損壞 47
3.2.1 初始調查 49
3.2.2 內存調試工具 53
3.2.3 堆與棧內存損壞對比 53
3.2.4 工具箱 54
3.3 實戰故事2:神秘的字節序轉換 55
3.3.1 症狀 55
3.3.2 分析和調試 56
3.3.3 錯誤和有價值的點 64
3.4 實戰故事3:覆寫棧變量 65
3.4.1 症狀 65
3.4.2 分析和調試 65
3.5 本章小結 68
第4章 C 物件布局 69
4.1 對齊和大小端 69
4.1.1 對齊 69
4.1.2 大小端 70
4.2 C 物件布局 71
4.3 實戰故事4:訪問已經釋放的數據 94
4.3.1 症狀 94
4.3.2 分析和調試 94
4.4 搜索引用樹 95
4.5 本章小結 101
第5章 優化後的二進制 102
5.1 調試版和發行版的區別 102
5.2 調試優化代碼的挑戰 106
5.3 匯編代碼介紹 108
5.3.1 寄存器 109
5.3.2 指令集 111
5.3.3 程序匯編的結構 113
5.3.4 函數調用習慣 116
5.4 分析優化後的代碼 127
5.5 調試優化後的代碼示例 130
5.6 本章小結 141
第6章 進程鏡像 142
6.1 二進制文件格式 144
6.2 運行期加載和鏈接 148
6.3 進程映射表 153
6.3.1 可執行文件 154
6.3.2 共享庫 156
6.3.3 線程棧 157
6.3.4 無名區域 157
6.3.5 攔截 158
6.3.6 鏈接時替換 158
6.3.7 預先加載代理函數 159
6.3.8 修改導入和導出表 159
6.3.9 對目標函數進行手術改變 164
6.3.10 核心轉儲文件格式 166
6.3.11 核心轉儲文件分析工具 169
6.4 本章小結 170
第7章 調試多線程程序 171
7.1 競爭條件 171
7.2 它是競爭條件嗎 172
7.3 調試競爭條件 174
7.4 實戰故事5:記錄重要區域 175
7.4.1 症狀 175
7.4.2 分析調試 175
7.5 死鎖 177
7.6 本章小結 179
第8章 更多調試方法 180
8.1 重現錯誤 180
8.1.1 歸因 181
8.1.2 收集環境信息 182
8.1.3 重建環境 184
8.2 防止未來的bug 184
8.2.1 知識保留和傳遞 185
8.2.2 增強提前檢查 185
8.2.3 編寫更好調試的代碼 185
8.3 不要忘記這些調試規則 189
8.3.1 分治法 189
8.3.2 退一步,獲取新的觀點 189
8.3.3 保留調試歷史 190
8.4 逆向調試 190
8.4.1 rr:Record and Replay 191
8.4.2 rr注意事項 191
8.5 本章小結 192
第9章 拓展調試器能力 193
9.1 使用Python拓展GDB 193
9.1.1 美化輸出 194
9.1.2 編寫自己的美觀打印器 195
9.1.3 將重復的工作變成一個命令 197
9.1.4 更快地調試bug 198
9.1.5 使用Python設置斷點 200
9.1.6 通過命令行來啟動程序和設置斷點 203
9.2 GDB自定義命令 203
9.3 本章小結 206
第10章 內存調試工具 207
10.1 ptmalloc’s MALLOC_CHECK_ 208
10.2 Google Address Sanitizer 212
10.3 AccuTrak 213
10.4 有效地調試內存損壞 225
10.5 實戰故事6:內存管理器的崩潰問題 228
10.5.1 症狀 229
10.5.2 分析和調試 229
10.6 本章小結 235
第11章 Core Analyzer 236
11.1 使用示例 237
11.2 主要功能 239
11.2.1 搜索引用的物件(水平搜索) 239
11.2.2 查詢地址及其底層物件(垂直搜索) 240
11.2.3 內存模式分析 241
11.2.4 查詢堆內存塊 242
11.2.5 堆遍歷(檢查整個堆以發現損壞並獲取內存使用統計) 242
11.3 本章小結 246
第12章 更多調試工具 247
12.1 strace 247
12.1.1 常用功能 247
12.1.2 常用附加選項 248
12.2 實戰故事7:僵尸進程 248
12.2.1 遇到難題 248
12.2.2 揭示bug的真相 249
12.3 Perf 249
12.4 eBPF 250
12.4.1 準備環境 251
12.4.2 編寫代碼 251
12.4.3 編譯程序 252
12.4.4 加載和運行程序 254
12.5 實戰故事8:鏈接問題 255
12.5.1 切入 255
12.5.2 更奇怪的事情 258
12.5.3 柳暗花明 259
12.5.4 補充 260
12.5.5 結論 261
12.6 實戰故事9:臨時變量的生命周期 261
12.7 本章小結 264
第13章 崩潰發送機制 265
13.1 客戶端 266
13.2 遠程報告收集服務器 267
13.3 終端集成器 268
13.4 本章小結 268
第14章 內存泄漏 269
14.1 為什麼RAII是基石 269
14.2 分析 270
14.3 調試內存泄漏 273
14.4 本章小結 275
第15章 協程 276
15.1 C 協程 277
15.2 協程的切分點 279
15.3 協程之諾 281
15.4 本章小結 283
第16章 遠程調試 284
16.1 GDB遠程調試 285
16.2 Visual Studio遠程調試 286
16.3 本章小結 287
第17章 容器世界 288
17.1 容器示例 288
17.2 容器應用 289
17.3 C/C 容器調試 291
17.4 實戰故事10:CrashLoopBackOff 292
17.5 實戰故事11:liveness failure 292
17.6 本章小結 294
第18章 盡量不要調試程序 295
18.1 借助編譯器來提前發現錯誤 295
18.2 編寫簡短的實驗代碼 295
18.3 日志和監控 296
18.3.1 日志 296
18.3.2 監控 297
18.4 遵循最佳編碼實踐 297
18.5 本章小結 298
附錄A 調試混合語言 299
附錄B 在Windows/x86環境下進行程序調試 301
B.1 PE文件格式 301
B.2 Windows Minidump格式 306
附錄C 一個簡單的C coroutine程序 309
大陸出版品因裝訂品質及貨運條件與台灣出版品落差甚大,除封面破損、內頁脫落等較嚴重的狀態,其餘商品將正常出貨。
特別提醒:部分書籍附贈之內容(如音頻mp3或影片dvd等)已無實體光碟提供,需以QR CODE 連結至當地網站註冊“並通過驗證程序”,方可下載使用。
無現貨庫存之簡體書,將向海外調貨:
海外有庫存之書籍,等候約45個工作天;
海外無庫存之書籍,平均作業時間約60個工作天,然不保證確定可調到貨,尚請見諒。
為了保護您的權益,「三民網路書店」提供會員七日商品鑑賞期(收到商品為起始日)。
若要辦理退貨,請在商品鑑賞期內寄回,且商品必須是全新狀態與完整包裝(商品、附件、發票、隨貨贈品等)否則恕不接受退貨。