基于IntelVt技术的Linux内核调试器 - 2
4 基于IntelVt技術的Linux內核調試器- 調試器設計與實現(2):調試核心4.1反匯編引擎 如果說調試框架是一個調試器的靈魂,那么接口與反匯編引擎就是一個調試器的身體。我們在調試過程中是要閱讀指令代碼的,而反匯編引擎則提供將二進制元指令翻譯成可閱讀的匯編代碼這個功能。 設計并實現一個初級的反匯編引擎很簡單,但是計算機指令系統并不簡單,將這個反匯編引擎實現到可以實際應用的級別需要不斷地調試與修復Bugs,這個過程需要耗費大量精力。所以我選擇了開源反匯編引擎。雖然網上有很多開源反匯編引擎,但是大部分都依賴于用戶態的C庫,導致不能很好地移植到內核模塊上使用,而且具有優秀效率的反匯編引擎很少。這里我選擇了libudis86,它是一個開源反匯編引擎,我只對其語法轉換部分進行了一些修改,使其反匯編出來的指令格式符合比較好的閱讀習慣。 4.1.1libudis86的基本使用方法Libudis86庫的使用很簡單,只要一個提供各種信息的結構體,調用ud_disasmembe函數即可。例如下面定義了一個反匯編某個指令的函數: ULONGOriDisasm(PUCHAR str,ULONG Eip) { UDISud_obj; ULONGlen; ? ud_init(&ud_obj);?//初始化結構體 ud_set_mode(&ud_obj,32);?//設置為32位元CPU模式 ud_set_syntax(&ud_obj,UD_SYN_INTEL);?//結果使用Intel語法 ud_set_pc(&ud_obj,(int)Eip);?//設置反匯編起點 ud_set_input_buffer(&ud_obj,(uint8_t*)Eip,32);?//設置輸入緩沖區 len=ud_disassemble(&ud_obj);?//開始反匯編 strcpy(str,ud_insn_asm(&ud_obj));?//復制結果 returnlen; } 該函數反匯編Eip指向的二進制元指令碼,將結果復制到str指向的緩沖區中。 4.1.2使用libudis86反匯編某段程序上面的函數OriDisasm只能反匯編一條指令,通常我們的調試器需要反匯編一段程序。OriDisasm在反匯編一條指令結束后會返回該指令長度(位元組),遞增指令指針循環繼續這項操作即可反匯編一段代碼。 4.1.3向上反匯編:遞減命中率算法調試器的反匯編窗口應該像控制臺窗口一樣具有翻頁功能,這樣便于我們查閱反匯編代碼,但是單純地使用libudis86只能從上往下進行反匯編,如果我們需要向上翻頁,則需要從下往上翻頁。看似簡單,實際上有一個很重要的難題。 我們知道x86匯編指令是不定長的,一條指令可能最小只占用1個字節,最長可能占用15個字節,而我們并不知道某一條指令它的上一條指令占用多少個位元組,因此不能直接獲取某一條指令的上一條指令是什么。例如下面的指令: 004017F0 75 64 jnz short 00401856 004017F2 8B45 10 mov eax, dword ptr [ebp+10] 004017F5 C1E8 10 shr eax, 10 004017F8 83F8 07 cmp eax, 7 第一列是指令位址,第二列是指令二進制元編碼,第三列是對應的匯編代碼。假設當前我們反匯編的位置是4017F8那么我們對這個內存位置的83F8 07進行反匯編可以得到cmpeax,7這條指令,但是我們并不知道這條指令的上一條指令從什么位址開始,如果我們貿然猜測上一條指令的位址顯然我們會得到一個錯誤的反匯編結果。例如對4017F6反匯編得到: 004017F6 E8 1083F807 call 08389B0B 結果是一個占用5個位元組的call指令,而且這個指令覆蓋了位于4017F8本來正確的結果。 我設計了一個算法用以解決這個問題,算法的思想是遞減地址指針,從這個指標開始向下循環反匯編,當長度剛剛好到達我們預期的位置時,記錄上一條指令的位置。然后將這些結果進行統計。 例如當前指令是 004017F8 83F8 07 cmp eax, 7 我們想要知道他的上一條指令是什么,就遞減地址,得到4017F7,反匯編得到: 004017F7 1083 F807754B adc byte ptr [ebx+4B7507F8], al 結果覆蓋了4017F8,此結果作廢。繼續向上遞減。 004017F6 E8 1083F807 call 08389B0B 結果覆蓋了4017F8,此結果作廢。繼續向上遞減。 004017F5 C1E8 10 shr eax, 10 004017F8 83F8 07 cmp eax, 7 結果可能正確,記錄下上一條指令位址4017F5,命中率為1次。 繼續向上遞減。 004017F4 10C1 adc cl, al 004017F6 E8 1083F807 call 08389B0B 覆蓋了4017F8,此結果作廢。繼續向上遞減。 004017F3 45 inc ebp 004017F4 10C1 adc cl, al 004017F6 E8 1083F807 call 08389B0B 覆蓋了4017F8,此結果作廢。繼續向上遞減。 004017F2 8B45 10 mov eax, dword ptr [ebp+10] 004017F5 C1E8 10 shr eax, 10 004017F8 83F8 07 cmp eax, 7 剛好得到4017F8,記錄下4017F8的上一條指令是4017F5,因為剛剛記錄命中了一次,因此當前的統計結果是:4017F8上一條指令4017F5,命中率2次。 就這樣繼續不斷遞減地址,從遞減后的地址向下反匯編,當結果剛好可以到達4017F8時記錄下上一條指令位址。假設我們向上遞減200字節,記錄結果可能如下。 4017F5 40次 XXXXXX 1次 那么根據統計結果上看,上一條指令有很大可能是從4017F5開始,那么我就可以認為上一條指令是4017F5。當然這個結論可能不正確,因為這畢竟是統計學結果。如果想要得到更準確的結果,我們可以采樣更多的數據,例如向上遞減1000字節。以保證結果的可靠性。 實現好的代碼片段如下: typedefstruct { ULONGpInstrAddr; //上一條指令的位址 ULONGHitCount; //命中次數 }PREV_INSTR_HITTEST,*PPREV_INSTR_HITTEST; ? ULONGGetPrevIp(ULONG Eip) { PREV_INSTR_HITTESTHitTest[16]; ULONGCurrentAddr = Eip - 1; ULONGPrevAddr; ULONGDisasmLimit = 0x100; ULONGlen; ULONGi; ULONGPrevAddr_MaxHit = 0; ULONGMaxHit = 0; ? if(!Eip) returnFALSE; ? memset(&HitTest,0,sizeof(HitTest)); while(DisasmLimit) { PrevAddr= CurrentAddr; if(!IsAddressExist(PrevAddr))?//保證地址空間可讀 break; ? while(1) { len= FastDisasm(PrevAddr); if(len!= -1 && len)? { if(len+ PrevAddr >= Eip) { AddHit(&HitTest[0],16,PrevAddr); break; } elseif(len + PrevAddr > Eip) { break; } } else { break; } PrevAddr+= len; } ? DisasmLimit--; CurrentAddr--; } ? for(i= 0; i < 16; i++) { if(HitTest[i].HitCount> MaxHit) { MaxHit= HitTest[i].HitCount; PrevAddr_MaxHit= HitTest[i].pInstrAddr; } } returnPrevAddr_MaxHit; } |
轉載于:https://www.cnblogs.com/lucelujiaming/p/9467907.html
總結
以上是生活随笔為你收集整理的基于IntelVt技术的Linux内核调试器 - 2的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lc js 35
- 下一篇: Win7硬盘安装方法