C++ 對象內存模型


1. 先看一下整體代碼的內存布局

from:https://manybutfinite.com/post/anatomy-of-a-program-in-memory/

2. 簡單用個實例來體現程序中各個變量的內存位置(引用於《C專家編程》截圖)

我們這邊着重講一下堆(heap),棧(stack)

  • 堆(heap):堆是用於存放進程執行中被動態分配的內存段。它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時。新分配的內存就被動態加入到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
  • 棧(stack):棧又稱堆棧, 是用戶存放程序暫時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包含static聲明的變量。static意味着在數據段中存放變量)

 這里說個我自己困擾很久的問題,就是為什么堆是向上增長而棧向下增長????

也是網上搜到的個人覺得比較有說服力的解釋:

歷史原因:堆和棧的相向生長

在沒有MMU的時代,為了最大的利用內存空間,堆和棧被設計為從兩端相向生長。那么哪一個向上,哪一個向下呢?
人們對數據訪問是習慣於向上的,比如你在堆中new一個數組,是習慣於把低元素放到低地址,把高位放到高地址,所以堆向上生長比較符合習慣。而棧則對方向不敏感,一般對棧的操作只有PUSH和pop,無所謂向上向下,所以就把堆放在了低端,把棧放在了高端
存儲器的低端分配給堆棧使用,當堆棧指針減到0時就會產生堆棧溢出,檢測堆棧指針是否為0是很容易實現的,在有MMU的情況下,頁面越界即為堆棧溢出;若堆棧向高地址增長,在有MMU的情況下,亦可根據頁面越界判斷堆棧溢出,但無MMU時則不易實現,因為堆棧頂端可以隨時變化。
這樣設計可以使得堆和棧能夠充分利用空閑的地址空間。如果棧向上漲的話,我們就必須得指定棧和堆的一個嚴格分界線,但這個分界線怎么確定呢?平均分?但是有的程序使用的堆空間比較多,而有的程序使用的棧空間比較多。所以就可能出現這種情況:一個程序因為棧溢出而崩潰的時候,其實它還有大量閑置的堆空間呢,但是我們卻無法使用這些閑置的堆空間。所以呢,最好的辦法就是讓堆和棧一個向上漲,一個向下漲,這樣它們就可以最大程度地共用這塊剩余的地址空間,達到利用率的最大化!!

3. 虛擬內存內部實現

4. C++幾種對象內存模型介紹(from:https://tangocc.github.io/2018/03/20/cpp-class-memory-struct/

4.1 無繼承

Class Base { public: int a; int b; virtual void function(); }

如上圖所示,對於無繼承狀態的C++布局:
1)首先是虛函數表指針,該指針是由編譯器 定義和初始化(編譯階段,編譯器在構造函數內增加代碼實現)
2)成員函數代碼存儲在 代碼段,堆上構造虛函數表,將虛成員函數的地址存儲在虛函數內。
3)數據成員按照聲明的順序布局;

4.2 單繼承

Class Base {

  public:
  int a;
  int b;
  virtual void function();
}


Class Derive : public Base {

  public:
    int c;
  virtual void function_2();
}

如上圖所示,對於無繼承狀態的C++布局:
1) 首先是基類虛函數表指針
2) 基類數據成員
3) 子類數據成員
4) 子類實現基類的虛函數,並覆蓋基類虛函數表中相應的函數的地址
5) 子類擴展基類的虛函數表,將子類的虛函數地址存儲在基類虛函數表中
6) 內存中只存在一張虛函數表

4.3 多繼承

Class Base1 {

  public:
  int a;
  int b;
  virtual void function_1();
}

Class Base2 {

  public:
  int c;
  virtual void function_2();
}

Class Derive : public Base1,public Base2 {

  public:
  int d;
  virtual void function_3();
}

 

 如上圖所示,對於無繼承狀態的C++布局:

1) 首先是基類1虛函數表指針
2) 基類1數據成員
3) 基類2虛函數表指針
4) 基類2數據成員
5) 子類數據成員
6) 子類實現基類的虛函數,並覆蓋基類虛函數表中相應的函數的地址
7) 子類擴展第一個基類的虛函數表,將子類的虛函數地址存儲在基類虛函數表中
8) 內存中存在2張虛函數表

5. C++內存問題及常用的解決方法

5.1. 內存管理功能問題

由於C++語言對內存有主動控制權,內存使用靈活和效率高,但代價是不小心使用就會導致以下內存錯誤:

• memory overrun:寫內存越界 
• double free:同一塊內存釋放兩次 
• use after free:內存釋放后使用 
• wild free:釋放內存的參數為非法值 
• access uninitialized memory:訪問未初始化內存 
• read invalid memory:讀取非法內存,本質上也屬於內存越界 
• memory leak:內存泄露 
• use after return:caller訪問一個指針,該指針指向callee的棧內內存 
• stack overflow:棧溢出

常用的解決內存錯誤的方法

  • 代碼靜態檢測

       靜態代碼檢測是指無需運行被測代碼,通過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證明,在整個軟件開發生命周期中,30%至70%的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。在C++項目開發過程中,因為其為編譯執行語言,語言規則要求較高,開發團隊往往要花費大量的時間和精力發現並修改代碼缺陷。所以C++靜態代碼分析工具能夠幫助開發人員快速、有效的定位代碼缺陷並及時糾正這些問題,從而極大地提高軟件可靠性並節省開發成本。

靜態代碼分析工具的優勢:

  1、自動執行靜態代碼分析,快速定位代碼隱藏錯誤和缺陷。

  2、幫助代碼設計人員更專注於分析和解決代碼設計缺陷。

  3、減少在代碼人工檢查上花費的時間,提高軟件可靠性並節省開發成本。

      一些主流的靜態代碼檢測工具:

       免費的cppcheck,clang static analyzer;商用的coverity,pclint等

    各個工具性能對比:   http://www.51testing.com/html/19/n-3709719.html

  • 代碼動態檢測

     所謂的代碼動態檢測,就是需要再程序運行情況下,通過插入特殊指令,進行動態檢測和收集運行數據信息,然后分析給出報告。

      1. 為了檢測內存非法使用,需要hook內存分配和操作函數。hook的方法可以是用C-preprocessor,也可以是在鏈接庫中直接定義(因為Glibc中的malloc/free等函數都是weak symbol),或是用LD_PRELOAD。另外,通過hook strcpy(),memmove()等函數可以檢測它們是否引起buffer overflow。 
      2. 為了檢查內存的非法訪問,需要對程序的內存進行bookkeeping,然后截獲每次訪存操作並檢測是否合法。bookkeeping的方法大同小異,主要思想是用shadow memory來驗證某塊內存的合法性。至於instrumentation的方法各種各樣。有run-time的,比如通過把程序運行在虛擬機中或是通過binary translator來運行;或是compile-time的,在編譯時就在訪存指令時就加入檢查操作。另外也可以通過在分配內存前后加設為不可訪問的guard page,這樣可以利用硬件(MMU)來觸發SIGSEGV,從而提高速度。 
      3. 為了檢測棧的問題,一般在stack上設置canary,即在函數調用時在棧上寫magic number或是隨機值,然后在函數返回時檢查是否被改寫。另外可以通過mprotect()在stack的頂端設置guard page,這樣棧溢出會導致SIGSEGV而不至於破壞數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM