工作中經常會遇到需要使用匯編知識來解決的問題,比如查找崩潰堆棧定位在一些未提供源碼的第三方庫的崩潰原因等,但是由于匯編不是常用語言,因此很多知識并不會常記于心,常常需要反復查閱搜索才能得到答案,為了節(jié)省工作量,這里嘗試將搜集到的資料進行整理歸納,方便后續(xù)使用。
為了兼容各種機器,這里以Intel Architecture 32-bit(簡稱IA-32,屬于X86體系的32位版本,從最早的80386芯片到后續(xù)的Pentium 4使用的都是這種架構)為例,而之后的其他架構都是在IA-32的基礎上進行擴展,其基本概念跟具體原理大同小異,暫時先做簡單介紹,后續(xù)有需要再進行擴充。
IA-64架構是Intel推出的64位處理器架構,基于這種架構的芯片具備64位運算能力,64位尋址空間以及64位數據通路,在數據處理能力,系統(tǒng)穩(wěn)定性,安全性,可用性等方面都具有突破性的提升。
x86-64架構,有時候會簡稱為x64,是IA-32架構的擴展版本,在這種架構下,芯片既可以支持原有的32位運算,同時也具備了處理64位運算的能力。

8086 CPU的工作流程見上圖,詳細的解釋可以參考寄存器基礎知識四之段寄存器。
1. 數據
計算機運行的程序由數據與指令共同表達,其中數據在匯編上主要可以分成三類:
- 寄存器,指的是數據存放在寄存器中
- 內存引用,指的是存儲在內存中的數據,通過地址引用的方式對數據進行讀寫
- 立即數,指的是參與計算的常量
1.1 寄存器
寄存器是直接內嵌在CPU上的存儲組件,其特點是存取速度快,容量小,小到幾乎每個寄存器都有自己單獨的名字,甚至有多個名字。
IA-32架構總共提供了16個基本的寄存器,這些寄存器按照功能可以分成如下4類:
- 通用寄存器
- 段寄存器
- 狀態(tài)和控制寄存器
- 指令寄存器
1.1.1 通用寄存器
IA-32的通用寄存器總共為8個,每個都是32bits,他們的名字與對應的含義給出如下:
| Name | Meaning | 0~16 bits | 0~8 bits | 8 ~ 16 bits |
|---|---|---|---|---|
| EAX | 累加和結果寄存器 | AX | AL | AH |
| EBX | 數據指針寄存器 | BX | BL | BH |
| ECX | 循環(huán)計數器 | CX | CL | CH |
| EDX | i/o指針 | DX | DL | DH |
| ESI | 源地址寄存器 | SI | - | - |
| EDI | 目的地址寄存器 | DI | - | - |
| ESP | 堆棧指針 | SP | - | - |
| EBP | 棧指針寄存器 | BP | - | - |
上面表格中的各個寄存器的含義指的只是一般的用法,并沒有限定其使用范圍,在有需要的情況下也可以用作他用。
EAX、EBX、ECX和EDX不僅可傳送數據、暫存數據保存算數邏輯運算結果,而且也可作為指針寄存器(基址和變址寄存器),存儲運算數據在內存中的存放位置。
寄存器AX和AL通常稱為累加器(Accumulator),可用于乘、除、輸入、輸出等操作,使用頻率很高。
寄存器BX稱為基地址寄存器(Base Register),它可作用存儲器指針來使用。
寄存器CX稱為計數寄存器(Count Register),在循環(huán)和字符串操作時,用于控制循環(huán)次數;在進行多位移位操作時時,CL會用于指示移位的位數。
寄存器DX稱為數據寄存器(Data Register),在進行乘、除運算時,會被用作默認的操作數參與運算,此外也可用于存放I/O的端口地址。
寄存器ESI、EDI、SI和DI被稱為變址寄存器(Index Register),它們主要用于存放存儲單元在段內的偏移量(即地址偏移量),可以用于實現多種存儲器操作數的尋址方式,需要注意的是,變址寄存器不支持8位細分。另外,作為通用寄存器,這兩個寄存器也可用于存儲運算的操作數和運算結果。
寄存器EBP、ESP、BP、SP稱為指針寄存器(Pointer Register),主要用于存放堆棧中內存單元的偏移量(偏移地址),可用于實現多種存儲器操作數的尋址方式,同樣不支持8位細分,同樣可用于存儲運算的操作數和運算結果。這兩個寄存器主要用于訪問堆棧內的存儲單元,并且有如下約束:
- BP為基指針(Base Pointer)寄存器,用它可直接存取堆棧中的數據;
- SP為堆棧指針(Stack Pointer)寄存器,用它只可訪問棧頂。
IA-64架構下,通用寄存器從8個擴展到了16個,前面提到的8個32bits寄存器依然存在(不過料想應該被擴展到了64bits),在IA-64架構下被統(tǒng)稱為r8d - r15d,之后,在此基礎上新增了額外的8個寄存器:
RAX, RBX, CX, RDX, RDI, RSI, RBP, RSP,這批新增寄存器被統(tǒng)稱為r8-r15。
1.1.2 段寄存器
在IA-32架構體系中,內存地址在概念上有如下三種類型:
1. 邏輯地址,指的是在機器語言指令中用于表明某個操作數或某條指令的地址。MS- DOS或Windows會把程序分成若干段,每一個邏輯地址都由一個段(segment)和偏移量(offset或displacement)組成。
2. 線性地址,也稱虛擬地址virtual address,是一個32位無符號整數,可以用來表示高達4GB的地址,通常用十六進制數字表 示,范圍:0x0000 0000到0xffff ffff。
3. 物理地址,用于內存芯片硬件上的內存單元尋址。它們與從微處理器的地址引腳發(fā)送到內存總線上的電信號相對應。物理地址由32位或36位無符號整數表示。
所謂的段寄存器指的就是邏輯地址中用于表明每個特殊段的起始地址的寄存器。段寄存器總共有6個,分別為CS, DS, SS, ES, FS, GX,每個段寄存器對應一個段(segment),表達的是某個段在內存中的地址(指針)。
CS,這個寄存器是code segment register的縮寫,指的是代碼段寄存器,存儲的是下一條指令將要執(zhí)行的指令存儲的代碼段的基地址,與IP共同作用,指定需要執(zhí)行的指令的具體地址
DS, ES, FS, GS,這四個寄存器對應的都是數據段寄存器:
- DS: Data Segment Register,數據段寄存器,其值為數據段的段值;
- ES: Extra Segment Register,附加段寄存器,其值為附加數據段的段值;
- FS/GS: 80386起新增的輔助寄存器,其用法與含義與ES完全相同,新增的目的是為了降低ES的負擔;
SS,指的是stack segment register,堆棧段寄存器
在IA-64位架構下,段寄存器的數目并無變化,只是使用上會略有不同。
1.1.3 狀態(tài)和控制寄存器
狀態(tài)和控制寄存器只有一個,名字為eflags,這個寄存器表示的意義比較復雜,幾乎每一個bit都對應一個標記,程序并不直接操控此寄存器,每個標記的賦值是通過運算的結果自動進行修正的。
| Bit No. | Name | Meaning |
|---|---|---|
| 0 | CF | 進位標識,算術操作進行了進位和借位,則此位被設置 |
| 2 | PF | 奇偶標識,結果包含奇數個1,則設置此位 |
| 4 | AF | 輔助進位標識,結果的第3位像第4位借位,則此位被設置 |
| 6 | ZF | 零標識,結果為零,此位設置 |
| 7 | SF | 符號標識,若為負數則設置此位 |
| 8 | TF | 陷阱標識,設置進程可以被單步調試 |
| 9 | IF | 中斷標識,設置能夠響應中斷請求 |
| 10 | DF | 方向標識,用于標示字符處理過程中指針移動方向。 |
| 11 | OF | 溢出標識,結果像最高位符號位進行借位或者進位,此標志被設置 |
上面列舉了其中部分bit的含義,其中8~10位為控制標記,剩下的0,2,4,6,7,11等為狀態(tài)位,表示的是在某些操作完成后用于表示結果類型的標記。
1.1.3.1 運算結果標志位
1.進位標志CF(Carry Flag)
進位標志CF主要用來反映運算是否產生進位或者借位。如果運算結果的最高位產生了一個進位或者借位,那么其值為1,否則為0。使用該標志位的情況有:多字(字節(jié))數的加減運算,無符號的大小比較運算,移位操作,字(字節(jié))之間移位,專門改變CF值的指令等。
2.奇偶標志位PF(Parity Flag)
奇偶標志PF用于反映運算結果中“1”的個數的奇偶性,如果“1”的個數為偶數,則PF的值為1,否則其值為0。利用PF可以進行奇偶校驗檢查,或者產生奇偶校驗位。在數據傳送過程中,為了提供傳送的可靠性,如果采用奇偶校驗的方法,就可使用該標志。
3.輔助進位標志AF(Auxiliary Carry Flag)
在發(fā)生下列情況時,輔助進位標志的值被置為1,否則其值為0:
- 在字操作時,發(fā)生低字節(jié)向高字節(jié)進位或借位時;
- 在字節(jié)操作時,發(fā)生低4位向高4位進位或借位時;
對以上6個運算結果的標志位,在一般情況編程情況下,標志位CF、ZF、SF、F的使用頻率較高,而標志位PF和AF的使用頻率較低。
4.零標志(Zero Flag)
零標志ZF用來反映運算結果是否為0.如果運算結果為0,則其值為1,否則其值為0.在判斷運算結果是否為0時可以使用此標志位。
5.符號標志SF(Sign Flag)
符號標志SF用來反映運算結果的符號位,它與運算結果的最高位相同。在危機系統(tǒng)中,有符號數采用補碼表示法,所以,SF也就反映運算結果的正負號。運算結果為正數時,SF的值為0,否則其值為1。
6.溢出標志OF(Overflow Flag)
溢出標注OF用于反映有符號數加減運算所得結果是否溢出。如果運算結果超過當前運算位數所能表示的范圍,則稱為溢出,OF的值被置為1,否則,OF的值被置為0。“溢出”和“進位”是兩個不同含義的概念。
1.1.3.2 狀態(tài)控制標志位
狀態(tài)控制標志位是用來控制CPU操作的,它們要通過專門的指令才能使之發(fā)生改變。
1.追蹤標志TF(Trap Flag)
當追蹤標志TF被置為1時,CPU進入單步執(zhí)行方式,即每執(zhí)行一條指令,產生一個單步中斷請求,這種方式主要用于程序的調試。指令系統(tǒng)中沒有專門的指令來改變標志位TF的值,但程序員可用其它辦法來改變其值。
2.中斷允許標志IF(Interrupt-enable Flag)
中斷允許標志IF是用來決定CPU是否響應CPU外部的可屏蔽中斷發(fā)出的中斷請求。但不管該標志位何值,CPU都必須響應CPU外部的不可屏蔽中斷所發(fā)出的中斷請求,以及CPU內部產生的中斷請求。具體規(guī)定如下:
- 當IF=1時,CPU可以響應CPU外部的可屏蔽中斷發(fā)出的中斷請求;
- 當IF=0時,CPU不響應CPU外部的可屏蔽中斷發(fā)出的中斷請求。
CPU的指令系統(tǒng)中也有專門的指令來改變標志位IF的值。
3.方向標志DF(Direction Flag)
方向標志DF用來決定在串操作指令執(zhí)行時有關指針寄存器發(fā)生調整的方向。在微機的指令系統(tǒng)匯總,還提供了專門的指令來改變標志位DF的值。
IA-64位架構將控制寄存器擴展到了64位,但是高32bits依然處于空閑狀態(tài)并未使用,因此表示的含義與用法都與IA-32架構相同。
1.1.4 指令寄存器
指令寄存器依然只有一個,名字為EIP,表示的是當前進程中將要執(zhí)行的指令所在的位置。因為每個段的最大范圍為64k(?),因此EIP的高16位均為0,其低16位名稱為IP(Instruction Pointer),存放的是下次將要執(zhí)行的指令在代碼段中的偏移地址,即CS*0x10 + IP共同作用,給出下一條指令的位置。
IA-64架構將此寄存器擴展為RIP 64位寄存器。
1.2 尋址
前面說過,匯編指令中用到的數據有三種類型,不同的類型,其訪問的方式(尋址方式)有所不同,這里用一張表給出匯編中常用的幾種尋址方式:

這里對上表中一些描述不夠詳細的點進行一下簡單展開:
間接尋址指的是以寄存器中存儲的數值作為內存中的地址,從這個地址表達的存儲單元中取出數據的尋址方式。
基址+偏移量尋址指的是以給出的立即數作為基址,以寄存器中存儲的數值作為偏移量,兩者相加作為內存地址的尋址方式。
變址尋址指的是用兩個寄存器存儲的數值分別作為基址與偏移得到的內存地址的尋址方式,這種尋址方式還有增加立即數作為額外基址的變體以及添加額外參數作為縮放比例的比例變址尋址方式。
語言描述過于抽象,這里給出一個例子:

1.3 數據類型
因為不存在數據結構與類型的說法,因此匯編中的數據類型,指的實際上是立即數的類型,而立即數的類型在IA-32架構下有如下的幾種:

2. 指令
在介紹匯編指令之前,先來介紹程序運行的一些基本知識。
一段程序從高級語言編寫的角度來看,是由一個個的函數來表達的,在每個函數中又會出現函數間的跳轉以及流程控制等邏輯,除此之外,就是線性執(zhí)行的指令代碼。
正因為函數之間的跳轉與調用,因此在任意時刻執(zhí)行的代碼從上到下都包含了多層函數調用的層級關系,為了維護這些層級關系,需要一套結構來對函數調用之間的數據進行存儲,方便調用前的參數傳遞與調用完成后的現場恢復等,而實現這個功能的結構我們稱之為棧幀結構,如下圖所示:

調用時間越早的函數,其對應的幀越接近棧底(即先入棧),對應的地址也就越大(在x86的環(huán)境下,棧是朝著低地址的方向伸長的)。每次函數調用的指令call觸發(fā)的時候,都需要進行現場保護與參數傳遞,之后就進入被調用函數代碼段的執(zhí)行邏輯,此時指令計數器IP(EIP/RIP)存放的就是被調用函數的起始地址,如下圖所示:

介紹完基本的函數堆棧結構之后,我們一起來看下具體的匯編指令,匯編指令從功能上來看,可分成流程控制指令以及順序執(zhí)行指令兩大類,在這個框架下還可以做進一步的細分控制,具體后面會講。
2.1 順序執(zhí)行指令
2.1.1 數據傳送指令
數據傳送指令指的是將一個數據從某個位置搬移到另一個位置,其限制條件為,源地址與目標地址不能同時為內存空間,即不支持從內存到內存的數據搬運,想要實現內存到內存的數據搬運,就需要分兩次完成。
具有較多的變種,具體如下面幾張圖所示:




圖中給出的指令變種數看起來多,但從功能上來看,可以分成MOVZ與MOVS兩種,兩者的區(qū)別在于高位用符號位擴展(MOVS)還是零擴展(MOVZ)進行擴充:
- 用符號位擴展,目的位置的所有高位用源值的最高位數值進行填充。
- 用零擴展,所有高位都用零填充。
上面給出的傳送指令是按照從左到右的方式進行數據傳送的,但是在實踐中(VS提供的匯編代碼)中我們發(fā)現,并不是所有的匯編指令都是按照這種順序的,因此需要根據具體情況進行分析。
此外,有些匯編語言的風格可能跟上面介紹的風格不太一樣,不會使用數目眾多的變體指令來表達不同類型的操作數,而是會直接將操作數類型限定在參數上:
mov dword ptr [rsp+44h], eax
比如上述指令指的是將eax寄存器中的double world數據賦值給起始地址為段地址為RSP(棧頂),偏移量為44h的堆棧存儲單元。
數據傳送指令除了MOV之外,還有一個LEA,這個指令與MOV的區(qū)別在于:
lea是“l(fā)oad effective address”的縮寫,簡單的說,lea指令可以用來將一個內存地址直接賦給目的操作數,例如:
lea eax,[ebx+8]就是將ebx+8這個值直接賦給eax,而不是把ebx+8處的內存地址里的數據賦給eax。
mov指令則恰恰相反,例如:mov eax,[ebx+8]則是把內存地址為ebx+8處的數據賦給eax。
作者:匿名用戶
鏈接:https://www.zhihu.com/question/40720890/answer/110774673
來源:知乎
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
2.1.2 算數邏輯操作
算數邏輯操作包含基本的加減乘除與與或異或移位等位操作,具體見下圖:


2.1.3 push/pop指令
push與pop指令,是用于實現棧操作的兩個指令,棧頂地址由SS * 0x10 + SP共同指定。
PUSH指令完成的工作包括:
- 減少 ESP 的值(操作數是 16 位的,則 ESP 減 2,操作數是 32 位的,則 ESP 減 4)
- 將源操作數復制到堆棧
POP指令完成的工作包括:
- 把 ESP 指向的堆棧元素內容復制到一個 16 位或 32 位目的操作數中
- 增加 ESP 的值,如果操作數是 16 位的,ESP 加 2,如果操作數是 32 位的,ESP 加 4
2.2 流程控制指令
常見的流程控制包括條件判斷語句,循環(huán)語句,跳轉語句,函數調用與返回語句。
2.2.1 條件跳轉語句
條件跳轉流程控制是通過對此前介紹過的狀態(tài)與控制寄存器各個標記位的判斷來實現的,而對這些標記位的設置則涉及到較多的指令。
2.2.1.1 標記位設置指令

CMP指令根據兩個操作數之間的差值來對標記位進行設置,其執(zhí)行的結果不會更改參數計算的寄存器的數據,除此之外,跟SUB指令的結果完全一樣。
TEST指令的行為則是除了不會更改到參與計算的寄存器的數據之外,其他行為與AND指令一樣。

上圖中的條件碼指的就是我們前文中說的標記位,這里給出的是除了條件判斷跳轉指令之外的其他獲取標記位結果的方法:將標記位數據賦值給某個寄存器的低8bits。
2.2.1.2 條件判斷跳轉指令

條件跳轉指令具有較多的變體,每個變體分別對應于不同的判斷條件或者不同的跳轉目標。這里的跳轉目標有直接跳轉與間接跳轉兩種,這兩種的區(qū)別見下圖所示:

2.2.2 函數跳轉指令
函數跳轉指令call,其后可以接地址或者標記作為參數,先來看一個例子:
global main
eax_plus_1s:
add eax, 1
ret
ebx_plus_1s:
add ebx, 1
ret
main:
mov eax, 0
mov ebx, 0
call eax_plus_1s
call eax_plus_1s
call ebx_plus_1s
add eax, ebx
ret
這段代碼很好理解,這里就不做解釋了。需要注意的是,每次函數調用前,都需要保存現場,簡單來說就是需要將函數調用之后的指令存入到前面介紹過的棧幀結構中,棧幀結構的棧頂是通過rsp/esp來存儲的,且是朝著地址減小的方向增長的,因此這個過程差不多可以用如下的指令來模擬(實際執(zhí)行中,并不會出現這兩條指令,是硬件自動完成的):
sub esp, 4
mov dword ptr[esp], eip
等到函數返回,執(zhí)行了ret指令之后,就會從esp中讀取之前保存的現場,再執(zhí)行一次退棧操作。
mov eip,dword ptr[esp]
add esp, 4
3. 案例
3.1 函數調用
VkRenderPass RenderPassHandle;
VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkCreateRenderPass2KHR(InDevice.GetInstanceHandle(), &CreateInfo, VULKAN_CPU_ALLOCATOR, &RenderPassHandle));
00007FFD697C8113 xor eax,eax
00007FFD697C8115 test rax,rax
00007FFD697C8118 je CreateRenderPass2KHR+848h (07FFD697C8128h)
00007FFD697C811A mov qword ptr [rsp+0A8h],0
00007FFD697C8126 jmp CreateRenderPass2KHR+857h (07FFD697C8137h)
00007FFD697C8128 lea rax,[VulkanRHI::GAllocationCallbacks (07FFD6999CE10h)] //參數1入棧
00007FFD697C812F mov qword ptr [rsp+0A8h],rax //參數1入棧,rsp為棧頂
00007FFD697C8137 mov rax,qword ptr [InDevice]
00007FFD697C813F mov rax,qword ptr [rax+8] //參數2入棧
00007FFD697C8143 mov qword ptr [rsp+118h],rax //參數2入棧,rsp為棧頂
00007FFD697C814B lea r9,[RenderPassHandle] //參數3調用前準備
00007FFD697C8153 mov r8,qword ptr [rsp+0A8h] //參數1調用前準備
00007FFD697C815B lea rdx,[CreateInfo] //參數4調用前準備
00007FFD697C8163 mov rcx,qword ptr [rsp+118h] //參數2調用前準備
00007FFD697C816B call qword ptr [VulkanDynamicAPI::vkCreateRenderPass2KHR (07FFD6999DE08h)] //函數調用
00007FFD697C8171 mov dword ptr [rsp+44h],eax //結果寫回棧,rsp為棧頂
00007FFD697C8175 cmp dword ptr [rsp+44h],0 //條件判斷,確認是否調用成功
00007FFD697C817A jge CreateRenderPass2KHR+8B9h (07FFD697C8199h) //未成功進入后續(xù)錯誤處理邏輯
00007FFD697C817C mov r9d,689h
00007FFD697C8182 lea r8,[__real@477fff00+2130h (07FFD69937970h)]
00007FFD697C8189 lea rdx,[__real@477fff00+2180h (07FFD699379C0h)]
00007FFD697C8190 mov ecx,dword ptr [rsp+44h]
00007FFD697C8194 call VulkanRHI::VerifyVulkanResult (07FFD698F13B0h)
return RenderPassHandle;
上面給出了一個實際項目中遇到的崩潰堆棧,崩潰出現在call后一條語句上,從上下文分析,應該是將函數調用的結果寫回到棧中,不知道為何觸發(fā)了寫權限報錯:
Exception thrown at 0x0000000000000000 in UE4Editor.exe: 0xC0000005: Access violation executing location 0x0000000000000000.
經過在匯編中單步執(zhí)行最終定位到,問題出在函數調用內部,在執(zhí)行一條跳轉語句時,跳轉目的地是通過寄存器RAX指定的,而RAX中的地址對應的空間為undefined區(qū)域,因此觸發(fā)異常。
參考
[1]. 匯編筆記:寄存器介紹
[2]. 關于OS系統(tǒng)的x86、x64與IA32、IA64的關系
[3]. 寄存器基礎知識四之段寄存器
[4]. 段選擇符 段寄存器
[5]. X86_64匯編與IA32比較
[6]. 匯編語言入門七:函數調用(一)
[7]. 匯編語言PUSH和POP指令(壓棧和出棧)