不知大家在平時想過沒有,我們放在磁盤(之前我一直認為Windows的C盤是主存,DEF盤是磁盤,哈哈,應該沒有像我這樣無知的人吧)上的一個可執行文件(或者應用程序)是如何得到執行的,而且為什么我們在寫程序的時候怎么感覺程序中的一些變量的地址好像在各個不同的程序中都差不多,同時這個地址到底真正對應的是什么?是我們可執行文件對應所在位置的磁盤地址嗎?下面我就以linux為平臺(Windows也一樣,只是將命令方式變為圖形方式了)為大家詳細講解一下一個可執行文件是如何得到執行的。
在Linux中當我們打開shell時,我們相當于已經新建了一個進程,這個進程運行的是shell這個應用程序。當在shell中輸入一個可執行目標文件的名字時,shell會用fork()函數創建一個新的進程,在這個新進程中調用execve()函數來加載和執行這個可執行文件。
我準備詳細來說明一下這個execve()函數是如何來工作的,比如它是如何將磁盤上的目標文件拷貝到主存中來讓CPU運行的?程序中我們所看到的地址到底是什么?帶著這些問題我們來一步一步分析。
首先因為execve()函數是在shell這個進程的子進程中運行的,而子進程必定會拷貝(其實也不是拷貝,要不然這個進程設計的也太臃腫了,是一種叫寫時拷貝的機制)很多父進程已存在的內容,所以必須刪除掉。
然后它開始映射(看到映射有沒有想到數學中叫函數映射的東西,本質上都是一樣的)我們可執行文件中的內容,談到映射那必然是X------>Y,現在Y是我們的可執行文件,那X呢?先給大家補充一點進程中的知識,等補充完了,才能說X。每個進程中都有一個叫頁表的東西,頁表有很多項,每一項叫頁表項(為了簡化問題的復雜性我們就假設Linux是一級頁表吧),同時在操作系統中一般一個頁或者物理塊的大小為4KB(對應為12位的頁內地址),所以在一個32的操作系統中只需要保存2^20個頁表項就可以表示地址從0x00000000到0xffffffff的范圍,其中這個地址的后12位為頁內地址,而我們在程序中所見到的地址就是這個地址,根本不是什么我們程序對應的物理地址。記住,這個地址并不是真正對應的磁盤或者內存的地址,而是虛擬的,叫虛擬地址。如果現在還不太明白等我全部講完就會懂的。
講到這里大家先稍微理解理解,免得看的一頭霧水。那我開始,剛剛我們說到進程中的頁表項,每一個頁表項從開始到結束對應的編號為0x00000-0xfffff(一共2^20個,大家可以畫一畫),這個頁表項主要有兩個部分,第一個部分用來指向磁盤的物理塊或者內存上的塊,第二個部分表明所指向的塊是在磁盤上還是內存上或者這部分就根本沒用。
那么我們現在可以說X是什么了,就是虛擬地址!說完了X,Y,那還有映射規則呢,對于我們程序中的文本塊,數據塊,棧,堆等在Linux中分別對應不同的虛擬地址,而且是固定的,對所有程序都一樣。這也就可以解釋為什么不同的程序不同的變量有時候地址卻差不多,因為他們的虛擬地址都是從0x00000000---0xffffffff,因此當他們的變量都保存在棧中時,對應的虛擬地址也很接近。
映射完之后,execve()調用啟動代碼,啟動代碼將調用main()函數,大家一定會想現在可執行目標不是還在磁盤上嗎?它是怎么拷貝到內存上,然后被CPU執行的呢?確實如此,因此當啟動代碼將main()函數的虛擬地址傳遞給CPU時,CPU通過解析虛擬地址發現內存中沒有main()相對應的頁或者物理塊,然后CPU通過進程中的頁表項找到我們可執行文件所在的磁盤位置,將磁盤上的塊拷貝到內存中,這樣CPU就可以順利的執行我們的程序了。
新聞熱點
疑難解答