1. 程式人生 > >從彙編角度看C++類的方法訪問類成員的原理

從彙編角度看C++類的方法訪問類成員的原理

C++編譯後最終也是生成了機器碼,不需要直譯器或虛擬機器來執行。相比C語言,C++的類大大的方便了程式碼結構的組織,使得構建大程式簡便容易了很多。例項化一個類後,類的成員方法就可以訪問這個類的成員了,那麼從彙編角度看,到底是如何實現的呢?其實這個原理也十分簡單,類例項後的物件從記憶體上和其所有成員變數組成的結構體是一樣的,每個類的方法第一個入參就是把這個結構體的地址穿進去,類的方法通過這樣的機制實現了訪問類的成員。Python的類中self滿天飛機制上也有點類似。下面用一個例子來具體看下:

class Test{
public:
    int a;
    int b;
    int
c; int d; int e; Test(int a, int b, int c, int d, int e) { this->a = a; this->b = b; this->c = c; this->d = d; this->e = e; } int fun() { int a1 = a; int b1 = b; int c1 = c; int d1 = d; int
e1 = e; return 100; } }; int main() { Test test(1, 2, 3, 4, 5); test.fun(); return 0; }

上面的程式碼非常簡單,主要看一下反彙編後的彙編程式碼,用下面命令編譯 g++ -g -m64 main.c, 編譯生成 a.out ELF檔案, 然後反彙編 objdump -dS a.out, 即可看到原始碼和反彙編的彙編程式碼一一對應的呈現, 也可以 gdb a.out, 用 disass /m fun_name 看與原始碼對應的函式反彙編。

首先看下類Test的大小, Test型別大小就是其成員變數組成的結構體的大小。

(gdb) pt Test
type = class Test {
  public:
    int a;
    int b;
    int c;
    int d;
    int e;

    Test(int, int, int, int, int);
    int fun(void);
}
(gdb) p sizeof(Test)
$1 = 20

然後看下Test類建構函式的反彙編,函式呼叫約定可以看:從彙編看Linux C函式的呼叫約定和引數傳遞的細節,按照標準呼叫約定,第一個入參 %rdi 暫存器並不是程式碼形參,而就是這個物件本身的地址。

    Test(int a, int b, int c, int d, int e) {
  //400530:   55                      push   %rbp
  //400531:   48 89 e5                mov    %rsp,%rbp
  //400534:   48 89 7d f8             mov    %rdi,-0x8(%rbp)  // 把呼叫約定引數1,test對應例項的地址
  //400538:   89 75 f4                mov    %esi,-0xc(%rbp)  // 呼叫約定裡引數2
  //40053b:   89 55 f0                mov    %edx,-0x10(%rbp) // 呼叫約定裡引數3
  //40053e:   89 4d ec                mov    %ecx,-0x14(%rbp) // 呼叫約定裡引數4
  //400541:   44 89 45 e8             mov    %r8d,-0x18(%rbp) // 呼叫約定裡引數5
  //400545:   44 89 4d e4             mov    %r9d,-0x1c(%rbp) // 呼叫約定裡引數6
        this->a = a;
  //400549:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  //40054d:   8b 55 f4                mov    -0xc(%rbp),%edx
  //400550:   89 10                   mov    %edx,(%rax)
        this->b = b;
  //400552:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  //400556:   8b 55 f0                mov    -0x10(%rbp),%edx
  //400559:   89 50 04                mov    %edx,0x4(%rax)
        this->c = c;
  //40055c:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  //400560:   8b 55 ec                mov    -0x14(%rbp),%edx
  //400563:   89 50 08                mov    %edx,0x8(%rax)
        this->d = d;
  //400566:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  //40056a:   8b 55 e8                mov    -0x18(%rbp),%edx
  //40056d:   89 50 0c                mov    %edx,0xc(%rax)
        this->e = e;
  //400570:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  //400574:   8b 55 e4                mov    -0x1c(%rbp),%edx
  //400577:   89 50 10                mov    %edx,0x10(%rax)
    }
  //40057a:   5d                      pop    %rbp
  //40057b:   c3                      retq

然後看下Test成員函式的反彙編,隱含著吧這個類例項的首地址作為第一個引數傳了進來:

000000000040057c <_ZN4Test3funEv>:
    int fun() {
  //40057c:   55                      push   %rbp
  //40057d:   48 89 e5                mov    %rsp,%rbp
  //400580:   48 89 7d d8             mov    %rdi,-0x28(%rbp) // 把呼叫約定的引數1放到棧中
        int a1 = a;
        int b1 = b;
  //400584:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  //400588:   8b 00                   mov    (%rax),%eax
  //40058a:   89 45 ec                mov    %eax,-0x14(%rbp)
        int c1 = c;
  //40058d:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  //400591:   8b 40 04                mov    0x4(%rax),%eax
  //400594:   89 45 f0                mov    %eax,-0x10(%rbp)
        int d1 = d;
  //400597:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  //40059b:   8b 40 08                mov    0x8(%rax),%eax
  //40059e:   89 45 f4                mov    %eax,-0xc(%rbp)
        int e1 = e;
  //4005a1:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  //4005a5:   8b 40 0c                mov    0xc(%rax),%eax
  //4005a8:   89 45 f8                mov    %eax,-0x8(%rbp)
        return 100;
  //4005ab:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  //4005af:   8b 40 10                mov    0x10(%rax),%eax
  //4005b2:   89 45 fc                mov    %eax,-0x4(%rbp)
    }
  //4005b5:   b8 64 00 00 00          mov    $0x64,%eax
};
  //4005ba:   5d                      pop    %rbp
  //4005bb:   c3                      retq

最後看下 main 函式如何在棧裡定義個 Test物件並呼叫其方法:

int main()
{
  //4004ed:   55                      push   %rbp
  //4004ee:   48 89 e5                mov    %rsp,%rbp

    Test test(1, 2, 3, 4, 5);
  //4004f1:   48 83 ec 20             sub    $0x20,%rsp
  //4004f5:   48 8d 45 e0             lea    -0x20(%rbp),%rax
  //4004f9:   41 b9 05 00 00 00       mov    $0x5,%r9d  // 呼叫約定裡入參6, 從右向左引數壓棧
  //4004ff:   41 b8 04 00 00 00       mov    $0x4,%r8d  // 呼叫約定裡入參5
  //400505:   b9 03 00 00 00          mov    $0x3,%ecx  // 呼叫約定裡入參4
  //40050a:   ba 02 00 00 00          mov    $0x2,%edx  // 呼叫約定裡入參3
  //40050f:   be 01 00 00 00          mov    $0x1,%esi  // 呼叫約定裡入參2
  //400514:   48 89 c7                mov    %rax,%rdi  // 呼叫約定裡入參1, test的地址
  //400517:   e8 14 00 00 00          callq  //400530 <_ZN4TestC1Eiiiii>

    test.fun();
  //40051c:   48 8d 45 e0             lea    -0x20(%rbp),%rax
  //400520:   48 89 c7                mov    %rax,%rdi  // test變數的地址作為了第一個引數
  //400523:   e8 54 00 00 00          callq  //40057c <_ZN4Test3funEv>

    return 0;
  //400528:   b8 00 00 00 00          mov    $0x0,%eax
}
  //40052d:   c9                      leaveq
  //40052e:   c3                      retq

通過反彙編C++編譯後的ELF檔案, 可以很容易看出,C++ 的class物件大小就是其成員變數組成對應接頭的大小,class方法(成員函式)可以訪問成員變數就是因為每個函式隱式的把物件的首地址傳給了成員函式。知道了這點,就更加容易知道this指標的本質,也可以思考python的類self為什麼滿天飛。