C程式執行的背後
一個成功的男人背後,至少有一個偉大的女人;一個不成功的男人,至少有一雙手。
而一個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相差懸殊,使用者空間的程式總會終結的原因。
相關文章
- jenkins後臺程式執行Jenkins
- 將程式在後臺執行和殺掉後臺的程式
- Linux 下後臺執行和按照守護程式方式後臺執行的坑Linux
- 原程式執行良好,Pyinstaller封裝後執行出錯 的分析封裝
- C/C++ 效能優化背後的方法論:TMAMC++優化
- 動態執行c#程式碼C#
- C程式從編譯到執行C程式編譯
- Linux程式後臺執行實踐Linux
- 讓.py程式後臺執行(Linux)Linux
- C,java,Python,這些名字背後的江湖!JavaPython
- C++中的多執行緒及其之後的周邊C++執行緒
- 【28】VsCode如何執行C#程式碼VSCodeC#
- AVEVA MARINE C# 程式執行MarJobLauncher工作C#
- c++多執行緒程式設計:C2672C++執行緒程式設計
- intellij 關閉後程式還在執行IntelliJ
- XYHCMS 3.6 後臺程式碼執行漏洞
- Nodejs 揭秘:單執行緒魔法背後的真相以及它如何為高效能應用程式提供動力NodeJS執行緒
- 7個連環問揭開java多執行緒背後的彎彎繞Java執行緒
- 解讀銀行卡支付背後的原理
- 程式碼背後的智慧:20條程式設計感悟程式設計
- C#多執行緒程式設計實戰1.1建立執行緒C#執行緒程式設計
- C#多執行緒程式設計-基元執行緒同步構造C#執行緒程式設計
- Linux C/C++程式設計中的多執行緒程式設計基本概念LinuxC++程式設計執行緒
- C# 以管理員身份執行WinForm程式C#ORM
- Linux C++ 多執行緒程式設計LinuxC++執行緒程式設計
- 【程式媛曬83行程式碼】雲棲社群聚能聊專家,背後83行程式碼的故事行程
- Java程式碼寫好後怎麼執行?Java
- IT行業高薪的背後:未來9成的程式設計師會被淘汰?行業高薪程式設計師
- 08 Windows批處理之執行編譯後的程式Windows編譯
- c++是如何執行的C++
- inject 不生效?!依賴注入背後的實現原理和執行邏輯是怎樣的?依賴注入
- ELF PHP 可執行程式執行後載入重型指令碼的過程PHP行程指令碼
- 執行python指令碼後臺執行Python指令碼
- windows的nohup後臺執行Windows
- Liunx C 程式設計之多執行緒與Socket程式設計執行緒
- C#多執行緒(4):程式同步Mutex類C#執行緒Mutex
- C#呼叫IronPython動態執行Python程式碼C#Python
- C語言 之 多執行緒程式設計C語言執行緒程式設計
- IT去中心化背後的低程式碼平臺中心化