程序員的自我修養
可執行文件的裝載與進程
進程虛擬地址空間
-
什么是程序?什么是進程?
- 程序是一個靜態的概念,它就是一些預先編譯好的指令和數據的集合
- 進程是一個動態的概念.它是程序運行時的一個過程
- CPU比作是人, 程序比作是菜譜, 硬件等資源比作是菜,廚具之類的東西.
- 進程就是整個炒菜的過程 計算機安裝程序的指示把輸入數據加工成輸出數據, 就好像廚師按照菜譜指導人把原料做成美味的菜一樣
-
每個進程都有自己獨立的虛擬地址空間, 進程只能使用操作系統分配的地址空間內的地址
- 如果訪問未經允許的空間, 操作系統就會捕獲到這些訪問, 將進程強制結束, 比如Windows 進程因為非法操作需要關閉 , Linux 的Segment fault等
-
在Linux下
-
0XC0000000是操作系統和用戶進程地址空間的划分線 ,系統用了1GB, 進程可用的3GB
-
那如果進程想跑一個3GB以上的怎么辦?或者說程序使用的空間能不能大於3GB呢?
-
從虛存的角度來說是不行的
-
從實際的內存來說是可以的
- windows下有個叫PAE AWE的東西
- 可以從高於4GB的內存空間里申請ABCD等多塊物理空間, 然后根據需要把某段虛存映射到這不同的ABCD塊
-
-
-
裝載的方式
-
最簡單的辦法就是把程序和數據全部存入內存中, 這就是靜態裝載
-
根據程序局部性原理, 還可以把程序最常用的部分駐留在內存中 ,不常用的放在 硬盤上 這就是動態裝載
-
動態裝載分成 覆蓋裝入overlay和頁映射Paging
-
覆蓋裝入是上古時期的產物了,程序員在寫代碼的時候要手動替換模塊, 而且要思考清楚 模塊的依賴關系, 最后可以用樹 這種數據結構來描述
- 子主題 1
-
頁映射
-
可以說Modern OS都是采用這種方式
- 頁就是由操作系統的存儲管理器來做一個 裝載管理器的工作,
- 由MMU來完成虛擬地址轉換成物理地址的過程
- 把一般為4KB 也就是0x00001000大小段的頁 讀進內存
- 當然內存滿了之后有替換算法, 比如FIFO,之類的
-
-
-
從操作系統的角度來看 可執行文件的裝載
-
進程的建立
- 一個進程最關鍵的特點是 它擁有獨立的虛擬地址空間
-
進程建立的三個步驟
-
1.創建一個獨立的虛擬地址空間
- 實際上很簡單, 就是操作系統給你分配了一個頁目錄(Page Directory)
-
2.讀取可執行文件的頭部 ,做好可執行文件ELF和虛擬地址空間的映射,
-
首先回憶一下缺頁中斷會發生什么
- 操作系統首先從空閑的物理內存中分配一個物理頁, 然后我們就是要加載磁盤上的頁到這個物理頁上,最后設置好這個物理頁的物理地址和虛擬地址的關系
- 那么問題來了, 我們怎么知道程序當前需要的頁到底在什么位置呢? 這正是第2點 , 可執行文件和虛存映射要做的事情
-
實際上看圖, 這種映射關系被保存在操作系統內部的一個數據結構 叫VMA(virtual Memory Area)
-
比如操作系統創建進程后 會在進程相應的數據結構里設置一個對應.text段的VMA, 這個VMA還會帶有一些權限的限制, 比如只讀, 后續我們還會進行合並
-
實際上操作系統發生段錯誤的時候, 通過查找這樣的數據結構來定位頁錯誤在可執行文件中的位置, 從而可以把正確的可執行文件的頁加載進來
-
-
-
3.將CPU的指令寄存器 設置成 可執行文件的入口地址, 啟動運行
-
這步其實最簡單, 通過設置CPU的指令寄存器將CPU時間片交給進程,
-
在操作系統層面比較復雜
- 涉及到內核堆棧和用戶堆棧的切換, CPU運行權限的切換
-
不過對程序來說,
- 不就是執行了一條跳轉指令嗎, 跳到ELF文件的入口地址
-
-
-
-
頁錯誤
-
再重復一下剛才的過程, 就當是總結了吧
- 比如那個入口地址是0x08048000, 執行是發現頁面0x08048000- 0x08049000是個空頁面, 這時候觸發缺頁中斷, CPU將控制權交給OS, OS查詢那個VMA ,然后計算出對應ELF文件的偏移, 然后找一個空閑的物理地址, 建立好虛存和物理內存的映射關系(應該是由MMU)來完成的 ,最后回到進程剛才page fault的地方繼續執行
-
-
進程虛存分布
-
剛才說的虛存和ELF文件的映射關系會產生碎片的問題, 而你站在操作系統的角度來看它其實並不關系這虛存對應的到底是.bss段還是.text段 ,操作系統只關心這些段的權限問題(read write exec)
-
所以把相同權限的section合並成一個虛存段segment是一個很自然的想法
- 子主題 1
-
這樣做的好處是顯著減少了頁面內部碎片, 從而節省了內存空間
-
其實無非就是虛存的segment合並了 ELF的幾個section罷了
-
一般ELF會分成兩個段
- VMA0
- VMA1
-
-
-
-
堆和棧
-
首先在linux下可以 cat /proc/21963/map
-
這個可以看到究竟划分成了幾個段
-
子主題 3
-
一般來說是5個
-
VMA0
-
VMA1
-
stack VMA
-
heap VMA
-
vdso
- 這個地址是屬於大於0xC0000000的, 也就是屬於內核的地址了
- 這個是進程可以用來訪問內核, 做一些通信
-
-
-
進程除了那些segement之外還有自己的stack, 和Heap
-
每個線程都有屬於自己的堆棧
-
比如這個進程的heap 140KB, stack 88KB
-
那如果是單線程的話
- 整個heap都是這個線程的
-
-
堆在linux下理論3GB, 實際大概可以2.9GB
-
windows
-
理論2G
- 實際大概1.5G
-
-
-
進程虛存空間分布
-
ELF文件鏈接視圖和執行視圖
- 操作系統並不關心可執行文件各個段的內容, 值只關心和裝載相關的問題, 最主要是段的權限(可讀, 可寫 ,可執行)
- 子主題 2
-
進程棧初始化
- 進程剛啟動的時候, 必須知道一些進程運行的環境, 最基本的就是環境變量和 進程的運行參數(argc, argv)
- 子主題 2
- 進程啟動 以后, 程序的庫部分會把堆棧里的初始化信息中的參數信息傳給main函數, 也就是我們熟知的argc和argv
Linux內核裝載ELF過程簡介
-
首先在用戶層面,bash進程會調用 fork系統調用創建一個新的進程, 然后新的進程調用execve()系統調用 執行指定的ELF文件, 原先的bash進程 返回繼續等待過程啟動的新進程結束, 然后繼續等待用戶輸入命令
- execve()在unistd.h
-
minibash
-
在進入execve系統調用后, Linux內核開始進入真正的裝載工作.
- 在內核中,execve系統調用相應的入口是sys_execve()
- 在進行一些參數的復制后, 調用do_execve()
- do_execve()會先查找被執行的文件, 如果找到了, 讀前128個字節,
- 因為linux支持的可執行文件不止一種, a.out java等
- 我們通過魔數來判斷究竟是哪種可執行文件
- 當do_execve()讀取了128個byte后, 調用search_binary_handle()去搜索和匹配 合適的 可執行文件裝載處理過程
- 比如ELF可執行文件對應的裝載過程的函數 名叫 load_elf_binary
- a.out叫 load_aout_binary
- 腳本類叫 load_script_binary
-
load_elf_binary
-
1.檢查文件有效性 比如魔數, segment數量
-
2.尋找.interp段, 設置動態鏈接器路徑
-
3.根據ELF文件程序頭表的描述 ,對ELF文件進行映射, 比如代碼, 數據,只讀數據
-
4.初始化ELF進程環境,
-
5.將系統調用的返回地址修改成ELF文件可執行文件的入口點
-
這個入口點對於靜態鏈接的
- e_entry所指的地址
-
對於動態鏈接
- 入口是動態鏈接器
-
-
-
當load_elf_binary()執行完成后,系統調用的返回地址已經修改成被裝載的ELF文件的入口地址了, sys_execve()系統調用()從內核態返回到用戶態的時候, EIP寄存器直接跳轉到了 ELF程序的入口地址
-
至此, 新的程序開始執行, ELF可執行文件裝載完成
分支主題 2
分支主題 3
XMind: ZEN - Trial Version