程式時如何被連結和載入的???
阿新 • • 發佈:2019-02-08
執行 PE 可執行檔案
啟動一個 PE 可執行程式的過程是相對簡單的。
讀入檔案的第一頁,其中有 DOS 頭部,PE 頭部和區段頭部等。
確定地址空間的目標區域是否有效,如果不可用則另分配一塊區域。
根據各區段頭部的資訊,將檔案中的所有區段對映到地址空間的適當位置上。
如果檔案並沒有被載入到它的目標地址中,則進行重定位。
遍歷匯入區段中的 DLL 列表,將任何未載入的庫都載入(該過程可以是遞迴的)。
解析所有在匯入區段中的匯入符號。
根據 PE 頭部的值建立初始的棧和堆。
建立初始執行緒並啟動該程序。
所有的現代連結器都可以處理庫,即按照被連結程式的需要加入的目標檔案集合。
一個庫檔案在建立後,連結器還要能夠對它進行搜尋。庫的搜尋通常發生在連結器的
bbs.theithome.com
第一遍掃描時,在所有單獨的輸入檔案都被讀入之後。如果一個或多個庫具有符號目錄,那
麼連結器就將目錄讀入,然後根據連結器的符號表依次檢查每個符號。如果該符號被使用但
是未定義,連結器就會將符號所屬檔案從庫中包含進來。僅將檔案標識為稍後載入是不夠的,
連結器必須像處理那些在顯式被連結的檔案中的符號那樣,來處理庫裡各個段中的符號。段
會記入段表,而符號,包括定義的和未定義的,都會記入全域性符號表。一個庫例程引用了另
一個庫中例程的符號是相當普遍的現象,譬如諸如 printf 這樣的高階 I/O 例程會引用像 put
c 或 write 這樣的低階例程。
ELF 可執行檔案
一個 ELF 可執行檔案具有與可重定位 ELF 檔案相同的通用格式,但對資料部分進行了調
整以使得檔案可以被對映到記憶體中並執行。檔案中會在 ELF 頭部後面存在程式頭部。程式頭
bbs.theithome.com
部定義了要被對映的段。如圖 15 所示為程式頭部,是一個由段描述符組成的陣列。
---------------------------------------------------------------------------------------------
圖 3-15:ELF 程式頭部
int type; //型別:可載入程式碼或資料,動態連結資訊,等
int offset; //段在檔案中的偏移量
int virtaddr; //對映段的虛擬地址
int physaddr; //實體地址,未使用
int filesize; //檔案中的段大小
int memsize; //記憶體中的段大小(如果包含 BSS 的話會更大些)
int flags; //讀,寫,執行標誌位
int align; //對齊要求,根據硬體頁尺大小不同有變動
---------------------------------------------------------------------------------------------
一個可執行程式通常只有少數幾種段,如程式碼和資料的只讀段,可讀寫資料的可讀寫
段。所有的可載入區段都歸併到適當型別的段中以便系統可以通過少數的一兩個操作就可以
完成檔案對映。
ELF 格式檔案進一步擴充套件了 QMAGIC 格式的 a.out 檔案中使用的“頭部放入地址空間”的
技巧,以使得可執行檔案儘可能的緊湊,相應付出的代價就是地址空間顯得凌亂了些。一個
段可以開始和結束於檔案中的任何偏移量處,但是段的虛擬起始必須和檔案中起始偏移量具
有低位地址模對齊的關係,例如,必須起始於一頁的相同偏移量處。系統必須將段起始所在
頁到段結束所在頁之間整個的範圍都對映進來,哪怕在邏輯上該段只佔用了被對映的第一頁
和最後一頁的一部分。圖 16 所示為一個典型的段分佈方式。
---------------------------------------------------------------------------------------------
圖 3-16:ELF 可載入段
+---------------------+------------+------------+------------+
| | 檔案偏移量 | 載入地址 | 型別 |
+---------------------+------------+-------------------------+
| ELF 頭部 | 0 | 0x8000000 | |
+---------------------+------------+------------+------------+
| 程式頭部 | 0x40 | 0x8000040 | |
+---------------------+------------+------------+------------+
| 只讀文字 | 0x100 | 0x8000100 |可載入 |
|(尺寸為 0x4500) | | |可讀,可執行|
+---------------------+------------+------------+------------+
| 可讀/寫資料 | 0x4600 | 0x8005600 |可載入 |
| (檔案中尺寸為 0x2200| | |可讀,可寫 |
| 記憶體中尺寸為 0x3500)| | |可執行 |
+---------------------+------------+------------+------------+
| 不可載入資訊和 | | | |
| 可選的區段頭部 | | | |
bbs.theithome.com
+---------------------+------------+------------+------------+
---------------------------------------------------------------------------------------------
被對映的文字段包括 ELF 頭部,程式頭部,和只讀文字,這樣 ELF 頭部和程式頭部都會
在文字段開頭的同一頁中。檔案中僅有的可讀寫資料段緊跟在文字段的後面。檔案中的這一
頁會同時被對映為記憶體中文字段的最後一頁和資料段的第一頁(以 copy-on-write 的方式)。
如果計算機具有 4K 的頁,並在可執行檔案中文字段結束於 0x80045ff,然後資料段起始於 0
x8005600。檔案中的這一頁(即同時存有文字和資料段的頁)在記憶體 0x8004000 處被對映為
文字段的最後一頁(頭 0x600 個位元組包含文字段中 0x8004000 到 0x80045ff 之間的內容),
並在 0x8005000 處被對映為資料段(0x600 以後的部分包含資料段從 0x8005600 到 0x80056ff
的內容)。
BSS 段也是在邏輯上也是跟在資料段的可讀寫區段後,在本例中長度為 0x1300 位元組,
即檔案中尺寸與記憶體中尺寸的差值。資料段的最後一頁會從檔案中對映進來,但是在隨後操
作系統將 BSS 段清零時,copy-on-write 系統會該段做一個私有的副本。
如果檔案中包含.init 或.fini 區段,這些區段會成為只讀文字段的一部分,並且連結
器會在程式入口點處插入程式碼,使得在呼叫主程式之前會呼叫.init 段的程式碼,並在主程式
返回後呼叫.fini 區段的程式碼。
ELF 共享目標包含了可重定位和可執行檔案的所有東西。它在檔案的開頭具有程式頭部
表,隨後是可載入段的各區段,包括動態連結資訊。在構成可載入段的各區段之後的,是重
定位符號表和連結器在根據共享目標建立可執行程式時需要的其它資訊,最後是區段表。
載入:
載入是將一個程式放到主存裡使其能執行的過程。這一章我們看看載入過程,並將注
意力集中在載入那些已經連結好的程式。很多系統曾經都有過將連結和載入合為一體的連結
載入器,但是現在除了我知道的執行 MVS 的硬體和第十章將會談到的動態連結器外,其它的
實際上已經基本消失了。連結載入器和單純的載入器沒有太大的區別,主要和最明顯的區別
在於前者的輸出放在記憶體重而不是在檔案中。
基本載入
在第三章的目標檔案設計中,我們已經接觸了大多數載入的基本知識。依賴於程式是
通過虛擬記憶體系統被對映到程序地址空間,還是通過普通的 I/O 呼叫讀入,載入會有一點小
小的差別。
在多數現代系統中,每一個程式被載入到一個新的地址空間,這就意味著所有的程式
都被載入到一個已知的固定地址,並可以從這個地址被連結。這種情況下,載入是頗為簡單
的:
從目標檔案中讀取足夠的頭部資訊,找出需要多少地址空間。
分配地址空間,如果目的碼的格式具有獨立的段,那麼就將地址空間按獨立的段
劃分。
將程式讀入地址空間的段中。
將程式末尾的 bss 段空間填充為 0,如果虛擬記憶體系統不自動這麼做得話。
如果體系結構需要的話,建立一個堆疊段(stack segment)。
設定諸如程式引數和環境變數的其他執行時資訊。
開始執行程式。
如果程式不是通過虛擬記憶體系統對映的,讀取目標檔案就意味著通過普通的 read 系統
呼叫讀取檔案。在支援共享只讀程式碼段的系統上,系統檢查是否在記憶體中已經載入了該程式碼
段的一個拷貝,而不是生成另外一份拷貝。
在進行記憶體對映的系統上,這個過程會稍稍複雜一些。系統載入器需要建立段,然後
以頁對齊的方式將檔案頁對映到段中,並賦予適當的許可權,只讀(RO)或寫時複製(COW)。在
某些情況下,相同的頁會被對映兩次,一個在一個段的末尾,另一個在下一個段的開頭,分
別被賦予 RO 和 COW 許可權,格式上類似於緊湊的 UNIX a.out。由於資料段通常是和 bss 段是
緊挨著的,所以載入器會將資料段所佔最後一頁中資料段結尾以後的部分填充為 0(鑑於磁
盤版本通常會有一些符號之類的東西在那裡),然後在資料分配足夠的空頁面覆蓋 bss 段。
帶重定位的基本載入
bbs.theithome.com
僅有一小部分系統還仍然為執行程式在載入時進行重定位,大多數都是為共享庫在加
載時進行重定位。諸如 MS-DOS 的系統,很少使用硬體的重定位;另外一些如 MVS 的系統,
具有硬體重定位(卻是從一個沒有硬體重定位的系統繼承來的);還有一些系統,具有硬體
重定位,但是卻可以將多個可執行程式和共享庫載入到相同的地址空間。所以連結器不能指
望某些特定地址是有效的。
如第七章討論的,載入時重定位要比連結時重定位簡單的多,因為整個程式作為一個
單元進行重定位。例如,如果一個程式被連結為從位置 0 開始,但是實際上被載入到位置 15
000,那麼需要所有程式中的空間都要被修正為“加上 15000”。在將程式讀入主存後,載入
器根據目標檔案中的重定位項,並將重定位項指向的記憶體位置進行修改。
載入時重定位會表現出效能的問題,由於在每一個地址空間內的修正值均不同,所以
被載入到不同虛擬地址的程式碼通常不能在地址空間之間共享。MVS 使用的,並被 Windows 和
AIX 擴充套件的一種方法,使建立一個出現在多個地址空間的共享記憶體區域,並將常用的程式加
載到其中(MVS 將其稱為 link pack 區域)。這仍然存在普通程序不能獲取可寫資料的單獨復
本的問題,所以應用程式必須在編寫時明確地為它可寫區域分配空間。
啟動一個 PE 可執行程式的過程是相對簡單的。
讀入檔案的第一頁,其中有 DOS 頭部,PE 頭部和區段頭部等。
確定地址空間的目標區域是否有效,如果不可用則另分配一塊區域。
根據各區段頭部的資訊,將檔案中的所有區段對映到地址空間的適當位置上。
如果檔案並沒有被載入到它的目標地址中,則進行重定位。
遍歷匯入區段中的 DLL 列表,將任何未載入的庫都載入(該過程可以是遞迴的)。
解析所有在匯入區段中的匯入符號。
根據 PE 頭部的值建立初始的棧和堆。
建立初始執行緒並啟動該程序。
所有的現代連結器都可以處理庫,即按照被連結程式的需要加入的目標檔案集合。
一個庫檔案在建立後,連結器還要能夠對它進行搜尋。庫的搜尋通常發生在連結器的
bbs.theithome.com
第一遍掃描時,在所有單獨的輸入檔案都被讀入之後。如果一個或多個庫具有符號目錄,那
麼連結器就將目錄讀入,然後根據連結器的符號表依次檢查每個符號。如果該符號被使用但
是未定義,連結器就會將符號所屬檔案從庫中包含進來。僅將檔案標識為稍後載入是不夠的,
連結器必須像處理那些在顯式被連結的檔案中的符號那樣,來處理庫裡各個段中的符號。段
會記入段表,而符號,包括定義的和未定義的,都會記入全域性符號表。一個庫例程引用了另
一個庫中例程的符號是相當普遍的現象,譬如諸如 printf 這樣的高階 I/O 例程會引用像 put
c 或 write 這樣的低階例程。
ELF 可執行檔案
一個 ELF 可執行檔案具有與可重定位 ELF 檔案相同的通用格式,但對資料部分進行了調
整以使得檔案可以被對映到記憶體中並執行。檔案中會在 ELF 頭部後面存在程式頭部。程式頭
bbs.theithome.com
部定義了要被對映的段。如圖 15 所示為程式頭部,是一個由段描述符組成的陣列。
---------------------------------------------------------------------------------------------
圖 3-15:ELF 程式頭部
int type; //型別:可載入程式碼或資料,動態連結資訊,等
int offset; //段在檔案中的偏移量
int virtaddr; //對映段的虛擬地址
int physaddr; //實體地址,未使用
int filesize; //檔案中的段大小
int memsize; //記憶體中的段大小(如果包含 BSS 的話會更大些)
int flags; //讀,寫,執行標誌位
int align; //對齊要求,根據硬體頁尺大小不同有變動
---------------------------------------------------------------------------------------------
一個可執行程式通常只有少數幾種段,如程式碼和資料的只讀段,可讀寫資料的可讀寫
段。所有的可載入區段都歸併到適當型別的段中以便系統可以通過少數的一兩個操作就可以
完成檔案對映。
ELF 格式檔案進一步擴充套件了 QMAGIC 格式的 a.out 檔案中使用的“頭部放入地址空間”的
技巧,以使得可執行檔案儘可能的緊湊,相應付出的代價就是地址空間顯得凌亂了些。一個
段可以開始和結束於檔案中的任何偏移量處,但是段的虛擬起始必須和檔案中起始偏移量具
有低位地址模對齊的關係,例如,必須起始於一頁的相同偏移量處。系統必須將段起始所在
頁到段結束所在頁之間整個的範圍都對映進來,哪怕在邏輯上該段只佔用了被對映的第一頁
和最後一頁的一部分。圖 16 所示為一個典型的段分佈方式。
---------------------------------------------------------------------------------------------
圖 3-16:ELF 可載入段
+---------------------+------------+------------+------------+
| | 檔案偏移量 | 載入地址 | 型別 |
+---------------------+------------+-------------------------+
| ELF 頭部 | 0 | 0x8000000 | |
+---------------------+------------+------------+------------+
| 程式頭部 | 0x40 | 0x8000040 | |
+---------------------+------------+------------+------------+
| 只讀文字 | 0x100 | 0x8000100 |可載入 |
|(尺寸為 0x4500) | | |可讀,可執行|
+---------------------+------------+------------+------------+
| 可讀/寫資料 | 0x4600 | 0x8005600 |可載入 |
| (檔案中尺寸為 0x2200| | |可讀,可寫 |
| 記憶體中尺寸為 0x3500)| | |可執行 |
+---------------------+------------+------------+------------+
| 不可載入資訊和 | | | |
| 可選的區段頭部 | | | |
bbs.theithome.com
+---------------------+------------+------------+------------+
---------------------------------------------------------------------------------------------
被對映的文字段包括 ELF 頭部,程式頭部,和只讀文字,這樣 ELF 頭部和程式頭部都會
在文字段開頭的同一頁中。檔案中僅有的可讀寫資料段緊跟在文字段的後面。檔案中的這一
頁會同時被對映為記憶體中文字段的最後一頁和資料段的第一頁(以 copy-on-write 的方式)。
如果計算機具有 4K 的頁,並在可執行檔案中文字段結束於 0x80045ff,然後資料段起始於 0
x8005600。檔案中的這一頁(即同時存有文字和資料段的頁)在記憶體 0x8004000 處被對映為
文字段的最後一頁(頭 0x600 個位元組包含文字段中 0x8004000 到 0x80045ff 之間的內容),
並在 0x8005000 處被對映為資料段(0x600 以後的部分包含資料段從 0x8005600 到 0x80056ff
的內容)。
BSS 段也是在邏輯上也是跟在資料段的可讀寫區段後,在本例中長度為 0x1300 位元組,
即檔案中尺寸與記憶體中尺寸的差值。資料段的最後一頁會從檔案中對映進來,但是在隨後操
作系統將 BSS 段清零時,copy-on-write 系統會該段做一個私有的副本。
如果檔案中包含.init 或.fini 區段,這些區段會成為只讀文字段的一部分,並且連結
器會在程式入口點處插入程式碼,使得在呼叫主程式之前會呼叫.init 段的程式碼,並在主程式
返回後呼叫.fini 區段的程式碼。
ELF 共享目標包含了可重定位和可執行檔案的所有東西。它在檔案的開頭具有程式頭部
表,隨後是可載入段的各區段,包括動態連結資訊。在構成可載入段的各區段之後的,是重
定位符號表和連結器在根據共享目標建立可執行程式時需要的其它資訊,最後是區段表。
載入:
載入是將一個程式放到主存裡使其能執行的過程。這一章我們看看載入過程,並將注
意力集中在載入那些已經連結好的程式。很多系統曾經都有過將連結和載入合為一體的連結
載入器,但是現在除了我知道的執行 MVS 的硬體和第十章將會談到的動態連結器外,其它的
實際上已經基本消失了。連結載入器和單純的載入器沒有太大的區別,主要和最明顯的區別
在於前者的輸出放在記憶體重而不是在檔案中。
基本載入
在第三章的目標檔案設計中,我們已經接觸了大多數載入的基本知識。依賴於程式是
通過虛擬記憶體系統被對映到程序地址空間,還是通過普通的 I/O 呼叫讀入,載入會有一點小
小的差別。
在多數現代系統中,每一個程式被載入到一個新的地址空間,這就意味著所有的程式
都被載入到一個已知的固定地址,並可以從這個地址被連結。這種情況下,載入是頗為簡單
的:
從目標檔案中讀取足夠的頭部資訊,找出需要多少地址空間。
分配地址空間,如果目的碼的格式具有獨立的段,那麼就將地址空間按獨立的段
劃分。
將程式讀入地址空間的段中。
將程式末尾的 bss 段空間填充為 0,如果虛擬記憶體系統不自動這麼做得話。
如果體系結構需要的話,建立一個堆疊段(stack segment)。
設定諸如程式引數和環境變數的其他執行時資訊。
開始執行程式。
如果程式不是通過虛擬記憶體系統對映的,讀取目標檔案就意味著通過普通的 read 系統
呼叫讀取檔案。在支援共享只讀程式碼段的系統上,系統檢查是否在記憶體中已經載入了該程式碼
段的一個拷貝,而不是生成另外一份拷貝。
在進行記憶體對映的系統上,這個過程會稍稍複雜一些。系統載入器需要建立段,然後
以頁對齊的方式將檔案頁對映到段中,並賦予適當的許可權,只讀(RO)或寫時複製(COW)。在
某些情況下,相同的頁會被對映兩次,一個在一個段的末尾,另一個在下一個段的開頭,分
別被賦予 RO 和 COW 許可權,格式上類似於緊湊的 UNIX a.out。由於資料段通常是和 bss 段是
緊挨著的,所以載入器會將資料段所佔最後一頁中資料段結尾以後的部分填充為 0(鑑於磁
盤版本通常會有一些符號之類的東西在那裡),然後在資料分配足夠的空頁面覆蓋 bss 段。
帶重定位的基本載入
bbs.theithome.com
僅有一小部分系統還仍然為執行程式在載入時進行重定位,大多數都是為共享庫在加
載時進行重定位。諸如 MS-DOS 的系統,很少使用硬體的重定位;另外一些如 MVS 的系統,
具有硬體重定位(卻是從一個沒有硬體重定位的系統繼承來的);還有一些系統,具有硬體
重定位,但是卻可以將多個可執行程式和共享庫載入到相同的地址空間。所以連結器不能指
望某些特定地址是有效的。
如第七章討論的,載入時重定位要比連結時重定位簡單的多,因為整個程式作為一個
單元進行重定位。例如,如果一個程式被連結為從位置 0 開始,但是實際上被載入到位置 15
000,那麼需要所有程式中的空間都要被修正為“加上 15000”。在將程式讀入主存後,載入
器根據目標檔案中的重定位項,並將重定位項指向的記憶體位置進行修改。
載入時重定位會表現出效能的問題,由於在每一個地址空間內的修正值均不同,所以
被載入到不同虛擬地址的程式碼通常不能在地址空間之間共享。MVS 使用的,並被 Windows 和
AIX 擴充套件的一種方法,使建立一個出現在多個地址空間的共享記憶體區域,並將常用的程式加
載到其中(MVS 將其稱為 link pack 區域)。這仍然存在普通程序不能獲取可寫資料的單獨復
本的問題,所以應用程式必須在編寫時明確地為它可寫區域分配空間。