先理解一件事,在intel匯編層面來說,直接調(diào)用和間接調(diào)用的區(qū)別。
直接調(diào)用語法: call 地址?? 硬編碼為 :e8
間接調(diào)用語法:?? call [ ...] ? ? 硬編碼為:??? FF
那么在C++語法中,實(shí)現(xiàn)多態(tài)的前提是父類需要實(shí)現(xiàn)多態(tài)的成員方法前面加入virtual。我們先來看一個(gè)例子,這次試驗(yàn)用的是windows平臺下的VS編譯器不同編譯器的細(xì)節(jié)是不一樣的原理大差不差。
#include<iostream>
using namespace std;
class Base {
public:
void func1() {
printf("這是func1");
}
virtual void func2() {
printf("這是func2");
}
};
int main() {
Base b1;
b1.func1();
b1.func2();
return 0;
}
? 這個(gè)代碼示例中我們只寫了一個(gè)類,并且用對象去調(diào)用成員方法。運(yùn)行結(jié)果如下:
我們觀察下匯編代碼:
此時(shí)均為e8-call,屬于直接調(diào)用。接下來我們換一種調(diào)用方式,我們通過指針的方式去調(diào)用對象方法:
#include<iostream>
using namespace std;
class Base {
public:
void func1() {
printf("這是func1");
}
virtual void func2() {
printf("這是func2");
}
};
int main() {
Base b1;
Base* p;
p = &b1;
p->func1();
p->func2();
return 0;
}
運(yùn)行結(jié)果沒有變化,但是此時(shí)我們觀察反匯編我們就發(fā)現(xiàn)了不一樣的地方:
fun1就是普通的成員函數(shù),反匯編依然是那樣。但是我們看加入關(guān)鍵字的virtual變成虛函數(shù)后,匯編代碼變化了。真正調(diào)用的其實(shí)是call eax這句匯編代碼,我們看到他的硬編碼是FF D0。說明他是個(gè)間接調(diào)用。我們可以理解這段反匯編代碼,p是指向?qū)ο蟮氖椎刂返囊簿褪莟his指針。將this指針的第一項(xiàng)放入eax,又將eax的第一項(xiàng)復(fù)制給edx,此時(shí)將this指針給ecx(如果是linux平臺g++編譯this指針是賦值給rdi的 _x64情況下)最后將edx的第一項(xiàng)賦值給eax。最后去執(zhí)行。這段翻譯可能有點(diǎn)繞??梢宰约喝ダ斫庀?。
現(xiàn)在我們知道虛函數(shù),如果是通過對象去調(diào)用,和普通成員方法沒什么區(qū)別,但是如果通過指針或者引用去調(diào)用。那么他就是個(gè)間接調(diào)用。一些細(xì)節(jié)后面再講。
下面我們再分析一個(gè)問題,這個(gè)類到底有多大。
#include<iostream>
using namespace std;
class Base {
public:
int a;
int b;
void func1() {
printf("這是func1");
}
void func2() {
printf("這是func2");
}
};
int main() {
//Base b1;
//Base* p;
//p = &b1;
//p->func1();
//p->func2();
printf("Base結(jié)構(gòu)體大小為=%d", sizeof(Base));
return 0;
}
這個(gè)問題應(yīng)該很簡單,只算數(shù)據(jù)大小,int類型是占4個(gè)字節(jié)因此這個(gè)Base對象的大小應(yīng)該是8,因?yàn)槌蓡T方法是在代碼區(qū)的,這里其實(shí)跟C語言的結(jié)構(gòu)體沒什么不一樣的:
那如果現(xiàn)在我們將其中一個(gè)成員方法改成虛函數(shù)呢?
神奇的一幕發(fā)生了,變成了12。
那是不是說類中每多一個(gè)虛函數(shù),就會(huì)多4字節(jié)大小呢?(32位而言,64位就是8)
我們將fun1也變成虛函數(shù):
我們看到?jīng)]有變化。也就是說,跟你在類中定義多少個(gè)虛函數(shù)木的關(guān)系。那這多出來的4字節(jié)是個(gè)什么鬼東西呢?這就是我們接下來要探究的東西。
我們觀察。現(xiàn)在沒有任何函數(shù)的情況下,我們的類是這樣布局的。 0x00cffb80也就是b1對象的首地址。
下面我們加入一個(gè)虛函數(shù):
我們觀察此時(shí)對象的內(nèi)存布局:
什么都沒有,接著往下走:
再往下走:
再往下走:
我們發(fā)現(xiàn)對象首地址存的不再是1。而是一個(gè)地址。那這個(gè)地址是什么呢?這就是我們接下來要探究的東西。寫一個(gè)demo:
#include<iostream>
using namespace std;
class Base {
public:
int a;
int b;
virtual void func1() {
printf("這是func1");
}
};
int main() {
Base b1;
b1.a = 1;
b1.b = 2;
Base* p;
p = &b1;
p->func1();
return 0;
}
我們跟過去反匯編:
我們現(xiàn)在看這兩行匯編的意思就很明朗了。
1.mov eax [p]?? //將對象的this指針放入eax
2.mov edx [eax] //將this指針首地址里面存的虛函數(shù)表放入edx
3.mov ecx [p] ? //將this指針傳給ecx
3,mov eax ,[edx] //將虛函數(shù)表里的第一項(xiàng)放入eax
4.call eax //調(diào)用fun1函數(shù)。
相比看懂了上面的流程就明白了??偨Y(jié)圖如下:
那我們能驗(yàn)證虛函數(shù)表中的函數(shù)就是我們的想要調(diào)用的函數(shù)嘛?demo如下:
#include<iostream>
using namespace std;
class Base {
public:
int a;
int b;
virtual void func1() {
printf("這是func1\n");
}
};
int main() {
Base b1;
b1.a = 1;
b1.b = 2;
Base* p;
p = &b1;
p->func1();
printf("b1對象的地址=%p\n", &b1);
printf("虛函數(shù)表地址=%p\n", *((int*)&b1));
printf("func1函數(shù)地址=%p\n", *((int*)*((int*)&b1)));
int p2 = *((int*)*((int*)&b1));
_asm {
call p2;
}
return 0;
}
這里因?yàn)槲沂怯?2位寫的demo因此我們用內(nèi)聯(lián)匯編測試下。代碼邏輯就是取出虛函數(shù)表中的第一項(xiàng),然后用匯編調(diào)用,看是否和我們用指針調(diào)用的是同一個(gè)函數(shù)運(yùn)行結(jié)果如下:
文章來源:http://www.zghlxwxcb.cn/news/detail-821069.html
通過這個(gè)實(shí)驗(yàn)我們確實(shí)驗(yàn)證了虛函數(shù)表中存的就是我們的虛函數(shù)的地址。這也是C++編譯器實(shí)現(xiàn)多態(tài)的一個(gè)先提條件。文章來源地址http://www.zghlxwxcb.cn/news/detail-821069.html
到了這里,關(guān)于C++逆向分析--虛函數(shù)(多態(tài)的前置)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!