可執行文件(ELF)的裝載與進程


程序員的自我修養

可執行文件的裝載與進程

進程虛擬地址空間

  • 什么是程序?什么是進程?

    • 程序是一個靜態的概念,它就是一些預先編譯好的指令和數據的集合
    • 進程是一個動態的概念.它是程序運行時的一個過程
    • 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM