本章學到了什麼
- 調試技巧:在VS中斷點調試,檢視反彙編代碼,step into進行步進調試,運作過程中檢視寄存器、記憶體位址、變量值變化等。
- 機器碼構造能力:使用C/C++中的直接在C代碼裡寫彙編語言的功能(_asm)。學會了常見的彙編指令,接觸了幾個帶有循環、跳轉的彙編語言代碼。
- 指針機制:對C/C++中的指針機制有了更深的了解。
- 函數調用機制:函數調用過程中棧的變化。函數調用約定的大緻了解。
- 數組模型
- 結構體模型
- 對齊:搞清楚了為什麼對齊這麼重要。
- switch分析:switch的彙編實作原理。
- 加載期重定位:二進制編譯和加載過程中的重定位機制究竟是怎麼回事。
學習感想
- “猜測 - 實證 - 建構”的學習方法。
在學習底層相關的知識時,總會遇到很多問題。那麼遇到問題,一定要追根究底,不能馬馬虎虎就過去了。不能“大概明白了”,而是要真的搞明白。這個具體的過程就是,在學習的過程中,提出問題,猜想這個問題的答案可能是什麼,根據自己的猜想去寫代碼或者調試求證。證明了之後,提煉出這個問題包含的知識點,再建構例子與驗證它。這個完整的過程搞明白了,對于一個知識點才算是真正地了解了。
反觀我自己之前的學習方法,看資料的時候,往往一帶而過,缺乏追根問底的精神,是以感覺一個東西好像搞明白了,其實根本就沒有。在平日學習中遇到的問題,很多時候也是上網搜一下知道怎麼回事就算了,卻沒有自己動手做一做,下一次一遇到的時候,還是不會。
- 體系化的學習可遇而不可求,要學會零散式地學習。
學習底層知識時,大部分知識點都是零散的,是以我們不應該好高骛遠,追求一蹴而就。在學習的過程中,我們會遇到很多實際問題,然後發現自己這也不會那也不會,感覺好像無從下手。我認為,應該就事論事,遇到問題,就去搜尋對應的解決方案,發現了一個問題解決一個問題,每次解決一個小的知識點,整個知識網絡就在解決這些小知識點的過程中慢慢建構起來了。打個比喻,我們的學習一開始全是漏洞,補都不補過來,但是不要失去信心,每次解決一個小問題,慢慢地零散的知識就結成網了,慢慢變得滴水不漏。
學習筆記
- 猜測 - 實證 - 建構
- 使用VS2008的反彙編、監視視窗、記憶體視窗、單步、斷點、全局變量指派的反彙編。
- C語言中的指針大小為4,隻存放了位址,那麼類型資訊有什麼用呢?類型資訊決定了在該位址處理資料的大小,即指派/讀取時寫/讀多少位元組。例如int *p,那麼對應彙編指令會使用dword,就是4位元組。
- 指針強制轉換的影響不是發生在轉換時(因為位址都是4位元組),而是在轉換後指派的時候,通路記憶體的位元組大小。要保證指針強制轉換是安全的,必須保證轉換後的指針指向的資料類型大小小于原資料類型大小。
- 對于一個補碼形式的負數求其正數值,就是求反加1。
-
x86系列CPU的call指令尋址方式為:用與call指令相關的偏移量定位到跳轉的位址。
偏移量計算:偏移量 = 跳轉到的位址 - call指令後一條指令的起始位址。
-
call指令将傳回位址儲存在記憶體中,而且ESP寄存器指向了該記憶體。實際上這塊記憶體就是棧——ESP指向棧的棧頂。每次壓棧,棧頂位址變小,即ESP的值變小。
實際上call指令相當于兩條指令的組合:
push 傳回位址
jmp 函數入口位址
- C語言的參數傳遞是從右往左壓棧傳遞參數。
-
ESP的存在是為了指明棧頂的位置,那麼EBP存在是為了什麼呢?為了每一次調用函數時,能夠順利找到壓到棧中的參數位置。怎麼找?利用一個确定的基址加上偏移。這個基址就是EBP。為了確定EBP的正确,每個函數調用時都要有儲存和恢複EBP的過程。
push ebp
…
pop ebp
在被調用的函數裡執行時,EBP用來作為基址擷取參數的值。
在函數内配置設定局部變量的時候,要使用棧更低位址的空間,也就是繼續壓棧。
是以,在使用EBP尋址的函數中,EBP+偏移量就是參數的位址(要回溯尋找),EBP-偏移量就是局部變量的位址。
-
ret指令将棧頂儲存的位址值彈入寄存器EIP,即pop eip。
編譯器必須保證執行ret時,ESP正好指向call指令壓棧儲存傳回位址的那段記憶體。
- 編譯器習慣上使用eax作為存儲傳回值的寄存器,被調用方在ret前設定eax,傳回後,調用方從eax擷取到該值。
- lea eax, [ebp + 10h] 即eax = ebp + 10h
- 平衡棧/清棧的兩種方式:①調用方清棧,call傳回後,執行add esp, x指令。②被調用方清棧:執行ret x指令。前者用空間代價換取了變參功能。
-
調用慣例calling convention
(1)是寄存器還是棧傳遞參數
(2)棧傳遞時,參數是從右往左還是從左往右壓棧
(3)誰來清棧,是調用方還是被調用方
例如,C語言的調用方式是棧傳參,參數從右往左壓棧,調用方清棧。
_stdcall是微軟系統調用采用的慣例,除清棧是被調用方外,其他同C語言方式。
_fastcall是寄存器傳遞。
-
函數指針
函數指針包括入口位址和函數原型(函數參數表、傳回類型、調用慣例)兩方面的資訊。
函數指針指派的原則是:隻能将與指針原型比對的函數的入口位址指派給它。
大多數函數指針強制類型轉換都會出錯,是以不要進行函數指針的強制類型轉換。
函數指針可用于虛函數調用。
-
基本資料(如單位元組、雙位元組、四位元組整數)存放處的位址必須能被自己資料類型的大小整除。
對齊的規律:首先標明一個盒子,然後依序将字段往盒子中放,當盒子放不下後,又用下一個盒子存放,直至所有字段都存放完畢。
盒子長度 = min{max{sizeof(成員變量)}, 對齊長度}
字段放入盒子的可放置位置如下:
離盒子頭部偏移位元組數 = n * sizeof(成員變量)(n=0, 1, 2, …)
在程式設計的時候,遇到結構體,要注意是否有對齊問題。
-
switch不能處理浮點數的原因是,它會将該數映射為數組的索引。
實際上是一種跳轉位址表的方法,計算複雜度不因分支的增加而增加,在大部分情況下比if-else要快。
-
在CPU保護模式下,每個執行程序(程式的一個執行個體)都擁有自己獨立的線性位址空間,這種機制叫虛存系統。使用者态程式無法直接通路實體記憶體。
每個程序都有自己獨立的0~4GB的線性位址空間。
編譯的時候就知道全局變量位址。這是因為編譯器就能确定所有全局變量相對頭部的偏移量,隻要程式加載到編譯器希望加載的位址,則所有全局變量位址在編譯器都可以計算出。
全局變量位址 = 程式頭部加載位址b + 全局變量相對程式頭部的偏移量a
程式中要存儲這個希望加載的位址,稱為image base(基址)。
如果不得不加載到一個不是希望加載的位址,那麼就要進行重定位(顯然這種情況經常出現)。如何確定修改成功呢?隻要所有變量與基址的相對位置的偏移量是确定的,那麼就沒問題了。這樣不需要去了解指令類型。
relocAddr = actualBase + a
*relocAddr = *relocAddr(重定位前) + actualBase - expectedAddr
整個程式中有好多偏移量,是以程式中有一個表來存儲這些偏移量,稱為重定位表。
Windows中的真實重定位是這樣的:為了節省重定位項的空間,不是用4位元組表示到程式頭部的偏移量,而是将需要重定位的部分劃分成一個個區(section)
總偏移 = 區起始的偏移 + 2位元組表示的區中的偏移
-
動态連結庫中的重定位
3個重要的API:LoadLibrary,将DLL從硬碟加載到記憶體;GetProcAddress,接收函數名作為輸入,傳回該函數入口位址。FreeLibrary,當獲得函數位址後,調用此API解除安裝已加載庫。
在Win7之前,為了達到讓所有程式共享同一DLL代碼的目的,系統會将所有DLL加載到同一位址,就可以共享代碼段進而節省空間。但是系統DLL固定加載基址這個特性,會被Windows下的溢出攻擊利用。該個性如果不存在就幾乎可以消除Windows下的溢出攻擊利用。Windows 7中引入了dll随機加載的選項。(其實就是ASLR)
-
利用RTL學習彙編:
(1)首先用彙編實作開發環境所帶的運作時庫(Run Time Library, RTL)中的函數,如C語言的RTL包括strlen、strcpy等。
(2)然後分析系統庫實作的這些函數,因為它們調用頻繁,是以要求有很高的效率,基本都用彙編撰寫。
(3)再做性能實驗,測試自己版本與系統版本的差異,并分析修改(可利用指令級分析工具Vtune)。
(4)最後分析不同庫實作的異同和好壞,如VC、C++ Builder、Delphi、GCC。
- 還有一種融彙底層知識的方法:通過編寫攻防軟體,将作業系統、彙編、編譯原理、網絡等知識在極為幽微處(如位元組層次)貫通起來。