2011-11-07 58 views
0

我正在編譯器設計上的任務。在代碼生成部分,我堅持如何創建指令,以確保在運行時調用適當的方法。該語言是C++的一個非常小的子集。如何實現跳轉調用到適當的vtable條目

讓我們說:

void main() 
{ 
    Animal* a; 
    a = new Cow; 
    //what code should be generated to ensure that object 'a' calls Cow::Init here 
    a->Init(5); 
} 

class Cow : public Animal{ 
void Init(int h) 
{ 
    height = h; 
} 
} 

class Animal { 
int height; 
virtual void Init(int h){ 
    height = h; 
    } 
} 
+0

它應該是'Animal * a;'你爲什麼不通過生成彙編代碼來看看自己?除此之外'vtable''vptr'機制具有很高的實現特定性。 –

+0

@Als的任何一般想法都會有所幫助。事情是我不擅長閱讀彙編代碼。作業有一組生成彙編代碼的函數。 –

+0

這個編輯過的新代碼是這樣做的。 –

回答

2

這樣做的一個非常簡單的方法(注意:這不包括優化在編譯時知道調用): 如果你的類有任何虛成員(包括繼承),那麼它的非常第一個成員變成了一個虛擬表格的指針。 vftable是恆定的每類定義這就是爲什麼你只需要一個指針。每個唯一的函數在那個vftable中被分配一個索引,所以每個唯一的名字(注:通過名字我的意思是符號名稱包括類型,但沒有類名稱空間限定)具有唯一的索引,那麼表格是從繼承樹最頂層的類填充到當前的工作類定義。

這樣做,虛擬功能的更新重定義將覆蓋共享索引的較舊條目。調用函數然後變得微不足道,因爲您只需爲該函數的名稱索引生成對索引的調用即可。

所以在你的榜樣,Animal有1項,Init(int),其賦值爲0。唯一索引vftable讓您擁有一個vftable看起來像這樣:

;Animal - vftable 
&Animal::Init //note: this isn't a class member pointer in the C++ sense, its a namespaced function pointer if you will 

那麼當你建立vftable爲Cow,您使用Animals爲基礎,並添加了虛擬功能,在這種情況下Init(int),但它已經爲0的唯一索引,所以我們在索引0覆蓋功能:

;Cow - vftable 
&Cow::Init 

那麼如果我們有電話:

a->Init(5); 

我們只是變換到:

a->vftable[0](5); 

其中0是分配給Init(int)唯一索引。

以防萬一組裝例子,可以幫助:

;ecx contains our class pointer 
mov eax,[ecx] ;get the vftable ptr 
mov eax,[eax] ; get the ptr at (vftable + (unique_index * sizeof(func_ptr))) 
push 5 ;push our arg 5, ecx is already setup for __thiscall 
call eax ; let it rip! 

注:這一切都假定你的符號表被設置爲能夠檢測或通過繼承通過虛函數那些成爲虛擬從繼承。


如果要優化這個地方,你可以分析a,發現其只賦值一次,因此,你可以變身及一流的類就被分配,Cow的價值。然後看到你在派生鏈的末端處有一個類,你可以摺疊vftable呼叫並直接使用Cow::Init,這是多麼棘手的問題,並且有很多方法可以優化出vftable調用,對於一個項目來說不重要。

+0

謝謝你這麼好的解釋。 –

0

這個概念通常用一個thunk來實現,它是一個編譯器生成的包裝函數。

+0

Thunk有時與vtables結合使用,但通常不被視爲替代品。你打包什麼,以及如何解決動態調度問題? – MSalters

2

這可以用輕量級C++來表示,如果你發現它比程序集更可讀(我願意)。我會將自己限制在C(主要),並且只是添加繼承以避免大量的投射。

爲了清楚起見,實現細節將以__爲前綴。請注意,這些標識符通常是爲實現保留的,因此您通常不應該在您的程序中使用它們。


的類型安全虛擬調度方法

注:僅限於簡單的繼承(單鹼基,沒有虛繼承)

,讓我們創建Animal類。

struct __AnimalTableT; 

struct Animal { __AnimalTableT const * const __vptr; int height; } 

void AnimalInit(Animal* a, int height) { 
    a->height = height; 
} 

我們預留空間的指針,在動物的虛擬表,並表示該方法作爲外部功能使this明確。

接下來,我們「創建」虛擬表。請注意,C中的數組需要由相似的元素組成,因此這裏我們將使用稍高級別的方法。

struct __AnimalTableT { 
    typedef void (*InitFunction)(int); 

    InitFunction Init; 
}; 
static __AnimalTableT const __AnimalTable = { &AnimalInit }; 

現在,讓我們創建一個牛:

struct Cow: Animal {}; 

void CowInit(Animal* a, int height) { 
    Cow* c = static_cast<Cow*>(a); 
    c->height = height; 
} 

和相關的表:

// Note: we could have new functions here (that only Cow has) 
// they would be appended after the "Animal" part 
struct __CowTableT: __AnimalTableT {}; 

static __CowTableT const __CowTable = { &CowInit }; 

和使用:

typedef void (*__AnimalInitT)(Animal*,int); 

int main() { 
    Cow cow = { &__CowTable, 0 }; 

    __AnimalInitT const __ai = cow.__vptr->Init; 
    (*__ai)(&cow, 5); 
} 

真的嗎?

真正的使用稍微複雜一些,但建立在相同的想法上。

正如你可以注意到,很奇怪CowInit需要一個Animal*指針作爲它的第一個參數。問題是你需要一個兼容的函數指針類型和原來的重載方法。在線性繼承的情況下,這並不重要,但在多繼承或虛擬繼承的情況下,事情變得非常忙碌,並且Cow的子部分可能不會在開始時展開,從而導致指針調整。

在現實生活中,我們的thunk:

好了,我們可以改變的CowInit簽名更自然:

void CowInit(Cow* cow, int height); 

然後,我們的「橋」通過創建一個空白一個「咚」,使適應:

void __CowInit(Animal* a, int height) { 
    CowInit(static_cast<Cow*>(a), height); 
} 

static __CowTableT const __CowTable = { &__CowInit }; 

在現實生活中,我們有表:

另一種說法是,結構的使用非常好,但我們在這裏討論的是實現細節,因此不需要精確。在一般情況下,編譯器,因此使用普通的陣列,而不是一個結構:

typedef (void)(*__GenericFunction)(); 

static __GenericFunction const __AnimalTable[] = { 
    __GenericFunction(&AnimalInit) 
}; 

static __GenericFunction const __CowTable[] = { 
    __GenericFunction(&__CowInit) 
}; 

這會稍微改變呼叫:使用索引而不是屬性名的,你需要轉換回相應的功能類型。

typedef void (*__AnimalInitT)(Animal*,int); 

int main() { 
    Cow cow = { &__CowTable, 0 }; 

    // old line: __AnimalInitT const __ai = cow.__vptr->Init; 
    __AnimalInit const __ai = __AnimalInit(cow.__vptr[0]); 
    (*__ai)(&cow, 5); 
} 

正如您所看到的,表的使用實際上是一個實現細節。

這裏真正重要的一點是引入一個thunk來調整函數簽名。請注意,thunk是在創建派生的類(此處爲Cow)表時創建的。在我們的例子中,這是沒有必要的,因爲在低級別這兩個對象都有相同的地址,所以我們可以不用,而且一個智能編譯器不會生成它並直接採取&CowInit

相關問題