x86 反彙編/物件和類
X86 反彙編華夏公益教科書的“物件和類”頁面是一個存根。您可以透過擴充套件此部分來提供幫助。
面向物件(OO)程式設計為我們提供了一個新的程式結構單元:物件。本章將研究從 C++ 中反彙編的類。本章不會直接處理 COM,但它將為將來討論反向 COM 元件(僅限 Windows 使用者)奠定許多基礎。
一個沒有繼承任何東西的基本類可以分為兩個部分,變數和方法。非靜態變數被塞進一個簡單的資料結構中,而方法則像其他任何函式一樣進行編譯和呼叫。
當你開始新增繼承和多型時,事情變得更加複雜。為了簡單起見,物件的結構將在沒有繼承的情況下進行描述。然而,在最後,將涵蓋繼承和多型。
在類中定義的所有靜態變數都駐留在應用程式整個生命週期內的靜態記憶體區域。在類中定義的每個其他變數都放置在一個稱為物件的資料結構中。通常,當建構函式被呼叫時,變數會按照順序放置到物件中,請參見圖 1。
A
class ABC123 {
public:
int a, b, c;
ABC123():a(1), b(2), c(3) {};
};
B
0x00200000 dd 1 ;int a
0x00200004 dd 2 ;int b
0x00200008 dd 3 ;int c
|
圖 1:記憶體中物件的示例 |
但是,編譯器通常需要將變數分離成大小為字(2 位元組)的倍數,以便定位它們。並非所有變數都滿足此要求,特別是 char 陣列;一些未使用的位可能會被用來填充變數,以滿足此大小要求。這在圖 2中有所說明。
A
class ABC123{
public:
int a;
char b[3];
double c;
ABC123():a(1),c(3) { strcpy(b,"02"); };
};
B
0x00200000 dd 1 ;int a ; offset = abc123 + 0*word_size
0x00200004 db '0' ;b[0] = '0' ; offset = abc123 + 2*word_size
0x00200005 db '2' ;b[1] = '2'
0x00200006 db 0 ;b[2] = null
0x00200007 db 0 ;<= UNUSED BYTE
0x00200008 dd 0x00000000 ;double c, lower 32 bits ; offset = abc123 + 4*word_size
0x0020000C dd 0x40080000 ;double c, upper 32 bits
|
圖 2:具有填充變數的物件的示例 |
為了使應用程式訪問這些物件變數之一,需要對物件指標進行偏移以找到所需的變數。每個變數的偏移量由編譯器知道,並在需要時寫入物件程式碼中。圖 3顯示瞭如何偏移指標以檢索變數。
;abc123 = pointer to object
mov eax, [abc123] ;eax = &a ;offset = abc123+0*word_size = abc123
mov ebx, [abc123+4] ;ebx = &b ;offset = abc123+2*word_size = abc123+4
mov ecx, [abc123+8] ;ecx = &c ;offset = abc123+4*word_size = abc123+8
圖 3:這顯示瞭如何偏移指標以檢索變數。第一行將變數 'a' 的地址放入 eax。第二行將變數 'b' 的地址放入 ebx。最後一行將變數 'c' 放入 ecx。
在低階,函式和方法幾乎沒有區別。反編譯時,有時很難區分兩者。它們都駐留在文字記憶體空間中,並且都以相同的方式被呼叫。方法呼叫示例可以在圖 4中看到。
A
//method call
abc123->foo(1, 2, 3);
B
push 3 ; int c
push 2 ; int b
push 1 ; int a
push [ebp-4] ; the address of the object
call 0x00434125 ; call to method
|
圖 4:方法呼叫。 |
方法呼叫中值得注意的特點是作為引數傳入的物件的地址。但是,這不是一個始終可靠的指標。圖 5顯示了第一個引數是按引用傳入的物件的函式。結果是看起來與方法呼叫相同的函式。
A
//function call
foo(abc123, 1, 2, 3);
B
push 3 ; int c
push 2 ; int b
push 1 ; int a
push [ebp+4] ; the address of the object
call 0x00498372 ; call to function
|
圖 5:函式呼叫。 |
繼承和多型完全改變了類的結構,物件不再只包含變數,它們還包含指向繼承方法的指標。這是因為多型要求在執行時確定方法或內部物件的地址。
考慮圖 6。應用程式如何知道要呼叫 D::one 還是 C::one?答案是編譯器弄清楚了一個約定,用於在物件內部對變數和方法指標進行排序,這樣,當引用它們時,對於任何繼承了其方法和變數的物件,偏移量都相同。
A *obj[2];
obj[0] = new C();
obj[1] = new D();
for(int i=0; i<2; i++)
obj[i]->one();
|
| 圖 6:一個小的 C++ 多型迴圈,它呼叫一個函式 one。類 C 和 D 都繼承了一個抽象類 A。為了使此程式碼工作,類 A 必須具有一個名為“one”的虛方法。 |
抽象類 A 充當編譯器的藍圖,定義了任何繼承它的類的預期結構。在類 A 中定義的每個變數以及在 A 中定義的每個虛方法都將在其任何子類中具有完全相同的偏移量。圖 7聲明瞭一個可能的繼承方案及其在記憶體中的結構。注意 C::one 的偏移量與 D::one 相同,而 C 中 A::a 的副本的偏移量與 D 中的副本相同。在此,我們的多型迴圈只需遍歷指標陣列並確切地知道在何處找到每個方法。
A
class A{
public:
int a;
virtual void one() = 0;
};
class B{
public:
int b;
int c;
virtual void two() = 0;
};
class C: public A{
public:
int d;
void one();
};
class D: public A, public B{
public:
int e;
void one();
void two();
};
B
;Object C
0x00200000 dd 0x00423848 ; address of C::one ;offset = 0*word_size
0x00200004 dd 1 ; C's copy of A::a ;offset = 2*word_size
0x00200008 dd 4 ; C::d ;offset = 4*word_size
;Object D
0x00200100 dd 0x00412348 ; address of D::one ;offset = 0*word_size
0x00200104 dd 1 ; D's copy of A::a ;offset = 2*word_size
0x00200108 dd 0x00431255 ; address of D::two ;offset = 4*word_size
0x0020010C dd 2 ; D's copy of B::b ;offset = 6*word_size
0x00200110 dd 3 ; D's copy of B::c ;offset = 8*word_size
0x00200114 dd 5 ; D::e ;offset = 10*word_size
|
圖 7:一個多型繼承方案。
圖 7.A 定義了繼承方案。它顯示類 C 繼承了類 A,而類 D 繼承了類 A 和類 B。 |