反匯編(Disassembly) 即把目標二進制機器碼轉為匯編代碼的過程,該技術常用於軟件破解、外掛技術、病毒分析、逆向工程、軟件漢化等領域,學習和理解反匯編對軟件調試、系統漏洞挖掘、內核原理及理解高級語言代碼都有相當大的幫助,軟件一切神秘的運行機制全在反匯編代碼里面。
函數是任何一個高級語言中必須要存在的一個東西,使用函數式編程可以讓程序可讀性更高,充分發揮了模塊化設計思想的精髓,今天我將帶大家一起來探索函數的實現機理,探索編譯器到底是如何對函數這個關鍵字進行實現的,從而更好地理解編譯行為。
先來研究函數,函數是任何一門編程語言中都存在的關鍵字,使用函數式編程可以讓程序可讀性更高,充分發揮模塊化設計思想的精髓,而函數傳參的底層實現就是通過堆棧來實現的,首先我們來理解一下堆棧.
當有參函數被執行時,通常會根據不同的調用約定來對參數進行壓棧存儲
以STDcall約定為例,棧的調用原則是先進后出,最先被push到堆棧中的數據會被最后釋放出來,而CPU中有兩個寄存器專門用於維護堆棧的變化,ESP棧頂寄存器,EBP棧底寄存器(基址),這兩個寄存器就像是好基友,兩個寄存器相互配合,來讓堆棧有條不亂.

棧幀:就是ESP -> EBP 之間的空間,通常是調用函數時,函數的參數,從一個函數切換到另一個函數上,棧幀也會發生變化,當函數調用結束后,則需要平棧幀,不然會發生訪問沖突,平棧幀的過程都是有編譯器來解決的。
逆向分析函數實現機制
函數與堆棧的基礎: 下面一個簡單的函數調用案例,我們來看看匯編格式是怎樣的.
#include <stdio.h>
int VoidFunction()
{
printf("hello lyshark\n");
return 0;
}
int main(int argc, char* argv[])
{
VoidFunction();
return 0;
}
編譯上面的這段代碼,首先我們找到main函數的位置,然后會看到call 0x4110E1這條匯編指令就是在調用VoidFunction()函數,觀察函數能發現函數下方並沒有add esp,xxx這樣的指令,則說明平棧操作是在函數的內部完成的,我們直接跟進去看看函數內部到底做了什么見不得人的事情.
0041142C | 8DBD 40FFFFFF | lea edi,dword ptr ss:[ebp-0xC0] |
00411432 | B9 30000000 | mov ecx,0x30 |
00411437 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
0041143C | F3:AB | rep stosd |
0041143E | E8 9EFCFFFF | call 0x4110E1 | 調用VoidFunction()
00411443 | 33C0 | xor eax,eax | main.c:13
00411445 | 5F | pop edi | main.c:14, edi:"閉\n"
00411446 | 5E | pop esi | esi:"閉\n"
00411447 | 5B | pop ebx |
此時我們直接跟進call 0x4110E1這個函數中,分析函數內部是如何平棧的,進入函數以后首先使用push ebp保存當前EBP指針位置,然后調用mov ebp,esp這條指令來將當前的棧幀付給EBP也就是當基址使用,sub esp,0xC0則是分配局部變量,接着是push ebx,esi,edi則是因為我們需要用到這幾個寄存器所以應該提前將原始值保存起來,最后用完了就需要pip edi,esi,ebx恢復這些寄存器的原始狀態,並執行add esp,0xC0對局部變量進行恢復,最后mov esp,ebp還原到原始的棧頂指針位置,首尾呼應.
004113C0 | 55 | push ebp | 保存棧底指針 ebp
004113C1 | 8BEC | mov ebp,esp | 將當前棧指針給ebp
004113C3 | 81EC C0000000 | sub esp,0xC0 | 抬高棧頂esp,開辟局部空間
004113C9 | 53 | push ebx | 保存 ebx
004113CA | 56 | push esi | 保存 esi
004113CB | 57 | push edi | 保存 edi
004113CC | 8DBD 40FFFFFF | lea edi,dword ptr ss:[ebp-0xC0] | 取出次函數可用棧空間首地址
004113D2 | B9 30000000 | mov ecx,0x30 | ecx:"閉\n", 30:'0'
004113D7 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
004113DC | F3:AB | rep stosd |
004113DE | 8BF4 | mov esi,esp | main.c:5
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] | 調用printf
004113EB | 83C4 04 | add esp,0x4 | 降低棧頂esp,釋放printf局部空間
004113EE | 3BF4 | cmp esi,esp | 檢測堆棧是否平衡,ebp!=esp則不平衡
004113F0 | E8 46FDFFFF | call 0x41113B | 堆棧檢測函數:檢測平衡,不平衡則報錯
004113F5 | 33C0 | xor eax,eax | main.c:6
004113F7 | 5F | pop edi | 還原寄存器edi
004113F8 | 5E | pop esi | 還原寄存器esi
004113F9 | 5B | pop ebx | 還原寄存器ebx
004113FA | 81C4 C0000000 | add esp,0xC0 | 恢復esp,還原局部變量
00411400 | 3BEC | cmp ebp,esp |
00411402 | E8 34FDFFFF | call 0x41113B |
00411407 | 8BE5 | mov esp,ebp | 還原原始的ebp指針
00411409 | 5D | pop ebp |
0041140A | C3 | ret |
上方的代碼其實默認走的是STDCALL的調用約定,一般情況下在Win32環境默認遵循的就是STDCALL,而在Win64環境下使用的則是FastCALL,在Linux系統上則遵循SystemV的約定,這里我整理了他們之間的異同點.

這里我們來演示CDECL的調用約定,其實我們使用的Printf()函數就是在遵循__cdecl()約定,由於Printf函數可以有多個參數傳遞,所以只能使用__cdecl()約定來傳遞參數,該約定的典型特點就是平棧不在被調用函數內部完成,而是在外部通過使用一條add esp,0x4這種方式來平棧的.
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] |
004113EB | 83C4 04 | add esp,0x4 | 平棧
004113EE | 3BF4 | cmp esi,esp |
004113F0 | E8 46FDFFFF | call 0x41113B |
004113F5 | 8BF4 | mov esi,esp | main.c:6
004113F7 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113FC | FF15 14914100 | call dword ptr ds:[<&printf>] | 平棧
00411402 | 83C4 04 | add esp,0x4 |
在使用Release版對其進行優化的話,此段代碼將會采取復寫傳播優化,將每次參數平衡的操作進行歸並,一次性平衡棧頂指針esp,從而可以大大的提高程序的執行效率,匯編代碼如下:
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] |
004113F7 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113FC | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411402 | 83C4 04 | add esp,0x8 | 一次性平棧加上0x8,平了前面的2個push
通過以上分析發現_cdecl與_stdcall兩者只在參數平衡上有所不同,其余部分都一樣,但經過優化后_cdecl調用方式的函數在同一作用域內多次使用,會在效率上比_stdcall髙,這是因為_cdecl可以使用復寫傳播,而_stdcall的平棧都是在函數內部完成的,無法使用復寫傳播這種優化方式.
除了前面的兩種調用約定以外_fastcall調用方式的效率最髙,其他兩種調用方式都是通過棧傳遞參數,唯獨_fastcall可以利用寄存器傳遞參數,但由於寄存器數目很少,而參數相比可以很多,只能量力而行,故在Windows環境中_fastcall的調用方式只使用了ECX和EDX寄存器,分別傳遞第1個參數和第2個參數,其余參數傳遞則依然使用堆棧傳遞.
#include <stdio.h>
void _fastcall VoidFunction(int x,int y,int z,int a)
{
printf("%d%d%d%d\n", x, y, z, a);
}
int main(int argc, char* argv[])
{
VoidFunction(1,2,3,4);
return 0;
}
反匯編后觀察代碼發現call 0x4110E6就是在調用我們的VoidFunction()函數在調用之前分別將參數壓入了不同的寄存器和堆棧中,接着我們繼續跟進到call函數內部,看它是如何取出參數的.
0041145E | 6A 04 | push 0x4 | 第四個參數使用堆棧傳遞
00411460 | 6A 03 | push 0x3 | 第三個參數使用堆棧傳遞
00411462 | BA 02000000 | mov edx,0x2 | 第二個參數使用edx傳遞
00411467 | B9 01000000 | mov ecx,0x1 | 第一個參數使用ecx傳遞
0041146C | E8 75FCFFFF | call 0x4110E6 |
00411471 | 33C0 | xor eax,eax | main.c:11
進入call 0x4110E6這個函數中,觀察發現首先會通過mov指令將前兩個參數提取出來,然后再從第四個參數開始依次將參數取出來並壓棧,最后讓Printf函數成功調用到.
004113E0 | 8955 EC | mov dword ptr ss:[ebp-0x14],edx | edx => 提取出第二個參數
004113E3 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | ecx => 提取出第一個參數
004113E6 | 8BF4 | mov esi,esp | main.c:5
004113E8 | 8B45 0C | mov eax,dword ptr ss:[ebp+0xC] | 保存第四個參數
004113EB | 50 | push eax |
004113EC | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 保存第三個參數
004113EF | 51 | push ecx |
004113F0 | 8B55 EC | mov edx,dword ptr ss:[ebp-0x14] | 保存第二個參數
004113F3 | 52 | push edx |
004113F4 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 保存第一個參數
004113F7 | 50 | push eax |
004113F8 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d%d%d%d\n"
004113FD | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411403 | 83C4 14 | add esp,0x14 | 平棧
定義並使用有參函數: 我們給函數傳遞些參數,然后分析其反匯編代碼,觀察代碼的展示形式.
#include <stdio.h>
int Function(int x,float y,double z)
{
if (x = 100)
{
x = x + 100;
y = y + 100;
z = z + 100;
}
return (x);
}
int main(int argc, char* argv[])
{
int ret = 0;
ret = Function(100, 2.5, 10.245);
printf("返回值: %d\n", ret);
return 0;
}
下方的反匯編代碼就是調用函數ret = Function()的過程,該過程中可看出壓棧順序遵循的是從后向前壓入的.
0041145E | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | main.c:17
00411465 | 83EC 08 | sub esp,0x8 | main.c:18
00411468 | F2:0F1005 70584100 | movsd xmm0,qword ptr ds:[<__real@40247d70a3d70a3d>] | 將10.245放入XMM0寄存器
00411470 | F2:0F110424 | movsd qword ptr ss:[esp],xmm0 | 取出XMM0中內容,並放入堆棧
00411475 | 51 | push ecx |
00411476 | F3:0F1005 68584100 | movss xmm0,dword ptr ds:[<__real@40200000>] | 將2.5放入XMM0
0041147E | F3:0F110424 | movss dword ptr ss:[esp],xmm0 | 同理
00411483 | 6A 64 | push 0x64 | 最后一個參數100
00411485 | E8 51FDFFFF | call 0x4111DB | 調用Function函數
0041148A | 83C4 10 | add esp,0x10 |
0041148D | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax | 將返回值壓棧
00411490 | 8BF4 | mov esi,esp | main.c:19
00411492 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] |
00411495 | 50 | push eax |
00411496 | 68 58584100 | push consoleapplication1.415858 | 415858:"返回值: %d\n"
0041149B | FF15 14914100 | call dword ptr ds:[<&printf>] | 輸出結果
004114A1 | 83C4 08 | add esp,0x8 |
壓棧完成以后我們可以繼續跟進call 0x4111DB這個關鍵CALL,此處就是運算數據的關鍵函數,跟進去以后,可發現其對浮點數的運算,完全是依靠XMM寄存器實現的.
004113F1 | 8945 08 | mov dword ptr ss:[ebp+0x8],eax |
004113F4 | F3:0F1045 0C | movss xmm0,dword ptr ss:[ebp+0xC] | main.c:8
004113F9 | F3:0F5805 8C584100 | addss xmm0,dword ptr ds:[<__real@42c80000>] |
00411401 | F3:0F1145 0C | movss dword ptr ss:[ebp+0xC],xmm0 |
00411406 | F2:0F1045 10 | movsd xmm0,qword ptr ss:[ebp+0x10] | main.c:9
0041140B | F2:0F5805 80584100 | addsd xmm0,qword ptr ds:[<__real@4059000000000000>] |
00411413 | F2:0F1145 10 | movsd qword ptr ss:[ebp+0x10],xmm0 |
00411418 | 8B45 08 | mov eax,dword ptr ss:[ebp+0x8] | main.c:11
向函數傳遞數組/指針: 這里我們以一維數組為例,二維數組的傳遞其實和一維數組是相通的,只不過在尋址方式上要使用二維數組的尋址公式,此外傳遞數組其實本質上就是傳遞指針,所以數組與指針的傳遞方式也是相通的.
#include <stdio.h>
void Function(int Array[], int size)
{
for (int i = 0; i<size; ++i)
{
printf("輸出元素: %d \n", Array[i]);
}
}
int main(int argc, char* argv[])
{
int ary[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
Function(ary, 10);
return 0;
}
以下代碼就是Function(ary,10)函數的調用代碼,首先壓棧傳遞0A也就是10,接着傳遞ary首地址,最后調用call指令.
004114B4 | 6A 0A | push 0xA | 10
004114B6 | 8D45 D4 | lea eax,dword ptr ss:[ebp-0x2C] | ary 首地址
004114B9 | 50 | push eax | push eax
004114BA | E8 63FCFFFF | call 0x411122 | 調用Function()
004114BF | 83C4 08 | add esp,0x8 | 堆棧修復
函數中返回指針,其實就是返回一個內存地址,我們可以打印出這個內存地址具體的值,如下是一段測試代碼,這里的原理於上方都是相通的,此處就不在浪費篇幅了.
#include <stdio.h>
int GetAddr(int number)
{
int nAddr;
nAddr = *(int*)(&number-1);
return nAddr;
}
int main(int argc, char* argv[])
{
int address = 0;
address = GetAddr(100);
printf("%x\n",address);
return 0;
}
函數的參數傳遞就到此結束了,其實其他的參數傳遞無外乎就是上面的這幾種傳遞形式,只是在某些實現細節上略有差異,但大體上也就是這些東西,在真正的逆向過程中還需要考慮編譯器的版本等具體細節,每一個編譯器在實現參數傳遞上都略微不同,這也就是編譯特性所影響的,我們應該靈活運用這些知識,才能更好地分析這些字節碼.
變量作用域解析
接着我們來研究一下變量的作用域,在C語言中作用域可分為局部變量與全局變量,兩種變量又分為靜態變量和動態變量,接下來我們將通過反匯編學習研究他們之間的異同點.
探索全局變量的奧秘: 全局變量與常量有很多相似的地方,兩者都是在程序執行前就存在的,這是因為編譯器在編譯時就將其寫入到的程序文件里,但是在PE文件中的只讀數據節里,常量的節屬性被修飾為不可寫入,而全局變量和靜態變量的屬性為可讀可寫,PE文件加載器在加載可執行文件時,會率先裝載這些常量與全局變量,然后才會運行程序入口代碼,因此這些全局變量可以不受作用域的影響,在程序中的任何位置都可以被訪問和使用,來看一段C代碼:
#include <stdio.h>
int number1 = 1;
int number2 = 2;
int main(int argc, char* argv[])
{
scanf("%d", &number1);
printf("您輸入的數字: %d\n", number1);
number2 = 100;
return 0;
}
如下反匯編代碼可以看出,全局變量的訪問是直接通過立即數push consoleapplication1.415858訪問的,此立即數是通過編譯器編譯時就寫入到了程序中的,所以也就可以直接進行訪問了.
004113E0 | 68 00804100 | push <consoleapplication1._number1> | 此處的壓棧參數就是全局變量
004113E5 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113EA | FF15 10914100 | call dword ptr ds:[<&scanf>] |
004113F0 | 83C4 08 | add esp,0x8 | 保存第二個參數
004113F3 | 3BF4 | cmp esi,esp |
004113F5 | E8 41FDFFFF | call 0x41113B |
004113FA | 8BF4 | mov esi,esp | main.c:9
004113FC | A1 00804100 | mov eax,dword ptr ds:[<_number1>] |
00411401 | 50 | push eax |
00411402 | 68 5C584100 | push consoleapplication1.41585C | 41585C:"您輸入的數字: %d\n"
00411407 | FF15 18914100 | call dword ptr ds:[<&printf>] |
0041140D | 83C4 08 | add esp,0x8 |
00411410 | 3BF4 | cmp esi,esp |
00411412 | E8 24FDFFFF | call 0x41113B |
00411417 | C705 04804100 64000000 | mov dword ptr ds:[<_number2>],0x64 | main.c:11, 64:'d'
00411421 | 33C0 | xor eax,eax | main.c:12
探索局部變量的奧秘: 局部變量的訪問是通過棧指針相對間接訪問,也就是說局部變量是程序動態創建的,通常是調用某個函數或過程時動態生成的,局部變量作用域也僅限於函數內部,且其地址也是一個未知數,編譯器無法預先計算.
#include <stdio.h>
int main(int argc, char* argv[])
{
int num1 = 0;
int num2 = 1;
scanf("%d", &num1);
printf("%d", num1);
num2 = 10;
return 0;
}
反匯編代碼,局部變量就是通過mov dword ptr ss:[ebp-0x8],0x0動態開辟的空間,其作用域就是在本函數退出時消亡.
004113DE | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | 申請局部變量
004113E5 | C745 EC 01000000 | mov dword ptr ss:[ebp-0x14],0x1 | main.c:6
004113EC | 8BF4 | mov esi,esp | main.c:8
004113EE | 8D45 F8 | lea eax,dword ptr ss:[ebp-0x8] |
004113F1 | 50 | push eax |
004113F2 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113F7 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
說到局部變量,不得不提起局部靜態變量,局部靜態變量的聲明只需要使用static關鍵字聲明,該變量比較特殊,他不會隨作用域的結束而消亡,並且也是在未進入作用域之前就已經存在了,其實局部靜態變量也是全局變量,只不過它的作用域被限制在了某一個函數內部而已,所以它本質上還是全局變量,來一段代碼驗證一下:
#include <stdio.h>
int main(int argc, char* argv[])
{
static int g_number = 0;
for (int x = 0; x <= 10; x++)
{
g_number = x;
printf("輸出: %d\n", g_number);
}
return 0;
}
觀察這段反匯編代碼,你能夠清晰的看出,同樣是使用mov eax,dword ptr ds:[<g_number>]從全局數據區取數據的,這說明局部變量聲明為靜態屬性以后,就和全局變量變成了一家人了.
004113DE | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | main.c:7
004113E5 | EB 09 | jmp 0x4113F0 |
004113E7 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] |
004113EA | 83C0 01 | add eax,0x1 |
004113ED | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax |
004113F0 | 837D F8 0A | cmp dword ptr ss:[ebp-0x8],0xA | A:'\n'
004113F4 | 7F 27 | jg 0x41141D |
004113F6 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.c:9
004113F9 | A3 30814100 | mov dword ptr ds:[<g_number>],eax |
004113FE | 8BF4 | mov esi,esp | main.c:10
00411400 | A1 30814100 | mov eax,dword ptr ds:[<g_number>] | 與全局變量是一家人
00411405 | 50 | push eax |
00411406 | 68 58584100 | push consoleapplication1.415858 | 415858:"輸出: %d\n"
0041140B | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411411 | 83C4 08 | add esp,0x8 |
00411414 | 3BF4 | cmp esi,esp |
00411416 | E8 1BFDFFFF | call 0x411136 |
0041141B | EB CA | jmp 0x4113E7 | main.c:11
0041141D | 33C0 | xor eax,eax | main.c:12
探索堆變量的奧秘: 堆變量是最容易識別的一種變量類型,因為分配堆區的函數就幾個calloc/malloc/new等,所以這類變量往往能被調試器直接補貨到,這種變量同樣屬於局部變量的范疇,因為它也是通過函數動態申請的一段內存空間,這里只給出一個案例吧,反編譯大家可以自己研究,這一個是很簡單的了.
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
int *pMalloc = (int*)malloc(10);
printf("變量地址: %x", pMalloc);
free(pMalloc);
return 0;
}
結構體與共用體
針對C語言的反匯編,就剩一個結構體與共用體了,這里的內容比較少,我就不再新的文章里寫了,直接在這里把它給寫完,C語言的反匯編就到此結束。
C語言提供給我們了一些由系統定義的數據類型,我們也可以自己定義這樣的數據類型,結構體與共用體就是用來定義一些比較復雜的數據結構的這么一個方法,定義結構很簡單只需要使用struct關鍵字即可,定義共用體則使用union來實現,接下來將分別演示它們之間的反匯編狀態.
首先我們來定義tag結構體,假設結構體中的當前數據成員類型長度為M,指定對其值為N,那么實際對其值為Q = min(M,N),其成員的地址將被編譯器安排在Q的倍數上,例如默認8字節對齊,則需要安排在8,16,24,32字節之間,如下結構體.
struct tag{
short sShort; // 占用2字節的空間
int nInt; // 占用4字節的空間
double dDouble; // 占用8字節的空間
}
在VS編譯器中默認數據塊的對其值是8字節,上方定義的tag結構中sShort占用2個字節的空間,而nInt則占用4字節的空間,dDouble則占用8字節的存儲空間,那么結構體成員的總長度8+4+2=14bytes按照默認的對其值8字節來對其,結構體分配空間需要被8整除,也就是最低要分配16字節的空間給tag這個結構,那么編譯器會自動在14字節的基礎上增加2字節的墊片,來保證tag結構體內被系統更好的接受.
默認情況下編譯器會自動找出最大的變量值double dDouble使用它的字節長度來充當數據塊對齊尺寸,例如上方代碼中最大值是double 8字節,那么相應的對齊尺寸就應該是8字節,不足8字節的變量編譯器會自動補充墊片字節,當然我們也可以通過預編譯指令#pragma pack(N)來手動調整對齊大小.
定義結構體成員: 首先定義Student結構,然后動態的賦值,觀察其參數的變換.
需要注意的是,結構體類型與結構體變量是不同的概念,通常結構體類型的定義並不會分配空間,只有結構體變量被賦值后編譯器才會在編譯時對其進行處理,結構體類型與結構體變量,其在內存中的表現形式都是普通變量,而結構則是編譯器對語法進行的一種處理,編譯時會將其轉為普通的變量來對待.
#include <stdio.h>
struct Student
{
long int number;
char name[20];
char sex;
};
int main(int argc, char* argv[])
{
struct Student num1;
scanf("%d", &num1.number);
scanf("%s", &num1.name);
scanf("%c", &num1.sex);
printf("編號: %d 姓名: %s 性別: %c", num1.number, num1.name, num1.sex);
return 0;
}
為了驗證上面的猜測,我們將其反匯編,觀察代碼會發現結構體之間的變化,通過0x20-0x1c可得到第一個結構的大小,同理0x1c-0x08得到的則是第二個結構以此類推,就可推測出部分結構成員的類型.
004113E0 | 8D45 E0 | lea eax,dword ptr ss:[ebp-0x20] | 第一個結構
004113E3 | 50 | push eax |
004113E4 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113E9 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
004113EF | 83C4 08 | add esp,0x8 |
004113F2 | 3BF4 | cmp esi,esp |
004113F4 | E8 42FDFFFF | call 0x41113B |
004113F9 | 8BF4 | mov esi,esp | main.c:14
004113FB | 8D45 E4 | lea eax,dword ptr ss:[ebp-0x1C] | 第二個結構
004113FE | 50 | push eax |
004113FF | 68 5C584100 | push consoleapplication1.41585C | 41585C:"%s"==L"猥"
00411404 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
0041140A | 83C4 08 | add esp,0x8 |
0041140D | 3BF4 | cmp esi,esp |
0041140F | E8 27FDFFFF | call 0x41113B |
00411414 | 8BF4 | mov esi,esp | main.c:15
00411416 | 8D45 F8 | lea eax,dword ptr ss:[ebp-0x8] | 第三個結構
00411419 | 50 | push eax |
0041141A | 68 60584100 | push consoleapplication1.415860 | 415860:"%c"==L"揮"
0041141F | FF15 10914100 | call dword ptr ds:[<&scanf>] |
00411425 | 83C4 08 | add esp,0x8 |
定義結構體數組: 結構體數組中每個數組元素都是一個結構體類型的數據,他們都分別包括各個成員項.
#include <stdio.h>
#include <string.h>
struct Student
{
char name[20];
int count;
};
int main(int argc, char* argv[])
{
int x, y;
char leader_name[20];
struct Student leader[3] = { "admin", 0, "lyshark", 0, "guest", 0 };
for (x = 0; x <= 10; x++)
{
scanf("%s", leader_name);
for (y = 0; y < 3; y++)
{
if (strcmp(leader_name, leader[y].name) == 0)
leader[y].count++;
}
}
for (int z = 0; z < 3; z++)
{
printf("用戶名: %5s 出現次數: %d\n", leader[z].name, leader[z].count);
}
system("pause");
return 0;
}
逆向上方這段代碼,我們主要觀察它的尋址方式,你會發現其本質上就是數組尋址,並沒有任何的特別的.
004114F9 | 83BD 74FFFFFF 03 | cmp dword ptr ss:[ebp-0x8C],0x3 | 指定循環次數 3
00411500 | 7D 31 | jge 0x411533 |
00411502 | 6B85 74FFFFFF 18 | imul eax,dword ptr ss:[ebp-0x8C],0x18 | 每次遞增0x18 => char name[20] + int count = 24
00411509 | 8BF4 | mov esi,esp |
0041150B | 8B4C05 C8 | mov ecx,dword ptr ss:[ebp+eax-0x38] | 找到 count
0041150F | 51 | push ecx | ecx:"guest"
00411510 | 6B95 74FFFFFF 18 | imul edx,dword ptr ss:[ebp-0x8C],0x18 |
00411517 | 8D4415 B4 | lea eax,dword ptr ss:[ebp+edx-0x4C] | 找到 name[20]
0041151B | 50 | push eax |
0041151C | 68 78584100 | push consoleapplication1.415878 | 415878:"用戶名: %5s 出現次數: %d\n"
00411521 | FF15 20914100 | call dword ptr ds:[<&printf>] |
00411527 | 83C4 0C | add esp,0xC |
指向結構體數組的指針: 結構體指針就是指向結構體變量的指針,結構體變量的前4字節就是該結構體的指針,將該指針存放到一個指針變量中,那么這個指針變量就可以叫做結構指針變量,結構體指針定義如下.
#include <stdio.h>
#include <string.h>
struct Student
{
int number;
char name[20];
};
struct Student stu[3] = { { 1, "admin" }, { 2, "lyshark" }, { 3, "guest" } };
int main(int argc, char* argv[])
{
struct Student *structPTR;
for (structPTR = stu; structPTR < stu + 3; structPTR++)
{
printf("編號: %d 名字: %s \n", (*structPTR).number, structPTR->name);
}
system("pause");
return 0;
}
觀察以下這段反匯編代碼,你會發現其實和前面的指針數組尋址一個道理,並沒有什么野路子.
004113DE | C745 F8 00804100 | mov dword ptr ss:[ebp-0x8],0x418000 | 此處獲取結構體指針 => structPTR = stu
004113E5 | EB 09 | jmp 0x4113F0 |
004113E7 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:_stu
004113EA | 83C0 18 | add eax,0x18 | 遞增 structPTR++ 每次遞增一個結構
004113ED | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax | 將遞增后的指針回寫
004113F0 | 817D F8 48804100 | cmp dword ptr ss:[ebp-0x8],0x418048 | 對比指正是否結束
004113F7 | 73 26 | jae 0x41141F |
004113F9 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.c:18, [ebp-8]:_stu
004113FC | 83C0 04 | add eax,0x4 | eax:"admin"
004113FF | 8BF4 | mov esi,esp |
00411401 | 50 | push eax | 將 structPTR->name 壓棧
00411402 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | [ebp-8]:_stu
00411405 | 8B11 | mov edx,dword ptr ds:[ecx] | 取出計數地址
00411407 | 52 | push edx |
00411408 | 68 58584100 | push consoleapplication1.415858 | 415858:"編號: %d 名字: %s \n"
0041140D | FF15 18914100 | call dword ptr ds:[<&printf>] | 輸出結果
00411413 | 83C4 0C | add esp,0xC |
向函數內傳遞結構體: 將函數的形參列表定義為結構體參數,該函數就可以接收一個結構體列表了,收到列表后我們可以取出里面的最大值並返回.
#include <stdio.h>
#include <string.h>
struct Student
{
int number;
char name[20];
float aver;
};
struct Student stud[3] = { { 1, "admin" ,89}, { 2, "lyshark" ,76}, { 3, "guest",98 }};
int GetMaxID(struct Student stu[])
{
int x , item = 0;
for (x = 0; x < 3; x++)
{
if (stu[x].aver > stu[item].aver)
item = x;
}
return stu[item].number;
}
int main(int argc, char* argv[])
{
int item;
item = GetMaxID(stud);
printf("成績最高的學生編號: %d", item);
system("pause");
return 0;
}
這里不啰嗦,直接看反匯編代碼能發現在主函數調用call 0x4110e6之前是將push <console._stud>結構體的首地址傳入了函數內部執行的.
0041146C | 8DBD 34FFFFFF | lea edi,dword ptr ss:[ebp-0xCC] |
00411472 | B9 33000000 | mov ecx,0x33 | 33:'3'
00411477 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
0041147C | F3:AB | rep stosd |
0041147E | 68 00804100 | push <console._stud> | 將結構體首地址傳遞到call內部
00411483 | E8 5EFCFFFF | call 0x4110E6 |
00411488 | 83C4 04 | add esp,0x4 |
最后一段C代碼是實現了返回結構體的結構,就是說將處理好的結構體返回給上層調用,其原理也是利用了指針,這里只把代碼放出來,自己分析一下吧.
#include <stdio.h>
struct tag{
int x;
int y;
char z;
};
tag RetStruct()
{
tag temp;
temp.x = 10;
temp.y = 20;
temp.z = 'A';
return temp;
}
int main(int argc, char* argv[])
{
tag temp;
temp = RetStruct();
printf("%d \n",temp.x);
printf("%d \n",temp.y);
printf("%d \n",temp.z);
return 0;
}
定義並使用共用體類型: 有時候我們想要使用同一段內存數據來表示不同的數據類型,那么我們就可以使用共用體類型.
結構體與共用體的定義形式相似,但他們的含義完全不同,結構體變量所占用的內存長度是各成員占的內存長度之和,每個成員分別占有其自己的內存單元,而共用體變量所占用的內存長度則等於共用體中的最長的成員的長度,首先我們先來研究C代碼.
#include <stdio.h>
union Date
{
int num;
char ch;
float f;
}dat;
int main(int argc, char* argv[])
{
dat.num = 97;
printf("以整數形式輸出: %d\n", dat.num);
printf("以字符形式輸出: %c\n", dat.ch);
printf("以浮點數形式輸出: %f\n", dat.f);
system("pause");
return 0;
}
以上代碼我們通過dat.num = 97;給共用體賦予了整數類型的初始值,后面則是按照不同的形式輸出這段內存,其反匯編代碼如下,觀察代碼可發現共用體僅僅儲存一份變量數據在程序的常量區,當我們調用不同類型的共用體是則進行相應的轉換,其實這些都是編譯器為我們做的,本質上共用體其實也是一個個普通的變量.
004113DE | C705 48854100 61000000 | mov dword ptr ds:[<_dat>],0x61 | main.c:12, 00418548:L"a", 61:'a'
004113E8 | 8BF4 | mov esi,esp | main.c:13
004113EA | A1 48854100 | mov eax,dword ptr ds:[<_dat>] | 使用整數方式輸出
004113EF | 50 | push eax |
004113F0 | 68 58584100 | push consoleapplication1.415858 | 415858:"以整數形式輸出: %d\n"
004113F5 | FF15 18914100 | call dword ptr ds:[<&printf>] |
004113FB | 83C4 08 | add esp,0x8 |
004113FE | 3BF4 | cmp esi,esp |
00411400 | E8 36FDFFFF | call 0x41113B |
00411405 | 0FBE05 48854100 | movsx eax,byte ptr ds:[<_dat>] | 輸出字符
0041140C | 8BF4 | mov esi,esp |
0041140E | 50 | push eax |
0041140F | 68 70584100 | push consoleapplication1.415870 | 415870:"以字符形式輸出: %c\n"
00411414 | FF15 18914100 | call dword ptr ds:[<&printf>] |
0041141A | 83C4 08 | add esp,0x8 |
0041141D | 3BF4 | cmp esi,esp |
0041141F | E8 17FDFFFF | call 0x41113B |
00411424 | F3:0F5A05 48854100 | cvtss2sd xmm0,dword ptr ds:[<_dat>] | 輸出浮點數
0041142C | 8BF4 | mov esi,esp |
0041142E | 83EC 08 | sub esp,0x8 |
00411431 | F2:0F110424 | movsd qword ptr ss:[esp],xmm0 |
00411436 | 68 88584100 | push consoleapplication1.415888 | 415888:"以浮點數形式輸出: %f\n"
0041143B | FF15 18914100 | call dword ptr ds:[<&printf>] |
00411441 | 83C4 0C | add esp,0xC |
既然了解了共用體的結構類型,那不妨編譯以下代碼然后逆向分析它的尋址方式,觀察與數組指針是否一致呢?
#include <stdio.h>
struct
{
char job; // s=學生 t=老師
union
{
int clas; // 學生學號
char position[20]; // 老師職務
}category;
}person[2];
int main(int argc, char* argv[])
{
for (int x = 0; x < 2; x++)
{
scanf("%c", &person[x].job); // 輸入人物類型
if (person[x].job == 't')
{
scanf("%s", &person[x].category.position); // 如果是老師則輸入職務
}
else if (person[x].job == 's')
{
scanf("%d", &person[x].category.clas); // 如果是學生則輸入學號
}
}
for (int y = 0; y < 2; y++)
{
if (person[y].job == 's')
printf("學生學號: %d\n", person[y].category.clas);
else if (person[y].job == 't')
printf("老師職務: %s\n", person[y].category.position);
}
system("pause");
return 0;
}
定義並使用枚舉類型: 如果一個變量只有幾種可能,那么我們就可以定義一個枚舉字典,通過循環的方式枚舉元素,編譯以下代碼觀察變化,其中的枚舉{red,yellow,blue,white,black}會被編譯器在編譯時替換為{0,1,2,3,4}等數字,所以反匯編以下代碼你回范縣並沒有出現字符串,而是使用數字來代替了.
#include <stdio.h>
int main(int argc, char* argv[])
{
enum Color {red,yellow,blue,white,black};
enum Color x;
for (x = red; x <= black; x++)
{
printf("元素值: %d\n",x);
switch (x)
{
case red: printf("red 出現了\n"); break;
case blue: printf("blue 出現了\n"); break;
}
}
system("pause");
return 0;
}
至此,我們的C語言反匯編的內容就結束了,接下來我們將領略C++ 的反匯編技巧,C++ 是重頭戲,其中的類,構造析構函數,等都是重點,不過C++ 在識別上其實更加的容易,因為其封裝的更加徹底,對C語言的封裝。
