C程式啟動並執行背後(1)

來源:互聯網
上載者:User

標籤:

一個成功的男人背後,至少有一個偉大的女人;一個不成功的男人,至少有一雙手。

而一個C程式,無論成功不成功,它的背後一定有一個作業系統,一個shell,一套工具鏈。

世界本就不公平。隱藏在顯而易見的事實背後的,你若能看透,便可以站在對自己公平的那一端。

1、進程地址空間

一個進程一旦建立,就會自認為佔有4G記憶體(X86_32),這個記憶體被稱作虛擬記憶體,也就是進程的地址空間。在Linux下,進程地址空間的布局大致如所示,其中的使用者空間大致由這些部分組成:

  1. 程式碼片段
  2. 初始化資料區段
  3. 未初始化資料區段

這些段,反映到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檔案,並查看它們的記憶體布局:

[[email protected] ~]# size memlayout.o   text       data        bss        dec        hex    filename     66          0          0         66         42    memlayout.o[[email protected] ~]# 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相差懸殊,使用者空間的程式總會終結的原因。

C程式啟動並執行背後(1)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.