緒論2:應用視角的作業系統

7hu95b發表於2024-06-30

本文重點內容:

  • 強行構造最小的 Hello World 程式
  • 狀態機模型
  • 作業系統的程式剖析

一、強行構造最小的 Hello World 程式?

(1)精簡 hello.c 程式:去掉標頭檔案、主函式為空,但是發現編譯時報錯。

  • 掌握常用的 GDB 除錯指令、發現並追蹤 return 錯誤的整個過程。比如 starti 代表從第一條指令開始執行。
  • 從 C 語言入手精簡的困難較高,於是轉向彙編程式。

(2)編寫 minimal.S 彙編程式。

  • 在程式中新增三行系統呼叫,能讓程式主動停止,避開 return 報錯。
  • 掌握從 *.S → *.s → *.o → *.out 的編譯過程。
  • 對比預編譯得到的 *.s 相比 *.S 新增了什麼內容。以 # 行代表了編譯指令。

高階程式設計語言可以視為“看得見的語句”,而系統呼叫往往由編譯器新增,所以可以視為“看不見的語句”,最後的指令便由這兩部分組成。

作業系統的所有程式都是建立在這些有限的系統呼叫之上的,它們也被稱為作業系統的 API。

二、程式設計的狀態機和編譯器的功能

(一)狀態機模型

作業系統中的任何程式都是呼叫 syscall 的狀態機。

(1)彙編程式碼的狀態機模型:

  • 狀態 = 記憶體 M + 暫存器 R
  • 初始狀態 = ABI 規定 (例如有一個合法的 %rsp)
  • 狀態遷移 = 執行一條指令

(2)簡單 C 程式的狀態機模型(語義):

  • 狀態 = 堆 + 棧。
  • 初始狀態 = main 的第一條語句。
  • 狀態遷移 = 執行一條語句中的一小步。

對應到程式碼實現:

  • 狀態:Stack frame 的列表 + 全域性變數。
  • 初始狀態:僅有一個 frame: main(argc, argv) ;全域性變數為初始值
  • 狀態遷移:
    • 執行 frames.top.PC 處的簡單語句
    • 函式呼叫 = push frame (frame.PC = 入口)
    • 函式返回 = pop frame

(二)編譯器

  • 編譯器的主要功能就是“翻譯”:.s = compile(.c)
  • 編譯器可以對程式碼進行最佳化,只要能保證最終結果的準確性(外部觀測結果和編譯執行結果一致)。
    • 使用 volatile 禁用對該變數的任何最佳化,每次都從暫存器取值。
    • 使用“編譯屏障” compiler barrier,其作用類似記憶體屏障。

三、作業系統的程式

(一)檢視可執行檔案

  • 使用 vim+xxd 檢視
  • 使用 objdump -d a.out 檢視二進位制執行檔案的組成
  • 使用 vscode 的 binary editor 外掛。

(二)系統級的應用程式(庫)舉例

建議多看一下這些系統級的程式庫的實現程式碼,提高程式碼水平。為了初學者快速入門,可以先看最初版的實現,它們往往都比較簡短(最新版往往都很長)。

  • GNU Coreutils, GNU Binaryutils(objdump 的實現就在其中)。
  • busybox, toybox 等。

(三)使用 strace 等工具對可執行檔案進行跟蹤和除錯

程式執行的三個階段:

  • 載入:執行 execve 設定初始狀態。
  • 狀態機執行:程序管理(fork)、檔案管理(open, close...)、記憶體管理(mmap, brk...)...
  • 退出:呼叫 _exit 退出

使用 strace gcc hello.c |& vim - 檢視 gcc 的編譯過程。使用 |& 是因為 strace 的輸出會導向“錯誤輸出裝置”,所以需要使用 & 將管道合併,統一給 vim 顯示。除此之外,strace 還可以除錯像 x11 這樣的圖形介面程式。

相關文章