一個成功的男人背后,至少有一個偉大的女人;一個不成功的男人,至少有一雙手。
而一個C程序,無論成功不成功,它的背后一定有一個操作系統,一個shell,一套工具鏈。
世界本就不公平。隱藏在顯而易見的事實背后的,你若能看透,便可以站在對自己公平的那一端。
1、進程地址空間
一個進程一旦建立,就會自認為占有4G內存(X86_32),這個內存被稱作虛擬內存,也就是進程的地址空間。在Linux下,進程地址空間的布局大致如下圖所示,其中的用戶空間大致由這些部分組成:
- 代碼段
- 初始化數據段
- 未初始化數據段
- 堆
- 棧
這些段,反映到ELF格式的目標文件(object file)中,就又可能由許多不同的節(section)組成。節這個東西更加細致復雜,暫且不表。
代碼段
保存的是可執行指令,通常是只讀的,防止指令被程序自身修改。但程序是無法防止被人為修改,否則哪來那么多的修改器。Vim就可以直接編輯二進制文件,指令的機器碼任意修改。
存儲實例:
push %ebp
movl %esp, %ebp
初始化數據段
保存的是已初始化了的全局變量和靜態變量,它可以進一步划分為只讀區域和可讀寫區域。
存儲實例:
Char *string = “hello world”(全局)
“hello world”在只讀區域,指針string在可讀寫區域
而Char string[] = “hello world”(全局)
就只存儲string在讀寫區域中。因為string已被分配存儲空間。
Static int class = 6 (全局/局部)
全局的容易理解。局部靜態變量的意義,在於函數調用完后,其占用的存儲單元也不被釋放。如此便不可以存放到棧中,而又已被初始化,那么存放到這個段自然是合理的。
未初始化數據段
通常稱為bss段,名字來自於“block started by symbol”—由符號開始的塊。存放於此段的變量,在程序執行之前就被初始化為0或Null指針。
注意,未賦值的指針會被初始化為空指針!如果程序中定義的指針沒有初始化,而后面又引用它指向的內存區域,那么在Linux下會引發“段錯誤”。
棧
這就是個狗皮膏葯,用處大,卻難搞。函數調用時,對棧的操作基本上由編譯器完成。函數一旦被調用,就會生成一個棧幀(stack frame),棧幀的范圍由兩個 “棧指針”寄存器%ebp、%esp限定。
存儲實例:
Caller的返回地址;
Caller的寄存器信息,如%ebp,%eax;
Callee自身的局部變量
堆
用戶手動分配內存的區域,malloc和free,誰用誰知道。另外,共享庫和動態加載的模塊,也存放於堆中。
那么問題來了,實際編譯好的目標文件是否真的是這樣的呢?
以一個非常簡單的C程序—memlayout.c—作為例程:
int main() { return 0; }
用GCC分別編譯生成memlayout.o和memlayout文件,並查看它們的內存布局:
[root@localhost ~]# size memlayout.o text data bss dec hex filename 66 0 0 66 42 memlayout.o [root@localhost ~]# size memlayout text data bss dec hex filename 1055 272 4 1331 533 memlayout
這個程序沒有定義任何的變量,由memlayout.o可以看出,data、bss為0是符合預期的。
段依然還是那些段,可最終的可執行文件如何卻把它們都搞大了?
我並沒有調用exit,為何程序自動流產?
男人的直覺也很准的,特別是程序出軌的時候。憑男人的直覺,我想,一定是編譯器(實質是鏈接器)在某個地方插了一腳。
這也是一個細瑣的問題,先做簡要說明,容以后再表。
2、程序的生命周期
編譯好的C程序是躺在磁盤里的,這時只能叫文件。加載到內存並撒腿狂奔的時候,才叫進程。老師們也告訴過我們,一個運行的“hello world”也是一個進程。所以一定要先有一個進程環境,程序才有狂奔的空間。我的家里沒有草原,所以董小姐沒有理我。
一個C程序的前世今生大概是這樣的:
- Shell首先創建一個子進程,設置好進程環境;
- 子進程調用execve而陷入內核;
- 內核調用加載器程序,加載器清理子進程環境后,再加載可執行文件到子進程環境中;
- 加載器跳轉到該程序的入口點(entry point),開始執行C啟動代碼;
- 調用main函數,執行真正的C程序;
- 調用_exit,把控制交還給內核。
也就是說,在寫好的main函數之前,編譯器添加了一段C啟動代碼,是C程序執行之前的准備工作;在main函數之后,編譯器至少添加(調用)了_exit()來保證進程的正確終止。這也是為什么,中間目標文件和最終可執行文件size相差懸殊,用戶空間的程序總會終結的原因。