在前面的文章中,梳理了Google做Fuchsia的動因和野心,也將我在編譯和測試Fuchsia過程中遇到的問題和解決的方法做了整理。

本文將會以一個簡單的例子為線索,介紹Google在設計Fuchsia的時候的思路。

本文的內容將包括:

  • 一個Rust命令行程序,運行後會輸出Hello, world.
  • Fuchsia與Linux/Unix進程的差異
  • Fuchsia的包機制
  • Fuchsia的編譯機制

編譯環境

通常情況下,我們有2個方式來編譯Fuchsia應用程序:

  • 使用Fuchsia SDK
  • 在Fuchsia源碼樹中增加自己的軟體包

當前來說,第二種方案是最為方便,也是我個人比較推薦的方案,其原因主要在於:

  1. Google在設計Fuchsia操作系統的時候,採用的是完全模塊化的設計;而同時,Google也把這個理念引入到了Fuchsia的SDK中[1]。所以Google發布的SDK稱為Fuchsia Core SDK[2],它只包含了SDK的metadata、頭文件、庫文件等,你需要再使用SDK後端才能將它轉換為一個包含編譯環境和工具鏈的SDK。而後者的這個過程,你需要有Fuchsia本身的編譯環境才能完成。
  2. Fuchsia使用了標準的ELF格式作為可執行文件的格式,但不兼容Linux的ABI,所以Google對LLVM工具鏈做了修改,以增加選項--target=x86_64-fuchsia以生成Fuchsia ABI的程序。而該選項目前只有在Google特定編譯的版本的clang中才包含,需要單獨從CIPD下載。還不如直接使用Fuchsia的源碼樹,它包含了更完整、方便的編譯環境。

相信隨著時間的推移,Fuchsia和Android的生態進一步融合以後,這個體驗會得到有效的改善。

由於眾所周知的原因,在國內訪問Fuchsia的源代碼有一定的困難,我準備了完整的代碼和虛擬機供大家使用,下載鏈接可以參看前面一篇文章:

黃珏珅:如何編譯和運行Fuchsia操作系統?

zhuanlan.zhihu.com
圖標

編寫代碼

其實在Fuchsia的源碼樹中已經有完整的C++和Rust的示例代碼,我們可以不必自己編寫。

不過,個人覺得通過自己一步步的完成一個程序的編寫與構建是一件更有成就感的事情,也得以通過這個過程管中窺豹,了解到Fuchsia的設計思想。

選擇語言

Fuchsia支持使用多種不同的語言來構建其上運行的程序,包括C/C++, Dart, Go, Python和Rust。

不過,縱觀Fuchsia的代碼,可以看到其底層組件大部分是由C++和Rust實現的。經過對Fuchsia源碼的分析,其中有大約43萬行的Rust代碼(且在持續增長中)。而這些代碼主要集中在Fuchsia的Connectivity框架(包括了網路棧、乙太網、WiFi、藍牙、蜂窩數據通訊等)中。

由於C++的各種示例已經非常普及,所以在這裡希望選擇Rust[3]作為示例的語言。一方面是希望大家在閱讀本文的過程中,能夠多一點新鮮感;另一方面,也存在我個人的一些私心,希望大家對Rust語言投以更多的關注。關於Rust語言本身的學習資料,大家可以首先閱讀官方的文檔[4],然後 @張漢東 的 《Rust編程之道》[5]也是非常不錯的進一步學習的資料。

建立文件結構

在Fuchsia中,通常會採用層次化的方式來組織代碼,為了與Fuchsia的其他代碼做區分,我們將本系列文章中涉及的所有代碼都放置於$FUCHSIA_ROOT/th目錄下。

這裡的FUCHSIA_ROOT指的是Fuchsia源代碼的根目錄,如果大家使用的是在上一篇文章中提供的虛擬機的話,這個目錄就是指~/fuchsia目錄。

我們首先建立目錄結構

export FUCHSIA_ROOT=~/fuchsia
mkdir -p $FUCHSIA_ROOT/th/bin/th_hello_world/src

編寫主程序

然後我們需要編寫Rust的主程序,並放置到上述目錄中:

// $FUCHSIA_ROOT/th/bin/th_hello_world/src/main.rs

fn main() {
println!("Hello, World!");
}

這是最簡單的一個Rust程序,裡面只調用了println!宏來列印Hello, World!。

包(Packages)

在Fuchsia中,操作系統的基本組成單位是包(Package),和Android中的包類似,它包括了可執行程序(Executables)、庫(Libraries)、資源文件(Resources)和元信息(Metadata)。

與Android不同的是,Fuchsia中的所有系統服務,也是以包的形式存在的。這一點與大多數的Linux發行版非常類似。

這種設計給Fuchsia帶來了一個好處,就是無論是系統服務,還是應用程序,都可以單獨進行升級和維護,不依賴於完整操作系統和固件的發布周期。舉例來說,如果在hostapd[6]中發現了一個安全漏洞,那麼對於Android來說,需要升級整個固件,才能完成補丁的修復;而對於Fuchsia或者類似Ubuntu這樣基於包的發行版來說,只需單獨升級hostapd即可。

不過Fuchsia的包系統的設計與Linux發行版的設計又有著顯著的不同,它其實更接近於docker或者snap app的設計思想。

Linux發行版的包,可以認為是一份針對你的根文件系統進行修改的說明。比如其中有一個二進位文件,需要安裝到/usr/bin目錄中;有一個動態鏈接庫,需要安裝到/usr/lib目錄中;有一個配置文件,需要放到/etc目錄下。這個設計其實還是沿用自unix的設計體系,好處是一目了然,但壞處是容易出現安全和兼容性風險。比如安裝了samba軟體包,它會在/etc/samba目錄下安裝一些配置文件(如smbd.conf)。而通常,我們使用這個服務的時候,都需要修改這個配置文件以滿足我們的使用需求。當軟體包更新了版本以後,我們應當使用用戶修改過的配置文件,還是新的包中包含的配置文件,就會成為一個很棘手的問題。[7]

Fuchsia的包與docker或者snap app類似,它可以被視為一個只讀的微文件系統,它不會被複制,或者mount到一個所謂的根文件系統(實際上,在Fuchsia中,不存在根文件系統)中。它本身就是這個應用的根文件系統(換言之,它是一個安全沙盒,除非顯式授予,否則它只能訪問沙盒內的資源,具體的內容會在後續的章節做更詳細的介紹)。

創建包元數據

我們需要在$APP_ROOT下的meta目錄下,創建一個名為th_hello_world.cmx的文件,用以描述該包的一些元數據:

{
"program": {
"binary": "bin/th_hello_world"
},
"sandbox": {
"services": [
"fuchsia.logger.LogSink"
]
}
}

元數據的前面一半很簡單,就是說了如果啟動這個包,那麼啟動的可執行文件是bin/th_hello_world。比較有意思的是後面一半,也反映出了Fuchsia的設計哲學。

首先,它的名字是sandbox,表示這是一個安全沙箱。其次它裡面寫了services和fuchsia.logger.LogSink,說明給它授予一個日誌的服務許可權。熟悉依賴注入設計模式[8]的朋友應該會對這個設計非常熟悉了,它表明了應用需要這個服務,而服務的提供者由操作系統負責查找和注入。

這裡就引申出了Fuchsia設計中一個非常重要的概念,就是進程模型與其他操作系統的進程模型的區別。

Fuchsia進程模型與Linux進程模型的區別

Linux的進程模型

在Linux中,進程的創建是通過fork和exec兩個系統調用來實現的,這是從Unix時代繼承下來的設計。當執行fork系統調用的時候,操作系統會創建一個跟當前進程一模一樣的進程出來。也就是說子進程會繼承父進程的一切資源,包括不限於內存、文件描述符、用戶和用戶組、許可權等等。

於是,為了在Linux下構建安全沙箱,父進程必須一點點的將這些資源去除。比如修改進程的用戶、chroot到一個虛擬的文件系統等等。最後再通過exec系統調用載入新進程的可執行文件,開始運行。

這樣就會帶來一個顯著的問題,比如操作系統做了修改,或是應用程序本身做了修改,增加新的資源,而在創建子進程時沒有注意,就會導致安全風險。大量的沙箱逃逸漏洞,都是基於這個原理。

總結來說,它是一個減法的進程安全模型,漏減了就會造成安全風險。

Fuchsia的進程模型

在Fuchsia中,Google選擇了一個完全不同的模型。當你創建進程時候,它是一片空白。父進程需要手動的將子進程需要的資源加入到沙盒中,然後再啟動進程。這樣就大大降低了沙盒逃逸的風險。而父進程能夠給子進程賦予的資源,也是受限於父進程所擁有的資源的。

也就是說,Fuchsia是一個加法的進程模型,只要沒有給,就不會有危險。

加入到編譯系統中

最後一步,自然是要將新寫的包加入到編譯系統中。

Fuchsia採用的是ninja + GN(都是Google自己搞的私貨,ninja的定位類似於make,GN的定位類似於cmake)的分層編譯系統。這是一套非常高性能的編譯系統。

編寫以下的GN腳本,並放入$APP_ROOT/BUILD.gn

# ~/fuchsia/th/bin/th_hello_world/BUILD.gn

import("//build/package.gni")
import("//build/rust/rustc_binary.gni")

rustc_binary("bin") {
name = "th_hello_world"
with_unit_tests = false
edition = "2018"

deps = []
}

package("th_hello_world") {
deps = [
":bin",
]

binaries = [
{
name = "th_hello_world"
},
]

meta = [
{
path = rebase_path("meta/th_hello_world.cmx")
dest = "th_hello_world.cmx"
},
]
}

(由於知乎不支持GN的代碼顯亮,所以我選擇的是JS,將就看看)

在Fuchsia的編譯系統中,並沒有採用Rust常見的Cargo作為依賴管理和構建工具,而是基於rustc自己構建的編譯體系,所以可以看到並沒有Cargo.toml文件[9]

GN的具體含義和使用,本文不做贅述,在後續的文章中,我可以另外做系統性的介紹。

開始編譯

首先,執行fx set,將這個應用納入到編譯中:

fx set terminal.x64 --with //th/bin/th_hello_world

接下來,可以跟往常一樣,執行fx build來構建,不過有一個更簡單的方法是,直接使用fx build-push來編譯並將軟體包推送到設備上:

fx build-push th_hello_world

執行後,可能會遇到一個錯誤:

ERROR: It looks like serve-updates is not running.
ERROR: You probably need to start "fx serve"

這個錯誤信息的意思是,你沒有啟動本地的Fuchsia Package Repo。你需要在另一個命令行窗口中執行:

fx serve

然後,再次執行上述的fx build-push指令以安裝軟體包。

在設備上執行

首先,我們可以通過fx shell打開遠程設備的shell:

fx shell

然後使用run指令即可執行我們的程序:

run th_hello_world

此時,我們就能看到我們輸出的Hello world了。

小思考

  1. 為何執行run th_hello_world的時候會出現:

Found fuchsia-pkg://fuchsia.com/th_hello_world#meta/th_hello_world.cmx

2. 如果有另外一個包,名字叫做th_hello_world_cpp,執行上述指令時會出什麼問題?

延伸閱讀

黃珏珅:為什麼Google需要Fuchsia操作系統?

zhuanlan.zhihu.com圖標黃珏珅:如何編譯和運行Fuchsia操作系統?

zhuanlan.zhihu.com
圖標

參考

  1. ^Fuchsia SDK Strategy https://fuchsia.dev/fuchsia-src/development/sdk#strategy
  2. ^Download Fuchsia Core SDK https://fuchsia.dev/fuchsia-src/development/sdk/download#core
  3. ^這是一種由Mozilla主導開發的,現在由社區維護的,主打0成本抽象和內存安全的程序設計語言
  4. ^The Book - Rust Programming Language https://doc.rust-lang.org/book/
  5. ^Rust編程之道 https://www.zhihu.com/pub/book/119621179
  6. ^用於WiFi功能的一個組件
  7. ^現在一部分發行版解決該問題的辦法是提供一個conf.d目錄,用以存放用戶自己的配置文件,標準文件不變
  8. ^依賴注入設計模式 https://baike.baidu.com/item/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC/1158025?fromtitle=%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5&fromid=5177233
  9. ^編譯系統中支持通過GN自動生成對應的Cargo.toml,方便編輯器的智能感知功能使用

推薦閱讀:

相关文章