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