做者 | 軒轅之風O
php
來源 | 編程技術宇宙(ID:xuanyuancoding)程序員
頭圖 | CSDN 下載自東方IC
前幾天,讀者羣裏有小夥伴提問:從進程建立後,究竟是怎麼進入我寫的 main 函數的?
shell
今天這篇文章就來聊聊這個話題。編程
首先先劃定一下這個問題的討論範圍:C/C++語言。數據結構
這篇文章主要討論的是操做系統層面上對於進程、線程的建立初始化等行爲,而像 Python、Java 等基於解釋器、虛擬機的語言,如何進入到 main 函數執行,這背後的路徑則更長(包含了解釋器和虛擬機內部的執行流程),之後有機會再討論。因此這裏就重點關注 C/C++這類 native 語言的 main 函數是如何進入的。多線程
本文會兼顧敘述 Linux 和 Windows 兩個主要平臺上的詳細流程。異步
建立進程
第一步,建立進程。函數
在 Linux 上,咱們要啓動一個新的進程,通常經過 fork + exec 系列函數來實現,前者將當前進程「分叉」出一個孿生子進程,後者負責替換這個子進程的執行文件,來執行子進程的新程序文件。spa
這裏的 fork、exec 系列函數,是操做系統提供給應用程序的 API 函數,在其內部最終都會經過系統調用,進入操做系統內核,經過內核中的進程管理機制,來完成一個進程的建立。操作系統
操做系統內核將負責進程的建立,主要有下面幾個工做要作:
建立內核中用於描述進程的數據結構,在Linux上是task_struct
建立新進程的頁目錄、頁表,用於構建新進程的內存地址空間
在 Linux 內核中,因爲歷史緣由,Linux 內核早期並無線程的概念,而是用任務:task_struct 來描述一個程序的執行實例:進程。
在內核中,一個任務對應就是一個 task_struct,也就是一個進程,內核的調度單元也是一個個的 task_struct。
後來,多線程的概念興起,Linux 內核爲了支持多線程技術,task_struct 實際上表示的變成了一個線程,經過將多個 task_struct 合併爲一組(經過該結構內部的組 id 字段)再來描述一個進程。所以,Linux 上的線程,也稱爲輕量級進程。
系統調用 fork 的一個重要使命就是要去建立新進程的 task_struct 結構,建立完成後,進程就擁有了調度單元。隨後將開始能夠參與調度並有機會得到執行。
加載可執行文件
經過 fork 成功建立進程後,此時的子進程和父進程至關於一個細胞進行了有絲分裂,兩個進程「幾乎」是如出一轍的。
而要想子進程執行新的程序,在子進程中還須要用到exec系列函數來實現對進程可執行程序的替換。
exec系列函數一樣是系統調用的封裝,經過調用它們,將進入內核sys_execve來執行真正的工做。
這個工做細節比較多,其中有一個重要的工做就是加載可執行文件到進程空間並對其進行分析,提取出可執行文件的入口地址。
咱們使用 C、C++ 等高級語言編寫的代碼,最終經過編譯器會編譯生成可執行文件,在 Linux 上,是 ELF 格式,在 Windows 上,稱之爲 PE 文件。
不管是 ELF 文件仍是 PE 文件,在各自的文件頭中,都記錄了這個可執行文件的指令入口地址,它指示了程序該從哪裏開始執行。
這個入口指向哪裏,是咱們的 main 函數嗎?這裏賣一個關子,先來解決在這以前的一個問題:進程建立後,是如何來到這個入口地址的?
無論在 Windows 仍是 Linux 上,應用線程都會常常在用戶空間和內核空間來回穿梭,這可能出如今如下幾種狀況發生時:
系統調用
中斷
異常
從內核返回時,線程是如何知道本身從哪裏進來的,該回到應用空間的哪裏去繼續執行呢?
答案是,在進入內核空間時,線程將自動保存上下文(其實就是一些寄存器的內容,好比指令寄存器EIP)到線程的堆棧上,記錄本身從哪裏來的,等到從內核返回時,再從堆棧上加載這些信息,回到原來的地方繼續執行。
前面提到,子進程是經過sys_execve系統調用進入到內核中的,在後面完成可執行文件的分析後,拿到了ELF文件的入口地址,將會去修改原來保存在堆棧上的上下文信息,將EIP指向ELF文件的入口地址。這樣等sys_execve系統調用結束時,返回到用戶空間後,就可以直接轉到新的程序入口開始執行代碼。
因此,一個很是重要的特色是:exec系列函數正常狀況下是不會返回的,一旦進入,完成使命後,執行流程就會轉向新的可執行文件入口。
另外須要提一下的是,在Linux上,除了ELF文件,還支持一些其餘格式的可執行文件,如MS-DOS、COFF。
除了二進制的可執行文件,還支持shell腳本,這個狀況下將會將腳本解釋器程序做爲入口來啓動。
從ELF入口到main函數
上面交代了,一個新的進程,是如何執行到可執行文件的入口地址的。
同時也留了一個問題,這個入口地址是什麼?是咱們的main函數嗎?
這裏有一個簡單的C程序,運行起來後輸出經典的hello world:
#include <stdio.h>int main() { printf("hello, world!\n"); return 0;}
經過 gcc 編譯後,生成了一個 ELF 可執行文件,經過 readelf 指令,能夠實現對 ELF 文件的分析,這裏能夠看到 ELF 文件的入口地址是 0x400430:
![](http://static.javashuo.com/static/loading.gif)
隨後,咱們經過反彙編神器,IDA 打開分析這個文件,看一下位於0x400430入口的地方是什麼函數?
![](http://static.javashuo.com/static/loading.gif)
能夠看到,入口地方是一個叫作 _start 的函數,並非咱們的 main 函數。
在_start 的結尾,調用了 __libc_start_main 函數,而這個函數,位於libc.so中。
你可能疑惑,這個函數是哪裏冒出來的,咱們的代碼中並無用到它呢?
其實,在進入 main 函數以前,還有一個重要的工做要作,這就是:C/C++運行時庫的初始化。上面的 __libc_start_main 就是在完成這一工做。
在經過 GCC 進行編譯時,編譯器將自動完成運行時庫的連接,將咱們的 main 函數封裝起來,由它來調用。
glibc 是開源的,咱們能夠在 GitHub 上找到這個項目的 libc-start.c文件,一窺 __libc_start_main 的真面目,咱們的 main 函數正是被它在調用。
![](http://static.javashuo.com/static/loading.gif)
完整流程
到這裏,咱們梳理了,從進程建立 fork,到經過 exec 系列函數完成可執行文件的替換,再到執行流程進入到 ELF 文件的入口,再到咱們的 main 函數的完整流程。
![](http://static.javashuo.com/static/loading.gif)
Windows 上的一些區別
下面簡單介紹下 Windows 上這一流程的一些差別。
首先是建立進程的環節,Windows 系統將 fork+exec 兩步合併了一步,經過 CreateProcess 系列函數一步到位,在其參數中指定子進程的可執行文件路徑。
不一樣於 Linux 上進程和線程的邊界模糊,在 Windows 操做系統上,內核是有明確的進程和線程概念定義,進程用 EPROCESS 結構表示,線程用 ETHREAD 結構表示。
因此在 Windows 上,進程相關的工做準備就緒後,還須要單首創建一個參與內核調度的執行單元,也就是進程中的第一個線程:主線程。固然,這個工做也封裝在了 CreateProcess 系列函數中了。
新進程的主線程建立完成後,便開始參與系統調度了。主線程從哪裏開始執行呢?內核在建立時就明確進行了指定:nt!KiThreadStartup,這是一個內核函數,線程啓動後就從這裏開始執行。
線程從這裏啓動後,再經過Windows的異步過程調用APC機制執行提早插入的APC,進而將執行流程引入應用層,去執行Windows進程應用程序的初始化工做,好比一些核心DLL文件的加載(Kernel32.dll、ntdll.dll)等等。
隨後,再次經過APC機制,再轉向去執行可執行文件的入口點。
這後面和Linux上的機制相似,一樣沒有直接到main函數,而是須要先進行C/C++運行時庫的初始化,這以後通過運行時函數的包裝,才最終來到咱們的main函數。
下面是Windows上,從建立進程到咱們的main函數的完整流程(高清大圖:https://bbs.pediy.com/upload/attach/201604/501306_qz5f5hi1n3107kt.png):
![](http://static.javashuo.com/static/loading.gif)
如今你清楚,從進程啓動是怎麼一步步到你的main函數的了嗎?有疑惑和不解的地方,歡迎留言交流。
更多精彩推薦
☞Unity 「出圈」:遊戲引擎的技術革新和跨界商機 ☞大寫的服!用耳朵也能寫代碼?盲人程序員自學編程成爲全棧工程師 ☞小霸王被申請破產重整;虎牙員工自曝被HR擡出公司;Office 2010被微軟終止服務|極客頭條 ☞有了圖分析,可解釋的AI還遠嗎? ☞移動雲11.11,鉅惠High不停! ☞提升警戒!國內虛擬貨幣犯罪形勢日漸嚴峻
點分享點點贊點在看