程序是怎么一步步運行起來的?
大家好,我是小風哥,今天聊聊程序是怎么一步步運行起來的。
第一步我們需要知道到底什么是可執(zhí)行程序,所謂可執(zhí)行程序就是一個保存一系列機器指令的文件:
圖片
chrome.exe就是上千萬上億條指令組成的一個普通文件,和你寫的txt文件沒有任何本質的區(qū)別,只不過txt文件中的內容是給人看的,而可執(zhí)行程序中的內容是給CPU執(zhí)行的。
圖片
現(xiàn)在有了可執(zhí)行程序,接著我們來運行它,運行一個程序很簡單,雙擊圖標或者在命令行中運行命令:
圖片
這一步發(fā)生了什么?
我們已經知道可執(zhí)行程序其實就是一個文件,文件是保存在磁盤上的。
當我們雙擊或者在命令行中運行命令后,第一件事就要找到可執(zhí)行文件保存在了磁盤的哪個位置:
圖片
誰來完成這件事?答案就是操作系統(tǒng)。
實際上操作系統(tǒng)也是一個程序,操作系統(tǒng)是管理我們寫的程序的程序。
操作系統(tǒng)在文件系統(tǒng)的幫助下找到可執(zhí)行程序,接下來操作系統(tǒng)開始解析可執(zhí)行程序,實際上可執(zhí)行程序中并不只包含機器指令,這里還有很多其它信息,在Linux下可執(zhí)行程序一般遵循ELF文件格式:
圖片
根據可執(zhí)行程序的格式操作系統(tǒng)就能找到機器指令或程序運行依賴的全局變量等信息保存在了文件的哪個位置。
既然操作系統(tǒng)已經識別出了可執(zhí)行程序,接下來就是重要的一步:加載,load。
所謂加載就是把磁盤上可執(zhí)行程序中的指令和程序依賴的全局變量等數(shù)據copy到內存中:
圖片
既然是copy到內存,那么顯然操作系統(tǒng)需要為接下來要運行的程序分配內存。
操作系統(tǒng)在內存中找到一段大小合適的空閑內存分配給接下來要運行的程序:
圖片
然后在該內存中劃分出幾個區(qū)域,這幾個區(qū)域就是我們熟悉的代碼區(qū)、數(shù)據區(qū)、堆區(qū)和棧區(qū):
圖片
其中代碼區(qū)和數(shù)據區(qū)中的內容來自可執(zhí)行程序的代碼段和數(shù)據段:
圖片
而堆區(qū)和棧區(qū)則是程序在運行過程中使用的,這兩個區(qū)域中的內容不依賴可執(zhí)行程序本身。
值得注意的是,所謂的堆區(qū)和棧區(qū)只是一個抽象的概念,真正的物理內存中并沒有一塊所謂的堆區(qū)或者棧區(qū)。
任何一段內存都可以被用作堆區(qū)或者棧區(qū),這就像停車場有vip區(qū)或者普通區(qū),所謂vip區(qū)只不過一種約定,普通區(qū)和vip區(qū)的停車位沒有任何本質的不同,作為停車場管理員只要你高興實際上可以把任何一塊普通區(qū)劃分為vip區(qū)。
當然,程序的內存區(qū)域中除了看到的這些區(qū)域可能還有其它區(qū)域,這取決于程序是否依賴動態(tài)庫。
如果該程序依賴動態(tài)庫,那么在程序運行時還需要把依賴的動態(tài)庫也加載進來,加載到哪里呢?
不要忘了堆區(qū)和棧區(qū)的增長方向是相反的,因此這中間的空閑區(qū)域正好可以利用起來存放動態(tài)庫:
圖片
這一步完成之后程序就算加載完畢接下來可以運行了,但程序是怎么運行的呢?CPU怎么能知道該從哪里開始運行這個程序呢?
答案還得在可執(zhí)行程序中尋找。
編譯器在編譯生成可執(zhí)行程序時會記錄下這個程序第一條指令的所在位置,以elf可執(zhí)行程序為例,使用readelf工具你可以查看elf可執(zhí)行程序的內容:
圖片
注意看Entry point address這一項,這就是該程序的第一條機器指令所在地址。
當操作系統(tǒng)決定把CPU分配給剛創(chuàng)建的程序時會用這個值去初始化CPU的指令寄存器,這樣CPU就知道該從哪里開始運行該程序了。
圖片
就這樣程序開始運行。
這就是你雙擊一個圖標背后的故事。