C++與彙編小結

C++與彙編小結


本文通過C++反編譯,幫助理解C++中的一些概念(指針引用、this指針、虛函數、析構函數、lambda表達式),
希望能在深入理解C++其它一些高級特性(多重繼承、RTTI、異常處理)能起到拋磚引玉的作用吧。

指針和引用

引用類型的存儲方式和指針是一樣的,都是使用內存空間存放地址值。
只是引用類型是通過編譯器實現尋址,而指針需要手動尋址。

void funRef(int &ref){
    ref++;
}

int main(){
    //定義int類型變量
    int var = 0x41;
    //int指針變量,初始化爲變量var的地址
    int *pnVar = &var;
    //取出指針pcVar指向的地址內容並顯示
    char *pcVar = (char*)&var;
    printf("%s",pcVar);

    //引用作爲參數,即把var的地址作爲參數
    funRef(var);

    return 0;
}

用godbolt查看的效果如圖,C++代碼與對應的彙編代碼用相同的顏色標註,非常方便查看。


switch

在分支數少的情況下可以用if–else if模擬,
但是分支比較大的情況下,需要比較的次數太多,
如果是有序線性的數值,可將每個case語句塊的地址預先保存在數組中,
考察switch語句的參數,並依次查詢case語句塊地址的數組,
從而得到對應case語句塊的首地址,
這樣可以降低比較的次數,提升效率。

int main(){
    int nIdx=1;
    scanf("%d",&nIdx);

    int result = 0;
    switch(nIdx){
        case 1:
            result = 1;
            break;
        case 2:
            result = 2;
            break;  
        case 3:
            result = 3;
            break;
        case 5:
            result = 3;
            break;  
        case 7:
            result = 3;
            break;      
    }

    return result;
}

如下圖,編譯器把switch跳轉表放到了.L4所指向的區域,其中的元素.L2、.L3 … .L8指向case對應代碼地址。


this指針

this指針中保存了所屬對象的首地址。

在調用成員函數的過程中,編譯器利用rdi寄存器保存了對象的首地址,
並以寄存器傳參的方式傳遞到成員函數中。

#include <stdio.h>

class Location{
public:
    Location(){
        //this指針指向一塊16字節的內存區域
        m_x = 1;
        //m_x是一個8字節類型,所以mov一個4字
        //mov     DWORD PTR [rax], 1
        m_y = 2;
        //mov     WORD PTR [rax+4], 2
    }

    short getY(){
        //獲取this指針(對象首地址)偏移4處的數據,即m_y的值
        //movzx   eax, WORD PTR [rbp-8+4]
        return m_y;
    }

private:
    int m_x; //佔4字節
    short m_y; //佔2字節
    //由於內存對齊,整個對象佔8字節
};


int main(){
    //在棧上分配16字節,其中有8字節分配給是loc
    //把棧上loc的內存地址(即this指針)作爲參數調用Location構造函數。
    Location loc;

    //把棧上loc的內存地址(即this指針)作爲參數調用getY成員函數。
    short y = loc.getY();
    //y變量位於[rbp-2]處
    //mov     WORD PTR [rbp-2], ax
    return 0;
}

對應的彙編如下:

虛函數和虛表

編譯器會爲每一個包含虛函數的類(或通過繼承得到的子類)生成一個表,其中包含指向類中每一個虛函數的指針。
這樣的表就叫做虛表(vtable)。
此外,每個包含虛函數的類都獲得另外一個數據成員,用於在運行時指向適當的虛表。
這個成員通常叫做虛表指針(vtable pointer),並且是類中的第一個數據成員。

在運行時創建對象時,對象的虛表指針將設置爲指向合適的虛表。
如果該對象調用一個虛函數,則通過在該對象的虛表中進行查詢來選擇正確的函數。

代碼舉例如下,詳細代碼在這裏

class BaseClass {
public:
   BaseClass(){x=1;y=2;};
   virtual void vfunc1() = 0;
   virtual void vfunc2(){};
   virtual void vfunc3();
   virtual void vfunc4(){};
   void hello(){printf("hello,y=%d",this->y);};
private:
   int x;//4字節
   int y;
};

class SubClass : public BaseClass {
public:
   SubClass(){z=3;};
   virtual void vfunc1(){};
   virtual void vfunc3();
   virtual void vfunc5(){};
private:
   int z;
};

虛表佈局

下圖是一個簡化後的內存佈局,它動態分配了一個SubClass類型的對象,編譯器會確保該對象的第一個字段虛表指針指向正確的虛表。虛表指向編譯器爲每個類在只讀段創建的一塊區域,即虛表,類似於數組,其中的大部分元素指向在代碼段中的成員函數地址。C++編譯器會在編譯階段給這些函數名做name mangling,以實現c++中函數重載、namespace等標準。
虛表佈局

vtable for SubClass:
        .quad   0
        .quad   typeinfo for SubClass ;RTTI相關
        .quad   SubClass::vfunc1() ;this指針中的虛表指針一般指向這個偏移處
        .quad   BaseClass::vfunc2()
        .quad   SubClass::vfunc3()
        .quad   BaseClass::vfunc4()
        .quad   SubClass::vfunc5()
vtable for BaseClass:
        .quad   0
        .quad   typeinfo for BaseClass ;RTTI相關
        .quad   __cxa_pure_virtual ;vfunc1是純虛函數
        .quad   BaseClass::vfunc2()
        .quad   BaseClass::vfunc3()
        .quad   BaseClass::vfunc4()

SubClass 中包含兩個指向屬於BaseClass的函數( BaseClass::vfunc2 和 BaseClass::vfunc4)的指針。
這是因爲 SubClass 並沒有重寫這2個函數,而是直接繼承自BaseClass 。
由於沒有針對純虛函數BaseClass::vfunc1的實現,因此,在 BaseClass的虛表中並沒有存儲 vfunc1 的地址。
這時,編譯器會插入一個錯誤處理函數的地址,名爲 purecall,萬一被調用,它會令程序終止或者其他編譯器想要發生的行爲。
另外,一般的成員函數不在虛表裏面,因爲不涉及動態調用,如BaseClass中的hello()函數。

創建對象

這裏已在堆上動態創建對象爲例。
調用new操作符,在堆上動態分配一塊SubClass大小的內存,rax指向這塊內存的開始。
SubClass需要的內存大小爲24字節=8(虛表指針)+4*3(3個int類型的成員變量)+4(內存對齊)
對象首地址的值作爲參數調用SubClass構造函數。

   BaseClass *a_ptr = new SubClass();
main:
        ;...
        mov     edi, 24 ;SubClass需要24字節的內存
        call    operator new(unsigned long)
        mov     rbx, rax
        mov     rdi, rbx ;this指針作爲參數
        call    SubClass::SubClass()

SubClass的構造函數,在完成自身的任務之前會調用基類的構造函數,然後對this指針的內存的虛表指針修改爲指向SubClass自身的虛表。

SubClass::SubClass():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax ;this指針
        call    BaseClass::BaseClass()
        mov     edx, OFFSET FLAT:vtable for SubClass+16 ;指向SubClass虛表
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx ;this指針的虛表指針字段賦值
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+16], 3 ;z=3
        nop
        leave
        ret

BaseClass的構造函數:

BaseClass::BaseClass():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi ;this指針
        mov     edx, OFFSET FLAT:vtable for BaseClass+16 ;指向BaseClass虛表
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx ;this指針的虛表指針字段賦值
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+8], 1 ;x=1
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+12], 2 ;y=2
        nop
        pop     rbp
        ret

調用成員函數

1、非虛函數

hello()是類BaseClass中的非虛成員函數,不需要通過虛表查找,編譯器直接生成調用語句call BaseClass::hello(),並且第一個參數默認爲this指針。

    BaseClass *a_ptr = new SubClass();
   //一般的成員函數,不在虛表裏
   a_ptr->hello();
main:
        ;...        
        mov     rdi, rax ;參數:this指針
        call    BaseClass::hello()

.LC0:
        .string "hello\357\274\214y=%d"
BaseClass::hello():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi  ;this指針放到棧上
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax+12] ;this指針偏移12處,即成員變量y的位置
        mov     esi, eax              ;參數:format的數據,即y的值
        mov     edi, OFFSET FLAT:.LC0 ;參數:format string
        mov     eax, 0                ;參數:fd,指向stdout
        call    printf
        nop
        leave
        ret

2、虛函數

a_ptr是BaseClass類型的指針,動態分配的是SubClass類型的內存。
call_vfunc函數的參數是基類BaseClass,再調用vfunc3函數時需要先根據虛表指針定位到虛表,再通過偏移,解引用找到vfunc3的代碼段地址,完成調用。

int main(){
    BaseClass *a_ptr = new SubClass();
    //對象首地址作爲參數調用函數call_vfunc
   call_vfunc(a_ptr);
}
void call_vfunc(BaseClass *a) {
   a->vfunc3();
}  
main:
    ;...    
    mov     rax, QWORD PTR [rbp-24]
    mov     rdi, rax ;rax爲this指針
    call    call_vfunc(BaseClass*)

call_vfunc(BaseClass*):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi ;把this指針放到棧上
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]   ;this指向的內存的開頭8個字節的數據複製給rax,即虛表指針
        add     rax, 16                ;找到虛表指針偏移16
        mov     rax, QWORD PTR [rax]   ;虛表指針偏移16處解引用,得到函數的SubClass::vfunc3的地址
        mov     rdx, QWORD PTR [rbp-8]
        mov     rdi, rdx              ;rdi爲this指針,作爲參數
        call    rax                   ;調用vfunc3
        nop
        leave
        ret   

析構函數

這裏以堆分配的對象析構爲例,完整代碼在這裏
堆分配的對象的析構函數在分配給對象的內存釋放之前通過 delete 操作符調用。
其過程如下:
1、如果類擁有任何虛函數,則還原對象的虛表指針,使其指向相關類的虛表。如果一個子類在創建過程中覆蓋了虛表指針,就需要這樣做。
2、執行程序員爲析構函數指定的代碼。
3、如果類擁有本身就是對象的數據成員,則執行這些成員的析構函數。
4、如果對象擁有一個超類,則調用超類的析構函數
5、如果是釋放堆的對象,則用一個代理析構函數執行1~4步驟,並在最後調用delete操作符釋放堆上的對象。

class BaseClass {
public:
   BaseClass(){x=1;y=2;};
   virtual ~BaseClass(){printf("~BaseClass()\n");};
   virtual void vfunc1() = 0;
private:
   int x;//4字節
   int y;
};

class SubClass : public BaseClass {
public:
   SubClass(){z=3;};
   virtual ~SubClass(){printf("~SubClass()\n");};
   virtual void vfunc1(){};
private:
   int z;
};


int main() {
   BaseClass *a_ptr = new SubClass();
   //觸發析構 
   delete a_ptr; 
} 
;只讀段中的虛表結構
vtable for SubClass:
    .quad   0
    .quad   typeinfo for SubClass
    .quad   SubClass::'scalar deleting destructor' ;代理析構函數的地址
    .quad   SubClass::~SubClass() ;析構函數的地址,這裏godbolt沒有把它們區分出來
    .quad   SubClass::vfunc1()

main:
     mov     QWORD PTR [rbp-24], rbx  ;rbx爲a_ptr的指針
     cmp     QWORD PTR [rbp-24], 0    ;判斷a_ptr是否爲null,這是編譯器加的。
     je      .L9                      ;如果爲null直接跳過析構
     mov     rax, QWORD PTR [rbp-24]
     mov     rax, QWORD PTR [rax]
     add     rax, 8                   ;this指針偏移8處,即指向代理析構函數
     mov     rax, QWORD PTR [rax]     ;rax爲代理析構函數的地址
     mov     rdx, QWORD PTR [rbp-24] 
     mov     rdi, rdx                 ;參數:this指針
     call    rax                      ;調用代理析構函數

;SubClass的代理析構函數
SubClass::'scalar deleting destructor':
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax              ;參數:this指針
        call    SubClass::~SubClass() ;調用析構函數
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 24               ;參數:釋放24字節大小的堆空間
        mov     rdi, rax              ;參數:堆空間的首地址
        call    operator delete(void*, unsigned long) ;釋放堆空間
        leave
        ret

 .LC1:
        .string "~SubClass()"       
;SubClass的析構函數,執行析構函數中的代碼
SubClass::~SubClass():
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     QWORD PTR [rbp-8], rdi
    mov     edx, OFFSET FLAT:vtable for SubClass+16
    mov     rax, QWORD PTR [rbp-8]
    mov     QWORD PTR [rax], rdx      ;還原對象的虛表指針,使其指向相關類的虛表
    mov     edi, OFFSET FLAT:.LC1
    call    puts                      ;調用puts函數,這裏編譯器把printf調用轉換成puts了。
    mov     rax, QWORD PTR [rbp-8]
    mov     rdi, rax                  ;參數:this指針 
    call    BaseClass::~BaseClass()   ;調用基類的析構函數
    nop
    leave
    ret

.LC0:
        .string "~BaseClass()"
;BaseClass的析構函數,執行析構函數中的代碼        
BaseClass::~BaseClass():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for BaseClass+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        nop
        leave
        ret

通過分析C++析構函數的調用過程,我們就知道了爲什麼C++基類的析構函數要聲明爲virtual了。我們希望當調用C++基類BaseClass的析構函數時能夠觸發動態綁定,能夠找到當前對象所屬類的虛函數表中的析構函數。
如果不聲明BaseClass的析構函數爲virtual,那麼在調用delete a_ptr時,將只會釋放BaseClass大小的內存,給SubClass中成員變量分配的內存將得不到釋放,從而導致內存泄漏。

C++11中的Lambda表達式

lambda表達式表示一個可調用的代碼單元。可以理解爲一個未命名的內聯函數。
lambda表達式具有如下形式:

[capture list](parameter list) -> return type {function body}

下面定義了一個C++函數,其中有一個lambda表達式。v1之前的&符號指出v1是以引用方式捕獲,當lambda返回v1時,它返回的是v1指向對象的值,所以j的值是0,而不是42.

void fcn1(){
    int v1 =42;
    auto f= [&v1] {return v1;};
    v1 = 0;
    auto j = f();
}

對應的反彙編代碼如下,可以看到編譯器爲fcn1中的lambda表達式在代碼段中生成了一段指令,當調用這個lambda時就會執行到這段指令,跟普通的函數調用一致。
可以看出傳遞給fcn1()::{lambda()#1}函數的參數rdi的值其實就是v1變量的地址,所以這個lambda是是採用引用方式捕獲變量的。

.Ltext0:
fcn1()::{lambda()#1}::operator()() const:

        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]
        mov     eax, DWORD PTR [rax]
        pop     rbp
        ret
fcn1():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-8], 42
        lea     rax, [rbp-8]
        mov     QWORD PTR [rbp-16], rax
        mov     DWORD PTR [rbp-8], 0
        lea     rax, [rbp-16]
        mov     rdi, rax
        call    fcn1()::{lambda()#1}::operator()() const
        mov     DWORD PTR [rbp-4], eax
        nop
        leave
        ret

參考

《IDA Pro權威指南》
《C++反彙編與逆向分析技術揭祕》
《C++ Primer(第5版)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章