沐浴在組合語言的溫暖中:從零開始構建 Web Server

在一個由高階框架和 AI 生成的樣板代碼主導的時代,回歸底層硬體有一種罕見且直觀的滿足感。這就是 ymawky (yuh maw kee),一個完全使用 ARM64 組合語言編寫的 Web Server。此專案是為 macOS 開發的,「僅使用 syscall、不使用 libc、每個連線一個 fork」的伺服器——這是一項工藝成就,既是技術練習,也是對現代軟體工程抽象層的一種反抗。

用組合語言構建 Web Server 並非為了傳統意義上的效能優化;而是為了理解應用程式與作業系統之間的根本契約。透過捨棄 C 標準函式庫 (libc),作者直接與核心進行互動,透過原始的系統呼叫 (syscall) 管理從 socket 建立到檔案 I/O 的一切。

技術架構與功能

儘管是用組合語言編寫的,ymawky 並非僅僅是一個概念驗證。它是一個功能齊全的靜態檔案 Web Server,擁有一系列令人驚訝的功能,足以媲美專業的實作方式:

核心功能

  • HTTP 方法支援:它處理 GETPUTDELETEOPTIONSHEAD 請求。
  • 進階檔案處理:伺服器支援廣泛擴展名的 MIME 類型檢測(從 .wasm.webp.opus.docx),並實作了 Range: bytes= 請求,實現了流暢的影片跳轉功能。
  • 原子性上傳PUT 請求透過寫入暫存檔 (www/.ymawky_tmp_<pid>) 並僅在成功完成後才重新命名,以實現原子性處理,防止損壞的檔案覆蓋現有數據。
  • 目錄列表:如果請求的資源是一個目錄,伺服器會自動列出其內容,但為了維護安全性,會排除根目錄。

安全性與穩健性

用組合語言編寫通常會引發記憶體安全問題,但 ymawky 加入了幾項「安全」措施來減輕常見的漏洞:

  • 路徑遍歷防護:伺服器明確封鎖了 .. 序列,以防止攻擊者逃脫文件根目錄,但仍允許檔名中包含多個點(例如 ohwell...txt)。
  • DoS 緩解:為了抵禦 Slowloris 類型的攻擊,伺服器對接收數據設定了 10 秒的超時限制,並對完整的 Header 設定了總超時限制。
  • 資源限制:它將最大並行程序數限制在 256 個,以防止 PID 耗盡,並拒絕超過 PATH_MAX (4096 bytes) 的路徑。
  • 符號連結保護:伺服器使用 O_NOFOLLOW_ANY 來拒絕包含符號連結 (symlink) 的路徑。

可移植性的挑戰

ymawky 最引人注目的方面之一是它與 macOS 核心的深度耦合。雖然作者的目標是可移植性,但實作細節揭示了類 Unix 系統在組合語言層級上的顯著差異:

  • Syscall 慣例:macOS 使用 x16 作為 syscall 編號,並使用 svc #0x80,而 Linux 使用 x8svc #0
  • 錯誤處理:macOS 在發生錯誤時會設置 carry flag,並將 errno 放在 x0 中,而 Linux 則在 x0 中返回負值。
  • 程序管理fork() 的行為有所不同,macOS 將子程序指示器放在 x1 中,而 Linux 使用 x0
  • 重定位:該專案利用了 Mach-O 重定位運算符 (@PAGE@PAGEOFF),這與 Linux 的 ELF 格式不相容。

或許最不尋常的選擇是訊號處理 (signal handling)。ymawky 利用了 macOS sigaction 結構體中的 sa_tramp 欄位,直接跳轉到處理程序,完全繞過了 libc 的 trampoline 和 sigreturn——這項技術若要應用於任何其他 POSIX 系統,都需要重新編寫整個系統。

社群回應

此專案在 Hacker News 社群中引起了強烈共鳴,引發了關於現代程式設計狀態的討論。許多人將此專案視為在 LLM 時代下的一種「人類藝術形式」。

"我哀悼人類藝術形式的消亡... 十年前,我會向某個足以構建這種東西的精英人士致敬敬禮。今天,我只會想:'LLM 需要多久才能寫出這個?'"

其他人則指出了實際的權衡,指出雖然組合語言提供了完全的控制權,但由於邊緣案例 (edge cases) 的數量極其龐大,組合語言對於大規模 PC 應用程式而言在本質上是不可持續的。

也有關於 macOS syscall 穩定性的技術警告。正如一位用戶所提到的,Go 團隊在 1.12 版本中著名的地將直接 syscall 轉向了 libSystem.dylib,因為與 Linux 不同,macOS 的 syscall 編號並不保證是穩定的。

結論

ymawky 不僅僅是一個 Web Server;它提醒了我們「黑客」精神——渴望構建一些困難的事情,不是因為它是產品開發的最有效路徑,但因為構建它的過程本身就能提供對機器本身的更深層理解。在一個充滿「情緒編碼」(vibe coding) 和代理型 LLM 的世界中,手動映射暫存器並管理堆疊框架 (stack frames) 的行為,仍然是尋找機器意義的強大方式。

Sources