反匯編(Disassembly) 即把目標二進制機器碼轉為匯編代碼的過程,該技術常用於軟件破解、外掛技術、病毒分析、逆向工程、軟件漢化等領域,學習和理解反匯編對軟件調試、系統漏洞挖掘、內核原理及理解高級語言代碼都有相當大的幫助,軟件一切神秘的運行機制全在反匯編代碼里面。下面將分析VS 2013 編譯器產生C代碼的格式與實現方法,研究一下編譯器的編譯特性。
C++ 基本輸入輸出
c語言使用printf函數輸出,printf函數的輸出方式很好理解,反匯編后會發現代碼不過就那么幾句,而C++則不同,C++輸出數據時,使用了一種流的輸出方式,通常是調用 ostream 類里面的cin或者是cout,你可以把輸入輸出理解為小河流水,如下反匯編代碼,先來觀察一下輸出格式的變化。
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
int number =100 ;
int age = 33;
std::cin >> number >> age;
std::cout << "number: " << number << "age: "<< age << std::endl;
system("pause");
return 0;
}
cin接收參數看起來是這樣
00415ED4 | 8D4D F8 | lea ecx,dword ptr ss:[ebp-0x8] |
00415ED7 | 51 | push ecx |
00415ED8 | 8B0D A8004200 | mov ecx,dword ptr ds:[<&?cin@std@@3V?$basic_istream@DU?$char_traits@D@std@@@1@A>] | 獲取cin地址
00415EDE | FF15 A4004200 | call dword ptr ds:[<&??5?$basic_istream@DU?$char_traits@D@std@@@std@@QAEAAV01@AAH@Z>] | 第一次cin接收參數
00415EE4 | 3BFC | cmp edi,esp |
00415EE6 | E8 53B4FFFF | call 0x41133E |
00415EEB | 8BC8 | mov ecx,eax |
00415EED | FF15 A4004200 | call dword ptr ds:[<&??5?$basic_istream@DU?$char_traits@D@std@@@std@@QAEAAV01@AAH@Z>] | 第二次cin接收參數
00415EF3 | 3BF4 | cmp esi,esp |
00415EF5 | E8 44B4FFFF | call 0x41133E |
cout打印參數也是如此。。
00415F12 | 68 84CC4100 | push consoleapplication2.41CC84 | 41CC84:"number: "
00415F17 | 8B15 AC004200 | mov edx,dword ptr ds:[<&?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A>] | 獲取cout地址
00415F1D | 52 | push edx |
00415F1E | E8 94B3FFFF | call 0x4112B7 |
00415F23 | 83C4 08 | add esp,0x8 |
00415F26 | 8BC8 | mov ecx,eax |
00415F28 | FF15 98004200 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z>] | 輸出第一個參數
00415F2E | 3BDC | cmp ebx,esp |
00415F30 | E8 09B4FFFF | call 0x41133E |
00415F35 | 50 | push eax |
00415F36 | E8 7CB3FFFF | call 0x4112B7 |
00415F3B | 83C4 08 | add esp,0x8 |
00415F3E | 8BC8 | mov ecx,eax |
00415F40 | FF15 98004200 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z>] | 輸出第二個參數
00415F46 | 3BFC | cmp edi,esp |
00415F48 | E8 F1B3FFFF | call 0x41133E |
00415F4D | 8BC8 | mov ecx,eax |
00415F4F | FF15 94004200 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV0 | 結束cout
ida 也同樣可識別出來。
分析類的實現原理
在C語言中我們學習過結構體類型,其實C++中的類就是在結構體這個數據類型的基礎上衍生出來的,兩者之間的反匯編代碼幾乎一致,結構體和類都具有構造函數,析構函數和成員函數,但兩者之間還是會有細微的不同,首先結構體的訪問控制默認是Public,而類的訪問控制是Private,這些屬性是由編譯器在編譯的時候進行檢查和確認的,一旦編譯成功,程序在執行過程中就不會在訪問控制方面做任何檢查和限制了.
簡單地定義一個類: 首先我們來定義一個Student
學生類,然后賦予不同的屬性,最后調用內部的成員函數,觀察其反匯編代碼.
#include <iostream>
using namespace std;
class Student
{
public:
int number;
char *name;
private: void p_display()
{
std::cout << "name:" << name << std::endl;
}
public:void display()
{
p_display();
std::cout << "num: " << number << std::endl;
}
};
int main(int argc, char* argv[])
{
Student student;
student.number = 22;
student.name = "lyshark";
student.display();
system("pause");
return 0;
}
通過反匯編這段代碼,你會發現其實類這個東西並不存在於匯編層面,因為所謂的面向對象其實都是編譯器來幫你映射成相應的C語言代碼,換句話說,其實並不存在面向對象,你所寫的面向對象代碼最終都會被編譯器降級為C語言代碼,然后C代碼又會被降級為機器指令,而中間的轉換過程都是由編譯器來完成的,只不過面向對象更易於使用,但是其本質上還是C代碼.
008E12A6 | 68 E0198E00 | push <class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::en | main.cpp:27
008E12AB | 51 | push ecx | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12AC | 8B0D 54308E00 | mov ecx,dword ptr ds:[<&?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A>] | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12B2 | BA BC318E00 | mov edx,consoleapplication2.8E31BC | 8E31BC:"name:"
008E12B7 | E8 E4040000 | call <class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::op |
008E12BC | BA CC318E00 | mov edx,consoleapplication2.8E31CC | 8E31CC:"lyshark"
008E12C1 | 8BC8 | mov ecx,eax | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12C3 | E8 D8040000 | call <class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::op |
008E12C8 | 83C4 04 | add esp,0x4 |
008E12CB | 8BC8 | mov ecx,eax | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12CD | FF15 5C308E00 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV0 |
008E12D3 | 8B0D 54308E00 | mov ecx,dword ptr ds:[<&?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A>] | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12D9 | BA C4318E00 | mov edx,consoleapplication2.8E31C4 | 8E31C4:"num: "
008E12DE | 68 E0198E00 | push <class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::en |
008E12E3 | 6A 16 | push 0x16 |
008E12E5 | E8 B6040000 | call <class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::op |
008E12EA | 8BC8 | mov ecx,eax | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12EC | FF15 34308E00 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z>] |
008E12F2 | 8BC8 | mov ecx,eax | ecx:&"ALLUSERSPROFILE=C:\\ProgramData"
008E12F4 | FF15 5C308E00 | call dword ptr ds:[<&??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV0 |
008E12FA | 68 D4318E00 | push consoleapplication2.8E31D4 | main.cpp:29, 8E31D4:"pause"==L"慰獵e"
008E12FF | FF15 E0308E00 | call dword ptr ds:[<&system>] |
008E1305 | 83C4 04 | add esp,0x4
IDA 的識別更能說明這一點。
this指針的使用: this指針在本類中可以調用自身的數據成員與成員函數,this指針屬於指針類型,默認在32位環境中占用4字節的存儲空間,指針中保存了所屬對象的首地址,當訪問數據成員時則通過加偏移的方式移動指針,我們先來看一個結構體的定義.
struct this{
int m_int; // 在結構體內偏移是0
float m_float; // 在結構體內偏移是4
}
int main(int argc, char* argv[])
{
struct this test; // 假設結構體變量地址是0x401000
struct test *p_test = &test; // 定義結構體指針
printf("%p",*p_test->m_float); // 輸出指針地址
}
如上結構體定義所示,其中p_test
中保存的地址就是0x401000
,而m_float
在結構體中的偏移量為4,於是乎可以得出p_test->m_float
的實際地址應該是0x401000(結構首地址) + 4(元素偏移) = 0x401004
這就是結構體成員變量的尋址方式,接着我們來研究this指針.
#include <iostream>
using namespace std;
class Student
{
public:
int m_nInt;
int m_nInt_01;
int m_nInt_02;
public: void SetNumber(int nNum){
m_nInt = nNum;
m_nInt_01 = nNum;
m_nInt_02 = nNum;
printf("this 指針數據: %d \n", this->m_nInt);
printf("this1 指針數據: %d \n", this->m_nInt_01);
printf("this2 指針數據: %d \n", this->m_nInt_02);
}
};
int main(int argc, char* argv[])
{
Student stu;
stu.SetNumber(5);
printf("打印參數: %d\n", stu.m_nInt);
system("pause");
return 0;
}
首先編譯上方代碼,並從主函數開始分析,來觀察其匯編代碼是如何使用this指針進行參數傳遞的.
0041532E | 6A 05 | push 0x5 | 壓如參數 5
00415330 | 8D4D F0 | lea ecx,dword ptr ss:[ebp-0x10] | 取出對象stu的首地址(thiscall)
00415333 | E8 16BFFFFF | call 0x41124E | 調用 std::setnumber()
00415338 | 8BF4 | mov esi,esp | main.cpp:25
0041533A | 8B45 F0 | mov eax,dword ptr ss:[ebp-0x10] | 取對象首地址處4字節的數據m_nInt
0041533D | 50 | push eax |
0041533E | 68 BCCC4100 | push consoleapplication2.41CCBC | 41CCBC:"打印參數: %d\n"
00415343 | FF15 80014200 | call dword ptr ds:[<&printf>] | 打印參數
00415349 | 83C4 08 | add esp,0x8 |
進入call 0x41124E
,分析this指針傳遞,代碼中可看出,在使用thiscall
調用約定時,默認利用ECX寄存器保存對象首地址,並以寄存器傳遞的方式將this指針傳遞到成員函數中,所有成員函數默認都有隱藏的this參數,即指向自身成員類型的指針.
0041375C | 51 | push ecx | ecx中保存的就是對象this指針首地址
0041375D | 8DBD 34FFFFFF | lea edi,dword ptr ss:[ebp-0xCC] |
00413763 | B9 33000000 | mov ecx,0x33 |
00413768 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
0041376D | F3:AB | rep stosd |
0041376F | 59 | pop ecx | 還原ecx到源寄存器中
00413770 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | 該地址保存着調用對象首地址,this指針
00413773 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 取出this指針
00413776 | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 從堆棧中取出參數,並賦值到ecx
00413779 | 8908 | mov dword ptr ds:[eax],ecx | 賦值 this->m_nInt
0041377B | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.cpp:13
0041377E | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 從堆棧中取出參數,並賦值到ecx
00413781 | 8948 04 | mov dword ptr ds:[eax+0x4],ecx | 賦值 this->m_nInt_01
00413784 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.cpp:14
00413787 | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 從堆棧中取出參數,並賦值到ecx
0041378A | 8948 08 | mov dword ptr ds:[eax+0x8],ecx | 賦值 this->m_nInt_02
0041378D | 8BF4 | mov esi,esp | main.cpp:15, esi:__enc$textbss$end+27B
this指針通常情況下是由編譯器維護的,在成員函數中訪問數據成員也是通過this指針間接訪問的,這便是為什么在成員函數內可以直接使用數據成員的原因,在類中使用數據成員或成員函數,編譯器會在編譯時自動為我們添加上了this指針.
class Student{
private:
int m_nInt;
public:void GetNumber(){
// 此處的m_nInt 其實默認隱藏了this指針,完整寫法應該是 this->m_nInt;
return m_nInt;
}
public:void Display(){
// 此處的GetNumber 其實等於 this->GetNumber();
printf("%d\n",GetNumber());
}
}
如下代碼: 在VS環境下,識別this指針的關鍵就在於函數調用的第一個參數是使用ECX寄存器傳遞,而並非通過棧頂傳遞,並且在ECX寄存器中保存的數據就是該對象的首地址,成員函數SetNumber
的調用方式為thiscall
該方式的平棧是由調用者維護的,該調用方式並不屬於預定義關鍵字,它是C++中成員函數特有的調用方式,在C語言中並不存在這種調用約定.
0041532E | 6A 05 | push 0x5 | 壓如參數 5
00415330 | 8D4D F0 | lea ecx,dword ptr ss:[ebp-0x10] | 取出對象stu的首地址(thiscall)
00415333 | E8 16BFFFFF | call 0x41124E | 調用 std::setnumber()
由於C++環境下thiscall不屬於關鍵字,因此函數無法被顯示聲明為thiscall調用方式,而類的成員函數默認是thiscall調用方式,所以在分析過程中,如果發現call 0x41124E
的上方第一個參數是lea ecx,dword ptr ss:[ebp-0x10]
那么我們可以猜測,該函數很有可能就是某個類中的成員函數.
靜態數據成員: 靜態數據成員與靜態變量原理相同,因此靜態數據成員的初始值也會被編譯到文件中,當程序加載時,系統會將可執行文件讀入內存,此時順帶着靜態數據成員也已經裝入到了內存,就算你還沒有實例化對象,其依然會被初始化.
靜態數據成員是全局性的,與類的關系不大,因為靜態數據成員有此特性,所以當我們計算類和對象的長度時,靜態數據成員並不會被計算在其中,如下代碼我們定義了兩個數據成員其長度是8字節,那么當我們輸出類的長度時只會顯示出8字節,至於靜態數據成員m_static_Int
則被忽略計算了,因為他在全局數據區,並不屬於類中的變量.
#include <iostream>
using namespace std;
class CStatic
{
public:
int m_Int; // 占用4字節
int x_Int; // 占用4字節
static int m_static_Int; // 不會占用類空間
};
int CStatic::m_static_Int = 1;
int main(int argc, char* argv[])
{
CStatic cs;
cs.m_Int = 2;
cs.x_Int = 3;
int nSize = sizeof(cs); // 顯示出類的長度
printf("類中成員大小: %d\n", nSize);
printf("靜態數據成員內存地址: %x\n", &cs.m_static_Int);
printf("普通數據成員內存地址: %x %x \n", &cs.m_Int,&cs.x_Int);
system("pause");
return 0;
}
編譯並調試匯編代碼,可發現靜態數據成員是一個常量值可以任意訪問,而普通數據成員則是通過棧空間傳遞的,所以在成員函數中使用這兩種數據成員時,靜態數據成員屬於全局變量,並且不屬於任何對象,因此訪問也無需使用this指針,而普通的數據成員屬於對象所有,訪問時默認會隱藏使用this指針,來看如下代碼清單:
#include <iostream>
using namespace std;
class CStatic
{
public:
int m_Int; // 普通數據成員:占用4字節
static int m_static_Int; // 靜態數據成員:不會占用類空間
public: void ShowNumber()
{
printf("普通數據成員: %d --> 靜態數據成員: %d \n", m_Int, m_static_Int);
}
};
int CStatic::m_static_Int = 1;
int main(int argc, char* argv[])
{
CStatic cs;
cs.m_Int = 2;
cs.ShowNumber();
system("pause");
return 0;
}
我們將上面的代碼反匯編一下,主要來看void ShowNumber()
函數內部是如何調用數據成員的,我們可以看到靜態數據成員在反匯編代碼中其展示形態與全局變量完全相同打印方式也與全局變量一致,而普通數據成員則需要使用mov ecx,dword ptr ss:[ebp-0x8]
獲取到this指針才可以輸出.
002137B3 | 8BF4 | mov esi,esp | main.cpp:11
002137B5 | A1 00F02100 | mov eax,dword ptr ds:[<public: static int CStatic::m_static_Int>] | 直接訪問靜態數據成員
002137BA | 50 | push eax | 第一個參數壓棧
002137BB | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | 獲取this指針
002137BE | 8B11 | mov edx,dword ptr ds:[ecx] | 通過this指針訪問數據成員
002137C0 | 52 | push edx | edx:"樾K"
002137C1 | 68 6CCC2100 | push consoleapplication2.21CC6C | 輸出兩個參數
002137C6 | FF15 80012200 | call dword ptr ds:[<&printf>] |
002137CC | 83C4 0C | add esp,0xC |
對象作為函數參數傳遞: 對象作為函數參數時,其傳遞過程與數組不同,數組變量的名稱就代表了數組的首地址,而對象變量名稱卻無法代表對象的首地址,傳參時不會像數組那樣以首地址作為參數傳遞,而是先將對象中的所有數據進行復制,然后將復制的數據作為形參傳遞到調用函數中使用.
默認情況下在32位系統中所有的數據類型都不會超過4字節大小,使用一個棧元素即可完成數據的復制和傳遞,而類對象是自定義數據類型,是除了自身以外的所有數據類型的集合,各個對象的長度不定,對象在傳遞的過程中是如何被復制和傳遞的呢,我們來分析如下代碼:
#include <iostream>
using namespace std;
class CFunction
{
public:
int m_nOne;
int m_nTwo;
};
void ShowFunction(CFunction fun)
{
printf("%d -->%d\n", fun.m_nOne, fun.m_nTwo);
}
int main(int argc, char* argv[])
{
CFunction fun;
fun.m_nOne = 1;
fun.m_nTwo = 2;
ShowFunction(fun);
system("pause");
return 0;
}
編譯這段代碼並反匯編,然后來到main函數,可看出編譯器在調用函數傳參的過程中分別將對象的兩個成員依次壓棧,也就是直接將兩個數據成員當成兩個int類型的數據,並將它們復制一份壓入堆棧存儲,這里壓棧的兩個參數雖然數值相等,但是因為是變量復制,所以它與對象中的兩個數據成員是沒有任何關系的,然后直接調用call 0xC31276
過程完成參數傳遞.
int main(int argc, char* argv[])
00C3530E | C745 F4 01000000 | mov dword ptr ss:[ebp-0xC],0x1 | 第一個參數
00C35315 | C745 F8 02000000 | mov dword ptr ss:[ebp-0x8],0x2 | 第二個參數
00C3531C | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.cpp:22
00C3531F | 50 | push eax | 壓棧存儲第一個參數 [ebp-0x8]
00C35320 | 8B4D F4 | mov ecx,dword ptr ss:[ebp-0xC] | [ebp-C]:__enc$textbss$end+FF
00C35323 | 51 | push ecx | 壓棧存儲第二個參數 [ebp-0c]
00C35324 | E8 4DBFFFFF | call 0xC31276 | 調用ShowFunction
00C35329 | 83C4 08 | add esp,0x8 | 堆棧平衡
void ShowFunction(CFunction fun)
00C337B0 | 8B45 0C | mov eax,dword ptr ss:[ebp+0xC] | 從堆棧中去除第一個參數
00C337B3 | 50 | push eax |
00C337B4 | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 從堆棧中去除第二個參數
00C337B7 | 51 | push ecx |
00C337B8 | 68 6CCCC300 | push consoleapplication2.C3CC6C | C3CC6C:"%d -->%d\n"
00C337BD | FF15 8001C400 | call dword ptr ds:[<&printf>] |
00C337C3 | 83C4 0C | add esp,0xC |
類對象中的數據成員的傳參順序為: 最先定義的數據成員最后壓棧,最后定的數據成員最先壓棧,當類的體積過大,或者其中定義有數組類型的成員時,那么參數傳遞就會發生一些變化,此時將變為含有數組數據成員的對象傳參
,如下代碼:
#include <iostream>
using namespace std;
class CFunction
{
public:
int m_nOne;
int m_nTwo;
char m_nName[32]; // 增加一個數組類型的變量,來觀察其發生的尋址變化
};
void ShowFunction(CFunction fun)
{
printf("%d -->%d --> %s \n", fun.m_nOne, fun.m_nTwo,fun.m_nName);
}
int main(int argc, char* argv[])
{
CFunction fun;
fun.m_nOne = 1;
fun.m_nTwo = 2;
strcpy(fun.m_nName, "lyshark");
ShowFunction(fun);
system("pause");
return 0;
}
通過反匯編觀察發現,其數組的傳遞方式依然使用的是首地址指針的傳遞,這一點與數組傳參道理是相同的.
int main(int argc, char* argv[])
00085325 | 8945 FC | mov dword ptr ss:[ebp-0x4],eax | 第三個參數: 就是 m_nName[32];
00085328 | C745 D0 01000000 | mov dword ptr ss:[ebp-0x30],0x1 | 第二個參數
0008532F | C745 D4 02000000 | mov dword ptr ss:[ebp-0x2C],0x2 | 第一個參數
00085336 | 68 84CC0800 | push consoleapplication2.8CC84 | push 字符串 "lyshark"
0008533B | 8D45 D8 | lea eax,dword ptr ss:[ebp-0x28] |
0008533E | 50 | push eax | 指向 字符串 "lyshark"
0008533F | E8 29BEFFFF | call 0x8116D | 調用 strcpy
00085344 | 83C4 08 | add esp,0x8 | 調整棧頂
00085347 | 83EC 28 | sub esp,0x28 | main.cpp:24
0008534A | B9 0A000000 | mov ecx,0xA | 設置填充大小,循環十次
0008534F | 8D75 D0 | lea esi,dword ptr ss:[ebp-0x30] | 獲取對象的首地址並保存到esi中
00085352 | 8BFC | mov edi,esp | 設置edi為當前棧頂
00085354 | F3:A5 | rep movsd |
00085356 | E8 20BFFFFF | call 0x8127B |
0008535B | 83C4 28 | add esp,0x28 |
void ShowFunction(CFunction fun)
000837C0 | 8D45 10 | lea eax,dword ptr ss:[ebp+0x10] | 取 m_nName[32];
000837C3 | 50 | push eax | eax:"lyshark"
000837C4 | 8B4D 0C | mov ecx,dword ptr ss:[ebp+0xC] | 取 m_nTwo;
000837C7 | 51 | push ecx |
000837C8 | 8B55 08 | mov edx,dword ptr ss:[ebp+0x8] | 取 m_nOne;
000837CB | 52 | push edx |
000837CC | 68 6CCC0800 | push consoleapplication2.8CC6C | 8CC6C:"%d -->%d --> %s \n"
000837D1 | FF15 84010900 | call dword ptr ds:[<&printf>] |
000837D7 | 83C4 10 | add esp,0x10 |
對象作為函數參數傳遞(新): 對象作為函數參數時,其傳遞過程與數組不同,數組變量的名稱就代表了數組的首地址,而對象變量名稱卻無法代表對象的首地址,傳參時不會像數組那樣以首地址作為參數傳遞,而是先將對象中的所有數據進行復制,然后將復制的數據作為形參傳遞到調用函數中使用.
#include <iostream>
using namespace std;
class CFunction
{
public:
int m_nOne;
int m_nTwo;
char m_nName[32];
};
void ShowFunction(CFunction fun) // 此處就是對象作為函數參數
{
printf("%d -->%d --> %s \n", fun.m_nOne, fun.m_nTwo,fun.m_nName);
}
int main(int argc, char* argv[])
{
CFunction fun;
fun.m_nOne = 1;
fun.m_nTwo = 2;
strcpy(fun.m_nName, "lyshark");
ShowFunction(fun);
system("pause");
return 0;
}
編譯代碼並反匯編,然后來到Main函數,首先我們對類中的數據成員依次賦值,前兩個是整數類型則直接賦值即可,最后一個char m_nName[32];
因為是數組類型,則需要通過lea取出其首地址然后將該地址壓入堆棧,當數據被初始化完成以后,則開始調用ShowFunction(fun);
函數,調用之前先來sub esp,0x28
開辟局部空間,之所以需要開辟0x28的空間是因為類的大小是int + int + char[32] = 0x28
,通過調用rep movsd
指令將參數拷貝到sub esp,0x28
的堆棧中.
004152CE | C745 D4 01000000 | mov dword ptr ss:[ebp-0x2C],0x1 | 第一個參數:int m_nOne;
004152D5 | C745 D8 02000000 | mov dword ptr ss:[ebp-0x28],0x2 | 第二個參數:int m_nTwo;
004152DC | 68 84CC4100 | push consoleapplication2.41CC84 | push 字符串 "lyshark"
004152E1 | 8D45 DC | lea eax,dword ptr ss:[ebp-0x24] | 第三個參數: char m_nName[32];
004152E4 | 50 | push eax | 將拷貝地址壓棧
004152E5 | E8 83BEFFFF | call 0x41116D | 調用 strcpy(dst,src)
004152EA | 83C4 08 | add esp,0x8 | 拷貝結束后,調整堆棧
004152ED | 83EC 28 | sub esp,0x28 | 開辟局部空間,與類CFunction數據成員一致
004152F0 | B9 0A000000 | mov ecx,0xA | 設置填充大小
004152F5 | 8D75 D4 | lea esi,dword ptr ss:[ebp-0x2C] | 取出類CFunction數據成員首地址
004152F8 | 8BFC | mov edi,esp | 當前堆棧棧幀給EDI
004152FA | F3:A5 | rep movsd | 拷貝到臨時空間里
004152FC | E8 7ABFFFFF | call 0x41127B | 調用CFunction函數
00415301 | 83C4 28 | add esp,0x28 | 堆棧平衡
接着我們繼續跟進到call 0x41127B
過程中,由於在Main函數中我們已經將類對象的數據成員全部壓入堆棧保存了,所以在內部過程中只需要通過ebp+0x
的方式即可找到傳遞過來的參數.
00413780 | 8D45 10 | lea eax,dword ptr ss:[ebp+0x10] | 取出三個參數 char m_nName[32];
00413783 | 50 | push eax |
00413784 | 8B4D 0C | mov ecx,dword ptr ss:[ebp+0xC] | 取出第二個參數 int m_nTwo;
00413787 | 51 | push ecx |
00413788 | 8B55 08 | mov edx,dword ptr ss:[ebp+0x8] | 取出第一個參數 int m_nOne;
0041378B | 52 | push edx |
0041378C | 68 6CCC4100 | push consoleapplication2.41CC6C | 41CC6C:"%d -->%d --> %s \n"
00413791 | FF15 84014200 | call dword ptr ds:[<&printf>] |
00413797 | 83C4 10 | add esp,0x10 |
對象作為返回值傳遞: 當對象作為函數參數返回時,我們並不能通過EAX寄存器返回,因為對象是一個復雜的數據結構,顯然寄存器EAX無法保存對象中的所有數據,所以在函數返回時,寄存器EAX不能滿足需求.
對象作為返回值與對象作為參數的處理方式類似,對象作為參數時,進入函數前預先將對象使用的棧空間保留出來,並將實參對象中的數據復制到棧空間中,該棧空間作為函數參數,用於函數內部的使用.
對象作為返回值時,進入函數后將申請返回對象使用的棧空間,在退出函數時,將返回對象中的數據復制到臨時的棧空間中,以這個臨時棧空間的首地址作為返回值返回給上層函數使用,首先編譯如下代碼,我來給大家解釋這段話的意思:
#include <iostream>
using namespace std;
class CReturn
{
public:
int m_nNumber; // 占用4字節
int m_nArray[10]; // 占用 4*10 = 40 字節
}; // 該類總大小為44字節
CReturn GetCReturn(){
CReturn RetObj;
RetObj.m_nNumber = 0;
for (int x = 0; x < 10; x++)
{
RetObj.m_nArray[x] = x + 1;
}
return RetObj; // 此處返回一個對象
}
int main(int argc, char* argv[])
{
CReturn obj;
obj = GetCReturn();
printf("類的首地址: 0x%x\n", &obj);
for (int x = 0; x < 10; x++)
{
printf("輸出元素: %d \n", obj.m_nArray[x]);
}
system("pause");
return 0;
}
將上方代碼反匯編觀察,此處我們只關心GetCReturn()
函數調用后的返回部分,首先GetCReturn函數的內部定義了一個CReturn RetObj;
類,只要有這樣的定義其默認都會在編譯時自動分配 sub esp,0x104
一段堆棧空間,其次當內層GetCReturn
函數執行完畢以后,返回到上層Main
函數之前會將內層類的堆棧數據自動填充到外層堆棧中,然后將外層堆棧的首地址作為指針傳遞到EAX寄存器中,以此來實現類數據成員的傳遞,此處可能不太好理解,其實就是內部類的數據運算完畢以后會直接拷貝到外部類的堆棧空間中,外部類則直接遍歷自己的堆棧空間就可以知道內部類的執行結果,從而實現結構的傳遞.
int main(int argc, char* argv[])
00415350 | 55 | push ebp | main.cpp:22
00415351 | 8BEC | mov ebp,esp |
00415353 | 81EC 6C010000 | sub esp,0x16C | 提前分配的棧空間
..........
0041537F | E8 28C0FFFF | call 0x4113AC | 調用GetCReturn函數
00415384 | 83C4 04 | add esp,0x4 |
00415387 | B9 0B000000 | mov ecx,0xB | 外部設置填充大小
0041538C | 8BF0 | mov esi,eax | 設置ESI源指針
0041538E | 8DBD 98FEFFFF | lea edi,dword ptr ss:[ebp-0x168] | 拷貝到EDI里面
00415394 | F3:A5 | rep movsd | 開始填充臨時空間 GetCReturn()
00415396 | B9 0B000000 | mov ecx,0xB | B:'\v'
0041539B | 8DB5 98FEFFFF | lea esi,dword ptr ss:[ebp-0x168] | 最后將臨時空間里的內容取出
004153A1 | 8D7D CC | lea edi,dword ptr ss:[ebp-0x34] | 拷貝到EDI里面
004153A4 | F3:A5 | rep movsd | 相當於: obj = GetCReturn();
004153A6 | 8BF4 | mov esi,esp | main.cpp:25
004153A8 | 8D45 CC | lea eax,dword ptr ss:[ebp-0x34] | 獲取到類返回值
004153AB | 50 | push eax |
004153AC | 68 6CCC4100 | push consoleapplication2.41CC6C | 41CC6C:"類的首地址: 0x%x\n"
004153B1 | FF15 80014200 | call dword ptr ds:[<&printf>] |
004153B7 | 83C4 08 | add esp,0x8 |
CReturn GetCReturn()
00413790 | 55 | push ebp | main.cpp:11
00413791 | 8BEC | mov ebp,esp |
00413793 | 81EC 04010000 | sub esp,0x104 | 提前分配的棧空間
..........
004137E6 | B9 0B000000 | mov ecx,0xB | 設置填充大小
004137EB | 8D75 CC | lea esi,dword ptr ss:[ebp-0x34] | 取出計算出來的結果的首地址
004137EE | 8B7D 08 | mov edi,dword ptr ss:[ebp+0x8] | 取出上層堆棧類的首地址
004137F1 | F3:A5 | rep movsd | 開始覆蓋
004137F3 | 8B45 08 | mov eax,dword ptr ss:[ebp+0x8] | 獲取到分配后的首地址
004137F6 | 52 | push edx | main.cpp:19
004137F7 | 8BCD | mov ecx,ebp |
004137F9 | 50 | push eax | 保存外層類首地址
在調用GetCReturn()
函數之前,編譯器將在Main函數中提前申請了一塊用於存儲返回對象的空間,接着我們在GetCReturn()
函數內部定義了CReturn RetObj;
對象,當GetCReturn
函數調用結束后會進行數據復制,將GetCReturn
函數中定義的局部對象RetObj
中的數據復制到外部的CReturn obj
對象的空間中,然后將外層堆棧的首地址作為指針傳遞到EAX寄存器中,外層的Main
函數接收到這個EAX寄存器指針,則可以拿着該指針遍歷到類中的所有數據成員.
嗯。。。表述的不太清晰,再來表述一遍。。。。。。。。。。。
如下是Main函數代碼,在調用GetCReturn()
函數之前,編譯器將在Main函數中提前申請sub esp,0x16C
一段堆棧空間,用於存儲返回對象的成員,接着主函數開始調用call 0x4113AC
並根據調用約定將返回對象中數據成員的指針放到EAX並將源指針ESI也指向EAX,然后設置目標EDI指針,最后執行rep movsd
將GetCReturn
函數堆棧中的數據拷貝到Main函數的堆棧中,之所以需要復制一份是因為,我們的成員函數在執行完畢后就返回了堆棧會被釋放,無法保證返回值所指向地址的數據的正確性,所以需要在外部保存一份.
00415350 | 55 | push ebp | main.cpp:22
00415351 | 8BEC | mov ebp,esp |
00415353 | 81EC 6C010000 | sub esp,0x16C | 提前分配的棧空間
.........| ............. | .............. |
0041537F | E8 28C0FFFF | call 0x4113AC | 調用GetCReturn函數
00415384 | 83C4 04 | add esp,0x4 | 平衡參數
00415387 | B9 0B000000 | mov ecx,0xB | 設置填充大小
0041538C | 8BF0 | mov esi,eax | 設置ESI源指針
0041538E | 8DBD 98FEFFFF | lea edi,dword ptr ss:[ebp-0x168] | 拷貝到EDI里面
00415394 | F3:A5 | rep movsd | 開始填充臨時空間 GetCReturn()
00415396 | B9 0B000000 | mov ecx,0xB |
0041539B | 8DB5 98FEFFFF | lea esi,dword ptr ss:[ebp-0x168] | 最后將臨時空間里的內容取出
004153A1 | 8D7D CC | lea edi,dword ptr ss:[ebp-0x34] | 拷貝到EDI指針里面
004153A4 | F3:A5 | rep movsd | 相當於調用: obj = GetCReturn();
004153A6 | 8BF4 | mov esi,esp | main.cpp:25
004153A8 | 8D45 CC | lea eax,dword ptr ss:[ebp-0x34] | 獲取到obj的首地址指針
004153AB | 50 | push eax |
004153AC | 68 6CCC4100 | push consoleapplication2.41CC6C | 41CC6C:"類的首地址: 0x%x\n"
004153B1 | FF15 80014200 | call dword ptr ds:[<&printf>] |
004153B7 | 83C4 08 | add esp,0x8 |
接着我們再來看一下GetCReturn()
函數的內部做了什么,首先我們在代碼中同樣定義了CReturn RetObj;
對象,所以其也會通過sub esp,0x104
分配臨時棧空間,接着就是運算並填充這段空間,最后將計算后的結果拷貝到上層Main函數提前申請的sub esp,0x16C
堆棧中,最后內層返回EAX,該寄存器里面存放的就是外層數據成員的首地址.
00413790 | 55 | push ebp | main.cpp:11
00413791 | 8BEC | mov ebp,esp |
00413793 | 81EC 04010000 | sub esp,0x104 | 提前分配的棧空間
.........| ............. | .............. |
004137E6 | B9 0B000000 | mov ecx,0xB | 設置填充大小
004137EB | 8D75 CC | lea esi,dword ptr ss:[ebp-0x34] | 取出計算出來的結果的首地址
004137EE | 8B7D 08 | mov edi,dword ptr ss:[ebp+0x8] | 取出main函數中堆棧類的首地址
004137F1 | F3:A5 | rep movsd | 開始覆蓋
004137F3 | 8B45 08 | mov eax,dword ptr ss:[ebp+0x8] | 獲取到分配后的首地址
004137F6 | 52 | push edx | main.cpp:19
004137F7 | 8BCD | mov ecx,ebp |
004137F9 | 50 | push eax | 保存外層類數據成員的首地址,最后返回
在GetCReturn()
函數內部定義了CReturn RetObj;
對象,當GetCReturn
函數調用結束后會進行數據復制,將GetCReturn
函數中定義的局部對象RetObj
中的數據復制到外部的CReturn obj
對象的空間中,然后將外層堆棧的首地址作為指針傳遞到EAX寄存器中,外層的Main
函數接收到這個EAX寄存器指針,則可以拿着該指針遍歷到類中的所有數據成員.
此處我們只關心GetCReturn()
函數調用后的返回部分,首先GetCReturn函數的內部定義了一個CReturn RetObj;
類,只要有這樣的定義其默認都會在編譯時自動分配 sub esp,0x104
一段堆棧空間,其次當內層GetCReturn
函數執行完畢以后,返回到上層Main
函數之前會將內層類的堆棧數據自動填充到外層堆棧中,然后將外層堆棧的首地址作為指針傳遞到EAX寄存器中,以此來實現類數據成員的傳遞,此處可能不太好理解,其實就是內部類的數據運算完畢以后會直接拷貝到外部類的堆棧空間中,外部類則直接遍歷自己的堆棧空間就可以知道內部類的執行結果,從而實現結構的傳遞.
分析構造/析構函數
構造函數與析構函數是類的重要組成部分,其中構造函數主要用於在對象創建時對數據成員的初始化工作,析構函數則主要負責在對象銷毀后釋放對象中所申請的各種資源,構造函數與析構函數都是類中特殊的成員函數,構造函數支持傳參且返回值是對象首地址,析構函數則不能傳遞任何參數且不能定義返回值.
局部對象(構造函數): 局部對象下的構造函數的出現時機很好識別,當對象產生時,就會自動的觸發構造函數,編譯器隱藏了這一過程,我們可以編寫一個簡單地案例,來逆向分析其中的奧秘.
#include <iostream>
using namespace std;
class CFunction
{
public:
int x_pos;
int y_pos;
public: CFunction(){
x_pos = 10;
y_pos = 20;
}
};
int main(int argc, char* argv[])
{
CFunction num;
printf("X坐標: %d Y坐標: %d", num.x_pos,num.y_pos);
system("pause");
return 0;
}
反匯編后觀察以下核心代碼,可以看到當進入Main函數時,首先執行的就是取出對象的首地址,並調用了call 0x4111FE
構造函數,然后進入到函數內部,因為構造函數也是成員函數,所以會通過pop ecx
取出this指針,構造函數調用結束后,會將this指針作為返回值mov eax,dword ptr ss:[ebp-0x8]
返回到上層Main函數中,所以說返回this指針就是構造函數的特征.
int main(int argc, char* argv[])
0041529E | 8D4D F4 | lea ecx,dword ptr ss:[ebp-0xC] | 取對象首地址
004152A1 | E8 58BFFFFF | call 0x4111FE | 調用構造函數
004152A6 | 8BF4 | mov esi,esp | main.cpp:19, esi:__enc$textbss$end+27B
004152A8 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 輸出X坐標
004152AB | 50 | push eax |
004152AC | 8B4D F4 | mov ecx,dword ptr ss:[ebp-0xC] | 輸出Y坐標
004152AF | 51 | push ecx |
004152B0 | 68 6CCC4100 | push consoleapplication2.41CC6C | 41CC6C:"X坐標: %d Y坐標: %d"
004152B5 | FF15 80014200 | call dword ptr ds:[<&printf>] |
004152BB | 83C4 0C | add esp,0xC |
public: CFunction()
0041301F | 59 | pop ecx | 恢復對象首地址
00413020 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx |
00413023 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.cpp:11
00413026 | C700 0A000000 | mov dword ptr ds:[eax],0xA | 將x_pos設置為10
0041302C | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.cpp:12
0041302F | C740 04 14000000 | mov dword ptr ds:[eax+0x4],0x14 | 將y_pos設置為20
00413036 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 將this指針存入eax返回
00413039 | 5F | pop edi |
0041303A | 5E | pop esi | esi:__enc$textbss$end+27B
0041303B | 5B | pop ebx |
0041303C | 8BE5 | mov esp,ebp |
0041303E | 5D | pop ebp |
0041303F | C3 | ret |
接着我們在上面代碼基礎上給構造函數傳遞三個參數,將C代碼稍微修改以下,編譯並觀察其發生的變化.
#include <iostream>
using namespace std;
class CFunction
{
public:
int x_pos;
int y_pos;
char *string;
public: CFunction(int x,int y,char *str){
x_pos = x;
y_pos = y;
strcpy(string, str);
}
};
int main(int argc, char* argv[])
{
CFunction num1(10,20,"admin");
printf("X坐標: %d Y坐標: %d 字符串: %s \n", num1.x_pos, num1.y_pos,num1.string);
CFunction num2(100, 200,"lyshark");
printf("X坐標: %d Y坐標: %d 字符串: %s \n", num2.x_pos, num2.y_pos,num2.string);
system("pause");
return 0;
}
堆對象(構造函數): 堆對象空間的申請需要使用malloc,new
等函數,例如我們可以CNumber *pNumber = new CNumber;
來申請類型為Cnumber
類的一個對象,使用指針PNumber
保存對象首地址,我們來看以下代碼做具體的分析.
#include <iostream>
using namespace std;
class CNumber
{
public:
int x_pos;
int y_pos;
public:CNumber(){ // 定義無參構造函數
this->x_pos = 10; // 對函數賦值
this->y_pos = 20;
printf("X_pos: %d Y_pos: %d \n", this->x_pos,this->y_pos);
}
};
int main(int argc, char* argv[])
{
CNumber *pNumber = NULL; // 定義一個指向CNumber的指針
pNumber = new CNumber; // 初始化堆變量
pNumber->x_pos = 100;
printf("PNumber X: %d PNumber Y: %d \n", pNumber->x_pos, pNumber->y_pos);
system("pause");
return 0;
}
觀察反匯編代碼,首先我們通過call 0x41137F
函數申請一段堆空間,該new函數有一個參數push 0x8
這里的8字節是CNumber
類的數據成員總大小,分配完成以后會執行je 0x41532D
判斷是否分配成功,成功則執行call 0x4111B8
構造函數並返回對象的首地址,失敗則不會執行構造函數並將指針填充為0,我們可以通過是否存在雙分支來推測構造函數的具體位置.
004152F3 | C745 EC 00000000 | mov dword ptr ss:[ebp-0x14],0x0 | 此處就是指針 CNumber *pNumber 初始化為0
004152FA | 6A 08 | push 0x8 | 壓入類的大小,用於堆內存申請
004152FC | E8 7EC0FFFF | call 0x41137F | 調用 new 分配臨時空間
00415301 | 83C4 04 | add esp,0x4 |
00415304 | 8985 20FFFFFF | mov dword ptr ss:[ebp-0xE0],eax | 使用臨時變量保存new返回值
0041530A | C745 FC 00000000 | mov dword ptr ss:[ebp-0x4],0x0 | 保存申請堆空間的次數
00415311 | 83BD 20FFFFFF 00 | cmp dword ptr ss:[ebp-0xE0],0x0 | 檢測堆是否分配成功
00415318 | 74 13 | je 0x41532D | 失敗則跳過構造函數的執行
0041531A | 8B8D 20FFFFFF | mov ecx,dword ptr ss:[ebp-0xE0] | 成功,則將對象首地址傳入ECX中
00415320 | E8 93BEFFFF | call 0x4111B8 | 調用構造函數 CNumber
00415325 | 8985 0CFFFFFF | mov dword ptr ss:[ebp-0xF4],eax | 構造函數返回的this指針
0041532B | EB 0A | jmp 0x415337 |
0041532D | C785 0CFFFFFF 00000000 | mov dword ptr ss:[ebp-0xF4],0x0 | 申請失敗,將指針設置為0
00415337 | 8B85 0CFFFFFF | mov eax,dword ptr ss:[ebp-0xF4] | 取出構造函數返回的this指針
局部對象(析構函數): 析構函數的作用就是在對象執行完畢以后完成一定的清理任務,通常析構函數只出現在C++語言中,且析構函數不能接收參數也不能返回數據,接下來我們來探索一下析構函數的匯編形態.
#include <iostream>
using namespace std;
class CFunction
{
public:
int x_pos;
int y_pos;
CFunction(int x,int y){
x_pos = x;
y_pos = y;
printf("構造函數執行: %d \n", x_pos);
}
~CFunction(){
printf("調用析構函數.");
}
};
int main(int argc, char* argv[])
{
CFunction num(10,20);
return 0;
}
相比於構造函數來說析構函數的執行就更加簡單了,析構函數不存在任何參數傳遞更不會返回任何值,並且會在類的最后面被執行,尋找析構函數只需要照着這兩點找一般都會准確地被定位到,需要注意析構函數雖然沒參數傳遞,但是this指針還是會存在的.
0041534E | 6A 14 | push 0x14 | main.cpp:22
00415350 | 6A 0A | push 0xA |
00415352 | 8D4D F4 | lea ecx,dword ptr ss:[ebp-0xC] | ecx:__enc$textbss$end+27B
00415355 | E8 0EBEFFFF | call 0x411168 | 本類中的構造函數
0041535A | C785 28FFFFFF 00000000 | mov dword ptr ss:[ebp-0xD8],0x0 | main.cpp:23
00415364 | 8D4D F4 | lea ecx,dword ptr ss:[ebp-0xC] | ecx:__enc$textbss$end+27B
00415367 | E8 A8BCFFFF | call 0x411014 | 本類中的析構函數
堆對象(析構函數): 上方構造函數中我們說過,可以使用new
申請堆空間,如果我們需要釋放這段堆空間則需要使用delete
關鍵字來完成,我們先來看看釋放堆空間前調用析構函數的過程.
#include <iostream>
using namespace std;
class CFunction
{
public:
int x_pos;
int y_pos;
};
int main(int argc, char* argv[])
{
CFunction *pNumber = NULL;
pNumber = new CFunction();
pNumber->x_pos = 10;
pNumber->y_pos = 20;
printf("傳遞參數: %d \n", pNumber->x_pos);
if (pNumber != NULL){
delete pNumber;
pNumber = NULL;
}
return 0;
}
當堆對象使用完以后需要手動的釋放這段空間,通常是獲取到堆對象的首地址,然后通過調用delete
函數完成對堆對象的釋放,如下是反匯編代碼.
004152D6 | 837D F8 00 | cmp dword ptr ss:[ebp-0x8],0x0 | 判斷堆是否分配成功
004152DA | 74 1F | je 0x4152FB |
004152DC | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 取堆空間首地址
004152DF | 8985 2CFFFFFF | mov dword ptr ss:[ebp-0xD4],eax |
004152E5 | 8B8D 2CFFFFFF | mov ecx,dword ptr ss:[ebp-0xD4] | 將堆空間首地址放入ecx
004152EB | 51 | push ecx | ecx:__enc$textbss$end+276
004152EC | E8 36BEFFFF | call 0x411127 | 執行代理函數,釋放堆
004152F1 | 83C4 04 | add esp,0x4 |
004152F4 | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | pNumber = NULL;
多態與虛函數
多態性是面向對象的重要組成部分,利用多態可以設計和實現易於擴展的程序,所謂多態顧名思義就是一個類函數有多重形態,在C++中多態的意思是,具有不同功能的函數可以用同一個函數名,實現使用一個函數名調用不同內容的函數,從而返回不同的結果,這就是多態性,從系統實現的角度來分析,多態性可分為兩類,靜態多態與動態多態:
靜態多態: 通常是通過函數或運算法重載實現的,靜態多態性又稱作編譯時的多態性.
動態多態: 動態多態性不在編譯時確定調用函數的功能,而是通過虛函數實現,它又被叫做運行時的多態性.
多數情況下靜態多態使用不多,我們主要關注動態多態性,在C++中使用關鍵字virtual
聲明函數為虛函數,當類中定義有虛函數時,編譯器會將該類中所有虛函數的首地址保存在一張地址表中,這張地址表被稱為虛函數表
,同時編譯器還會在類中添加一個隱藏數據成員,稱為虛表指針
,該指針中保存着虛表的首地址,用於記錄和查找虛函數.
#include <iostream>
using namespace std;
class CVirtual
{
private: int m_Number; // 整數占用4字節
public: virtual int GetNumber(){
return m_Number;
}
public: virtual void SetNumber(int num){
m_Number = num;
}
};
int main(int argc, char* argv[])
{
CVirtual cv;
return 0;
}
如上代碼片段中,我們只定義了一個m_number
的數據成員,如果此時該類中沒有定義虛函數的情況下,則類的大小為4字節,但如果我們定義了虛函數,那么編譯器會自動為我們增加一個隱藏的數據成員來用作虛表指針,因此該類的大小將變為8字節,虛表指針所指向的就是虛函數的指針數組,里面放着所有虛函數的首地址.,而這一切對程序員來說都是透明的,他們被蒙蔽了雙眼。
int main(int argc, char* argv[])
0041532E | 8D4D F4 | lea ecx,dword ptr ss:[ebp-0xC] | 取對象首地址
00415331 | E8 B2C0FFFF | call 0x4113E8 | 調用默認構造函數
00415336 | 33C0 | xor eax,eax | main.cpp:22
class CVirtual --> call 0x4113E8
0041301F | 59 | pop ecx | 還原對象指針
00413020 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | 存儲this指針
00413023 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 取出this指針,並作為虛表首地址
00413026 | C700 70CC4100 | mov dword ptr ds:[eax],<consoleapplic | 取出虛函數指針,放入變量中存儲
0041302C | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:"p藺"
0041302F | 5F | pop edi |
觀察上面的反匯編代碼,你會發現我們並沒有在類中定義構造函數,但是編譯器還是為我們加上了一個默認構造函數,該構造函數是必須要存在的,因為虛函數指針的獲取需要在類被創建時賦值到堆棧里,所以此處的默認構造函數就是用來初始化虛函數指針的,另外值得注意的是虛函數地址是編譯時固定到文件里的,一般虛函數地址是不會發生變化的.
繼續跟進看看,我們定義的兩個虛函數地址,可以看到了。
類繼承與派生
面向對象中非常重要的特性之一包括類之間的繼承,至於為什么出現繼承,主要是為了提高代碼的可用性降低冗余代碼,說白了就是讓你更好的偷懶,開發效率更快,讓你的頭發少掉一些,在繼承體系中,通常分為父類與子類,父類就是基類子類就是派生類.
定義簡單派生類: 編譯以下代碼,我們主要觀察父類與子類之間是如何被關聯到一起的.
#include <iostream>
using namespace std;
class CBase // 定義父類
{
public:
int m_nBase;
public:
CBase(){ printf("CBase 父類構造函數 \n"); }
~CBase(){ printf("CBase 父類析構函數 \n"); }
int GetBaseNumber(){ return m_nBase; }
};
class CDervie :public CBase // 定義派生類
{
public:
int pos_x;
int pos_y;
public:
CDervie(){ printf("CDervie 子類構造函數 \n"); }
~CDervie(){ printf("CDervic 子類析構函數 \n"); }
int GetCDervie(){ return pos_x; };
};
int main(int argc, char* argv[])
{
CDervie cd;
return 0;
}
觀察反匯編代碼,原來的CBase
父類成為了CDervie
的一個成員對象,當我們創建CDervie
類的對象時,將會在派生類中產生成員對象int m_nBase;
接着就會自動調用CBase
類中的構造函數,當CDervie類沒有構造函數時,編譯器同樣會提供默認構造函數,以實現繼承.當子類被銷毀時其父類也會被銷毀,同樣按照順序執行析構函數.
int main(int argc, char* argv[])
0041540E | 8D4D F0 | lea ecx,dword ptr ss:[ebp-0x10] | 傳遞 this指針
00415411 | E8 3FBCFFFF | call 0x411055 | 子類 CDervie 構造函數
00415416 | C785 24FFFFFF 00000000 | mov dword ptr ss:[ebp-0xDC],0x0 | main.cpp:28, [ebp-DC]:&"Y]A"
00415420 | 8D4D F0 | lea ecx,dword ptr ss:[ebp-0x10] |
00415423 | E8 28BCFFFF | call 0x411050 | 子類 CDervie 析構函數
call 0x411055 --> CDervie
0041308F | 59 | pop ecx | 獲得this指針
00413090 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx |
00413093 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | 傳遞this指針
00413096 | E8 71E2FFFF | call 0x41130C | 調用父類 CBase 構造函數
main函數里面執行的操作。
進入 call 0x411055
進入父類構造函數
子類調用父類函數: 兩個類之間同為父子關系,定義子類並調用父類,觀察反匯編代碼的展現方式.
#include <iostream>
using namespace std;
class CBase // 定義父類
{
public:
int m_nBase;
public:
CBase(){ printf("CBase 父類構造函數 \n"); }
~CBase(){ printf("CBase 父類析構函數 \n"); }
int GetBaseNumber(){ return m_nBase; }
};
class CDervie :public CBase // 定義派生類
{
public:
int pos_x;
int pos_y;
public:
CDervie(){ printf("CDervie 子類構造函數 \n"); }
~CDervie(){ printf("CDervic 子類析構函數 \n"); }
int GetCDervie(){ return pos_x; };
};
int main(int argc, char* argv[])
{
CDervie cd;
cd.m_nBase = 5;
cd.pos_x = 10;
cd.pos_y = 20;
int x = cd.GetBaseNumber();
printf("調用返回值: %d \n", x);
return 0;
}
觀察代碼,發現當我們在子類中調用父類方法GetBaseNumber()
時,編譯器直接取到了父類中函數的地址並直接call 0x4111DB
,傳遞的this指針則是子類的指針.
00415473 | 8D4D E4 | lea ecx,dword ptr ss:[ebp-0x1C] | 取出this指針
00415476 | E8 DABBFFFF | call 0x411055 | 定義 CDervie
0041547B | C745 FC 00000000 | mov dword ptr ss:[ebp-0x4],0x0 |
00415482 | C745 E4 05000000 | mov dword ptr ss:[ebp-0x1C],0x5 | cd.m_nBase
00415489 | C745 E8 0A000000 | mov dword ptr ss:[ebp-0x18],0xA | cd.pos_x = 10;
00415490 | C745 EC 14000000 | mov dword ptr ss:[ebp-0x14],0x14 | cd.pos_y = 20;
00415497 | 8D4D E4 | lea ecx,dword ptr ss:[ebp-0x1C] | 再次取this指針
0041549A | E8 3CBDFFFF | call 0x4111DB | 調用 CBase.GetBaseNumber()
0041549F | 8945 D8 | mov dword ptr ss:[ebp-0x28],eax |
類的多級繼承: 上面的案例僅僅只是繼承自一個類,接着我們來編寫以下代碼,首先定義派生類CDervie
並繼承自基類CBase,MBase
.
#include <iostream>
using namespace std;
class CBase // 定義父類1
{
public:
virtual void display(){ printf("我是基類CBase中的display"); }
virtual void Run(){ printf("我是基類CBase中的Run"); }
};
class MBase // 定義父類2
{
public:
virtual void display(){ printf("我是基類MBase中的display"); }
virtual void Run(){ printf("我是基類MBase中的Run"); }
};
class CDervie :public CBase,public MBase // 定義派生類,多繼承
{
public:
void display(){ printf("我是子類中的display"); }
void Run(){ printf("我是子類中的 Run"); }
};
int main(int argc, char* argv[])
{
CDervie cd;
cd.display();
return 0;
}
通過反匯編觀察我們可以看出,其實繼承多個類就是將CBase,MBase
父類全部合並到CDervie
這個派生類中.
004130AF | 59 | pop ecx | ecx:"櫫N"
004130B0 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx |
004130B3 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
004130B6 | E8 6FE2FFFF | call 0x41132A | 執行CBase的構造函數
004130BB | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
004130BE | 83C1 04 | add ecx,0x4 | ecx:"櫫N"
004130C1 | E8 A7E0FFFF | call 0x41116D | 執行MBase的構造函數
004130C6 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] |
我們以第一個call 0x41132A
構造函數為例,進入函數內部繼續觀察,其他地方都與單繼承相同,唯一不同點在於,此處子類自身構造中會復寫兩次虛表.
004130AF | 59 | pop ecx | 獲取this指針
004130B0 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | [ebp-8]:&"櫫N"
004130B3 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | [ebp-8]:&"櫫N"
004130B6 | E8 6FE2FFFF | call 0x41132A | 執行CBase構造函數
004130BB | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | [ebp-8]:&"櫫N"
004130BE | 83C1 04 | add ecx,0x4 |
004130C1 | E8 A7E0FFFF | call 0x41116D | 執行MBase構造函數
004130C6 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:&"櫫N"
004130C9 | C700 08CD4100 | mov dword ptr ds:[eax],0x41CD08 for CBase | 執行初始化CBase虛表
004130CF | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:&"櫫N"
004130D2 | C740 04 18CD4100 | mov dword ptr ds:[eax+0x4],0x41CD18 MBase | 執行初始化MBase虛表
004130D9 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:&"櫫N"
虛函數實現多態: 首先我們編譯下面一段代碼,我們想通過改變類指針的方式分別讓基類與子類中的同名函數打印出來.
#include <iostream>
using namespace std;
class CBase // 定義父類
{
public:
void display(){ printf("我是基類中的display\n"); }
};
class CDervie :public CBase // 定義派生類
{
public:
void display(){ printf("我是子類中的display\n"); }
};
int main(int argc, char* argv[])
{
CBase cb;
CDervie cd;
CBase *ptr = &cb;
ptr->display();
ptr = &cd; // 改變類指針
ptr->display();
return 0;
}
分析如下代碼,我們在主函數中定義了指向基類對象的指針ptr,並將其指向CBase類然后使用ptr指針調用基類CBase對象函數,接着我們將ptr指針指向CDervie對象,想要調用CDervie對象中的display函數,發現無論如何都只能調用到CBase里面的display函數.
004152AE | 8D45 FB | lea eax,dword ptr ss:[ebp-0x5] | 獲取this指針
004152B1 | 8945 E0 | mov dword ptr ss:[ebp-0x20],eax |
004152B4 | 8B4D E0 | mov ecx,dword ptr ss:[ebp-0x20] | 傳遞this指針
004152B7 | E8 8CC0FFFF | call 0x411348 | 調用CBase下面的Display
004152BC | 8D45 EF | lea eax,dword ptr ss:[ebp-0x11] | main.cpp:23
004152BF | 8945 E0 | mov dword ptr ss:[ebp-0x20],eax |
004152C2 | 8B4D E0 | mov ecx,dword ptr ss:[ebp-0x20] | main.cpp:24
004152C5 | E8 7EC0FFFF | call 0x411348 | 調用CBase下面的Display
004152CA | 33C0 | xor eax,eax | main.cpp:25
如果想要調用子類中的同名函數,只需要將CBase對象中的void display()
聲明為虛函數virtual void display()
則就會允許在其派生類中對該函數重新定義,賦予它新的功能,並可以通過指向基類的指針調用到子類的同名函數.
#include <iostream>
using namespace std;
class CBase // 定義父類
{
public:
virtual void display(){ printf("我是基類中的display"); }
virtual void Run(){ printf("我是基類中的 Run"); }
};
class CDervie :public CBase // 定義派生類
{
public:
void display(){ printf("我是子類中的display"); }
void Run(){ printf("我是子類中的 Run"); }
};
int main(int argc, char* argv[])
{
CBase cb;
CDervie cd;
CBase *ptr = &cb;
ptr->display();
ptr->Run();
ptr = &cd;
ptr->display();
return 0;
}
觀察反匯編代碼,你或許會有些頭緒,這里以CBase類中的兩個虛函數為例,虛函數表中存儲了這兩個函數的首地址,我們只需要通過mov eax,dword ptr ds:[edx+0x4]
遞增指針即可調用到不同的虛函數.
004154CE | 8D4D F8 | lea ecx,dword ptr ss:[ebp-0x8] |
004154D1 | E8 40BEFFFF | call 0x411316 | 調用CBase構造函數,初始化父虛表
004154D6 | 8D4D EC | lea ecx,dword ptr ss:[ebp-0x14] |
004154D9 | E8 72BBFFFF | call 0x411050 | 調用Dervie構造函數,初始化子虛表
004154DE | 8D45 F8 | lea eax,dword ptr ss:[ebp-0x8] | 獲取虛表首地址
004154E1 | 8945 E0 | mov dword ptr ss:[ebp-0x20],eax |
004154E4 | 8B45 E0 | mov eax,dword ptr ss:[ebp-0x20] | main.cpp:24, [ebp-20]:"p藺"
004154E7 | 8B10 | mov edx,dword ptr ds:[eax] | 取出虛函數首地址
004154E9 | 8BF4 | mov esi,esp |
004154EB | 8B4D E0 | mov ecx,dword ptr ss:[ebp-0x20] |
004154EE | 8B02 | mov eax,dword ptr ds:[edx] | 取出第一個虛函數地址 display()
004154F0 | FFD0 | call eax | 調用虛函數
004154F2 | 3BF4 | cmp esi,esp |
004154F4 | E8 F5BDFFFF | call 0x4112EE |
004154F9 | 8B45 E0 | mov eax,dword ptr ss:[ebp-0x20] | 再次獲取虛表首地址
004154FC | 8B10 | mov edx,dword ptr ds:[eax] |
004154FE | 8BF4 | mov esi,esp |
00415500 | 8B4D E0 | mov ecx,dword ptr ss:[ebp-0x20] | 取出虛函數首地址
00415503 | 8B42 04 | mov eax,dword ptr ds:[edx+0x4] | 在第一個虛函數基礎上+4字節指針
00415506 | FFD0 | call eax | 調用第二個虛函數 Run()
00415508 | 3BF4 | cmp esi,esp |
類的菱形繼承: 菱形繼承是最復雜的對象結構,菱形結構會將單繼承與多繼承進行組合,從而形成復雜的繼承關系.
#include <iostream>
using namespace std;
class CBase{
public:
int Base_x;
int Base_y;
public: void Base_Display(){ printf("超類的打印函數: %d \n",Base_x); }
CBase(){ printf("超類構造函數\n"); }
};
class Father1 : public CBase{
public:
int Father1_x;
public: void Father1_Display(){ printf("父類1的打印函數: %d \n", Father1_x); }
Father1(){ printf("Father1構造函數\n"); }
};
class Father2 : public CBase{
public:
int Father2_x;
public: void Father2_Display(){ printf("父類1的打印函數: %d \n", Father2_x); }
Father2(){ printf("Father2構造函數\n"); }
};
class Child : public Father1, public Father2{
public:
int Child_x;
public: void Child_Display(){ printf("派生類的打印函數: %d \n", Child_x); }
Child(){ printf("派生類構造函數\n"); }
};
int main(int argc, char* argv[])
{
Child cd;
cd.Father1::Father1_x = 1;
cd.Father1::Father1_Display();
cd.Father2::Father2_x = 2;
cd.Father2::Father2_Display();
return 0;
}
反匯編以后來看這三段代碼,首先主函數中率先執行Child構造函數,進入call 0x4112B7
以后調用了Father1/2
這兩個構造函數,接着進入其中一個Father發現內部調用了祖父CBase的構造函數,此時思路就清晰了.
0041550E | 8D4D E0 | lea ecx,dword ptr ss:[ebp-0x20] | main.cpp:35
00415511 | E8 10BDFFFF | call 0x411226 | 調用 Child 構造函數
00415516 | C745 E8 01000000 | mov dword ptr ss:[ebp-0x18],0x1 | main.cpp:36
0041551D | 8D4D E0 | lea ecx,dword ptr ss:[ebp-0x20] | main.cpp:37
00415520 | E8 92BDFFFF | call 0x4112B7 | cd.Father1::Father1_Display();
00415525 | C745 F4 02000000 | mov dword ptr ss:[ebp-0xC],0x2 | main.cpp:39
0041552C | 8D4D EC | lea ecx,dword ptr ss:[ebp-0x14] | main.cpp:40
0041552F | E8 EABAFFFF | call 0x41101E | cd.Father2::Father2_Display();
0041309F | 59 | pop ecx | ecx:"槔M"
004130A0 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx |
004130A3 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
004130A6 | E8 52E2FFFF | call 0x4112FD | 調用Father1的構造函數
004130AB | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
004130AE | 83C1 0C | add ecx,0xC | ecx:"槔M"
004130B1 | E8 E3E1FFFF | call 0x411299 | 調用Father2的構造函數
004130B6 | 8BF4 | mov esi,esp |
0041311F | 59 | pop ecx | ecx:"槔M"
00413120 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx |
00413123 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
00413126 | E8 EBE1FFFF | call 0x411316 | 調用CBase的構造函數
0041312B | 8BF4 | mov esi,esp |
純虛函數的使用: 在虛函數的結尾加上=0
這種虛函數被稱為純虛函數,純虛函數沒有實現只有聲明,它的存在就是為了讓類具有虛基類的功能,讓繼承自虛基類的子類都具有虛表以及虛表指針,利用虛基類指針可以更好地完成多態的工作.
#include <iostream>
using namespace std;
class CVirtualBase
{
public: virtual void Show() = 0; // 定義純虛函數
};
class CVirtualChild : public CVirtualBase{
public: virtual void Show(){ // 實現純虛函數
printf("虛基類分析 \n");
}
};
int main(int argc, char* argv[])
{
CVirtualChild cvcd;
return 0;
}
其實純虛函數就像一個占位符,在基類中霸占一段空間,在子類中實現其方法,但純虛函數也是存在虛函數表,只不過該虛表默認是空表,因為該代碼反匯編和前面所說的類相同,這里就不在分析了.