Android 核心 ABI 監控

您可以使用 Android 11 及更高版本中提供的應用程式二進位介面 (ABI) 監控工具來穩定 Android 核心的核心內 ABI。該工具收集並比較現有核心二進位( vmlinux + GKI 模組)的 ABI 表示。這些 ABI 表示形式是.stg檔案和符號列表。表示提供視圖的介面稱為內核模組介面(KMI)。您可以使用該工具來追蹤和緩解 KMI 的變更。

ABI 監控工具是在 AOSP 中開發的,並使用STG (或 Android 13 及更低版本中的libabigail )來產生和比較表示。

本頁介紹了工具、收集和分析 ABI 表示的過程,以及使用此類表示來為內核內 ABI 提供穩定性。此頁面還提供了對 Android 核心進行更改的資訊。

過程

分析內核的 ABI 需要多個步驟,其中大部分可以自動化:

  1. 建構核心及其 ABI 表示形式
  2. 分析建構和參考之間的 ABI 差異
  3. 更新 ABI 表示(如果需要)
  4. 使用符號列表

以下說明適用於您可以使用受支援的工具鏈(例如預先建置的 Clang 工具鏈)建置的任何核心repo manifests適用於所有 Android 常見核心分支和多個特定於裝置的內核,它們確保在建立內核發行版進行分析時使用正確的工具鏈。

符號列表

KMI 不包括內核中的所有符號,甚至不包括所有 30,000 多個導出的符號。相反,供應商模組可以使用的符號被明確地列在內核樹根部公開維護的一組符號列表檔案中。所有符號清單檔案中所有符號的並集定義了保持穩定的 KMI 符號集。範例符號清單檔案是abi_gki_aarch64_db845c ,它宣告了DragonBoard 845c所需的符號。

只有符號清單中列出的符號及其相關結構和定義被視為 KMI 的一部分。如果您需要的符號不存在,您可以將變更發佈到符號清單中。新介面位於符號清單中並成為 KMI 描述的一部分後,它們將保持穩定,並且不得從符號清單中刪除或在分支凍結後進行修改。

每個 Android 通用核心 (ACK) KMI 核心分支都有自己的一組符號列表。沒有嘗試在不同的 KMI 內核分支之間提供 ABI 穩定性。例如, android12-5.10的 KMI 完全獨立於android13-5.10的 KMI 。

ABI 工具使用 KMI 符號清單來限制必須監視哪些介面以確保穩定性。主符號清單包含 GKI 內核模組所需的符號。供應商應提交並更新額外的符號列表,以確保他們所依賴的介面保持 ABI 相容性。例如,若要查看android13-5.15的符號列表,請參閱https://android.googlesource.com/kernel/common/+/refs/heads/android13-5.15/android

符號清單包含據報告特定供應商或設備所需的符號。這些工具使用的完整清單是所有 KMI 符號清單檔案的並集。 ABI 工具確定每個符號的詳細信息,包括函數簽名和嵌套資料結構。

當KMI凍結時,不允許對現有的KMI介面進行任何更改;他們很穩定。但是,供應商可以隨時為 KMI 添加符號,只要添加不會影響現有 ABI 的穩定性即可。新加入的符號一旦被 KMI 符號清單引用,就會保持穩定。不應從內核清單中刪除符號,除非可以確認沒有裝置依賴該符號。

您可以使用如何使用符號清單中的說明為裝置產生 KMI 符號清單。許多合作夥伴為每個 ACK 提交一個符號列表,但這不是硬性要求。如果有助於維護,您可以提交多個符號清單。

擴充 KMI

雖然 KMI 符號和相關結構保持穩定(這意味著不能接受破壞具有凍結 KMI 的核心中的穩定介面的更改),但 GKI 核心仍然對擴展開放,以便今年稍後發貨的設備不需要定義所有在KMI 凍結之前它們的依賴關係。要擴展 KMI,您可以為 KMI 添加新符號以用於新的或現有的匯出核心函數,即使 KMI 已凍結也是如此。如果新的核心補丁不破壞 KMI,也可能會被接受。

關於 KMI 損壞

核心有源代碼,二進位檔案是從這些原始碼建構的。 ABI 監控的核心分支包括目前 GKI ABI 的 ABI 表示(以.stg檔案的形式)。建置二進位檔案( vmlinuxImage和任何 GKI 模組)後,可以從二進位檔案中提取 ABI 表示形式。對核心原始檔案所做的任何更改都可能影響二進位文件,進而影響提取的.stgAbiAnalyzer分析器將提交的.stg檔案與從建置工件中提取的檔案進行比較,如果發現語義差異,則在 Gerrit 中的變更上設定 Lint-1 標籤。

處理 ABI 破損

例如,以下補丁引入了非常明顯的 ABI 破壞:

diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index 42786e6364ef..e15f1d0f137b 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -657,6 +657,7 @@ struct mm_struct {
                ANDROID_KABI_RESERVE(1);
        } __randomize_layout;
 
+       int tickle_count;
        /*
         * The mm_cpumask needs to be at the end of mm_struct, because it
         * is dynamically sized based on nr_cpu_ids.

當您套用此補丁來執行建置 ABI 時,工具會退出並顯示非零錯誤程式碼,並報告類似下列內容的 ABI 差異:

function symbol 'struct block_device* I_BDEV(struct inode*)' changed
  CRC changed from 0x8d400dbd to 0xabfc92ad

function symbol 'void* PDE_DATA(const struct inode*)' changed
  CRC changed from 0xc3c38b5c to 0x7ad96c0d

function symbol 'void __ClearPageMovable(struct page*)' changed
  CRC changed from 0xf489e5e8 to 0x92bd005e

... 4492 omitted; 4495 symbols have only CRC changes

type 'struct mm_struct' changed
  byte size changed from 992 to 1000
  member 'int tickle_count' was added
  member 'unsigned long cpu_bitmap[0]' changed
    offset changed by 64

建置時偵測到的 ABI 差異

出現錯誤的最常見原因是驅動程式使用了核心中不在任何符號列表中的新符號。

如果符號未包含在符號清單 ( android/abi_gki_aarch64 ) 中,則您需要先驗證它是否使用EXPORT_SYMBOL_GPL( symbol_name )匯出,然後更新 ABI XML 表示形式和符號清單。例如,下列變更將新的增量 FS 功能新增至android-12-5.10分支,其中包括更新符號清單和 ABI XML 表示。

如果符號已匯出(由您匯出或先前已匯出),但沒有其他驅動程式使用它,您可能會收到類似於以下內容的建置錯誤。

Comparing the KMI and the symbol lists:
+ build/abi/compare_to_symbol_list out/$BRANCH/common/Module.symvers out/$BRANCH/common/abi_symbollist.raw
ERROR: Differences between ksymtab and symbol list detected!
Symbols missing from ksymtab:
Symbols missing from symbol list:
 - simple_strtoull

若要解決此問題,請更新核心和 ACK 中的 KMI 符號清單(請參閱更新 ABI 表示)。有關更新 ACK 中的 ABI XML 和符號清單的範例,請參閱aosp/1367601

解決核心 ABI 損壞問題

您可以透過重構程式碼以不更改 ABI更新 ABI 表示來處理內核 ABI 損壞。使用下表來確定適合您情況的最佳方法。

ABI 破損流程圖

圖 1. ABI 破損解決方案

重構程式碼以避免 ABI 更改

盡一切努力避免修改現有的 ABI。在許多情況下,您可以重構程式碼以刪除影響 ABI 的變更。

  • 重構結構字段更改。如果變更修改了偵錯功能的 ABI,請在欄位周圍新增#ifdef (在結構和來源參考中),並確保在生產 defconfig 和gki_defconfig中停用用於#ifdefCONFIG 。有關如何在不破壞 ABI 的情況下將偵錯配置新增至結構的範例,請參閱此補丁集

  • 重構功能以不改變核心內核。如果需要為 ACK 新增功能以支援合作夥伴模組,請嘗試重構變更的 ABI 部分,以避免修改核心 ABI。有關使用現有核心 ABI 新增附加功能而不更改核心 ABI 的範例,請參閱aosp/1312213

修復 Android Gerrit 上損壞的 ABI

如果您不是故意破壞內核 ABI,那麼您需要使用 ABI 監視工具提供的指導進行調查。損壞的最常見原因是資料結構變更和相關符號 CRC 更改,或由於導致上述任何情況的配置選項更改。首先解決該工具發現的問題。

您可以在本機上重現 ABI 結果,請參閱建置核心及其 ABI 表示形式

關於 Lint-1 標籤

如果您將變更上傳到包含凍結或最終確定的 KMI 的分支,則變更必須透過AbiAnalyzer以確保變更不會以不相容的方式影響穩定的 ABI。在此過程中, AbiAnalyzer會尋找在建置期間建立的 ABI 報表(執行正常建置的擴充構建,然後執行一些 ABI 擷取和比較步驟。

如果AbiAnalyzer發現非空報告,它會設定 Lint-1 標籤,並且變更將被阻止提交,直到解決為止;直到補丁集收到 Lint+1 標籤。

更新核心 ABI

如果修改 ABI 不可避免,那麼您必須將程式碼變更、ABI 表示和符號清單套用到 ACK。若要讓 Lint 刪除 -1 且不破壞 GKI 相容性,請依照下列步驟操作:

  1. 將程式碼變更上傳到 ACK

  2. 等待收到補丁集的程式碼審查+2。

  3. 更新參考 ABI 表示

  4. 合併您的程式碼變更和 ABI 更新變更。

將 ABI 程式碼變更上傳到 ACK

更新 ACK ABI 取決於所做變更的類型。

  • 如果 ABI 變更與影響 CTS 或 VTS 測試的功能相關,則通常可以按原樣選擇該變更以進行 ACK。例如:

  • 如果 ABI 變更是可與 ACK 共用的功能,則可以按原樣選擇該變更以用於 ACK。例如,CTS 或 VTS 測試不需要以下更改,但可以與 ACK 共用:

  • 如果 ABI 變更引入了不需要包含在 ACK 中的新功能,您可以使用存根將符號引入 ACK,如下節所述。

使用存根進行 ACK

只有在不利於 ACK 的核心核心變更(例如效能和功耗變更),才需要存根。以下列出了 GKI ACK 中存根和部分精選的詳細範例。

  • 核心隔離功能存根 ( aosp/1284493 )。 ACK 中的功能不是必需的,但符號需要出現在 ACK 中,以便您的模組使用這些符號。

  • 供應商模組的佔位符號 ( aosp/1288860 )。

  • 僅 ABI 精選的每進程mm事件追蹤功能 ( aosp/1288454 )。原始補丁被精心挑選為 ACK,然後進行修剪以僅包含解決task_structmm_event_count的 ABI 差異所需的變更。此補丁還更新了mm_event_type枚舉以包含最終成員。

  • 部分挑選熱結構 ABI 更改,需要的不僅僅是添加新的 ABI 欄位。

    • 補丁aosp/1255544解決了合作夥伴核心和 ACK 之間的 ABI 差異。

    • 補丁aosp/1291018修正了上一個補丁的 GKI 測試期間發現的功能問題。此修復包括初始化感測器參數結構以將多個熱區域註冊到單一感測器。

  • CONFIG_NL80211_TESTMODE ABI 變更 ( aosp/1344321 )。此修補程式添加了 ABI 所需的結構更改,並確保附加欄位不會導致功能差異,使合作夥伴能夠將CONFIG_NL80211_TESTMODE包含在其生產核心中,並且仍然保持 GKI 合規性。

在運行時強制執行 KMI

GKI 核心使用TRIM_UNUSED_KSYMS=yUNUSED_KSYMS_WHITELIST=<union of all symbol lists>設定選項,這些選項將匯出的符號(例如使用EXPORT_SYMBOL_GPL()匯出的符號)限制為符號清單中列出的符號。所有其他符號均未導出,並且拒絕載入需要未導出符號的模組。此限制在建置時強制執行,並標記缺失的條目。

出於開發目的,您可以使用不包含符號修剪的 GKI 核心建構(這意味著可以使用所有通常導出的符號)。要找到這些版本,請在ci.android.com上尋找kernel_debug_aarch64版本。

使用模組版本控制強制執行 KMI

通用核心映像 (GKI) 核心使用模組版本控制( CONFIG_MODVERSIONS ) 作為在運行時強制執行 KMI 合規性的附加措施。如果模組的預期 KMI 與vmlinux KMI 不匹配,則模組版本控制可能會導致模組載入時出現循環冗餘校驗 (CRC) 不匹配失敗。例如,以下是由於符號module_layout()的 CRC 不匹配而在模組載入時發生的典型故障:

init: Loading module /lib/modules/kernel/.../XXX.ko with args ""
XXX: disagrees about version of symbol module_layout
init: Failed to insmod '/lib/modules/kernel/.../XXX.ko' with args ''

模組版本控制的用途

模組版本控制很有用,原因如下:

  • 模組版本控制捕獲資料結構可見性的變化。如果模組更改不透明的資料結構,即不屬於 KMI 一部分的資料結構,則它們會在將來對該結構進行更改後中斷。

    作為範例,請考慮struct device中的fwnode欄位。此欄位必須對模組不透明,以便模組無法變更device->fw_node的欄位或對其大小做出假設。

    但是,如果模組包含<linux/fwnode.h> (直接或間接),則struct device中的fwnode欄位對其不再是不透明的。然後,該模組可以對device->fwnode->devdevice->fwnode->ops進行更改。這種情況有問題的原因如下:

    • 它可以打破核心內核程式碼對其內部資料結構所做的假設。

    • 如果未來的核心更新更改了struct fwnode_handlefwnode的資料類型),則該模組將不再適用於新核心。此外, stgdiff不會顯示任何差異,因為該模組透過直接操作內部資料結構來破壞 KMI,而僅透過檢查二進位表示無法擷取這些方式。

  • 當當前模組稍後由不相容的新核心載入時,該模組將被視為 KMI 不相容。模組版本控制添加了運行時檢查,以避免意外載入與核心不相容 KMI 的模組。此檢查可防止因 KMI 中未偵測到的不相容性而可能導致難以偵錯的執行時間問題和核心崩潰。

啟用模組版本控制可以防止所有這些問題。

無需啟動設備即可檢查 CRC 是否不匹配

stgdiff比較並報告內核之間的 CRC 不匹配以及其他 ABI 差異。

此外,啟用CONFIG_MODVERSIONS的完整核心建置會產生Module.symvers文件,作為正常建置流程的一部分。該檔案對於內核 ( vmlinux ) 和模組導出的每個符號都有一行。每行包含 CRC 值、符號名稱、符號命名空間、匯出符號的vmlinux或模組名稱以及匯出類型(例如EXPORT_SYMBOLEXPORT_SYMBOL_GPL )。

您可以比較 GKI 版本和您的版本之間的Module.symvers文件,以檢查vmlinux匯出的符號中是否存在 CRC 差異。如果vmlinux導出的任何符號中存在 CRC 值差異,且該符號由您在裝置中載入的模組之一使用,則該模組不會載入。

如果您沒有所有建置工件,但有 GKI 核心和您的核心的vmlinux文件,則可以透過在兩個核心上執行以下命令並比較輸出來比較特定符號的 CRC 值:

nm <path to vmlinux>/vmlinux | grep __crc_<symbol name>

例如,以下指令檢查module_layout符號的 CRC 值:

nm vmlinux | grep __crc_module_layout
0000000008663742 A __crc_module_layout

解決 CRC 不符問題

使用下列步驟解決載入模組時的 CRC 不符問題:

  1. 使用--kbuild_symtypes選項來建立 GKI 內核和您的裝置內核,如下列指令所示:

    tools/bazel run --kbuild_symtypes //common:kernel_aarch64_dist
    

    此指令為每個.o檔案產生一個.symtypes檔。有關詳細信息,請參閱Kleaf 中的KBUILD_SYMTYPES

    對於 Android 13 及更低版本,透過在用於構建內核的命令前面添加KBUILD_SYMTYPES=1來構建 GKI 內核和設備內核,如下列命令所示:

    KBUILD_SYMTYPES=1 BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh
    

    使用build_abi.sh,已隱含設定KBUILD_SYMTYPES=1標誌。

  2. 使用以下命令找到導出了 CRC 不匹配的符號的.c檔案:

    cd common && git grep EXPORT_SYMBOL.*module_layout
    kernel/module.c:EXPORT_SYMBOL(module_layout);
    
  3. .c文件在 GKI 中具有相應的.symtypes文件,以及您的設備內核建置工件。使用以下指令找到.c檔:

    cd out/$BRANCH/common && ls -1 kernel/module.*
    kernel/module.o
    kernel/module.o.symversions
    kernel/module.symtypes
    

    以下是.c檔的特徵:

    • .c檔案的格式是每個符號一行(可能很長)。

    • 行開頭的[s|u|e|etc]#表示符號的資料型別為[struct|union|enum|etc] 。例如:

      t#bool typedef _Bool bool
      
    • 行首缺少#前綴表示該符號是一個函數。例如:

      find_module s#module * find_module ( const char * )
      
  4. 比較兩個檔案並修復所有差異。

案例 1:由於資料類型可見性而導致的差異

如果一個核心保留對模組不透明的符號或資料類型,而另一個核心則不然,則兩個核心的.symtypes檔案之間就會出現這種差異。其中一個核心中的.symtypes檔案的符號UNKNOWN ,而另一個核心中的.symtypes檔案具有符號或資料類型的擴展視圖。

例如,將下列行新增至核心中的include/linux/device.h檔案會導致 CRC 不匹配,其中之一是module_layout()

 #include <linux/fwnode.h>

比較該符號的module.symtypes ,發現以下差異:

 $ diff -u <GKI>/kernel/module.symtypes <your kernel>/kernel/module.symtypes
  --- <GKI>/kernel/module.symtypes
  +++ <your kernel>/kernel/module.symtypes
  @@ -334,12 +334,15 @@
  ...
  -s#fwnode_handle struct fwnode_handle { UNKNOWN }
  +s#fwnode_reference_args struct fwnode_reference_args { s#fwnode_handle * fwnode ; unsigned int nargs ; t#u64 args [ 8 ] ; }
  ...

如果您的核心的值為UNKNOWN且 GKI 核心具有符號的擴充視圖(極不可能),則將最新的 Android Common Kernel 合併到您的核心中,以便您使用最新的 GKI 核心程式庫。

在大多數情況下,GKI 核心的值為UNKNOWN ,但由於對核心所做的更改,您的核心具有符號的內部詳細資訊。這是因為核心中的某個檔案加入了 GKI 核心中不存在的#include

通常,修復方法就像從genksyms隱藏新的#include一樣簡單。

#ifndef __GENKSYMS__
#include <linux/fwnode.h>
#endif

否則,要識別導致差異的#include ,請按照下列步驟操作:

  1. 開啟定義具有此差異的符號或資料類型的頭檔。例如,編輯include/linux/fwnode.h中的struct fwnode_handle

  2. 在頭文件頂部添加以下程式碼:

    #ifdef CRC_CATCH
    #error "Included from here"
    #endif
    
  3. 在存在 CRC 不符的模組的.c檔案中,將下列內容新增為任何#include行之前的第一行。

    #define CRC_CATCH 1
    
  4. 編譯你的模組。產生的建置時錯誤顯示了導致 CRC 不匹配的頭檔#include鏈。例如:

    In file included from .../drivers/clk/XXX.c:16:`
    In file included from .../include/linux/of_device.h:5:
    In file included from .../include/linux/cpu.h:17:
    In file included from .../include/linux/node.h:18:
    .../include/linux/device.h:16:2: error: "Included from here"
    #error "Included from here"
    

    #include鏈中的一個連結是由於核心中所做的更改造成的,而 GKI 核心中缺少該更改。

  5. 識別更改,將其恢復到核心中或將其上傳到 ACK 並進行合併

案例2:資料類型變化造成的差異

如果符號或資料類型的 CRC 不匹配不是由於可見性差異造成的,那麼就是由於資料類型本身的實際變更(新增、刪除或變更)造成的。

例如,在核心中進行以下變更會導致多個 CRC 不匹配,因為許多符號間接受到此類變更的影響:

diff --git a/include/linux/iommu.h b/include/linux/iommu.h
  --- a/include/linux/iommu.h
  +++ b/include/linux/iommu.h
  @@ -259,7 +259,7 @@ struct iommu_ops {
     void (*iotlb_sync)(struct iommu_domain *domain);
     phys_addr_t (*iova_to_phys)(struct iommu_domain *domain, dma_addr_t iova);
     phys_addr_t (*iova_to_phys_hard)(struct iommu_domain *domain,
  -        dma_addr_t iova);
  +        dma_addr_t iova, unsigned long trans_flag);
     int (*add_device)(struct device *dev);
     void (*remove_device)(struct device *dev);
     struct iommu_group *(*device_group)(struct device *dev);

devm_of_platform_populate()存在一種 CRC 不符。

如果您比較該符號的.symtypes文件,它可能如下所示:

 $ diff -u <GKI>/drivers/of/platform.symtypes <your kernel>/drivers/of/platform.symtypes
  --- <GKI>/drivers/of/platform.symtypes
  +++ <your kernel>/drivers/of/platform.symtypes
  @@ -399,7 +399,7 @@
  ...
  -s#iommu_ops struct iommu_ops { ... ; t#phy
  s_addr_t ( * iova_to_phys_hard ) ( s#iommu_domain * , t#dma_addr_t ) ; int
    ( * add_device ) ( s#device * ) ; ...
  +s#iommu_ops struct iommu_ops { ... ; t#phy
  s_addr_t ( * iova_to_phys_hard ) ( s#iommu_domain * , t#dma_addr_t , unsigned long ) ; int ( * add_device ) ( s#device * ) ; ...

若要識別變更的類型,請執行下列步驟:

  1. 在原始程式碼中尋找符號的定義(通常在.h檔案中)。

    • 對於您的核心和 GKI 核心之間的簡單符號差異,請透過執行以下命令來尋找提交:
    git blame
    
    • 對於已刪除的符號(其中一個符號在樹中被刪除,並且您還想在另一棵樹中刪除它),您需要找到刪除該行的變更。在刪除該行的樹上使用以下命令:
    git log -S "copy paste of deleted line/word" -- <file where it was deleted>
    
  2. 查看傳回的提交清單以找到變更或刪除。第一個提交可能就是您正在搜尋的提交。如果不是,請瀏覽列表,直到找到提交。

  3. 識別更改後,要么在核心中恢復它,要么將其上傳到 ACK 並合併