摘要
本文說明「建構變體」的實作方式。這是 Fuchsia 建構系統的功能,可建構主機和裝置二進位檔的檢測或特別最佳化版本。
讀者必須熟悉 GN toolchain() 執行個體,並應已閱讀下列文件:
建構變化版本總覽
Fuchsia 建構作業定義了幾種建構變體,例如:
asan和ubsan變體分別用於建構機器碼,並搭配 Clang 的 Address Sanitizer 和 Undefined Behaviour Sanitizer。甚至還有結合兩者的asan-ubsan變體。coverage變數用於建構機器碼,並啟用 Clang 的檢測式剖析功能,以支援程式碼涵蓋率收集作業。profile變體也用於建構插碼程式碼,但目的是支援設定檔引導最佳化。thinlto和lto變數用於建構二進位檔,並啟用連結時間最佳化。gcc變體用於使用 GCC 編譯器 (而非 Clang) 建構特定 Zircon 核心片段,有助於找出可能以非常重要的方式影響核心的細微機器碼產生問題。其他幾種變體則適用於特殊需求,這些變體全都在
//build/config/BUILDCONFIG.gn檔案中定義,並使用本文件其餘部分所述的慣例。
一般來說,單一建構變化版本會模擬:
一組額外設定,用於定義建構變體二進位檔及其依附元件時套用的編譯器、組譯器或連結器標記。
一組要加入建構圖中最終變體二進位目標的選用隱含依附元件 (即執行檔、可載入模組,有時甚至是共用程式庫)。
舉例來說,假設「asan」建構變化版本用於啟用 Clang 的 Address Sanitizer 支援功能。在實務上,建構啟用 Address Sanitizer 的 Fuchsia 可執行程式時,至少需要:
建構可執行檔及其所有依附元件時,將
-fsanitize=address標記同時傳遞至 Clang 編譯器和連結器 (如果是 Fuchsia,則包括 C 程式庫)。執行階段可用的 Asan 執行階段 (
libclang_rt.asan.so),以及其本身的依附元件 (即libc++.so、libc++abi.so和libunwind.so二進位檔的特殊預先建構版本)。
基礎和變體工具鍊
建構變化版本一律會套用至特定「基本」工具鍊,該工具鍊提供預設設定,並由變化版本本身擴增。這會建立新的 GN toolchain() 執行個體,稱為「變體工具鍊」,具有自己的 root_out_dir。例如:
//build/toolchain:host_x64是用於建構主機二進位檔的基本工具鍊,而其root_out_dir為${root_build_dir}/host_x64。//build/toolchain:host_x64-ubsan是套用ubsan變數所建立的變數工具鍊,而root_out_dir則是${root_build_dir}/host_x64-ubsan。//build/toolchain/fuchsia:x64是預設工具鍊 (以 x64 架構裝置為目標時),用於建構 Fuchsia 使用者層級的二進位檔。由於這是預設工具鍊,因此root_out_dir與root_build_dir相同。//build/toolchain/fuchsia:x64-asan是透過將asan變體套用至預設工具鍊所建立的變體工具鍊。其root_out_dir將${root_build_dir}/x64-asan。
一般而言,//${tc_dir}:${tc_name}-${variant} 是透過將 ${variant} 變體套用至名為 //${tc_dir}:${tc_name} 的基礎工具鍊所建立的變體工具鍊,而 root_out_dir 一律為 ${root_build_dir}/${tc_name}-${variant}。
如果基礎工具鍊有 shlib 工具鍊,則其任何變體工具鍊也會有。最後,單一變體可以套用至多個基本工具鍊。
舉例來說,//build/toolchain:host_x64-asan 和 //build/toolchain/fuchsia:x64-asan 是變數工具鍊,是將相同的 asan 變數套用至用於建構主機和 Fuchsia 裝置二進位的基本工具鍊所建立。
後者也會將 //build/toolchain/fuchsia:x64-asan-shared 做為 shlib 工具鍊,產生以 ELF 為基礎的共用程式庫。
您必須在建構作業中,使用 clang_toolchain_suite() 或 zircon_toolchain_suite() 定義基本工具鍊。這兩個範本最終都會呼叫 variant_toolchain_suite(),實作在需要時自動建立變數工具鍊的魔法。
工具鍊和變體標記
Fuchsia 建構作業中的每個基本工具鍊都可以有多個標記,這些標記是描述工具鍊屬性的任意形式字串。舉例來說,"kernel" 標記用於指出工具鍊是用來建構核心構件 (這很重要,因為沒有 C 程式庫、沒有標準 C++ 程式庫,以及其他一些對特定目標定義而言很重要的限制)。
所有有效的工具鍊標記清單位於 //build/toolchain/toolchain_tags.gni。
同樣地,每個變體定義都有許多標記,用於說明變體的屬性。舉例來說,"instrumentation" 標記用於指出這個變體會建立執行階段插碼的機器碼 (例如清除器或剖析器)。
如需所有有效變數標記的清單和說明文件,請參閱 //build/toolchain/variant_tags.gni。
建立變體工具鍊時,全域 toolchain_variant.tags 值會同時包含從基本工具鍊和變體繼承的標記。
工具鍊變數例項化
建構系統只會在需要時建立變體工具鍊。
可能的工具鍊 + 變種版本組合數量非常龐大,如果一次建立所有組合,gn gen 的速度會明顯變慢。
建構系統不會急於建立所有變體,而是根據下列條件決定要建立哪些工具鍊變體:
select_variant全域變數中顯示的變數選取器清單。顯示為
variant_toolchain_suite()範本的enable_variants引數的變體描述元名稱清單。即使select_variant為空白,這個引數也很少用於強制啟用幾個變體。舉例來說,用於建構 C 程式庫的工具鍊 ASan 和 UBSan 變數一律會啟用,因為建構 Core Fuchsia IDK 時需要這些變數 (請參閱
//sdk/lib/c/BUILD.gn)。variant_toolchain_suite()的exclude_variant_tags引數中顯示的變體標記清單。很少用於排除特定變體,以免套用至指定基礎工具鍊。舉例來說,系統啟動載入程式會排除含有
"instrumented"標記的變數,因為在啟動裝置時無法執行清除程式或剖析執行階段 (請參閱 `//
變體描述符
變體描述元是 GN 範圍,可向建構系統說明特定建構變化版本的屬性。這些是透過 //build/config/BUILDCONFIG.gn 中的 known_variants 變數定義,且每個範圍都應遵循下列嚴格結構定義:
configs:GN 設定標籤的選用清單,系統會自動將這些標籤新增至這個變體中的每個目標。請注意,在這個清單中,每個設定
${label}也必須有目標${label}_deps,這個變體中建構的每個目標都會自動依附於該目標。大多數情況下,這會是空的group()。remove_common_configs:選用的 GN 設定標籤清單,如果存在,應從使用這個變種版本建構的任何目標中移除。如果建構系統為二進位檔設定的部分預設設定不應適用於特定變體,有時就必須這麼做。remove_shared_configs:GN 設定標籤的選用清單,類似於remove_common_configs,但只會在建構shared_library()目標及其依附元件時套用。deps:GN 目標標籤的選用清單,會以隱含依附元件的形式,新增至使用這個變體建構的任何可連結目標。name:唯一命名變體描述元的字串,通常用於select_variant。如果省略name,configs不得為空白,且會用於衍生名稱 (方法是使用破折號加入名稱)。tags:選用清單,內含描述變體屬性的任意形式字串 (請參閱工具鍊和變體標記)。toolchain_args:選用範圍,其中定義的每個變數都會覆寫這個變體工具鍊環境中的建構引數。host_only和target_only:選用範圍,可包含上述任何欄位。這些值分別只用於主機或目標 (即裝置) 工具鍊。這裡包含的任何欄位也不應位於外部範圍。
以下是一些例子:
含有單一設定的變數描述元範例
{
configs = [ "//build/config/lto" ]
tags = [ "lto" ]
}
上述範圍定義了名為 "lto" 的變體描述元 (由於範圍中沒有 name 鍵,因此名稱是從 configs 中的值推斷而來,而這裡只包含單一項目)。
套用這個變種版本會新增 //build/config/lto:lto 設定 (定義於 //build/config/lto/BUILD.gn 中),且該檔案也應包含 //build/config/lto:lto_deps 空群組 (如果這類設定沒有隱含依附元件)。例如:
# //build/config/lto/BUILD.gn
config("lto") {
cflags = [ "-flto" ]
asmflags = cflags
ldflags = cflags
rustflags = [ "-Clto=fat" ]
}
group("lto_deps") {
# Implicit dependencies for "lto" config.
# This is an empty group since there are none.
}
這個描述元會使用 "lto" 標記,指出這個變體已執行連結時間最佳化。"thinlto" 描述元也可以使用這個標記,但會使用不同的設定。
含有多個設定的變數描述元範例
{
configs = [
"//build/config/sanitizers:ubsan",
"//build/config/sanitizers:sancov",
]
remove_common_configs = [ "//build/config:no_rtti" ]
tags = [
"instrumented",
"instrumentation-runtime",
"kernel-excluded",
"sancov",
"ubsan",
]
}
這會定義名為 "ubsan-sancov" 的變體描述元 (名稱是從 configs 清單衍生而來,方法是以破折號聯結設定名稱),用於建構機器碼,以便在執行階段偵測未定義的行為,並同時收集程式碼涵蓋率資訊。
請注意,這也需要定義 //build/config/sanitizers:ubsan_deps 和 //build/config/sanitizers:sancov_deps,才能列出這些設定的隱含依附元件。
這是因為 //build/config:no_rtti 是許多基本工具鍊的預設設定之一,但必須啟用 RTTI,UBSan 插樁才能正常運作。remove_common_config
使用的標記清單也更廣泛。請注意 "kernel-excluded" 標記,這個標記用於防止這個變體套用至任何核心機器碼。
含有 toolchain_args 的子類描述元範例
{
name = "fully_optimized"
toolchain_args = {
optimize = "speed"
}
}
這個變體描述元會明確命名,且不會新增任何設定或依附元件。另一方面,這可確保全域建構設定變數 optimize 會設為「speed」,這會變更相應變數工具鍊環境中定義的預設設定數量。
toolchain_variant 全域變數
在 BUILD.gn 或 *.gni 檔案中,全域 toolchain_variant 變數可用於擷取 current_toolchain 的變體相關資訊。這是具有下列結構定義的範圍:
name:建構變數描述元的名稱。在基本工具鍊的環境中,這是空字串;否則,這是用於建立目前 GNtoolchain()執行個體的變數描述元名稱。各種工具鍊情境的名稱範例:
//build/toolchain/fuchsia:x64 "" //build/toolchain/fuchsia:x64-shared "" //build/toolchain/fuchsia:x64-asan "asan" //build/toolchain/fuchsia:x64-asan-shared "asan"base:目前工具鍊的基礎工具鍊完整 GN 標籤。請注意,如果是工具鍊變體的 shlib 工具鍊,這會指向最終的基礎工具鍊。例如://build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-asan //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-shared //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-asan-shared //build/toolchain/fuchsia:x64tags:自由形式的字串清單,每個字串都描述目前工具鍊執行個體及其變體的屬性。這只是工具鍊和變數標記的聯集。instrumented:布林值標記,只有在tags清單包含"instrumentation"標記值時,才會設為 true,方便您取代 GN 中複雜的測試指令,例如:if (toolchain_variant.tags + [ "instrumentation" ] - [ "instrumentation" ] != toolchain_variant.tags) { # toolchain is instrumented ... }使用:
if (toolchain_variant.instrumented) { # toolchain is instrumented ... }is_pic_default:布林值,在可建構 ELF 位置獨立程式碼 (PIC) 的工具鍊中為 true。這表示 shlib 工具鍊 (例如//build/toolchain/fuchsia:x64-shared),或直接產生這類程式碼的基礎工具鍊 (例如//zircon/kernel/lib/userabi/userboot:userboot_arm64)。with_shared:布林值,如果目前的工具鍊有 shlib 工具鍊可建構 ELF 共用程式庫 (例如//build/toolchain/fuchsia:x64)或位於這類工具鍊 (例如//build/toolchain/fuchsia:x64-shared) 中,則為 true。configs、remove_common_configs、remove_shared_configs:要config()項目清單,直接來自目前的GN 描述元 (如有),否則為空白清單。deps:要新增為任何可連結目標的依附元件的 GN 標籤清單,如果有的話,會從變體描述元本身繼承。libprefix:如果是插碼變體,這是共用程式庫的安裝前置字串,否則為空字串。如需完整詳細資料,請參閱工具鍊變體 libprefix 區段。exclude_variant_tags:變體選取邏輯內部使用。繼承自clang_toolchain_suite()或zircon_toolchain_suite()呼叫,或直接來自目標定義。這是用來排除要套用至基本工具鍊或目標的變體標記清單,有時必須這麼做。suffix:這是"-${toolchain_variant.name}",如果名稱為空白,則為""。內部使用,可簡化擴展作業,不必使用條件。supports_cpp:如果這個工具鍊支援 C/C++,則為true的布林值。supports_rust:布林值,如果這個工具鍊支援 Rust,則為true。is_basic:布林值,如果這個工具鍊是由basic_toolchain()範本建立,且因此未使用 GN 內建的 C/C++ 和 Rust 支援功能,則為true。這個工具鍊只包含copy或action目標。
目標定義很少使用這個全域變數的內容,根據目前的工具鍊環境變更設定。這類情況大多發生在低階目標,例如 C 程式庫、核心構件或非預先建構的插樁執行階段支援。
工具鍊變數 libprefix
如要在單一 Fuchsia 套件中混合使用插碼和未插碼的二進位檔,建構系統必須執行特殊步驟:
使用插碼變數工具鍊建構的共用程式庫必須安裝至
"lib/<variant>/",而非預設的"lib/"位置。可執行的二進位檔必須使用類似
"-Wl,-dynamic-linker=<variant>/ld.so.1"的連結器引數編譯,這會覆寫預設值 ("ld.so.1",這是 Fuchsia Clang 預建工具鍊二進位檔中硬式編碼的值)。在特殊情況下,模糊測試建構變體會使用程式庫子目錄的非模糊測試建構變體名稱。
toolchain_variant.libprefix 變數的定義方式如下,可輕鬆支援所有這些項目:
variant name libdir libprefix note
no variant ---> lib/ "" (default target toolchain)
thinlto ---> lib/ "" (uninstrumented)
asan-ubsan ---> lib/asan-ubsan/ "asan-ubsan/" (instrumented)
asan-fuzzer ---> lib/asan/ "asan/" (instrumented + fuzzing)
這可用於判斷安裝位置 (如 "lib/${toolchain_variant.libprefix}") 和連結器標記 (如 "-Wl,-dynamic-linker=${toolchain_variant.libprefix}ld.so.1")。
選取變體
Fuchsia 建構系統支援選取要啟用哪些建構變體,以及這些變體適用的個別目標或目標群組。方法是在建構設定檔 (args.gn) 中定義 select_variant 變數。請參考以下範例:
# From out/default/args.gn
...
select_variant = [
{
label = [ "//src/sys/component_manager:bin" ]
variant = "lto"
},
"host_asan",
"thinlto/blobfs",
"ubsan",
]
清單中的每個值都是運算式,稱為「變數選取器」,可以是範圍或字串,用於設定建構作業如何將變數套用至不同的目標集。
如果已定義 select_variant 且不是空白清單,系統會使用其值,判斷如何建構可連結的目標 (例如可執行檔、可載入的模組和共用程式庫,這些目標會出現在基礎工具鍊環境的建構圖中),以及所有依附元件。
系統會依序比較 select_variant 中顯示的變數選取器,並選取第一個符合目前目標的選取器。因此,上述範例表示:
//src/sys/component/manager:bin程式二進位檔及其依附元件一律應使用lto變體建構。主機二進位檔應以
"asan"變體版本建構。請注意,"host_asan"不是變體描述元名稱,而是變體捷徑。blobfs程式裝置二進位檔一律應使用"thinlto"變體版本建構,該版本會執行連結時間最佳化。其他所有裝置二進位檔都應使用
"ubsan"變體版本建構。
變化版本選取器
變數選取器是可出現在全域 select_variant 建構設定變數中的值。建構系統會使用這些選取器,在基本工具鍊的環境中定義可連結的目標時,控管變數選取作業。
支援三種值類型:
定義一組目標的相符條件。該範圍的格式如下:
variant:特定變體描述元的名稱。只有在目前目標符合範圍其餘部分定義的所有條件時,才會使用這個描述元。label:如已定義,這必須是合格的 GN 標籤清單 (含:,但不含工具鍊標籤,例如//src/sys/foo:foo)。name:如果已定義,則為 GN 標籤目標名稱清單 (例如//src/sys/foo:bar目標的名稱為「bar」)。dir:如果已定義,則為 GN 標籤目錄路徑清單 (例如//src/sys/foo:bar目標的路徑為"//src/sys/foo")。output_name:如果已定義,則為目標output_name值清單 (預設為target_name)。target_type:如果已定義,則為與目標類型相符的字串清單。有效值為"executable"、"test"、"loadable_module"、"shared_library"和其他幾個值。testonly:如果已定義,則為布林值。如果為 true,選擇器會比對具有testonly=true的目標。如果為 false,選取器會比對沒有testonly=true的目標。host:如果已定義,則為布林值。如果為 true,選取器會比對主機工具鍊中的目標。如果為 false,選取器會比對目標工具鍊中的目標。
包含簡單名稱 (例如
"asan") 的字串,指向變體快速鍵,這是預先存在的選取器範圍值的別名。舉例來說,
"coverage"值等同於下列範圍:{ variant = "coverage" host = false }字串,內含變體快速鍵名稱和輸出名稱,並以目錄路徑分隔 (例如
"thintlo/blobfs")。這是方便使用的格式,可避免編寫對等範圍,在先前的範例中會如下所示:{ variant = "thinlto" host = false output_name = [ "blobfs" ] }
select_variant 清單中的選取器順序很重要:符合目前目標的第一個選取器會勝出,並決定該目標的建構方式。
變體快速鍵
除了變數描述元,建構作業還會設定許多「捷徑」,也就是幾個硬式編碼變數選取器範圍值的命名別名。建構作業會新增幾個硬式編碼值,並從已知變數清單建立其他值:
"host_asan"捷徑定義為使用"asan"變體描述元建構主機二進位檔,在技術上等同於下列選取器範圍值清單:# Definition for the `host_asan` variant shortcut [ { variant = "asan" host = true } ]同樣地,還有
host_asan-ubsan、host_coverage、host_profile和其他幾個。每個變體描述元名稱都有對應的捷徑,可專門將變體套用至裝置二進位檔。也就是說,
"ubsan"捷徑等同於這個選取器範圍值清單:[ { variant = "ubsan" host = false } ]因此,在
select_variant中使用變數描述元名稱只會將其套用至裝置二進位檔,如下所示:# Applies the `ubsan` variant to device binaries, not host ones! select_variant = [ "ubsan", ]
變體目標重新導向
variant_target() 範本
//build/config/BUILDCONFIG.gn中定義的 variant_target() 範本會實作核心建構變化版本選取機制。
這個範本不應直接從 BUILD.gn 檔案呼叫,而是由 Fuchsia 建構定義的包裝函式範本叫用,適用於 executable()、loadable_module()、shared_library() 和其他幾個對應可連結目標 (即使用靜態連結器建立的目標) 的範本。
這項功能會針對每個目標,在建構圖的每個工具鍊環境中,比較 select_variant 的內容與目標的屬性 (即目標類型和一些額外引數),以執行下列操作:
1) 計算目標的「建構工具鍊」,也就是用於建構實際二進位檔及其依附元件的 GN 工具鍊執行個體。
2) 如果目前的工具鍊是建構工具鍊,請照常建構目標。
3) 否則,請建立 group() 或 copy() 目標,在建構工具鍊中重新導向 (即公開依附) 目標。這是否為群組或副本,取決於實作 variant_target() 中詳細記錄的細微條件,但請參閱下列子章節瞭解部分說明。
如要保留某些可連結目標的輸出位置,就必須使用 copy() 目標;如果不需要,則使用 group()。
大多數時候,executable() 或 loadable_module() 目標會需要 copy(),而 shared_library() 則會需要 group()。
可連結變體二進位檔的輸出位置
GN 設定語言的重要設計限制是,除了少數例外狀況,特定目標定義對其依附元件一無所知,只知道依附元件的標籤。這會造成問題,因為在許多情況下,特定目標需要知道依附元件輸出內容的位置,或這些依附元件的實際目標類型。
為說明這點,請參考以下範例:
名為
//src/my/program:bin的executable()目標,會產生名為my_program的 Fuchsia 程式二進位檔。由於建構作業的運作方式,這會產生${root_build_dir}/exe.unstripped/my_program和${root_build_dir}/my_program,以及一些次要檔案 (在此忽略)。名為
//src/my/program:verify_binary的action()目標用於剖析程式二進位檔,以檢查或擷取其中的資訊 (假設是驗證匯入符號參照)。這個目標需要依附於第一個目標,但也要找出二進位檔的輸出位置,如下所示:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
executable("//src/my/program:bin")
output_name = "my_program"
# outputs: [
${root_build_dir}/exe.unstripped/my_program,
${root_build_dir}/my_program,
]
在這裡,action() 可以使用其目錄的 get_label_info("<label>", "root_out_dir") 猜測程式二進位檔的位置,並在動作本身中硬式編碼 output_name 值。這會違反抽象層,但考量到 GN 的限制,這是必要做法。
啟用建構變體後,二進位目標的實際輸出位置會根據 select_variant 而有所不同。如果使用簡單的 group() 實作變體重新導向,圖表會變成:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
group("//src/my/program:bin")
|
| public_deps
|
v
executable("//src/my/program:bin(//build/toolchain/fuchsia:x64-asan")
output_name = "my_program"
# outputs: [
# ${root_build_dir}/x64-asan/exe.unstripped/my_program,
# ${root_build_dir}/x64-asan/my_program,
# ]
問題在於頂層動作中的 inputs 值未變更,因此其指令會嘗試在舊位置 (${root_build_dir}/my_program) 尋找程式二進位檔,而不是新位置 (${root_build_dir}/x64-asan/my-program)。建構作業可能會使用過時的構件,或因缺少檔案而失敗。
在動作本身中剖析 select_variant 的成本太高,因此解決這個情況時,可執行和可載入的模組目標需要 copy() 目標,而不是 group(),確保未經剝除的二進位檔會複製到原始位置。圖表會變成:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
copy("//src/my/program:bin")
outputs = [ "${root_build_dir}/my_program" ]
sources = [ "${root_build_dir}/x64-asan/my_program" ]
|
| public_deps
|
v
executable("//src/my/program:bin(//build/toolchain/fuchsia:x64-asan")
output_name = "my_program"
# outputs: [
${root_build_dir}/x64-asan/exe.unstripped/my_program,
${root_build_dir}/x64-asan/my_program,
]
完成這項設定後,建構作業一律會成功,動作指令也一律會處理正確的二進位檔。
所有這些作業都會在建構過程中自動完成。最終效果是,依附元件不需要在意依附元件是否以特定變體建構,至少對於未經過剝除的二進位路徑而言,依附元件可以依賴輸出位置保持穩定。
ELF 共用程式庫的輸出位置
TBW
特殊 novariant 描述元
TBW
特殊全域變數
host_toolchain 和 host_out_dir 全域變數
TBW
zircon_toolchain 變數
TBW
variant() 範本
TBW