程式的結構體系(十)
我們本節來看看程式的結構體系。我們在平時所編寫的程式中,其實它的結構是非常值得研究的,這樣有助於我們編寫出效率更高的程式碼,並且也有助於我們理解整個可執行檔案的架構。好了,廢話不多說。那麼我們的程式究竟是由什麼構成的呢?它是由不同的段所構成的,包括程式碼段、資料段等。
程式有兩個特徵,靜態特徵和動態特徵。靜態特徵指的是指令和資料,而動態特徵則指的是執行指令處理資料的動作。下來我們來看看程式原始碼到可執行程式檔案的對應關係,如下圖所示
我們之前在 C 語言中曾經也說過這方面的知識,今天我們再次回顧下。由上圖我們可以看到初始化過的全域性變數和初始化過的 static 修飾的區域性變數都儲存在 .data 段,而沒有初始化的變數則儲存在 .bss 段,函式都儲存在 .text 段。
下來我們來一一介紹下上面的幾個段,首先是程式碼段(.text),程式碼段具有以下特徵
1、原始碼中的可執行語句編譯後進入程式碼段;
2、程式碼段在有記憶體管理單元的系統中具有隻讀屬性;
3、程式碼段的大小在編譯結束後就已經固定(不能動態改變);
4、程式碼段中可以包含常量資料(如常量字串)。
其次是資料段,它包括(.data, .bss, .rodata)。資料段用於處處原始碼中具有全域性生命期的變數,其中 .bss 段儲存未初始化(或初始化為 0)的變數、.data 段儲存具有非 0 的初始值的變數、.rodata 儲存 const 關鍵字修飾的變數。那麼我們思考下:我們在前面說初始化過的和未初始化的全域性變數和靜態區域性變數是分開來存放的,為什麼要搞的如此複雜呢?一般在程式載入後,.bss 段中的所有記憶體單元被初始化為 0,將程式檔案中 .data 段相關的初始值寫入對應的記憶體單元。因為 .bss 段中的變數不用在程式檔案中儲存初始值,從而減少可執行程式檔案的體積,並且提高程式的載入效率。
下來我們還是以程式碼為例來進行分析說明
int g_no_var_v; int g_var_v = 1; int g_main() { static no_var_v1; static var_v2 = 2; return 0; }
我們編譯來看看結果
我們看到在 .data 和 .bss 段中各佔了8位元組。接下來我們把全域性變數 g_no_var_v 的型別改為 char,然後再看看結果
我們看到結果還是 8,因為它是四位元組對齊的,因此結果還是 8,如果我們改為兩個 char 型別的,那麼結果便為 4 了。g_main() 的入口地址便是 .text 段的起始地址了,也再次證明了我們的程式是自己指定入口函式的了。
下來我們來看看棧(Stack)。棧在程式中的本質是一片連續儲存的記憶體空間,SP 暫存器作為棧頂“指標”實現入棧操作和出棧操作。如下圖所示
棧的作用大致有以下幾方面:
1、中斷髮生時,棧用於儲存暫存器的值;
2、函式呼叫時,棧用於儲存函式的活動記錄(棧幀資訊);
3、併發程式設計時,每一個執行緒擁有自己獨立的棧。
下來我們來看看堆(Heap),堆是一片“閒置”的記憶體空間,其目的是用於提供動態的記憶體分配;當然堆空間的分配是需要 malloc 函式的支援的,它在使用完成後也需要藉助於 free 函式進行手動的釋放空間。再來看看記憶體對映段(Memory Mapping Segment),在核心中,是直接將硬碟檔案的內容直接對映到記憶體對映段(mmap),動態連結庫在可執行程式載入時對映到記憶體對映段,以便程式在執行時能夠建立匿名對映區存放程式資料。
我們簡單來介紹下記憶體對映檔案的原理:
1、將硬碟上的檔案資料邏輯對映到記憶體中(零耗時);
2、通過缺頁中斷進行檔案資料的實際載入(一次資料拷貝);
3、對映後的記憶體的讀寫就是對檔案資料的讀寫;
對映關係如下:
在程式的結構體系中,它的整體分佈如下圖所示