動開發(fā)十年之路:從IRP、DPC到內(nèi)核調(diào)試的實戰(zhàn)避坑指南)
1. 我為什么走上網(wǎng)卡驅(qū)動這條窄路算起來我寫驅(qū)動的時間已經(jīng)有十個年頭了。這個系列之所以叫驅(qū)動之路是因為我確實把這條路一步一步地走成了自己的職業(yè)主線——從最初連DeviceIoControl和IRP都分不清的門外漢到后來能在幾個小時之內(nèi)定位一個內(nèi)核態(tài)內(nèi)存越界問題中間的過程談不上波瀾壯闊但踩過的坑一個比一個深學(xué)到的東西也一個比一個扎實。很多人問我應(yīng)用層開發(fā)崗位那么多為什么要去碰驅(qū)動這個吃力不討好的領(lǐng)域我的回答一直沒變——因為驅(qū)動開發(fā)是唯一一個能讓你同時感受到權(quán)力和責(zé)任的領(lǐng)域。你寫的代碼不是跑在一個有操作系統(tǒng)保護(hù)的進(jìn)程中而是跑在系統(tǒng)本身的心臟附近。正因如此出了問題你的第一責(zé)任人不是應(yīng)用層的日志而是藍(lán)屏分析器。1.1 起點從一次高危操作開始的興趣我入行的第一份工作是在某公司做Windows桌面應(yīng)用。那時有個項目需要給硬件設(shè)備寫一個簡單的驅(qū)動程序才能訪問設(shè)備的寄存器映射。我找遍了整個項目組發(fā)現(xiàn)沒人真正寫過驅(qū)動。于是我從微軟官方文檔翻起照著樣例搭了一個最簡單的KMDF驅(qū)動框架連夜編譯、安裝、運行——結(jié)果系統(tǒng)直接藍(lán)屏重啟后我愣在屏幕前半小時沒緩過來。當(dāng)時的我對內(nèi)核根本沒有概念。我以為驅(qū)動就是一個DLL只不過入口函數(shù)不同而已。后來我才明白我犯的錯誤是典型的用戶態(tài)思維做內(nèi)核態(tài)事我在驅(qū)動里用了大量的動態(tài)內(nèi)存分配和普通C異常處理卻不知道內(nèi)核里有一套完全不同的內(nèi)存分配規(guī)則也不知道異常處理機(jī)制和用戶態(tài)根本不是一回事。那次藍(lán)屏反而激發(fā)了我的好奇心。我花了整整一個月的業(yè)余時間把內(nèi)核對象、IRP分發(fā)、分頁與非分頁池、DPC、自旋鎖這些概念挨個啃了一遍。越學(xué)越覺得有意思——原來操作系統(tǒng)并不神秘它只是把規(guī)則定得比你想象中嚴(yán)格得多。而你一旦理解了這些規(guī)則就等于拿到了通往系統(tǒng)底層的鑰匙。1.2 跑通第一個內(nèi)核模塊的激動與恐懼真正讓我確定要靠驅(qū)動吃飯的時刻是我第一次成功編寫并加載一個字符設(shè)備驅(qū)動然后從用戶態(tài)程序通過IOCTL向內(nèi)核發(fā)送了一條自定義命令內(nèi)核再去操作一塊虛擬硬件最后把結(jié)果返回給應(yīng)用層。整個過程跑通的時候我激動得在工位上差點叫出聲。但同時我也很恐懼——因為我意識到這個驅(qū)動里任何一行代碼出了問題都不會有拋異常讓你接住的機(jī)會它只會讓你的電腦瞬間死機(jī)。也正是這種恐懼讓我養(yǎng)成了一個習(xí)慣每次修改驅(qū)動代碼之前先想清楚如果出問題會怎樣、用什么樣的方式隔離風(fēng)險。這個習(xí)慣后來幫我在無數(shù)個項目里活了下來。驅(qū)動開發(fā)不是膽大者的游戲恰恰是謹(jǐn)慎者的修行。1.3 選擇驅(qū)動這條路其實是選擇了一種思維方式做應(yīng)用開發(fā)的人關(guān)心的是功能給不給得到做驅(qū)動的人關(guān)心的是系統(tǒng)穩(wěn)不穩(wěn)定。這是兩種完全不同的思維方式。應(yīng)用層你可以依賴框架、依賴容器、依賴中間件幫你兜底驅(qū)動層沒有這些東西可以依賴你面對的就是操作系統(tǒng)本身你要么理解它要么被它擊敗。驅(qū)動之路這個系列就是我這些年理解操作系統(tǒng)的過程記錄。它不只是一堆代碼示例的集合更是一個開發(fā)者思維轉(zhuǎn)變的過程。我想把這個過程完整地寫出來讓后來的人少走一些我走過的彎路。2. 驅(qū)動開發(fā)與應(yīng)用開發(fā)兩種完全不同的世界觀如果讓我用一句話概括驅(qū)動開發(fā)的本質(zhì)我會說驅(qū)動是用代碼去直接對話硬件和操作系統(tǒng)內(nèi)核的過程。你寫的每一個函數(shù)、每一個回調(diào)、每一個分派例程都在操作系統(tǒng)最核心的空間里運行。2.1 內(nèi)核層與應(yīng)用層權(quán)力的代價我先用一個最直白的類比來解釋兩者的關(guān)系。應(yīng)用層程序就像住在酒店客房的客人有問題了可以打電話找前臺操作系統(tǒng)協(xié)調(diào)想要什么都不用自己動手——前臺幫你安排。而驅(qū)動就像酒店的工程部員工你必須拿著鑰匙進(jìn)入配電房、水管井這些核心區(qū)域去處理問題。你權(quán)限很高但一旦你把電閘拉錯了整個酒店都可能停電。這個類比背后有嚴(yán)格的技術(shù)支撐。運行在用戶態(tài)Ring 3的程序訪問內(nèi)存時要通過虛擬地址映射大部分操作都走系統(tǒng)服務(wù)分發(fā)而內(nèi)核態(tài)Ring 0的驅(qū)動直接訪問物理內(nèi)存和硬件端口。一個普通的空指針解引用在應(yīng)用層不過是拋一個異常放在內(nèi)核里就是一劑藍(lán)屏配方。權(quán)限越大責(zé)任越大這句話在驅(qū)動開發(fā)里不是空話。2.2 核心概念快速掃盲IRP、DPC、內(nèi)存、同步很多剛接觸驅(qū)動的朋友問我驅(qū)動里到底在寫什么說白了大部分工作都在圍繞幾個核心概念轉(zhuǎn)。IRPI/O請求包是驅(qū)動世界里的快遞單什么打開設(shè)備、讀數(shù)據(jù)、寫數(shù)據(jù)、控制設(shè)備所有的I/O請求都會被打包成一個IRP從設(shè)備棧頂部一路傳到底部每層驅(qū)動都有機(jī)會處理它處理完了再往下傳或者直接完成它。你的驅(qū)動的主要工作就是寫這些IRP的處理邏輯。DPC延遲過程調(diào)用是中斷處理的下半部用來處理那些不在中斷上下文里不能做太久的工作。如果你深入了解Windows中斷模型你會發(fā)現(xiàn)DISPATCH_LEVEL是一個關(guān)鍵的分水嶺——上了這個級別你連分頁內(nèi)存都沒法碰更別提做各種鎖等待了。內(nèi)存方面驅(qū)動必須明確區(qū)分分頁內(nèi)存和非分頁內(nèi)存。如果你在環(huán)級別較高的上下文里訪問了分頁內(nèi)存系統(tǒng)會因為無法處理缺頁故障而崩潰。我記得某公司有個同事寫了三年驅(qū)動都分不清這兩個概念直到一次內(nèi)存越界讓他修的驅(qū)動在客戶機(jī)器上反復(fù)藍(lán)屏他才真正理解了這句話的分量。同步更是一場嚴(yán)峻考驗。自旋鎖、互斥鎖、快速資源鎖、內(nèi)核信號量……每一種鎖都有它的適用場景和禁忌。最怕的就是在持有自旋鎖的代碼路徑里又去等一個別人的鎖造成死鎖。2.3 用戶態(tài)調(diào)試與內(nèi)核態(tài)調(diào)試的差距做過驅(qū)動的人都有這么一個深刻體會調(diào)試驅(qū)動的難度比調(diào)試應(yīng)用層高出至少一個量級。應(yīng)用層調(diào)試你可以開IDE斷點隨便打變量隨便看。驅(qū)動調(diào)試你得用內(nèi)核調(diào)試器最常見的是用WinDbg做雙機(jī)調(diào)試——一臺被調(diào)試的機(jī)器一臺調(diào)試機(jī)。你要么用虛擬機(jī)要么用串口/網(wǎng)絡(luò)連接。而且在內(nèi)核態(tài)你很多地方根本沒法斷下來——比如在中斷上下文里調(diào)試器一停整個系統(tǒng)狀態(tài)都僵住了。我記得有一次排查一個定時器引發(fā)的驅(qū)動崩潰我光是在WinDbg里看!analyze -v的輸出就用了整整兩天。那兩天里我反反復(fù)復(fù)地查看崩潰棧、內(nèi)存池標(biāo)記、IRQL歷史記錄最后才發(fā)現(xiàn)是某一次定時器回調(diào)里訪問了已經(jīng)釋放的設(shè)備擴(kuò)展對象。這種排查經(jīng)歷應(yīng)用層程序員可能一輩子都遇不到但對驅(qū)動工程師來說是家常便飯。3. 這十年來我踩過的驅(qū)動深坑以及它們教會我的事灰塵從左到右坑從淺到深。既然叫驅(qū)動之路就不能只講風(fēng)光得把真實的泥濘和翻車經(jīng)歷擺出來。這里分享我職業(yè)生涯中最有代表性的三個深坑每一個都讓我付出了真金白銀的代價。3.1 踩坑一Filter驅(qū)動導(dǎo)致的集體掉網(wǎng)某一年公司要做一個網(wǎng)絡(luò)監(jiān)控產(chǎn)品需要開發(fā)一個過濾驅(qū)動來截獲網(wǎng)絡(luò)包。我那時對WFP框架還不太熟悉就選擇在一層老式的過濾驅(qū)動上做文章。結(jié)果測試階段我們只在三臺測試機(jī)器上運行了驅(qū)動客戶那邊卻出現(xiàn)了大規(guī)模的掉網(wǎng)故障——整個辦公室里凡是裝了驅(qū)動的電腦全部斷網(wǎng)裝重啟后網(wǎng)絡(luò)仍然時斷時續(xù)。我最初以為是自己過濾邏輯的問題反復(fù)檢查包處理代碼怎么看都覺得沒問題。后來我才發(fā)現(xiàn)問題出在一個極其隱蔽的地方我在發(fā)送包的路徑上做了較長邏輯的處理導(dǎo)致網(wǎng)絡(luò)驅(qū)動的發(fā)送隊列被阻塞——而且我沒有重入保護(hù)同一個包在我自己的過濾邏輯里被遞歸處理了一次形成了無限遞歸。棧一溢出網(wǎng)絡(luò)棧直接掛了。這個坑教會我三件事第一過濾驅(qū)動最重要的是快速判斷、快速通過不要在熱路徑上做任何耗時操作第二必須做重入保護(hù)你的回調(diào)可能被多個上下文同時進(jìn)入第三任何改動都要先在小范圍試點別一上來就全量鋪開。3.2 踩坑二DPC上下文里做了鎖等待另一個讓我終身難忘的坑是我在一個DPC例程里試圖等待一個事件對象。聽起來似乎沒什么大不了的——線程可以等事件為什么DPC不行因為我忘了DPC運行在DISPATCH_LEVEL在這個級別上你不能做任何阻塞等待的操作。系統(tǒng)不會像應(yīng)用層那樣讓線程休眠到事件觸發(fā)而是直接拋出一個致命錯誤。當(dāng)時我調(diào)試了很久都找不到原因因為問題不在邏輯上而在執(zhí)行的上下文上。后來我花了幾個晚上把Windows的調(diào)試優(yōu)先級模型、內(nèi)核異步過程調(diào)用、工作線程的概念全部過了一遍才徹底明白了你在哪個級別上做事就必須遵守那個級別的規(guī)則這件事。從那以后我的代碼里凡是涉及DPC的路徑一律遵循一個原則DPC里只做快速非阻塞處理如果需要等待或依賴其他組件的結(jié)果就通過工作項投遞到系統(tǒng)工作線程繼續(xù)完成。3.3 踩坑三兼容性矩陣遠(yuǎn)比想象中恐怖驅(qū)動開發(fā)最大的隱藏成本是兼容性測試矩陣。你開發(fā)一個驅(qū)動不是只要適配自己那臺開發(fā)機(jī)就夠了。不同的Windows版本、不同的處理器架構(gòu)、不同的硬件平臺、不同的驅(qū)動模型版本W(wǎng)DM、KMDF、UMDF……這些排列組合起來測試的量是驚人的。我在某公司參與開發(fā)的一個存儲過濾驅(qū)動開發(fā)只需要三周兼容性測試和修復(fù)用了一個半月。很多bug不是功能錯誤而是在某種特定版本組合下才會觸發(fā)的條件性崩潰。比如某個版本的StorPort接口行為發(fā)生了變化我們的驅(qū)動沒有判斷對方版本就使用了新的參數(shù)結(jié)構(gòu)導(dǎo)致老版本系統(tǒng)上出現(xiàn)內(nèi)存訪問違例。后來我學(xué)乖了寫驅(qū)動之前先明確目標(biāo)OS版本范圍并且在代碼里四處加上RTL_VERIFY和版本判斷宏同時把兼容性測試矩陣做得比用戶期望的還寬一倍。寧可多測不可漏測。3.4 從坑里提煉出的一套工作流這些坑踩多了我逐漸總結(jié)出了一套適合自己的驅(qū)動開發(fā)工作流先寫設(shè)計文檔明確IRP分發(fā)路徑、上下文級別、同步策略和內(nèi)存使用策略搭建雙機(jī)調(diào)試環(huán)境和崩潰轉(zhuǎn)儲配置確保出問題能第一時間拿到現(xiàn)場小步迭代每增加一個功能模塊就做一次負(fù)載測試和壓力測試所有涉及時間、同步、內(nèi)存分配的地方都要加級別檢查注釋和斷言發(fā)布前跑一遍靜態(tài)分析工具和驅(qū)動驗證程序比如微軟提供的驅(qū)動程序驗證器在至少三個不同版本的操作系統(tǒng)上做回歸測試。這套流程看起來繁瑣但每一次都能精準(zhǔn)地在問題擴(kuò)大之前抓到它。我的經(jīng)驗是驅(qū)動開發(fā)流程走得越嚴(yán)格后期返工的時間就越少。4. 驅(qū)動之路系列想帶給你的東西既然這是開篇我有必要交代一下驅(qū)動之路這個系列的整體規(guī)劃。它不是一個隨緣更新的技術(shù)筆記而是一張我精心設(shè)計的地圖。4.1 系列規(guī)劃從入門到工程化整個系列我計劃按照如下脈絡(luò)來展開入門篇環(huán)境搭建、驅(qū)動框架選擇WDM/KMDF/UMDF、第一個驅(qū)動程序的完整編寫與加載核心機(jī)制篇IRP與設(shè)備棧、即插即用與電源管理、同步與并發(fā)、內(nèi)存管理、DMA和中斷處理進(jìn)階篇常見驅(qū)動類型的深入網(wǎng)絡(luò)過濾、存儲、文件系統(tǒng)過濾、顯示以及如何在驅(qū)動與應(yīng)用層之間設(shè)計高效的通信接口工程化篇調(diào)試工具鏈、崩潰轉(zhuǎn)儲分析、驅(qū)動簽名與發(fā)布、兼容性測試矩陣的設(shè)計、自動化測試方法實戰(zhàn)篇我會帶著讀者完整實現(xiàn)一個中等復(fù)雜度的驅(qū)動項目從需求到設(shè)計到編碼到測試走一遍全流程。這個規(guī)劃不是我想當(dāng)然定的而是基于我這些年帶新人、帶項目的經(jīng)驗總結(jié)出來的。太多人學(xué)習(xí)驅(qū)動開發(fā)時被卡在看不懂系統(tǒng)文檔或者不知道從哪下手這兩件事上。我希望用這個系列把這兩個門檻拆掉。4.2 你需要的環(huán)境與前置知識在開始學(xué)習(xí)驅(qū)動之前你需要準(zhǔn)備一套基本的開發(fā)環(huán)境。我不勸你一上來就買實機(jī)調(diào)試設(shè)備第一步用虛擬機(jī)完全可以。操作系統(tǒng)一臺Windows開發(fā)機(jī)建議用較新的64位版本一臺Windows虛擬機(jī)作為測試和調(diào)試目標(biāo)開發(fā)工具Visual Studio Windows SDK Windows Driver KitWDK目前WDK已經(jīng)和VS集成得很順暢裝好環(huán)境就能直接編譯驅(qū)動項目調(diào)試工具WinDbg可以直接從微軟商店或WDK里獲取建議配好調(diào)試通道前置知識C語言基礎(chǔ)指針、結(jié)構(gòu)體、鏈表操作、操作系統(tǒng)基礎(chǔ)概念進(jìn)程、線程、虛擬內(nèi)存、中斷。如果你上述前置知識都還比較薄弱也不用灰心。我在系列文章中會把每個概念掰開揉碎了講結(jié)合生活化類比幫大家建立直覺。比如IRP我不直接說IO請求包我會先說它是設(shè)備世界的快遞單等建立了直覺之后再引入精確的技術(shù)術(shù)語。4.3 我可以提供的和建議自學(xué)的這個系列能夠提供給你的是經(jīng)過實戰(zhàn)驗證的代碼模式和避坑清單。我會把每一個驅(qū)動模塊的骨架代碼、注釋、編譯配置全部貼出來還會標(biāo)注哪些地方容易踩坑、為什么這么寫。但我也要說一句實話驅(qū)動開發(fā)最核心的能力——面向內(nèi)核排查問題的能力——不是任何博客或教程可以直接教給你的。它需要你在真實或仿真環(huán)境中反復(fù)練習(xí)。所以我強(qiáng)烈建議每個讀者在看每篇文章的時候不要只看要親手把代碼編譯起來跑起來甚至故意改壞它、讓它藍(lán)屏然后通過調(diào)試器去分析原因。這個過程才是真正的驅(qū)動之路。5. 寫給想入坑驅(qū)動開發(fā)的你我的四點實在建議在結(jié)束這篇自序之前我想把最實在的建議給到那些準(zhǔn)備踏上驅(qū)動之路的讀者。這些話不是從教科書里搬來的是我一條一條從自己的經(jīng)歷里提煉出來的。5.1 優(yōu)先級順序先學(xué)會不崩潰很多初學(xué)者一上來就想著寫一個多么強(qiáng)大的過濾器做出多么復(fù)雜的功能。我勸你先壓住這種沖動。你入坑驅(qū)動開發(fā)的第一個目標(biāo)應(yīng)該是寫出永遠(yuǎn)不會讓系統(tǒng)藍(lán)屏的驅(qū)動哪怕它的功能只是打印一串日志。我自己帶過幾個新人我考核他們的方式從來不是你實現(xiàn)了一個多牛的功能而是你寫的模塊在壓力測試下跑三個小時系統(tǒng)還穩(wěn)不穩(wěn)定。穩(wěn)定是驅(qū)動開發(fā)的地基功能是地基之上的大樓。沒有地基什么都是空的。5.2 調(diào)試能力是驅(qū)動工程師的根如果你只會寫代碼而不會調(diào)試你很難在驅(qū)動開發(fā)這條路上走遠(yuǎn)。我建議你從學(xué)習(xí)WinDbg開始把以下這些命令練到滾瓜爛熟!analyze -v崩潰后的自動分析!process 0 0查看進(jìn)程信息!pool系列分析內(nèi)存池狀態(tài)!irql查看當(dāng)前IRQL!lock/!locks查看鎖狀態(tài)kb查看當(dāng)前調(diào)用棧。同時要學(xué)會配置和閱讀內(nèi)核轉(zhuǎn)儲文件。藍(lán)屏不可怕可怕的是沒有留下分析線索。好的驅(qū)動工程師不是在追求零藍(lán)屏——那不可能——而是追求每次藍(lán)屏都能在最短時間內(nèi)定位根因。5.3 保持懷疑保持好奇做驅(qū)動開發(fā)的這些年我最大的體會是操作系統(tǒng)里幾乎沒有不可能的事只有你還沒理解的事。每次遇到看似不可解釋的問題我都會強(qiáng)制自己先放下是不是系統(tǒng)bug的念頭回到文檔、反匯編、內(nèi)核追蹤工具上去找答案。大多數(shù)情況下最終都會發(fā)現(xiàn)是自己某個假設(shè)出了問題。所以我想對入坑者說要有耐心也要有好奇心。驅(qū)動之路之所以讓人著迷就是因為它永遠(yuǎn)有更深的東西等著你去發(fā)現(xiàn)。5.4 最后一句驅(qū)動開發(fā)從來不是一條好走的路它需要你耐得住寂寞、扛得住藍(lán)屏、靜得下心去啃文檔。但如果你真的走通了這條路你會發(fā)現(xiàn)你獲得的不僅僅是一項寫底層代碼的技能更是一種探究事物本質(zhì)的能力。這篇自序?qū)懙竭@里算是把驅(qū)動之路的起點立住了。后續(xù)的文章我會一篇一篇地帶著大家走下去——從搭建環(huán)境開始一直到完成一個真正可以發(fā)布的產(chǎn)品級驅(qū)動模塊。路還長坑還多我們一起走。