C语言反汇编 - 函数与结构体
反匯編(Disassembly) 即把目標(biāo)二進(jìn)制機(jī)器碼轉(zhuǎn)為匯編代碼的過程,該技術(shù)常用于軟件破解、外掛技術(shù)、病毒分析、逆向工程、軟件漢化等領(lǐng)域,學(xué)習(xí)和理解反匯編對軟件調(diào)試、系統(tǒng)漏洞挖掘、內(nèi)核原理及理解高級語言代碼都有相當(dāng)大的幫助,軟件一切神秘的運(yùn)行機(jī)制全在反匯編代碼里面。
函數(shù)是任何一個(gè)高級語言中必須要存在的一個(gè)東西,使用函數(shù)式編程可以讓程序可讀性更高,充分發(fā)揮了模塊化設(shè)計(jì)思想的精髓,今天我將帶大家一起來探索函數(shù)的實(shí)現(xiàn)機(jī)理,探索編譯器到底是如何對函數(shù)這個(gè)關(guān)鍵字進(jìn)行實(shí)現(xiàn)的,從而更好地理解編譯行為。
先來研究函數(shù),函數(shù)是任何一門編程語言中都存在的關(guān)鍵字,使用函數(shù)式編程可以讓程序可讀性更高,充分發(fā)揮模塊化設(shè)計(jì)思想的精髓,而函數(shù)傳參的底層實(shí)現(xiàn)就是通過堆棧來實(shí)現(xiàn)的,首先我們來理解一下堆棧.
當(dāng)有參函數(shù)被執(zhí)行時(shí),通常會根據(jù)不同的調(diào)用約定來對參數(shù)進(jìn)行壓棧存儲
以STDcall約定為例,棧的調(diào)用原則是先進(jìn)后出,最先被push到堆棧中的數(shù)據(jù)會被最后釋放出來,而CPU中有兩個(gè)寄存器專門用于維護(hù)堆棧的變化,ESP棧頂寄存器,EBP棧底寄存器(基址),這兩個(gè)寄存器就像是好基友,兩個(gè)寄存器相互配合,來讓堆棧有條不亂.
棧幀:就是ESP -> EBP 之間的空間,通常是調(diào)用函數(shù)時(shí),函數(shù)的參數(shù),從一個(gè)函數(shù)切換到另一個(gè)函數(shù)上,棧幀也會發(fā)生變化,當(dāng)函數(shù)調(diào)用結(jié)束后,則需要平棧幀,不然會發(fā)生訪問沖突,平棧幀的過程都是有編譯器來解決的。
逆向分析函數(shù)實(shí)現(xiàn)機(jī)制
函數(shù)與堆棧的基礎(chǔ): 下面一個(gè)簡單的函數(shù)調(diào)用案例,我們來看看匯編格式是怎樣的.
#include <stdio.h>int VoidFunction() {printf("hello lyshark\n");return 0; }int main(int argc, char* argv[]) {VoidFunction();return 0; }編譯上面的這段代碼,首先我們找到main函數(shù)的位置,然后會看到call 0x4110E1這條匯編指令就是在調(diào)用VoidFunction()函數(shù),觀察函數(shù)能發(fā)現(xiàn)函數(shù)下方并沒有add esp,xxx這樣的指令,則說明平棧操作是在函數(shù)的內(nèi)部完成的,我們直接跟進(jìn)去看看函數(shù)內(nèi)部到底做了什么見不得人的事情.
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 | 調(diào)用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 |此時(shí)我們直接跟進(jìn)call 0x4110E1這個(gè)函數(shù)中,分析函數(shù)內(nèi)部是如何平棧的,進(jìn)入函數(shù)以后首先使用push ebp保存當(dāng)前EBP指針位置,然后調(diào)用mov ebp,esp這條指令來將當(dāng)前的棧幀付給EBP也就是當(dāng)基址使用,sub esp,0xC0則是分配局部變量,接著是push ebx,esi,edi則是因?yàn)槲覀冃枰玫竭@幾個(gè)寄存器所以應(yīng)該提前將原始值保存起來,最后用完了就需要pip edi,esi,ebx恢復(fù)這些寄存器的原始狀態(tài),并執(zhí)行add esp,0xC0對局部變量進(jìn)行恢復(fù),最后mov esp,ebp還原到原始的棧頂指針位置,首尾呼應(yīng).
004113C0 | 55 | push ebp | 保存棧底指針 ebp 004113C1 | 8BEC | mov ebp,esp | 將當(dāng)前棧指針給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] | 取出次函數(shù)可用??臻g首地址 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>] | 調(diào)用printf 004113EB | 83C4 04 | add esp,0x4 | 降低棧頂esp,釋放printf局部空間 004113EE | 3BF4 | cmp esi,esp | 檢測堆棧是否平衡,ebp!=esp則不平衡 004113F0 | E8 46FDFFFF | call 0x41113B | 堆棧檢測函數(shù):檢測平衡,不平衡則報(bào)錯(cuò) 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 | 恢復(fù)esp,還原局部變量 00411400 | 3BEC | cmp ebp,esp | 00411402 | E8 34FDFFFF | call 0x41113B | 00411407 | 8BE5 | mov esp,ebp | 還原原始的ebp指針 00411409 | 5D | pop ebp | 0041140A | C3 | ret |上方的代碼其實(shí)默認(rèn)走的是STDCALL的調(diào)用約定,一般情況下在Win32環(huán)境默認(rèn)遵循的就是STDCALL,而在Win64環(huán)境下使用的則是FastCALL,在Linux系統(tǒng)上則遵循SystemV的約定,這里我整理了他們之間的異同點(diǎn).
這里我們來演示CDECL的調(diào)用約定,其實(shí)我們使用的Printf()函數(shù)就是在遵循__cdecl()約定,由于Printf函數(shù)可以有多個(gè)參數(shù)傳遞,所以只能使用__cdecl()約定來傳遞參數(shù),該約定的典型特點(diǎn)就是平棧不在被調(diào)用函數(shù)內(nèi)部完成,而是在外部通過使用一條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版對其進(jìn)行優(yōu)化的話,此段代碼將會采取復(fù)寫傳播優(yōu)化,將每次參數(shù)平衡的操作進(jìn)行歸并,一次性平衡棧頂指針esp,從而可以大大的提高程序的執(zhí)行效率,匯編代碼如下:
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個(gè)push通過以上分析發(fā)現(xiàn)_cdecl與_stdcall兩者只在參數(shù)平衡上有所不同,其余部分都一樣,但經(jīng)過優(yōu)化后_cdecl調(diào)用方式的函數(shù)在同一作用域內(nèi)多次使用,會在效率上比_stdcall髙,這是因?yàn)開cdecl可以使用復(fù)寫傳播,而_stdcall的平棧都是在函數(shù)內(nèi)部完成的,無法使用復(fù)寫傳播這種優(yōu)化方式.
除了前面的兩種調(diào)用約定以外_fastcall調(diào)用方式的效率最髙,其他兩種調(diào)用方式都是通過棧傳遞參數(shù),唯獨(dú)_fastcall可以利用寄存器傳遞參數(shù),但由于寄存器數(shù)目很少,而參數(shù)相比可以很多,只能量力而行,故在Windows環(huán)境中_fastcall的調(diào)用方式只使用了ECX和EDX寄存器,分別傳遞第1個(gè)參數(shù)和第2個(gè)參數(shù),其余參數(shù)傳遞則依然使用堆棧傳遞.
#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; }反匯編后觀察代碼發(fā)現(xiàn)call 0x4110E6就是在調(diào)用我們的VoidFunction()函數(shù)在調(diào)用之前分別將參數(shù)壓入了不同的寄存器和堆棧中,接著我們繼續(xù)跟進(jìn)到call函數(shù)內(nèi)部,看它是如何取出參數(shù)的.
0041145E | 6A 04 | push 0x4 | 第四個(gè)參數(shù)使用堆棧傳遞 00411460 | 6A 03 | push 0x3 | 第三個(gè)參數(shù)使用堆棧傳遞 00411462 | BA 02000000 | mov edx,0x2 | 第二個(gè)參數(shù)使用edx傳遞 00411467 | B9 01000000 | mov ecx,0x1 | 第一個(gè)參數(shù)使用ecx傳遞 0041146C | E8 75FCFFFF | call 0x4110E6 | 00411471 | 33C0 | xor eax,eax | main.c:11進(jìn)入call 0x4110E6這個(gè)函數(shù)中,觀察發(fā)現(xiàn)首先會通過mov指令將前兩個(gè)參數(shù)提取出來,然后再從第四個(gè)參數(shù)開始依次將參數(shù)取出來并壓棧,最后讓Printf函數(shù)成功調(diào)用到.
004113E0 | 8955 EC | mov dword ptr ss:[ebp-0x14],edx | edx => 提取出第二個(gè)參數(shù) 004113E3 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | ecx => 提取出第一個(gè)參數(shù) 004113E6 | 8BF4 | mov esi,esp | main.c:5 004113E8 | 8B45 0C | mov eax,dword ptr ss:[ebp+0xC] | 保存第四個(gè)參數(shù) 004113EB | 50 | push eax | 004113EC | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 保存第三個(gè)參數(shù) 004113EF | 51 | push ecx | 004113F0 | 8B55 EC | mov edx,dword ptr ss:[ebp-0x14] | 保存第二個(gè)參數(shù) 004113F3 | 52 | push edx | 004113F4 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 保存第一個(gè)參數(shù) 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 | 平棧定義并使用有參函數(shù): 我們給函數(shù)傳遞些參數(shù),然后分析其反匯編代碼,觀察代碼的展示形式.
#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; }下方的反匯編代碼就是調(diào)用函數(shù)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中內(nèi)容,并放入堆棧 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 | 最后一個(gè)參數(shù)100 00411485 | E8 51FDFFFF | call 0x4111DB | 調(diào)用Function函數(shù) 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>] | 輸出結(jié)果 004114A1 | 83C4 08 | add esp,0x8 |壓棧完成以后我們可以繼續(xù)跟進(jìn)call 0x4111DB這個(gè)關(guān)鍵CALL,此處就是運(yùn)算數(shù)據(jù)的關(guān)鍵函數(shù),跟進(jìn)去以后,可發(fā)現(xiàn)其對浮點(diǎn)數(shù)的運(yùn)算,完全是依靠XMM寄存器實(shí)現(xiàn)的.
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向函數(shù)傳遞數(shù)組/指針: 這里我們以一維數(shù)組為例,二維數(shù)組的傳遞其實(shí)和一維數(shù)組是相通的,只不過在尋址方式上要使用二維數(shù)組的尋址公式,此外傳遞數(shù)組其實(shí)本質(zhì)上就是傳遞指針,所以數(shù)組與指針的傳遞方式也是相通的.
#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)函數(shù)的調(diào)用代碼,首先壓棧傳遞0A也就是10,接著傳遞ary首地址,最后調(diào)用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 | 調(diào)用Function() 004114BF | 83C4 08 | add esp,0x8 | 堆棧修復(fù)函數(shù)中返回指針,其實(shí)就是返回一個(gè)內(nèi)存地址,我們可以打印出這個(gè)內(nèi)存地址具體的值,如下是一段測試代碼,這里的原理于上方都是相通的,此處就不在浪費(fèi)篇幅了.
#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; }函數(shù)的參數(shù)傳遞就到此結(jié)束了,其實(shí)其他的參數(shù)傳遞無外乎就是上面的這幾種傳遞形式,只是在某些實(shí)現(xiàn)細(xì)節(jié)上略有差異,但大體上也就是這些東西,在真正的逆向過程中還需要考慮編譯器的版本等具體細(xì)節(jié),每一個(gè)編譯器在實(shí)現(xiàn)參數(shù)傳遞上都略微不同,這也就是編譯特性所影響的,我們應(yīng)該靈活運(yùn)用這些知識,才能更好地分析這些字節(jié)碼.
變量作用域解析
接著我們來研究一下變量的作用域,在C語言中作用域可分為局部變量與全局變量,兩種變量又分為靜態(tài)變量和動態(tài)變量,接下來我們將通過反匯編學(xué)習(xí)研究他們之間的異同點(diǎn).
探索全局變量的奧秘: 全局變量與常量有很多相似的地方,兩者都是在程序執(zhí)行前就存在的,這是因?yàn)榫幾g器在編譯時(shí)就將其寫入到的程序文件里,但是在PE文件中的只讀數(shù)據(jù)節(jié)里,常量的節(jié)屬性被修飾為不可寫入,而全局變量和靜態(tài)變量的屬性為可讀可寫,PE文件加載器在加載可執(zhí)行文件時(shí),會率先裝載這些常量與全局變量,然后才會運(yùn)行程序入口代碼,因此這些全局變量可以不受作用域的影響,在程序中的任何位置都可以被訪問和使用,來看一段C代碼:
#include <stdio.h> int number1 = 1; int number2 = 2;int main(int argc, char* argv[]) {scanf("%d", &number1);printf("您輸入的數(shù)字: %d\n", number1);number2 = 100;return 0; }如下反匯編代碼可以看出,全局變量的訪問是直接通過立即數(shù)push consoleapplication1.415858訪問的,此立即數(shù)是通過編譯器編譯時(shí)就寫入到了程序中的,所以也就可以直接進(jìn)行訪問了.
004113E0 | 68 00804100 | push <consoleapplication1._number1> | 此處的壓棧參數(shù)就是全局變量 004113E5 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d" 004113EA | FF15 10914100 | call dword ptr ds:[<&scanf>] | 004113F0 | 83C4 08 | add esp,0x8 | 保存第二個(gè)參數(shù) 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:"您輸入的數(shù)字: %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探索局部變量的奧秘: 局部變量的訪問是通過棧指針相對間接訪問,也就是說局部變量是程序動態(tài)創(chuàng)建的,通常是調(diào)用某個(gè)函數(shù)或過程時(shí)動態(tài)生成的,局部變量作用域也僅限于函數(shù)內(nèi)部,且其地址也是一個(gè)未知數(shù),編譯器無法預(yù)先計(jì)算.
#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動態(tài)開辟的空間,其作用域就是在本函數(shù)退出時(shí)消亡.
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>] |說到局部變量,不得不提起局部靜態(tài)變量,局部靜態(tài)變量的聲明只需要使用static關(guān)鍵字聲明,該變量比較特殊,他不會隨作用域的結(jié)束而消亡,并且也是在未進(jìn)入作用域之前就已經(jīng)存在了,其實(shí)局部靜態(tài)變量也是全局變量,只不過它的作用域被限制在了某一個(gè)函數(shù)內(nèi)部而已,所以它本質(zhì)上還是全局變量,來一段代碼驗(yàn)證一下:
#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>]從全局?jǐn)?shù)據(jù)區(qū)取數(shù)據(jù)的,這說明局部變量聲明為靜態(tài)屬性以后,就和全局變量變成了一家人了.
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探索堆變量的奧秘: 堆變量是最容易識別的一種變量類型,因?yàn)榉峙涠褏^(qū)的函數(shù)就幾個(gè)calloc/malloc/new等,所以這類變量往往能被調(diào)試器直接補(bǔ)貨到,這種變量同樣屬于局部變量的范疇,因?yàn)樗彩峭ㄟ^函數(shù)動態(tài)申請的一段內(nèi)存空間,這里只給出一個(gè)案例吧,反編譯大家可以自己研究,這一個(gè)是很簡單的了.
#include <stdlib.h> #include <stdio.h>int main(int argc, char* argv[]) {int *pMalloc = (int*)malloc(10);printf("變量地址: %x", pMalloc);free(pMalloc);return 0; }結(jié)構(gòu)體與共用體
針對C語言的反匯編,就剩一個(gè)結(jié)構(gòu)體與共用體了,這里的內(nèi)容比較少,我就不再新的文章里寫了,直接在這里把它給寫完,C語言的反匯編就到此結(jié)束。
C語言提供給我們了一些由系統(tǒng)定義的數(shù)據(jù)類型,我們也可以自己定義這樣的數(shù)據(jù)類型,結(jié)構(gòu)體與共用體就是用來定義一些比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)的這么一個(gè)方法,定義結(jié)構(gòu)很簡單只需要使用struct關(guān)鍵字即可,定義共用體則使用union來實(shí)現(xiàn),接下來將分別演示它們之間的反匯編狀態(tài).
首先我們來定義tag結(jié)構(gòu)體,假設(shè)結(jié)構(gòu)體中的當(dāng)前數(shù)據(jù)成員類型長度為M,指定對其值為N,那么實(shí)際對其值為Q = min(M,N),其成員的地址將被編譯器安排在Q的倍數(shù)上,例如默認(rèn)8字節(jié)對齊,則需要安排在8,16,24,32字節(jié)之間,如下結(jié)構(gòu)體.
struct tag{short sShort; // 占用2字節(jié)的空間int nInt; // 占用4字節(jié)的空間double dDouble; // 占用8字節(jié)的空間 }在VS編譯器中默認(rèn)數(shù)據(jù)塊的對其值是8字節(jié),上方定義的tag結(jié)構(gòu)中sShort占用2個(gè)字節(jié)的空間,而nInt則占用4字節(jié)的空間,dDouble則占用8字節(jié)的存儲空間,那么結(jié)構(gòu)體成員的總長度8+4+2=14bytes按照默認(rèn)的對其值8字節(jié)來對其,結(jié)構(gòu)體分配空間需要被8整除,也就是最低要分配16字節(jié)的空間給tag這個(gè)結(jié)構(gòu),那么編譯器會自動在14字節(jié)的基礎(chǔ)上增加2字節(jié)的墊片,來保證tag結(jié)構(gòu)體內(nèi)被系統(tǒng)更好的接受.
默認(rèn)情況下編譯器會自動找出最大的變量值double dDouble使用它的字節(jié)長度來充當(dāng)數(shù)據(jù)塊對齊尺寸,例如上方代碼中最大值是double 8字節(jié),那么相應(yīng)的對齊尺寸就應(yīng)該是8字節(jié),不足8字節(jié)的變量編譯器會自動補(bǔ)充墊片字節(jié),當(dāng)然我們也可以通過預(yù)編譯指令#pragma pack(N)來手動調(diào)整對齊大小.
定義結(jié)構(gòu)體成員: 首先定義Student結(jié)構(gòu),然后動態(tài)的賦值,觀察其參數(shù)的變換.
需要注意的是,結(jié)構(gòu)體類型與結(jié)構(gòu)體變量是不同的概念,通常結(jié)構(gòu)體類型的定義并不會分配空間,只有結(jié)構(gòu)體變量被賦值后編譯器才會在編譯時(shí)對其進(jìn)行處理,結(jié)構(gòu)體類型與結(jié)構(gòu)體變量,其在內(nèi)存中的表現(xiàn)形式都是普通變量,而結(jié)構(gòu)則是編譯器對語法進(jìn)行的一種處理,編譯時(shí)會將其轉(zhuǎn)為普通的變量來對待.
#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; }為了驗(yàn)證上面的猜測,我們將其反匯編,觀察代碼會發(fā)現(xiàn)結(jié)構(gòu)體之間的變化,通過0x20-0x1c可得到第一個(gè)結(jié)構(gòu)的大小,同理0x1c-0x08得到的則是第二個(gè)結(jié)構(gòu)以此類推,就可推測出部分結(jié)構(gòu)成員的類型.
004113E0 | 8D45 E0 | lea eax,dword ptr ss:[ebp-0x20] | 第一個(gè)結(jié)構(gòu) 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] | 第二個(gè)結(jié)構(gòu) 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] | 第三個(gè)結(jié)構(gòu) 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 |定義結(jié)構(gòu)體數(shù)組: 結(jié)構(gòu)體數(shù)組中每個(gè)數(shù)組元素都是一個(gè)結(jié)構(gòu)體類型的數(shù)據(jù),他們都分別包括各個(gè)成員項(xiàng).
#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 出現(xiàn)次數(shù): %d\n", leader[z].name, leader[z].count);}system("pause");return 0; }逆向上方這段代碼,我們主要觀察它的尋址方式,你會發(fā)現(xiàn)其本質(zhì)上就是數(shù)組尋址,并沒有任何的特別的.
004114F9 | 83BD 74FFFFFF 03 | cmp dword ptr ss:[ebp-0x8C],0x3 | 指定循環(huán)次數(shù) 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 出現(xiàn)次數(shù): %d\n" 00411521 | FF15 20914100 | call dword ptr ds:[<&printf>] | 00411527 | 83C4 0C | add esp,0xC |指向結(jié)構(gòu)體數(shù)組的指針: 結(jié)構(gòu)體指針就是指向結(jié)構(gòu)體變量的指針,結(jié)構(gòu)體變量的前4字節(jié)就是該結(jié)構(gòu)體的指針,將該指針存放到一個(gè)指針變量中,那么這個(gè)指針變量就可以叫做結(jié)構(gòu)指針變量,結(jié)構(gòu)體指針定義如下.
#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; }觀察以下這段反匯編代碼,你會發(fā)現(xiàn)其實(shí)和前面的指針數(shù)組尋址一個(gè)道理,并沒有什么野路子.
004113DE | C745 F8 00804100 | mov dword ptr ss:[ebp-0x8],0x418000 | 此處獲取結(jié)構(gòu)體指針 => 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++ 每次遞增一個(gè)結(jié)構(gòu) 004113ED | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax | 將遞增后的指針回寫 004113F0 | 817D F8 48804100 | cmp dword ptr ss:[ebp-0x8],0x418048 | 對比指正是否結(jié)束 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] | 取出計(jì)數(shù)地址 00411407 | 52 | push edx | 00411408 | 68 58584100 | push consoleapplication1.415858 | 415858:"編號: %d 名字: %s \n" 0041140D | FF15 18914100 | call dword ptr ds:[<&printf>] | 輸出結(jié)果 00411413 | 83C4 0C | add esp,0xC |向函數(shù)內(nèi)傳遞結(jié)構(gòu)體: 將函數(shù)的形參列表定義為結(jié)構(gòu)體參數(shù),該函數(shù)就可以接收一個(gè)結(jié)構(gòu)體列表了,收到列表后我們可以取出里面的最大值并返回.
#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("成績最高的學(xué)生編號: %d", item);system("pause");return 0; }這里不啰嗦,直接看反匯編代碼能發(fā)現(xiàn)在主函數(shù)調(diào)用call 0x4110e6之前是將push <console._stud>結(jié)構(gòu)體的首地址傳入了函數(shù)內(nèi)部執(zhí)行的.
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> | 將結(jié)構(gòu)體首地址傳遞到call內(nèi)部 00411483 | E8 5EFCFFFF | call 0x4110E6 | 00411488 | 83C4 04 | add esp,0x4 |最后一段C代碼是實(shí)現(xiàn)了返回結(jié)構(gòu)體的結(jié)構(gòu),就是說將處理好的結(jié)構(gòu)體返回給上層調(diào)用,其原理也是利用了指針,這里只把代碼放出來,自己分析一下吧.
#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; }定義并使用共用體類型: 有時(shí)候我們想要使用同一段內(nèi)存數(shù)據(jù)來表示不同的數(shù)據(jù)類型,那么我們就可以使用共用體類型.
結(jié)構(gòu)體與共用體的定義形式相似,但他們的含義完全不同,結(jié)構(gòu)體變量所占用的內(nèi)存長度是各成員占的內(nèi)存長度之和,每個(gè)成員分別占有其自己的內(nèi)存單元,而共用體變量所占用的內(nèi)存長度則等于共用體中的最長的成員的長度,首先我們先來研究C代碼.
#include <stdio.h>union Date {int num;char ch;float f; }dat;int main(int argc, char* argv[]) {dat.num = 97;printf("以整數(shù)形式輸出: %d\n", dat.num);printf("以字符形式輸出: %c\n", dat.ch);printf("以浮點(diǎn)數(shù)形式輸出: %f\n", dat.f);system("pause");return 0; }以上代碼我們通過dat.num = 97;給共用體賦予了整數(shù)類型的初始值,后面則是按照不同的形式輸出這段內(nèi)存,其反匯編代碼如下,觀察代碼可發(fā)現(xiàn)共用體僅僅儲存一份變量數(shù)據(jù)在程序的常量區(qū),當(dāng)我們調(diào)用不同類型的共用體是則進(jìn)行相應(yīng)的轉(zhuǎn)換,其實(shí)這些都是編譯器為我們做的,本質(zhì)上共用體其實(shí)也是一個(gè)個(gè)普通的變量.
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>] | 使用整數(shù)方式輸出 004113EF | 50 | push eax | 004113F0 | 68 58584100 | push consoleapplication1.415858 | 415858:"以整數(shù)形式輸出: %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>] | 輸出浮點(diǎn)數(shù) 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:"以浮點(diǎn)數(shù)形式輸出: %f\n" 0041143B | FF15 18914100 | call dword ptr ds:[<&printf>] | 00411441 | 83C4 0C | add esp,0xC |既然了解了共用體的結(jié)構(gòu)類型,那不妨編譯以下代碼然后逆向分析它的尋址方式,觀察與數(shù)組指針是否一致呢?
#include <stdio.h>struct {char job; // s=學(xué)生 t=老師union{int clas; // 學(xué)生學(xué)號char position[20]; // 老師職務(wù)}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); // 如果是老師則輸入職務(wù)}else if (person[x].job == 's'){scanf("%d", &person[x].category.clas); // 如果是學(xué)生則輸入學(xué)號}}for (int y = 0; y < 2; y++){if (person[y].job == 's')printf("學(xué)生學(xué)號: %d\n", person[y].category.clas);else if (person[y].job == 't')printf("老師職務(wù): %s\n", person[y].category.position);}system("pause");return 0; }定義并使用枚舉類型: 如果一個(gè)變量只有幾種可能,那么我們就可以定義一個(gè)枚舉字典,通過循環(huán)的方式枚舉元素,編譯以下代碼觀察變化,其中的枚舉{red,yellow,blue,white,black}會被編譯器在編譯時(shí)替換為{0,1,2,3,4}等數(shù)字,所以反匯編以下代碼你回范縣并沒有出現(xiàn)字符串,而是使用數(shù)字來代替了.
#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 出現(xiàn)了\n"); break;case blue: printf("blue 出現(xiàn)了\n"); break;}}system("pause");return 0; }至此,我們的C語言反匯編的內(nèi)容就結(jié)束了,接下來我們將領(lǐng)略C++ 的反匯編技巧,C++ 是重頭戲,其中的類,構(gòu)造析構(gòu)函數(shù),等都是重點(diǎn),不過C++ 在識別上其實(shí)更加的容易,因?yàn)槠浞庋b的更加徹底,對C語言的封裝。
總結(jié)
以上是生活随笔為你收集整理的C语言反汇编 - 函数与结构体的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【现控】系统状态空间表达式
- 下一篇: 教学|zbrush雕刻头发笔刷介绍,巧用