《程式設計師的自我修養》讀書筆記
本讀書筆記從第六章開始,之前的內容會陸續補上。內容上主要對認為重要的內容進行記錄,《程式設計師的自我修養》確實是一本好書,歡迎大家一起對書中的內容進行討論。第六章的6.1-6.3節的內容總結如下:
書中前三節已經基本闡述清楚了可行性檔案、程序虛擬地址空間與實體地址空間三者之間的關係。在此簡單做一總結。程式要被執行,其程式碼與資料應被裝載進入記憶體方可被執行。因此作業系統首先應建立可執行檔案與虛擬地址空間之間的對映關係,即作業系統應該清晰程式碼與資料被放在了虛擬地址空間中的何處,此所謂可執行程式與虛擬地址空間之間的對映關係。而作業系統中同時執行多個程式,為使他們之間執行不互相干擾,因此不能讓程序直接訪問實體記憶體,而是由作業系統代為管理物理資源,當然這只是建立虛擬地址空間的原因之一,其他幾點原因不清楚的同學,可參考作業系統理論的有關書籍。書歸正傳,正由於虛擬地址空間與實體地址的不等同,虛擬地址空間與實體地址空間同樣存在某種對映關係,這兩者這間的關係涉及作業系統的記憶體管理,在此不再展開贅述。
6.4 程序虛存空間分佈
6.4.1 ELF檔案連結檢視與執行檢視6.4小節的主要內容就是闡述如何建立可執行檔案與程序虛擬地址空間之間的關係,憑我們最簡單的想法,由於可執行檔案已經被組織成了一個個不同的段,因此可執行檔案可以以段為單位直接與虛擬地址空間建立一一對應的關係,但由於虛擬地址空間是按照頁進行管理,因此直接建立對映關係,會造成大量的內碎片。如果換個排程思考問題,作業系統裝載可執行檔案其實不需要關心可執行檔案的內容,僅瞭解段的許可權即可,即保證程式在執行過程中不被錯誤訪問即可。基於以上想法,可採用的方法是將相同許可權的“段(section)”合併為一,進而組織成為“節(segment)”。借用書中的一句話:“從連結的角度看,ELF檔案是按'Section'儲存的,從裝載的角度看,ELF檔案是按'Segment'劃分的”。換句話說elf的檔案內容就在那裡不多、不少。在此還應理清一個概念就是虛擬記憶體區域(VMA,Virtual Memory Area),所謂VMA就是指可執行檔案與程序虛擬地址空間的對映關係,上述對映關係作為一個數據結構儲存在作業系統中。作業系統就是在此基礎上,將許可權相同的節對映進入一個VMA中。即“節”的組織是靜態的,是存在於硬碟中的,而VMA的組織的動態的,是存在於記憶體中的
我們可通過實驗的方式進一步理解這一概念,採用書中給出的實驗,使用readelf -S與readelf -h命令分別檢視可執行檔案的內容。在此要補充一點知識,不可使用objdump工具檢視elf檔案的內容,objdump -h與readelf -s輸出內容基本一致,但前者無法輸出.rel開頭的段和.shstrtab,.symtab,.strtab。readelf與objdump的詳細對比請見這篇部落格:http://my.oschina.net/kangchunhui/blog/152682
同時由於我的機器是64位,因此虛擬地址空間不是從08048000開始,通過實驗對比可進一步驗證“segment”與“section”是從不同角度來劃分同一個ELF檔案。
可執行檔案與共享庫檔案均有程式頭表(Program Header Table),由於目標檔案不需要被裝載,因此不需要程式頭表。結構體Elf32_Phdr 中記載了程式頭表中的內容,上述結構體同樣存在於/usr/inclde/elf.h中。
6.4.2-6.4.3
在作業系統中VMA除了被用來對映可執行檔案中的各個“Segment”外,作業系統通過使用VMA來對程序的地址空間進行管理。程序在執行過程中還需要堆、棧空間,事實上堆疊空間在程序的虛擬空間中的表現也是以VMA的形式存在的,很多情況下,一個程序中的棧和堆分別對應一個VMA。在linux下,可通過命令cat /proc/PID/maps 檢視程序的虛擬空間分佈。其中PID指程序ID,可通過命令./elf &命令檢視,其中elf為可執行檔名。
關於程序虛擬地址空間的概念總結如下:
作業系統通過給程序空間劃分出一個個VMA來管理程序的虛擬空間;基本原則是將相同許可權屬性的、有相同映像檔案的對映成一個VMA;一個程序基本上可以分為如下幾種VMA區域:
程式碼VMA,許可權只讀、可執行;有映像檔案。
資料VMA,許可權可讀寫、可執行;有映像檔案。
堆VMA,許可權可讀寫、可執行;無映像檔案,匿名,可向上擴充套件。
棧VMA,許可權可讀寫、不可執行;無映像檔案,匿名,可向下擴充套件。
上一下節中提到的不絕對的概念是指,Linux的程序虛擬空間管理的VMA的概念並非與“Segment”完全對應,linux規定一個VMA可以對映到某個檔案的一個區域,或者是沒有對映到任何檔案。
6.4.4 段地址對齊
通過前三小節的分析,我們已經基本瞭解了可執行檔案與程序虛擬地址空間之間的關係。但僅靠上述對映關係還不夠,因為虛擬地址空間是按照頁進行管理的,而可執行檔案仍然是按照節進行管理的,因此就需要把一個個不同的“節”裝入記憶體“頁”中。還是從最簡單的想法出發,直接以節為單位,對映進入虛擬地址空間中的“頁”。但虛擬地址空間是以頁作為管理單元,因此直接對映的方法肯定會造成內碎片問題,虛擬地址空間中的內碎片對映到實體地址空間就造成了資源的浪費。因此為解決上述問題,有些UNIX系統採用了一個很取巧的方法,就是讓那些各個段接壤部分共享一個物理頁面,然後將該物理頁面在虛擬地址空間中對映兩次。上述方法的採用雖然造成了虛擬地址空間的浪費,但將可執行檔案看作是一個整體,以“頁”為單位對其進行劃分。但在虛擬地址空間向實體地址空間進行對映時,由於減少了內碎片,因此就減少了對實體記憶體的浪費。
6.4.5 程序棧初始化 內容不多在此就不總結了。
6.5 Linux核心裝載ELF過程簡介
本小節主要介紹linux是如何裝載並執行ELF檔案的。
首先在使用者層面,bash程序會呼叫fork()系統呼叫建立一個新的程序,然後新的程序呼叫execve()系統呼叫執行指定的ELF檔案,原先的bash程序繼續返回等待剛才啟動的新程序結束,然後繼續等待使用者輸入命令。
由於已經將系統呼叫的返回地址改成了被裝載的ELF程式的入口地址了。所以當sys_execve()系統呼叫從核心態返回到使用者態時,EIP暫存器直接跳轉到了ELF程式的入口地址,於是新的程式開始執行,ELF可執行檔案裝載完成。1)execve()系統呼叫的入口是sys_execve()。
2)sys_execve()呼叫do_execve()。do_execve()的主要功能是查詢被執行檔案,如果找到檔案,則讀取檔案的前128個位元組以判斷可執行檔案型別。
3)呼叫search_binary_handle()去搜索和匹配合適的可執行檔案裝載處理工程。比如ELF可執行檔案的裝載處理過程為load_elf_binary()。
4)以load_elf_binary()為例,該函式最為重要的一步是將系統呼叫的返回地址修改成ELF可執行檔案的入口點,這個入口點取決於程式的連結方式,對於靜態連結的ELF可執行檔案,這個程式入口就是ELF檔案的檔案頭中e_entry所指的地址;對於動態連結的ELF可執行檔案,程式入口點就是動態聯結器。
5)按照呼叫順序逐級返回load_elf_binary()->do_execve()->sys_execve()。