寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。
? 華麗的分割線 ?
簡述
初入64位的核心世界,64位的彙編肯定是基礎。在64位的Win
作業系統,呼叫約定並不是原來的多種多樣,而是隻有一種呼叫約定FastCall
。並且在64位下,作業系統以及應用程式十分注重對齊(地址數值可以被16整除)和棧幀這個事情,並且SEH
的實現也不再基於堆疊,這一切將在本篇我會詳細介紹。
本部分討論的x64
是AMD64
與Intel64
的合稱,是指與現有x86
相容的64位CPU
。在64位系統中,記憶體地址為64位。64位環境下暫存器有比較大的變化,如下圖所示:
在介紹本節東西之前,我們先學習在64位下的僅有FastCall
呼叫約定,實行外平棧:
引數 | 型別 | 浮點型別 |
---|---|---|
第1個引數 | RCX | XMM0 |
第2個引數 | RDX | XMMI |
第3個引數 | R8 | XMM2 |
第4個引數 | R9 | XMM3 |
瞭解這些東西之後,我們接下來對64位的彙編進行鋪墊。
彙編鋪墊
當我們初步踏入64位彙編的世界時,我們先看看我們入門 羽夏看C語言 系列教程的時候會提供一個最簡單的示例來從彙編角度來看C/C++
,現在我們重新用64位來看看它們現在的樣子,如下是示例程式碼:
#include <iostream>
using namespace std;
int main()
{
int a = 1;
cout << a << endl;
return 0;
}
它的反彙編如下所示:
#include <iostream>
using namespace std;
int main()
{
00007FF628591860 push rbp
00007FF628591862 push rdi
00007FF628591863 sub rsp,108h
00007FF62859186A lea rbp,[rsp+20h]
int a = 1;
00007FF62859186F mov dword ptr [a],1
cout << a << endl;
00007FF628591876 mov edx,dword ptr [a]
00007FF628591879 mov rcx,qword ptr [__imp_std::cout (07FF6285A0170h)]
00007FF628591880 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF6285A0158h)]
00007FF628591886 lea rdx,[std::endl<char,std::char_traits<char> > (07FF628591037h)]
00007FF62859188D mov rcx,rax
00007FF628591890 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF6285A0150h)]
return 0;
00007FF628591896 xor eax,eax
}
00007FF628591898 lea rsp,[rbp+0E8h]
00007FF62859189F pop rdi
00007FF6285918A0 pop rbp
00007FF6285918A1 ret
可以看出,彙編似乎沒有太大的變化,依舊採用rbp
定址,但是這個定址看起來比較奇怪,下面我來逐步介紹這些奇怪之處。
lea rbp,[rsp+20h]
這句彙編程式碼看起來比較奇怪,其實這裡是預留給引數傳遞的空間,正好是4個引數的空間,在引數不多於4個的時候會採用,一共32個位元組。稍後我們會對此進行展開。
還有一個比較奇怪的點,如下所示:
00007FF628591863 sub rsp,108h
00007FF62859186A lea rbp,[rsp+20h]
……
00007FF628591898 lea rsp,[rbp+0E8h]
看到在恢復堆疊的時候這兩個數值不太一樣了嗎?這就是中間呼叫一些函式進行內平棧的結果,我們函式就寫一個return 0;
看看它的反彙編結果:
int main()
{
00007FF704AB1830 push rbp
00007FF704AB1832 push rdi
00007FF704AB1833 sub rsp,0C8h
00007FF704AB183A mov rbp,rsp
return 0;
00007FF704AB183D xor eax,eax
}
00007FF704AB183F lea rsp,[rbp+0C8h]
00007FF704AB1846 pop rdi
00007FF704AB1847 pop rbp
00007FF704AB1848 ret
這時候提升的堆疊和恢復的堆疊就是一模一樣了。
下面我們繼續來詳細介紹有關引數呼叫的細節,當我們傳參不多於4個的時候,它是怎樣傳參的,如下是測試程式碼:
#include <iostream>
using namespace std;
int add(int a, int b, int c, int d)
{
return a + b + c + d;
}
int main()
{
int a = 3, b = 4, c = 5, d = 6;
int e = add(a, b, c, d);
return 0;
}
先看add
函式的反彙編:
int add(int a, int b, int c, int d)
{
00007FF633681830 mov dword ptr [rsp+20h],r9d
00007FF633681835 mov dword ptr [rsp+18h],r8d
00007FF63368183A mov dword ptr [rsp+10h],edx
00007FF63368183E mov dword ptr [rsp+8],ecx
00007FF633681842 push rbp
00007FF633681843 push rdi
00007FF633681844 sub rsp,0C8h
00007FF63368184B mov rbp,rsp
return a + b + c + d;
00007FF63368184E mov eax,dword ptr [b]
00007FF633681854 mov ecx,dword ptr [a]
00007FF63368185A add ecx,eax
00007FF63368185C mov eax,ecx
00007FF63368185E add eax,dword ptr [c]
00007FF633681864 add eax,dword ptr [d]
}
00007FF63368186A lea rsp,[rbp+0C8h]
00007FF633681871 pop rdi
00007FF633681872 pop rbp
00007FF633681873 ret
對於開頭的彙編程式碼,可能有點難理解:
mov dword ptr [rsp+20h],r9d
mov dword ptr [rsp+18h],r8d
mov dword ptr [rsp+10h],edx
mov dword ptr [rsp+8],ecx
如上引數就是儲存在所謂的預留空間,示意圖如下:
這預留的棧空間是在主函式內完成的,這個暫且先不關注。後面的程式碼緊接著是經典的rbp
定址,但是眼尖的同志可能會發現,後面的運算都是用32位暫存器,沒有用64位的。
這裡我囉嗦一下,64位暫存器是對32位的擴充套件,但是有些彙編指令32位有但是64位沒有的,我們接下來探究這個事情。
對32位暫存器的寫操作,包括運算結果,對相應的64位暫存器的高32位清0。這個是64位不同於32位的操作,我們用一個動圖來展示一下該效果:
由於32位指令編碼比對應的64位指令編碼指令要短,為了優化就會使用較短的32位指令編碼。比如xor rax,rax
這條指令,它的硬編碼為48 33 C0
,而xor eax,eax
可以實現相同的功能,它的硬編碼為33 C0
,那麼編譯器會優先使用xor eax,eax
。
有些32位的彙編指令對應64位是沒有的,比如push
,在64位是沒有的:
記憶體優先使用相對偏移定址,直接定址指令較少。這個我們來看一個例子,如下圖所示:
可以看到硬編碼的結果了嗎?接的內容是0,但是指的是下一行地址,和32位下的jmp
的硬編碼方式是一樣的。但是如果間接定址的範圍無法表示了,就寫死地址,類似下面的結果:
當然,我們可以將間接定址的改為直接定址的,如下圖所示:
這裡再擴充套件比較有意思的nop
指令,如下圖所示,需要硬編碼進行輸入:
有關64位的彙編就介紹這麼多,我們會過來再看看add
函式的傳參情況。後面都是我們學過32位的ebp
定址都能看懂的程式碼了,接下來看主函式的反彙編:
int main()
{
00007FF6336817A0 push rbp
00007FF6336817A2 push rdi
00007FF6336817A3 sub rsp,188h
00007FF6336817AA lea rbp,[rsp+20h]
int a = 3, b = 4, c = 5, d = 6;
00007FF6336817AF mov dword ptr [rbp+4],3
00007FF6336817B6 mov dword ptr [rbp+24h],4
00007FF6336817BD mov dword ptr [rbp+44h],5
00007FF6336817C4 mov dword ptr [rbp+64h],6
int e = add(a, b, c, d);
00007FF6336817CB mov r9d,dword ptr [rbp+64h]
00007FF6336817CF mov r8d,dword ptr [rbp+44h]
00007FF6336817D3 mov edx,dword ptr [rbp+24h]
00007FF6336817D6 mov ecx,dword ptr [rbp+4]
00007FF6336817D9 call 00007FF6336813C5
00007FF6336817DE mov dword ptr [rbp+0000000000000084h],eax
return 0;
00007FF6336817E4 xor eax,eax
}
00007FF6336817E6 lea rsp,[rbp+0000000000000168h]
00007FF6336817ED pop rdi
00007FF6336817EE pop rbp
00007FF6336817EF ret
開頭我講了,後面又來了奇怪的區域性變數分配和初始化:
mov dword ptr [rbp+4],3
mov dword ptr [rbp+24h],4
mov dword ptr [rbp+44h],5
mov dword ptr [rbp+64h],6
可以看到,每個區域性變數之間差了0x20
個位元組,也就是32個位元組,這是為什麼呢?目前暫時搞不清楚為什麼,可能有對齊的意味在這裡。
下面我們來看看IDA
是如何分析這部分程式碼的:
; int __fastcall main()
main proc near
a= dword ptr -16Ch
b= dword ptr -14Ch
c= dword ptr -12Ch
d= dword ptr -10Ch
push rbp
push rdi
sub rsp, 188h
lea rbp, [rsp+20h]
mov [rbp+170h+a], 3
mov [rbp+170h+b], 4
mov [rbp+170h+c], 5
mov [rbp+170h+d], 6
mov r9d, [rbp+170h+d] ; d
mov r8d, [rbp+170h+c] ; c
mov edx, [rbp+170h+b] ; b
mov ecx, [rbp+170h+a] ; a
call j_?add@@YAHHHHH@Z ; add(int,int,int,int)
mov [rbp+84h], eax
xor eax, eax
lea rsp, [rbp+168h]
pop rdi
pop rbp
retn
main endp
我們繼續介紹FastCall
呼叫約定:push
和pop
指令僅用來儲存非易變暫存器,其他棧指標操作顯式寫暫存器rsp
。實現進入call
之前rsp
滿足0×10
位元組對齊。
通常不使用rbp
定址棧記憶體,所以rsp
在函式幀中儘量保持穩定,一次性分配區域性變數和引數空間但是。在我們的例項中,用到了rbp
定址,但在使用過程中rsp
保持比較穩定的狀態。
上面的介紹僅僅是冰山一角,讓你對64位的彙編指令和呼叫約定有一個整體的認識,具體細節請自行探索。
SEH
概述
之前我們在32位介紹SEH
的時候,它是用棧實現的,但是如果黑客利用構造特殊的程式碼對棧進行攻擊導致程式碼劫持,這是十分不安全的。所以,在64位下,SEH
不使用棧來實現。對於64位來說,函式有沒有異常處理程式的執行效率是一樣的,因為它並沒有類似32位掛SEH
的操作。我們通過程式碼示例看一下:
#include <iostream>
using namespace std;
int filter()
{
return 1;
}
int main()
{
__try
{
cout << "try1" << endl;
__try
{
cout << "try2" << endl;
__try
{
cout << "try3" << endl;
}
__finally
{
cout << "finally" << endl;
}
}
__except (filter())
{
cout << "except filter" << endl;
}
}
__except (1)
{
cout << "except 1" << endl;
}
return 0;
}
它的反彙編如下:
int main()
{
00007FF72C6222C0 push rbp
00007FF72C6222C2 push rdi
00007FF72C6222C3 sub rsp,0E8h
00007FF72C6222CA lea rbp,[rsp+20h]
__try
{
cout << "try1" << endl;
00007FF72C6222CF lea rdx,[string "try1" (07FF72C62AC24h)]
00007FF72C6222D6 mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C6222DD call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C6222E2 lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C6222E9 mov rcx,rax
00007FF72C6222EC call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
00007FF72C6222F2 nop
__try
{
cout << "try2" << endl;
00007FF72C6222F3 lea rdx,[string "try2" (07FF72C62AC2Ch)]
00007FF72C6222FA mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C622301 call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C622306 lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C62230D mov rcx,rax
00007FF72C622310 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
00007FF72C622316 nop
__try
{
cout << "try3" << endl;
00007FF72C622317 lea rdx,[string "try3" (07FF72C62AC34h)]
00007FF72C62231E mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C622325 call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C62232A lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C622331 mov rcx,rax
00007FF72C622334 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
00007FF72C62233A nop
}
__finally
{
cout << "finally" << endl;
00007FF72C62233B lea rdx,[string "finally" (07FF72C62AC40h)]
00007FF72C622342 mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C622349 call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C62234E lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C622355 mov rcx,rax
00007FF72C622358 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
}
}
00007FF72C62235E jmp main+0C4h (07FF72C622384h)
__except (filter())
{
cout << "except filter" << endl;
00007FF72C622360 lea rdx,[string "except filter" (07FF72C62AC50h)]
00007FF72C622367 mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C62236E call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C622373 lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C62237A mov rcx,rax
00007FF72C62237D call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
00007FF72C622383 nop
}
}
00007FF72C622384 jmp $LN8+24h (07FF72C6223AAh)
__except (1)
{
cout << "except 1" << endl;
00007FF72C622386 lea rdx,[string "except 1" (07FF72C62AC60h)]
00007FF72C62238D mov rcx,qword ptr [__imp_std::cout (07FF72C631198h)]
00007FF72C622394 call std::operator<<<std::char_traits<char> > (07FF72C62108Ch)
00007FF72C622399 lea rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]
00007FF72C6223A0 mov rcx,rax
00007FF72C6223A3 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]
00007FF72C6223A9 nop
}
return 0;
00007FF72C6223AA xor eax,eax
}
00007FF72C6223AC lea rsp,[rbp+0C8h]
00007FF72C6223B3 pop rdi
00007FF72C6223B4 pop rbp
00007FF72C6223B5 ret
可以看出生成的程式碼和我們認為的普通程式碼沒什麼兩樣,每一個對應的異常處理程式前都會用jmp
跳過,感覺十分奇怪。那麼64位是如何實現異常的SEH
處理的呢?
為了方便介紹,我們把編譯後的程式放到IDA
裡面,將會得到如下結果:
; int __fastcall main()
main proc near ; CODE XREF: j_main↑j
; DATA XREF: .pdata:000000014001F89C↓o
; __unwind { // j___C_specific_handler_0
push rbp
push rdi
sub rsp, 0E8h
lea rbp, [rsp+20h]
lea rdx, _Val ; "try1"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
nop
lea rdx, aTry2 ; "try2"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
nop
lea rdx, aTry3 ; "try3"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
nop
$LN18:
lea rdx, aFinally ; "finally"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
jmp short loc_140012384
; ---------------------------------------------------------------------------
$LN12:
lea rdx, aExceptFilter ; "except filter"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
nop
loc_140012384: ; CODE XREF: main+9E↑j
jmp short loc_1400123AA
; ---------------------------------------------------------------------------
$LN8:
lea rdx, aExcept1 ; "except 1"
mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, rax
call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
nop
loc_1400123AA: ; CODE XREF: main:loc_140012384↑j
xor eax, eax
lea rsp, [rbp+0C8h]
pop rdi
pop rbp
retn
; } // starts at 1400122C0
main endp
有關SEH
異常處理的資訊放在了PE
結構的Exception
目錄,如果對該方面一點不清楚的同志請學習 羽夏筆記——PE結構(不包含.Net) ,否則下面的介紹可能對你來說意義不太大。
RUNTIME_FUNCTION
在64位下,每一個非葉函式(葉函式就是既不呼叫函式,又沒有修改棧指標,也沒有使用SEH
的函式)都有一個結構體來描述該函式的SEH
處理資訊,那就是RUNTIME_FUNCTION
,它的結構如下:
typedef struct _RUNTIME_FUNCTION {
ULONG BeginAddress;
ULONG EndAddress;
ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
第一個成員標誌著開始RVA
,第二個成員標誌的是結束RVA
。我們來看看main
函式的RUNTIME_FUNCTION
:
RUNTIME_FUNCTION <rva main, rva byte_1400123B6, rva stru_14001C600>
IDA
幫我們給識別好了,我們來看看它的硬編碼:
C0 22 01 00 B6 23 01 00 00 C6 01 00
為了配合講解,我們把主函式的開始地址和結束地址看一下:
.text:00000001400122C0 ; int __fastcall main()
.text:00000001400122C0 main proc near ; CODE XREF: j_main↑j
.text:00000001400122C0 ; DATA XREF: .pdata:000000014001F89C↓o
.text:00000001400122C0 ; __unwind { // j___C_specific_handler_0
……
.text:00000001400123B5 main endp
.text:00000001400123B5
.text:00000001400123B5 ; ---------------------------------------------------------------------------
.text:00000001400123B6 byte_1400123B6 db 3Dh dup(0CCh) ; DATA XREF: .pdata:000000014001F89C↓o
也就是說,第一個成員的值就是0x122C0
,正好是我們程式的偏移(映象載入的地址為0x140000000
),第二個成員的值是0x123B6
也就是結束的位置偏移。
還有一個成員我們並沒有介紹,那就是UnwindData
,它其實是一個結構體,裝著異常發生時棧的回滾資訊,如下所示:
typedef struct _UNWIND_INFO {
UCHAR Version : 3;
UCHAR Flags : 5;
UCHAR SizeOfProlog;
UCHAR CountOfCodes;
UCHAR FrameRegister : 4;
UCHAR FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
//
// The unwind codes are followed by an optional DWORD aligned field that
// contains the exception handler address or a function table entry if
// chained unwind information is specified. If an exception handler address
// is specified, then it is followed by the language specified exception
// handler data.
//
// union {
// struct {
// ULONG ExceptionHandler;
// ULONG ExceptionData[];
// };
//
// RUNTIME_FUNCTION FunctionEntry;
// };
//
} UNWIND_INFO, *PUNWIND_INFO;
UNWIND_INFO
該結構前兩個成員是個位域,佔用一個UCHAR
大小。第一個成員是版本號,目前都是1,第二個成員是比較重要的成員,它標誌了它的型別,我們來看看:
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4
可以看到有四種型別,下面我們來看看它們的含義。
UNW_FLAG_NHANDLER
表示既沒有EXCEPT_FILTER
也沒有EXCEPT_HANDLER
,這個是最簡單的型別,它的示意圖如下:
UNW_FLAG_EHANDLER
表示該函式有EXCEPT_FILTER
和EXCEPT_HANDLER
,示意圖如下:
UNW_FLAG_UHANDLER
表示該函式有FINALLY_HANDLER
,它的結構如下:
UNW_FLAG_CHAININFO
表示該函式有多個UNWIND_INFO
並串接在一起。
SizeOfProlog
表示該函式的Prolog
指令的大小,單位是位元組。
CountOfCodes
表示當前UNWIND_INFO
包含多少個UNWIND_CODE
結構。
FrameRegister
如果函式建立了棧幀,它表示棧幀的索引,否則為0.
FrameOffset
表示FrameRegister
距離函式最初棧頂(剛進入函式,還沒有執行任何指令時的棧頂)的偏移,單位為位元組。
UnwindCode
是一個UNWIND_CODE
型別的不定長陣列,元素數量由CountOfCodes
決定。
這裡在說明幾點:如果Flags
設定了UNW_FLAG_EHANDLER
或UNW_FLAG_UHANDLER
,那麼在最後一個UNWIND_CODE
之後存放著ExceptionHandler
,它相當於 x86的EXCEPTION_REGISTRATION::handle
以及ExceptionData
它相當於x86的EXCEPTION_REGISTRATION::scopetable
。UnwindCode
陣列詳細記錄了函式修改棧、儲存非易失性暫存器的指令。
UNWIND_CODE
下面我們來看看UNWIND_CODE
結構體:
typedef enum _UNWIND_OP_CODES {
UWOP_PUSH_NONVOL = 0,
UWOP_ALLOC_LARGE, // 1
UWOP_ALLOC_SMALL, // 2
UWOP_SET_FPREG, // 3
UWOP_SAVE_NONVOL, // 4
UWOP_SAVE_NONVOL_FAR, // 5
UWOP_SPARE_CODE1, // 6
UWOP_SPARE_CODE2, // 7
UWOP_SAVE_XMM128, // 8
UWOP_SAVE_XMM128_FAR, // 9
UWOP_PUSH_MACHFRAME // 10
} UNWIND_OP_CODES, *PUNWIND_OP_CODES;
typedef union _UNWIND_CODE {
struct {
UCHAR CodeOffset;
UCHAR UnwindOp : 4;
UCHAR OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
由於我們這裡是知識鋪墊,具體細節就不去追究了,感興趣的可以自行探索。
下一篇
x64 番外篇——保護模式相關