新聞中心
前幾天,讀者群里有小伙伴提問:從進程創(chuàng)建后,到底是怎么進入我寫的main函數(shù)的?

金鄉(xiāng)ssl適用于網(wǎng)站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯(lián)建站的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:028-86922220(備注:SSL證書合作)期待與您的合作!
今天這篇文章就來聊聊這個話題。
首先先劃定一下這個問題的討論范圍:C/C++語言
這篇文章主要討論的是操作系統(tǒng)層面上對于進程、線程的創(chuàng)建初始化等行為,而像Python、Java等基于解釋器、虛擬機的語言,如何進入到main函數(shù)執(zhí)行,這背后的路徑則更長(包含了解釋器和虛擬機內部的執(zhí)行流程),以后有機會再討論。所以這里就重點關注C/C++這類native語言的main函數(shù)是如何進入的。
本文會兼顧敘述Linux和Windows兩個主要平臺上的詳細流程。
創(chuàng)建進程
第一步,創(chuàng)建進程。
在Linux上,我們要啟動一個新的進程,一般通過fork + exec系列函數(shù)來實現(xiàn),前者將當前進程“分叉”出一個孿生子進程,后者負責替換這個子進程的執(zhí)行文件,來執(zhí)行子進程的新程序文件。
這里的fork、exec系列函數(shù),是操作系統(tǒng)提供給應用程序的API函數(shù),在其內部最終都會通過系統(tǒng)調用,進入操作系統(tǒng)內核,通過內核中的進程管理機制,來完成一個進程的創(chuàng)建。
操作系統(tǒng)內核將負責進程的創(chuàng)建,主要有下面幾個工作要做:
- 創(chuàng)建內核中用于描述進程的數(shù)據(jù)結構,在Linux上是task_struct
- 創(chuàng)建新進程的頁目錄、頁表,用于構建新進程的內存地址空間
在Linux內核中,由于歷史原因,Linux內核早期并沒有線程的概念,而是用任務:task_struct來描述一個程序的執(zhí)行實例:進程。
在內核中,一個任務對應就是一個task_struct,也就是一個進程,內核的調度單元也是一個個的個task_struct。
后來,多線程的概念興起,Linux內核為了支持多線程技術,task_struct實際上表示的變成了一個線程,通過將多個task_struct合并為一組(通過該結構內部的組id字段)再來描述一個進程。因此,Linux上的線程,也稱為輕量級進程。
系統(tǒng)調用fork的一個重要使命就是要去創(chuàng)建新進程的task_struct結構,創(chuàng)建完成后,進程就擁有了調度單元。隨后將開始可以參與調度并有機會獲得執(zhí)行。
加載可執(zhí)行文件
通過fork成功創(chuàng)建進程后,此時的子進程和父進程相當于一個細胞進行了有絲分裂,兩個進程“幾乎”是一模一樣的。
而要想子進程執(zhí)行新的程序,在子進程中還需要用到exec系列函數(shù)來實現(xiàn)對進程可執(zhí)行程序的替換。
exec系列函數(shù)同樣是系統(tǒng)調用的封裝,通過調用它們,將進入內核sys_execve來執(zhí)行真正的工作。
這個工作細節(jié)比較多,其中有一個重要的工作就是加載可執(zhí)行文件到進程空間并對其進行分析,提取出可執(zhí)行文件的入口地址。
我們使用C、C++等高級語言編寫的代碼,最終通過編譯器會編譯生成可執(zhí)行文件,在Linux上,是ELF格式,在Windows上,稱之為PE文件。
無論是ELF文件還是PE文件,在各自的文件頭中,都記錄了這個可執(zhí)行文件的指令入口地址,它指示了程序該從哪里開始執(zhí)行。
這個入口指向哪里,是我們的main函數(shù)嗎?這里賣一個關子,先來解決在這之前的一個問題:進程創(chuàng)建后,是如何來到這個入口地址的?
不管在Windows還是Linux上,應用線程都會經(jīng)常在用戶空間和內核空間來回穿梭,這可能出現(xiàn)在以下幾種情況發(fā)生時:
- 系統(tǒng)調用
- 中斷
- 異常
從內核返回時,線程是如何知道自己從哪里進來的,該回到應用空間的哪里去繼續(xù)執(zhí)行呢?
答案是,在進入內核空間時,線程將自動保存上下文(其實就是一些寄存器的內容,比如指令寄存器EIP)到線程的堆棧上,記錄自己從哪里來的,等到從內核返回時,再從堆棧上加載這些信息,回到原來的地方繼續(xù)執(zhí)行。
前面提到,子進程是通過sys_execve系統(tǒng)調用進入到內核中的,在后面完成可執(zhí)行文件的分析后,拿到了ELF文件的入口地址,將會去修改原來保存在堆棧上的上下文信息,將EIP指向ELF文件的入口地址。這樣等sys_execve系統(tǒng)調用結束時,返回到用戶空間后,就能夠直接轉到新的程序入口開始執(zhí)行代碼。
所以,一個非常重要的特點是:exec系列函數(shù)正常情況下是不會返回的,一旦進入,完成使命后,執(zhí)行流程就會轉向新的可執(zhí)行文件入口。
另外需要提一下的是,在Linux上,除了ELF文件,還支持一些其他格式的可執(zhí)行文件,如MS-DOS、COFF
除了二進制的可執(zhí)行文件,還支持shell腳本,這個情況下將會將腳本解釋器程序作為入口來啟動
從ELF入口到main函數(shù)
上面交代了,一個新的進程,是如何執(zhí)行到可執(zhí)行文件的入口地址的。
同時也留了一個問題,這個入口地址是什么?是我們的main函數(shù)嗎?
這里有一個簡單的C程序,運行起來后輸出經(jīng)典的hello world:
- #include
- int main() {
- printf("hello, world!\n");
- return 0;
- }
通過gcc編譯后,生成了一個ELF可執(zhí)行文件,通過readelf指令,可以實現(xiàn)對ELF文件的分析,這里可以看到ELF文件的入口地址是0x400430:
隨后,我們通過反匯編神器,IDA打開分析這個文件,看一下位于0x400430入口的地方是什么函數(shù)?
可以看到,入口地方是一個叫做 _start 的函數(shù),并不是我們的main函數(shù)。
在_start的結尾,調用了 __libc_start_main 函數(shù),而這個函數(shù),位于libc.so中。
你可能疑惑,這個函數(shù)是哪里冒出來的,我們的代碼中并沒有用到它呢?
其實,在進入main函數(shù)之前,還有一個重要的工作要做,這就是:C/C++運行時庫的初始化。上面的 __libc_start_main 就是在完成這一工作。
在通過GCC進行編譯時,編譯器將自動完成運行時庫的鏈接,將我們的main函數(shù)封裝起來,由它來調用。
glibc是開源的,我們可以在GitHub上找到這個項目的libc-start.c文件,一窺 __libc_start_main 的真面目,我們的main函數(shù)正是被它在調用。
完整流程
到這里,我們梳理了,從進程創(chuàng)建fork,到通過exec系列函數(shù)完成可執(zhí)行文件的替換,再到執(zhí)行流程進入到ELF文件的入口,再到我們的main函數(shù)的完整流程。
Windows上的一些區(qū)別
下面簡單介紹下Windows上這一流程的一些差異。
首先是創(chuàng)建進程的環(huán)節(jié),Windows系統(tǒng)將fork+exec兩步合并了一步,通過CreateProcess系列函數(shù)一步到位,在其參數(shù)中指定子進程的可執(zhí)行文件路徑。
不同于Linux上進程和線程的邊界模糊,在Windows操作系統(tǒng)上,內核是有明確的進程和線程概念定義,進程用EPROCESS結構表示,線程用ETHREAD結構表示。
所以在Windows上,進程相關的工作準備就緒后,還需要單獨創(chuàng)建一個參與內核調度的執(zhí)行單元,也就是進程中的第一個線程:主線程。當然,這個工作也封裝在了CreateProcess系列函數(shù)中了。
新進程的主線程創(chuàng)建完成后,便開始參與系統(tǒng)調度了。主線程從哪里開始執(zhí)行呢?內核在創(chuàng)建時就明確進行了指定:nt!KiThreadStartup,這是一個內核函數(shù),線程啟動后就從這里開始執(zhí)行。
線程從這里啟動后,再通過Windows的異步過程調用APC機制執(zhí)行提前插入的APC,進而將執(zhí)行流程引入應用層,去執(zhí)行Windows進程應用程序的初始化工作,比如一些核心DLL文件的加載(Kernel32.dll、ntdll.dll)等等。
隨后,再次通過APC機制,再轉向去執(zhí)行可執(zhí)行文件的入口點。
這后面和Linux上的機制類似,同樣沒有直接到main函數(shù),而是需要先進行C/C++運行時庫的初始化,這之后經(jīng)過運行時函數(shù)的包裝,才最終來到我們的main函數(shù)。
下面是Windows上,從創(chuàng)建進程到我們的main函數(shù)的完整流程(高清大圖:https://bbs.pediy.com/upload/attach/201604/501306_qz5f5hi1n3107kt.png):
當前名稱:從創(chuàng)建進程到進入Main函數(shù),發(fā)生了什么?
標題路徑:http://www.dlmjj.cn/article/djpdshi.html


咨詢
建站咨詢
