本文重點內容:
- 強行構造最小的 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 這樣的圖形介面程式。