main函數執行之前都干啥了----C/C++運行時庫剖析


一、引言        

本文介紹運行時庫實現的功能,你會看到在main函數執行之前都做了什么。先來理解以下代碼:

 

#include <stdio.h>
void init(void) __attribute__((constructor));
void init(void){
    printf(“before enter main!\n”);
}
void exit_func(void){
    printf(“after leave main!\n”);
}
int main(void){
    int *m = malloc(5, sizeof(int));
    atexit(exit_fun);
    printf(“hello world!\n”);
}
 
思考:
1 程序的運行結果是?
2進入main之前做了哪些操作,如果進入main?
3 如何支持printf函數?
4 退出main之后做了什么事情?
5 如何MALLOC和FREE等堆操作?
6 程序會造成內存泄露嗎?
7 等等
二、運行時庫概述
任何一個C/C++程序,它的背后都有一套龐大的代碼來進行支撐,以使得該程序能夠正常運行。這套代碼至少包括入口函數,及其依賴的函數所構成的函數集合。當然,它還應該包括各種標准函數(如字符串,數學運算等)的實現。一般的程序運行過程如下:
1.操作系統創建進程后,把控制權交給程序的入口函數(gcc –e (_startEntryPoint)),   這個函數往往是運行時庫的某個入口函數。  glibc 的入口函數是_start,
          msvc(vc6.0)是mainCRTStartup
2. 入口函數對運行庫和程序運行環境進行初始化,包括堆,I/O,線程,全局變量構造(constructor)等。
3. 調用MAIN函數,正式開始執行程序主體。
4. 執行MAIN完畢,返回入口函數,進行清理工作,包括全局變量析構,堆銷毀,關閉I/O等,然后進行系統調用介紹進程

三、運行時庫主要功能模塊
1.啟動與退出,包括入口函數及其依賴函數
2.標准函數,C語言標准規定的標准函數
3.I/O,I/O功能的封裝和實現,如提供PRINT
4.堆,堆的封裝和實現
5.調試支持等

四、程序詳細運行過程
以下分析默認為WINDOWS靜態鏈接過程。
1. 程序執行前裝載器會把用戶的參數和環境變量壓入棧,接着操作系統把控制權交給mainCRTStartup入口函數。
用戶的參數:對應 int main(int argc,char *argv[])
環境變量:系統公用數據,系統搜索路徑等等。
程序需要獲取用戶參數和環境變量均是 從棧上獲取,需要理解棧幀的概念。

2. 初始化和OS版本相關的全局變量

3. 初始化堆,每個進程都有屬於自己的堆。它是一次性從系統中申請一塊比較大的虛擬空間(實際需要時(如malloc)才會映射到物理頁),以后在進程中由庫的堆管理算法來維護這個堆。當堆不夠用時再繼續申請一塊大的虛擬空間繼續分配。 可見,並非程序每次malloc都會調用系統API(API調用比較耗時,涉及到用戶態到內核態的上下文切換),效率比較高.
堆相關操作:
HeapCreate:創建一個堆,最終會調用virtualAlloc()系統API函數去創建堆。
HeapAlloc: malloc會調用該函數
HeapFree: free會調用該函數
HeapDestroy:摧毀一個堆

4. I/O初始化,繼承父進程打開文件表。可見,子進程是可以訪問父進程打開的文件。 如果父進程沒有打開標准的輸入輸出,該進程會初始化標准輸入輸出。即初始化以下指針變量: stdin,stdout,stderr. 它們都是FILE類型指針。 在linux和windows中,打開文件對應於操作一個內核對象,其處於內核態,因此用戶態是不能直接操作該內核對象的。用戶只能操作與內核對象相關聯的FILE結構指針。 對應關系是:


Printf其實是調用stdout指針在屏幕上輸出
#define printf(args…) fprintf(stdout, ##args)
Args…表示變長輸入參數。用以下四個宏根據棧來獲取。
Va_list、Va_start、Va_arg、Va_end

5. 獲取命令行參數和環境變量

6.  初始化C庫的一些數據

7. 全局變量構造,如各個全局類對象的構造函數調用和標記 __attribute__((constructor))屬性的各個函數。它們都應該在進入main前進行調用。
需要運行時庫和C/C++編譯器、連接器的配合才能實現這個功能。
1)編譯器編譯某個.cpp(設為main.cpp)文件時,會將所有的構造函數實現作為一個整體放到.init段,把析構函數實現放到.finit段,然后在.ctors段放置.init段的地址(該地址即是該文件的各個構造函數的總入口)。
2)運行時庫有一個庫是crtbegin.o,它的.ctors段放置的內容為-1,ctrend.o,它的.ctors段放置的內容也是-1。
3)用鏈接器進行連接:ld crtbegin.o main.o crtend.o一定要按這種順序,否則出錯。鏈接后的.ELF文件是將以上各個文件的.init/.finit/.ctors等段分別合並。當然.data/.text段也會相應合並。
全局變量構造時即是遍歷.ctors段的內容,從-1(crtbegin.o)開始,再到-1(crtend.o)結束,中間每四個字節即是各個文件的構造入口函數指針,如果非0,即進行調用。

8. 注冊析構函數
為了支持C++類的析構函數,和標記 __attribute__((deconstructor))屬性的各個函數在main之后會被調用,而且是按構造的相反順序進行調用,同樣需要編譯器和鏈接器以及運行時庫的支持,原理跟構造相仿。只是為了逆序,使用了atexit注冊各個虛構函數,注冊時在鏈表頭插入鏈接,main退出以后也從鏈表頭開始獲取鏈表函數,並進行調用。

9. 執行函數主體。
調用main函數執行,等待返回。 在這里可以用到之前已經初始化的各種資源,如I/O, 堆申請釋放等等

10. 調用析構函數

11. 釋放堆
12. 釋放其他資源
13 調用exit系統API退出進程


五、回答引言的問題。

1. 參考以上分析, 程序的打印結果是:
 before enter main!
 hello world!
 after leave main!

2. 程序並不會產生系統內存泄漏。進程退出,其會摧毀整個堆。所謂內存泄露是指在進程的運行中,不恰當、不合理地申請內存,但沒有釋放內存。


 


免責聲明!

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



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