
簡介madExcept 5 FS 是一套專為 Delphi 開發(fā)者設(shè)計的異常處理組件源碼版用于捕獲程序運行中的各類異常、生成堆棧跟蹤信息并提供錯誤報告能夠與 Delphi IDE 無縫集成適合需要快速定位 Bug、提升軟件穩(wěn)定性的中高級開發(fā)場景。壓縮包共 77.07MB包含 3509 個文件其中有 pas 源碼、dfm 窗體、dpk/dproj 工程文件等可供直接閱讀修改的開發(fā)素材也有 dcu 編譯單元、bpl 運行包、lib 靜態(tài)庫以及大量 h/hpp 頭文件等編譯產(chǎn)物兼顧源碼學習與實際使用并附有安裝與測試方法說明文檔。目前已有 424 人學習下載。借助 FullSource 特性使用者能深入剖析異常捕獲、堆棧解析與錯誤報告生成等內(nèi)部實現(xiàn)也可根據(jù)項目需求自定義組件行為豐富的平臺支持文件有助于適配不同 Delphi 版本對想研究異常處理機制或擴展組件功能的開發(fā)者是一部實用的參考。1. madExcept 5 FS先于崩潰發(fā)生的那一次攔截這幾年在群里幫人看閃退報告見得最多的不是“彈了錯誤框”而是“程序無聲無息就沒了”。日志停在最后一條業(yè)務(wù)操作Windows 事件查看器里只有一句 Access violation地址是0x0041AB2連哪個函數(shù)都沒寫。madExcept 5 FS 就是沖這個痛點去的。FS 在 madExcept 體系里指 First Chance 異常處理不等程序走到未處理異常、被系統(tǒng)強制終止的 Second Chance 階段而是在異常對象剛拋出的那一刻先攔一道把調(diào)用棧、寄存器、線程現(xiàn)場全部拍下來再根據(jù)你的策略決定是吞掉、記錄還是放行。適合那些線上閃退不可復現(xiàn)、客戶端反復被卸載、DLL 邊界出問題又背黑鍋的 Delphi/CBuilder 項目。這一篇直接講怎么把它接進工程、怎么把報告變成能定位問題的證據(jù)鏈。2. 為什么 FS 意味著“第一機會”異常處理鏈路與選型2.1 First Chance 與 Second Chance同一場事故的兩個時間點Windows 的異常分發(fā)機制分兩輪。第一輪叫 First Chance異常拋出后系統(tǒng)先讓進程內(nèi)的頂層異常處理器Top-Level Exception Handler過目這時候調(diào)用棧還是完整的、局部變量還沒被清理、異常源地還沒被棧展開破壞是拿現(xiàn)場證據(jù)的黃金窗口。如果沒有任何處理器決定攔截系統(tǒng)進入第二輪 Second Chance按默認規(guī)則終止進程這時再掛接的捕獲邏輯拿到的棧已經(jīng)部分失效甚至只剩一個地址。madExcept 5 FS 做的事就是在第一輪里插入自己的處理器。它先把異常鏈上的上下文全部記錄成結(jié)構(gòu)化報告然后問你要不要把這個異?!跋钡?。這個機制的價值在于傳統(tǒng) try-except 只能抓到當前線程當前代碼塊里的異常一旦異??邕^模塊邊界、被某個框架層吞了或者發(fā)生在后臺線程業(yè)務(wù)層根本感知不到FS 模式是從異常發(fā)生的源頭直接接住不依賴你在每一處業(yè)務(wù)代碼里手動包保護層。FS這個后綴對應(yīng)的就是這一整套 First Chance 采集與分發(fā)能力而不是某個獨立 IDE 插件。理解這一點很關(guān)鍵你裝進工程的是一個異常采集框架配置的靈活性核心在回調(diào)函數(shù)的請求與放行邏輯上。2.2 什么項目值得上 madExcept 5 FS三條選型判據(jù)不是所有項目都需要把崩潰報告做成產(chǎn)品級基礎(chǔ)設(shè)施。團隊小、發(fā)布頻率低、用戶量小到崩潰日志可以靠用戶口述的項目直接套個 showmessage 也能撐。如果你正在做以下三類項目之一madExcept 5 FS 就值得投進去第一類是面向大量終端用戶的工具軟件或業(yè)務(wù)客戶端用戶環(huán)境五花八門缺 DLL、顯卡驅(qū)動沖突、輸入法注入都可能觸發(fā)崩潰復現(xiàn)只能靠現(xiàn)場報告。第二類是含多線程、插件化或依賴第三方 DLL 的系統(tǒng)崩潰線程棧必須追溯到模塊邊界。第三類是對質(zhì)量和響應(yīng)速度有要求的項目領(lǐng)導要的是“這周修復率提升了多少”而不是“我們還在找復現(xiàn)步驟”。從我接觸的項目看還有一個隱性的判斷條件項目是否有持續(xù)集成環(huán)境。madExcept 5 FS 給出的報告質(zhì)量取決于符號文件與構(gòu)建產(chǎn)物的對應(yīng)關(guān)系臨時編譯一次就想靠它定位效果會打五折這點后面第 4 章會展開。2.3 把 madExcept 5 掛進工程的最小路徑常見做法是通過 IDE 的組件安裝機制把 madExcept 編譯進設(shè)計環(huán)境里然后在工程文件中手動引入兩個運行時單元。最小改動量下你的工程文件末尾會變成這個樣子program KClient; uses Vcl.Forms, madExcept in ..\madExcept\madExcept.pas, madExceptHook in ..\madExcept\madExceptHook.pas, KMainForm in KMainForm.pas {MainForm}; {$R *.res} begin Application.Initialize; Application.CreateForm(TKMainForm, MainForm); Application.Run; end.madExcept單元提供異常報告生成與接口定義madExceptHook負責在初始化階段把 FS 處理器注冊進系統(tǒng)異常分發(fā)鏈。把這兩個單元放在 uses 列表靠前的位置是為了保證它們的初始化代碼先于你的業(yè)務(wù)單元執(zhí)行否則在異常處理器注冊完成之前發(fā)生的崩潰依然會漏網(wǎng)。工程文件改完后重新編譯并運行一次主動制造一個除零異常試試。默認配置下madExcept 5 會彈出它自帶的崩潰報告對話框里面有完整的調(diào)用棧和異常類型。若彈不出來先確認這兩個單元確實被編進dcu輸出目錄再確認 IDE 里沒啟用“忽略特定異常類型”的過濾設(shè)置。這一步跑通之后我們再談 FS 模式的自定義回調(diào)。3. 配置 FS 模式從默認彈窗到自定義處理回調(diào)3.1 用全局 Hook 接管 First Chance 異常madExcept 5 默認行為是攔截未處理異常并顯示報告對話框但這不適合所有場景發(fā)布版客戶端不該把技術(shù)報告直接懟在用戶臉上而且有些異常是“可控但需要被記錄”的比如文件被占用、網(wǎng)絡(luò)瞬斷、配置項格式錯誤。FS 模式給了你一個注冊全局回調(diào)的入口讓每個異常在第一時間先經(jīng)過你的代碼。常見做法是調(diào)用RegisterExceptionHandler它的聲明如下function RegisterExceptionHandler( Handler: TExceptionHandler; ThreadMode: TThreadMode; Event: TExceptionEvent): Integer;回調(diào)函數(shù)的實現(xiàn)聽起來抽象落地下來一般長這個樣子function MyFSHandler(const exceptIntf: IMEException; out handled: Boolean): Boolean; var report: TStringList; begin // First Chance 階段handled 輸出參數(shù)決定異常是否被吞掉 if exceptIntf.ExceptionClass EInOutError then handled : True // 文件寫入類異常記錄但不崩 else handled : False; // 其他異常照常拋出進入 Second Chance report : TStringList.Create; try report.Text : exceptIntf.BugReport; report.SaveToFile(D:\logs\crash_ FormatDateTime(yyyymmdd_hhnnss, Now) .txt); finally report.Free; end; Result : True; end;代碼里的handled是最容易理解錯的參數(shù)。設(shè)為True表示這個異常已經(jīng)被你處理了系統(tǒng)不會再向上拋程序繼續(xù)往下跑設(shè)為False表示你只是“看了一眼”并記錄了現(xiàn)場異常繼續(xù)正常傳播流程。Result表示是否接管默認的 madExcept 報告界面——返回True時你自己寫了文件就要關(guān)掉它的默認彈窗否則用戶會看到兩層輸出。注冊這回調(diào)的最佳位置是工程文件begin塊的最前面或者主窗體OnCreate里只注冊一次initialization RegisterExceptionHandler(MyFSHandler, stDontSync, epMainThread); end.stDontSync表示回調(diào)不主動做線程同步適合只做內(nèi)存操作和文件寫入的輕量邏輯若你在回調(diào)里要訪問 VCL 控件或調(diào)用需要消息循環(huán)的代碼改成stSync會更穩(wěn)代價是可能阻塞當前線程一小段時間。epMainThread限定只在主線程上觸發(fā)回調(diào)后臺線程崩潰則會進入默認流程對全局記錄需求更強的項目我一般會注冊兩個回調(diào)主線程一個工作線程一個分別處理。3.2 上下文信息讓崩潰報告自帶現(xiàn)場崩潰報告如果只有調(diào)用棧你只能知道“在哪個函數(shù)崩了”不知道“用戶當時在做什么”。madExcept 的接口里暴露了一些上下文屬性但更可靠的業(yè)務(wù)上下文還得靠自己在回調(diào)里補充。我一般會在回調(diào)里讀取一個全局對象的狀態(tài)把它拼進報告文件function MyFSHandler(const exceptIntf: IMEException; out handled: Boolean): Boolean; var lines: TStringList; ctx: TKContextInfo; begin ctx : TKContextInfo.GetInstance; lines : TStringList.Create; try lines.Add(UserID ctx.UserID); lines.Add(CurrentForm ctx.CurrentFormName); lines.Add(NetworkState ctx.NetworkState); lines.Add(LastAction ctx.LastActionLog); lines.Add(--- bug report start ---); lines.Add(exceptIntf.BugReport); lines.SaveToFile(ctx.LogDir fs_ FormatDateTime(yyyymmdd_hhnnss, Now) .log); finally lines.Free; end; handled : False; // 只記錄不吞異常 Result : True; end;把業(yè)務(wù)上下文放在LastActionLog里是個好習慣。很多閃退是特定操作序列觸發(fā)的比如“上傳圖片后立即切換賬套”如果你有操作埋點報告里就能直接看到最后一步動作。這個變量每執(zhí)行一個關(guān)鍵操作就覆蓋寫入一次不用記全量日志避免文件體積失控。3.3 報告內(nèi)容與文件落盤的參數(shù)取舍FS 回調(diào)里最忌諱的事情是“什么都往報告里塞”。調(diào)用棧、線程列表、模塊列表、CPU 寄存器、系統(tǒng)信息全部打進去一份報告能到幾百 KB高頻崩潰時磁盤和網(wǎng)絡(luò)都會被拖死。我的經(jīng)驗是分兩個級別一種是把BugReport完整落盤保留所有細節(jié)僅限開發(fā)版或內(nèi)測版開啟另一種是發(fā)布版只記錄異常類型、異常地址、模塊名、調(diào)用棧前十幀和業(yè)務(wù)上下文。后者的信息量足夠定位 80% 的崩潰文件體積控制在 10 KB 以內(nèi)上傳快、干擾少。落盤目錄要選用戶有寫權(quán)限的路徑不要硬編碼D:\logs否則用戶以普通權(quán)限運行時報告根本寫不進去。用GetEnvironmentVariable(LOCALAPPDATA)拼一個KClient\CrashReports目錄是可移植且安全的選擇。目錄不存在就創(chuàng)建創(chuàng)建失敗則退回到臨時目錄這層容錯必須做因為異?;卣{(diào)本身就是程序處于不穩(wěn)定狀態(tài)不要再讓文件系統(tǒng)問題掩蓋真正要排查的事故。4. 根據(jù)崩潰報告定位問題?;厮?、線程現(xiàn)場與符號文件4.1 讀懂 BugReport 的關(guān)鍵字段madExcept 5 生成的報告結(jié)構(gòu)大致固定我一般在排障時只盯著幾個欄位看表格字段含義定位價值Exception Class異常類型如 EAccessViolation判斷是邏輯問題還是系統(tǒng)級內(nèi)存問題Exception Message異常附帶信息經(jīng)常直接寫著越界下標或文件名Exception Address崩潰指令所在的模塊偏移結(jié)合符號表換算成函數(shù)名Call Stack從崩潰點向外的調(diào)用鏈解決“哪條路徑調(diào)到這里”的問題Threads所有線程的??煺张挪槎嗑€程協(xié)作導致的崩潰Modules加載的模塊列表及版本排查 DLL 沖突、補丁不一致報告里最騙人的是Exception Message為空的 Access Violation。地址直接指向某個系統(tǒng) DLL 內(nèi)部看起來像第三方庫的鍋但順著Call Stack看前兩條棧幀往往會回到你自己的消息處理函數(shù)。棧回溯是理解崩潰路徑的主線索而不是只看落點地址。4.2 符號表與版本映射讓地址變函數(shù)名崩潰日志里如果只有00401234這種裸地址對現(xiàn)代項目來說基本等于廢紙。madExcept 要把地址解析成函數(shù)名依賴編譯時生成的調(diào)試符號。Delphi/CBuilder 的鏈接器選項里一般提供 map 文件的開關(guān)把詳細模式打開后編譯器會輸出完整的映射關(guān)系表madExcept 在崩潰時查找該表即可還原函數(shù)名。工程交付時記得把每次構(gòu)建的 map 文件或調(diào)試符號與 exe 一并歸檔。這里的規(guī)矩是報告里的模塊版本號和構(gòu)建號必須能對應(yīng)到唯一一份符號文件。我習慣在工程里加一個構(gòu)建版本常量并定義到資源或全局變量里const BUILD_ID 20250607_1300_trunk_4a3f2c;回調(diào)里把這個BUILD_ID寫進報告頭部歸檔符號文件時文件名也帶同一個BUILD_ID。線上反饋回來看報告里的構(gòu)建號找符號文件幾秒鐘就能對上。4.3 一個真實定位流程的三步走第一步先把報告里的Exception Address和Call Stack前三條記錄抄出來。第二步用報告里的BUILD_ID找到對應(yīng)的 map 文件把裸地址換算成函數(shù)名這一步 madExcept 報告里通常已經(jīng)替你做了——前提是發(fā)布構(gòu)建時 map 文件沒有被剝離。第三步打開源碼定位到那個函數(shù)重點看這幾點是否有對空對象解引用、是否有無符號整數(shù)比較、是否有回調(diào)函數(shù)在跨模塊傳遞時被錯誤約定調(diào)用。碰到Call Stack本身就殘廢的報告比如只在崩潰點有一條記錄后面全是Unknown大概率是優(yōu)化編譯把棧幀給省略了或者你的 hook 觸發(fā)在異常鏈已經(jīng)展開之后。這時候不要硬解地址轉(zhuǎn)而看Modules欄位確認崩潰模塊是否帶調(diào)試版本號再檢查編譯器優(yōu)化選項。報告里那份Threads列表也別忽略。很多“偶爾崩一次”的現(xiàn)場主線程棧是正常的反而是某個工作線程停在了釋放對象的代碼上。如果團隊里多線程代碼量大且頻繁 new/free 對象FS 報告里帶出線程棧會省掉你掛調(diào)試器復現(xiàn)一下午的時間。5. 避坑madExcept 5 FS 集成中常見的五個翻車現(xiàn)場5.1 現(xiàn)象一DLL 崩潰只出現(xiàn)“外部地址”現(xiàn)象主程序用 madExcept加載的一個業(yè)務(wù) DLL 里崩了報告里只顯示Unknown module和一個裸地址無法換算函數(shù)名。原因DLL 工程沒有編譯 madExcept 相關(guān)單元主程序拿不到 DLL 的符號表另外系統(tǒng)異常分發(fā)經(jīng)過模塊邊界時DLL 里的棧幀格式不會被主程序的 hook 完整解析。解決給 DLL 工程也加上 madExcept 相關(guān)單元并開啟同樣的符號輸出配置。發(fā)布時確認 DLL 的 map 文件與 DLL 本體同版本歸檔。若 DLL 是第三方且無符號文件則在報告里至少記錄模塊名與版本號反向聯(lián)系廠商時也算有據(jù)可查。5.2 現(xiàn)象二FS 回調(diào)里寫文件導致遞歸崩潰現(xiàn)象異常發(fā)生后回調(diào)函數(shù)里寫日志日志文件被占用或無權(quán)限寫文件操作又拋出異常madExcept 再次進回調(diào)進入無限遞歸程序徹底卡死。原因回調(diào)函數(shù)本身沒有做異常隔離。FS 回調(diào)運行在系統(tǒng)的異常分發(fā)鏈上任何一環(huán)再拋異常都會重新進入分發(fā)入口。解決回調(diào)函數(shù)整體包一層裸 try-except任何異常都吞掉確?;卣{(diào)自己永遠不拋文件寫入前先判斷目錄可寫寫不進去就放棄絕不讓日志行為本身變成新的事故源頭function MyFSHandler(const exceptIntf: IMEException; out handled: Boolean): Boolean; begin try // 原有邏輯 except // 記錄回調(diào)失敗次數(shù)不再做任何操作 end; handled : False; Result : True; end;5.3 現(xiàn)象三優(yōu)化編譯后?;厮輰Σ簧闲刑柆F(xiàn)象用戶現(xiàn)場報告的調(diào)用棧能列出函數(shù)名但點開源碼行號指向的位置和實際崩潰邏輯明顯不對。原因編譯器開了內(nèi)聯(lián)和幀指針省略優(yōu)化某些棧幀被優(yōu)化合并madExcept 依據(jù)靜態(tài)符號表換算出的行號是近似值不是精確落點。解決定位問題時把函數(shù)名當作線索而非最終答案重點看崩潰點所在函數(shù)整體邏輯。對懷疑有問題的單元單獨關(guān)閉優(yōu)化重新編譯做一次定向?qū)Ρ?。這不是 madExcept 的 bug是符號還原的固有精度問題理解為它給的是“案發(fā)附近區(qū)域”而不是“精確坐標”就能少走彎路。5.4 現(xiàn)象四報告發(fā)送阻塞主線程二次鎖死現(xiàn)象加了 FS 回調(diào)后崩潰現(xiàn)場偶爾變成“卡死不動”任務(wù)管理器顯示進程無響應(yīng)。原因回調(diào)里直接發(fā)了 HTTP 請求把崩潰報告?zhèn)鹘o服務(wù)端。崩潰發(fā)生時主線程狀態(tài)本身不穩(wěn)定同步網(wǎng)絡(luò)請求一直等超時界面消息循環(huán)被阻塞用戶感知為假死。解決回調(diào)內(nèi)不要做任何網(wǎng)絡(luò)操作只負責落盤。程序下次正常啟動時掃描崩潰報告目錄用后臺線程統(tǒng)一上傳上傳成功再刪文件。這個模式也叫本地落地加延遲上報是客戶端崩潰采集的常見做法。5.5 現(xiàn)象五打包時機不對報告里全是“Unknown”現(xiàn)象測試機上報的報告完全正常發(fā)布給用戶后全變成Unknown function和裸地址。原因發(fā)布包使用 Release 配置編譯且關(guān)閉了 map 文件輸出madExcept 運行時沒有任何符號可查。解決發(fā)布配置里也保留 Detailed map 文件生成只是不給最終用戶分發(fā)而已——直接把 map 文件歸檔到構(gòu)建服務(wù)器內(nèi)部存儲。同時確認構(gòu)建機上沒有開啟“生成輕量調(diào)試信息”之類的減配選項。這個問題在快速迭代的小團隊里幾乎每個月都能踩到一次屬于典型的因為“發(fā)布要精簡”而誤刪關(guān)鍵排障數(shù)據(jù)。6. 進階把崩潰報告變成持續(xù)可用的工程質(zhì)量數(shù)據(jù)6.1 用版本號與構(gòu)建 Mark 給報告打標早期接入 madExcept 時我見過一個項目報告文件存了幾千個命名全是crash_20250501_230011.log解析時根本分不清版本。后來我們做了一件事把構(gòu)建號、分支名、編譯時間全部塞進報告文件名和文件頭排序和歸因一下就清楚了。function BuildReportFileName: string; var tag: string; begin tag : Format(%s_%s_%s, [BUILD_ID, APP_VERSION, COMPILE_TIME]); Result : GetEnvironmentVariable(LOCALAPPDATA) \KClient\CrashReports\ tag _ FormatDateTime(yyyymmdd_hhnnss, Now) .txt; end;發(fā)布后每天掃一次報告目錄按BUILD_ID分組統(tǒng)計崩潰次數(shù)哪一次構(gòu)建增量引入的崩潰增量立刻顯現(xiàn)。別小看這步它把“收到一份崩潰日志”變成“一周崩了 47 次其中 39 次在新版本”的決策信息。6.2 本地落盤加延遲上傳的可靠發(fā)送模式報告最終是要匯總到服務(wù)端的。常見做法是寫一個后臺線程每次啟動時遍歷崩潰報告目錄文件逐條上傳成功后移動到 backup 目錄或直接刪除。上傳接口要冪等同一條報告重復提交時服務(wù)端按報告唯一 ID 去重。我在最近一個模擬項目X里給上傳線程加了一個簡單限制單次最多處理 20 份報告處理完退出線程避免程序啟動瞬間被大量歷史報告占滿網(wǎng)絡(luò)。崩潰報告是有時效性的優(yōu)先傳當天的歷史存量排后。線程里每傳完一條就Sleep(300)給主線程留資源這在低配用戶機上尤其重要。還有一個習慣值得分享每季度把報告里的Exception Class做一次分布統(tǒng)計。曾經(jīng)有個客戶端項目翻來覆去都是同一個數(shù)組越界問題開發(fā)組修了一版上線后統(tǒng)計看該類異常環(huán)比降了 90%那個數(shù)據(jù)比任何測試報告都有說服力。崩潰報告系統(tǒng)不是修一次就完事的它的價值隨時間線性累積前提是你把構(gòu)建號、符號文件、報告歸檔這套動作養(yǎng)成肌肉記憶。這條路我走下來最大的教訓只有一個——hook 要接得早符號要存得住報告不要當場發(fā)。希望幫到你。本文還有配套的精品資源點擊獲取