先看結論與判斷條件
- 先記錄每個 SO 的直接和傳遞依賴、裝載入口、初始化時機、目標 API 下限與呼叫方。
- 靜態 JNI 名稱發現和 RegisterNatives 動態註冊的約束不同,名稱處理前必須確認真實繫結方式。
- arm64-v8a、armeabi-v7a 與 x86_64 是不同產物和相容矩陣,一個架構透過不能外推到另一個架構。
- 保護強度驗收和執行穩定性驗收必須繫結同一最終 APK、同一簽名與同一 SO 集合。
先畫出 ELF 依賴圖和真實裝載時間線
一個 Android 應用可能由主庫、業務庫、演算法庫、第三方 SDK 和系統庫共同組成。Java 或 Kotlin 程式碼可以直接呼叫 System.loadLibrary,主庫也可以繼續透過 ELF 依賴或執行時裝載其他庫。最終崩潰點可能出現在被保護庫之外,因為符號解析和初始化順序由整條依賴鏈共同決定。
依賴圖應分別記錄每個 ABI 的庫檔案、DT_NEEDED 關係、最低系統 API、裝載入口、初始化函式、匯出介面和呼叫方。還要標記庫來自自研、開源依賴還是閉源 SDK,因為能夠修改和迴歸的責任邊界不同。
Android NDK 文件指出,原生符號通常在庫裝載時解析。程式碼引用了目標系統不存在的 API,即使執行時分支看似不會執行,也可能在 dlopen 階段直接失敗。因此 NDK API 下限和裝置系統版本必須進入依賴圖。
| 欄位 | 要記錄什麼 | 為什麼影響加固 | 驗證方式 |
|---|---|---|---|
| 庫與來源 | 主庫、傳遞依賴、第三方和系統庫 | 決定可修改範圍和迴歸責任 | 按最終 APK 的每個 ABI 解包核對 |
| 裝載入口 | 靜態依賴、System.loadLibrary 或執行時 dlopen | 決定最早失敗階段和日誌位置 | 記錄啟動時間線與裝載結果 |
| API 下限 | 構建 APP_PLATFORM 與使用的系統符號 | 高於裝置 API 可能在裝載時缺符號 | 目標系統真機啟動和符號核對 |
| 初始化順序 | JNI_OnLoad、建構函式與業務初始化 | 程式碼佈局或時序變化可能放大隱式依賴 | 比較未保護與保護候選時間線 |
| 異常與執行緒 | C++ 異常、執行緒歸屬、JNIEnv 使用 | 跨庫和跨執行緒錯誤常直接崩潰 | CheckJNI、符號化棧和業務迴歸 |
靜態註冊和動態註冊決定符號處理邊界
JNI 可以透過名稱約定發現 Native 方法,也可以在初始化階段使用 RegisterNatives 把 Java 方法與函式地址建立對映。靜態註冊依賴可預測的匯出名稱;動態註冊通常只需要暴露少量初始化入口,但依賴類查詢、方法簽名、註冊時機和載入執行緒。
如果加固或符號處理改變了靜態註冊名稱,系統可能找不到 Native 實現。如果動態登錄檔中的方法簽名、類名保留規則或初始化順序變化,也可能在首次呼叫前就失敗。僅檢查匯出符號數量,不能證明 JNI 繫結仍然正確。
Android 官方 JNI 指南還提醒 JNIEnv 具有執行緒約束,pending exception、錯誤引用和跨執行緒使用都可能導致崩潰。保護後的迴歸應覆蓋真實執行緒和異常路徑,而不是隻呼叫一個無引數的演示方法。
| 註冊方式 | 依賴 | 加固敏感點 | 最小驗證 |
|---|---|---|---|
| 靜態名稱發現 | Java 類名、方法名、簽名和匯出符號 | 重新命名、隱藏符號和簽名不匹配 | 逐個關鍵入口呼叫並檢查連結錯誤 |
| RegisterNatives | 類查詢、方法簽名、登錄檔和初始化時機 | 類名處理、註冊順序、函式地址變化 | 確認註冊完成並執行真實輸入輸出 |
| 第三方封裝 | SDK 內部規則和閉源實現 | 自校驗、隱式匯出和版本差異 | 按供應商支援矩陣和真實場景驗證 |
- 列出關鍵 Java 到 Native 對映
- 確認靜態或動態註冊方式
- 保留必要類名和簽名規則
- 驗證異常、執行緒和引用生命週期
ABI 必須按獨立釋出產物驗收
ABI 不只是目錄標籤。Android NDK 官方說明 ABI 約定了指令集、位元組序、呼叫約定、棧和暫存器使用、可執行檔案格式及 C++ 名稱改編。arm64-v8a 與 armeabi-v7a 的機器碼和依賴不同,x86_64 模擬器透過更不能代表 ARM 真機。
每個 ABI 都要確認所有直接和傳遞依賴是否存在,庫的 API 下限是否相容目標系統,打包工具是否誤刪某個架構的檔案。只有主庫包含 arm64-v8a,而第三方依賴缺少對應產物時,應用仍會在裝載階段失敗。
如果產品計劃只支援部分 ABI,應在釋出記錄、應用商店配置和測試範圍中明確寫出。未構建、未安裝或未執行關鍵 JNI 路徑的架構必須標記未覆蓋,不能用編譯透過替代執行證據。
| 檢查項 | arm64-v8a | armeabi-v7a | x86_64 | 判定規則 |
|---|---|---|---|---|
| 所有依賴存在 | 逐庫核對 | 按釋出範圍核對 | 僅在需要時核對 | 任一必需依賴缺失即阻斷 |
| 最低系統可裝載 | 目標 API 真機 | 目標 API 真機 | 模擬器或裝置 | 不得只在最新系統驗證 |
| JNI 關鍵路徑 | 真實輸入輸出 | 真實輸入輸出 | 不得替代 ARM 結果 | 每個釋出 ABI 獨立記錄 |
| 崩潰可歸因 | 保留匹配符號 | 保留匹配符號 | 保留匹配符號 | 符號必須對應同一候選包 |
保護動作會觸碰哪些 Native 執行條件
符號隱藏減少靜態線索,字串處理降低明文暴露,控制流或函式虛擬化改變選中程式碼的執行形式。它們對匯出符號、函式邊界、異常展開、間接呼叫、對齊和效能的影響不同。不能用處理後符號更少這一項代表完整 Native 保護,也不能用庫仍能被讀取直接否定全部保護效果。
啟動初始化、訊號處理、C++ 異常、回撥、執行緒區域性狀態、反射式符號查詢和第三方 SDK 自校驗屬於高敏感區域。選擇範圍時,應優先從輸入輸出明確、呼叫頻率可測、失敗能隔離的業務函式開始,避免第一次 PoC 就處理整條初始化鏈。
保護配置應對每個函式或函式組記錄選擇原因、呼叫階段、ABI、依賴、效能預算和回滾組。下面只展示公開安全的驗證清單形態,不包含內部符號或產品配置語法。
native_group: protocol-core-v1
abis: [arm64-v8a, armeabi-v7a]
load_phase: post-authentication
jni_registration: dynamic
execution:
frequency: bounded
main_thread: false
dependencies:
- business-runtime
- system-crypto
acceptance:
- dependency-resolution
- jni-registration
- exception-path
- output-parity
- latency-budget
rollback: native-group-v0用最早差異定位 SO 加固後的閃退
排查時先固定未保護基線和保護候選包的檔案身份、簽名、裝置、系統、安裝方式與業務輸入,然後按時間線尋找最早差異:程序建立、Application、庫裝載、JNI_OnLoad、註冊完成、首次 Native 呼叫、關鍵業務返回。最後一條崩潰日誌可能只是連鎖反應。
UnsatisfiedLinkError 通常指向庫缺失、ABI 不匹配、依賴或符號解析問題;註冊錯誤可能表現為找不到 Native 方法;錯誤的 JNIEnv、引用或 pending exception 可能在業務呼叫中崩潰。應保留與候選包匹配的 Native 符號用於內部歸因,但公開報告不暴露真實符號和地址。
CheckJNI 可以幫助發現部分 JNI 誤用,但它是診斷工具,不等於生產執行狀態,也不能覆蓋所有 Native 記憶體和併發問題。修復後必須回到目標釋出配置重跑關鍵路徑。
| 最早現象 | 優先檢查 | 需要的證據 | 不要先做什麼 |
|---|---|---|---|
| dlopen 失敗 | ABI、DT_NEEDED、API 下限和符號 | 同候選包的庫清單與裝載錯誤 | 反覆切換無關保護開關 |
| 找不到 Native 方法 | 註冊方式、類名、方法簽名和時機 | 登錄檔、保留規則和呼叫入口 | 只看匯出符號數量 |
| 首次呼叫崩潰 | 引數、引用、執行緒、異常和函式處理 | 符號化棧、輸入和基線對照 | 用另一版本符號檔案解釋 |
| 部分裝置失敗 | 系統 API、廠商差異、ABI 和第三方庫 | 裝置系統矩陣與最早差異 | 用模擬器透過替代真機結論 |
釋出結論要同時覆蓋強度、相容和可維護性
靜態強度觀察可以記錄符號、字串、程式碼結構和關鍵入口暴露面的變化;執行驗收則驗證裝載、JNI、關鍵業務、異常和目標 ABI。兩類證據互相補充,不能用其中一類替代另一類。
最終候選包還要完成安裝升級、簽名、渠道、啟動、崩潰監控、Native 符號歸檔和回滾演練。符號歸檔必須能用檔案身份找到匹配版本,否則線上 Native 崩潰無法正確符號化。
本文提供的是工程檢查框架,不是某個 SO 或某個保護配置的測試結論。沒有目標包、目標 ABI 和真實呼叫路徑時,只能判斷準備工作是否完整,不能承諾相容性或效能。
- 每個釋出 ABI 都有執行證據
- 關鍵 JNI 入口覆蓋正常和異常路徑
- Native 符號與最終候選身份匹配
- 裝載、業務和回滾結果可複核
- 未覆蓋的裝置與系統被明確限制
事實依據與適用邊界
以下內容區分官方事實、本文工程判斷和不能外推的範圍,避免把設計建議寫成未經驗證的產品結論。
| 本文判斷 | 事實或工程依據 | 適用限制 |
|---|---|---|
| ABI 必須作為獨立產物維度驗收。 | Android NDK 官方定義 ABI 覆蓋指令集、呼叫約定、暫存器、棧、ELF 格式和名稱改編。 | 文件定義不證明應用已經包含完整依賴或在目標裝置執行成功。 |
| 高於裝置系統的 Native API 引用可能在裝載時失敗。 | NDK 常見問題文件說明符號通常在庫裝載時解析,引用不存在的 API 不能依靠執行時分支規避。 | 具體失敗仍需結合構建引數、依賴和裝置日誌確認。 |
| JNI 錯誤需要執行緒、引用和異常級別的驗證。 | Android JNI 指南列出錯誤 JNIEnv、引用、pending exception 和方法註冊等可導致崩潰的問題。 | CheckJNI 只輔助發現一部分問題,不能替代完整業務和記憶體安全測試。 |
| 單一 x86_64 模擬器結果不能外推 ARM 真機。 | 不同 ABI 使用不同指令集和呼叫約定,最終庫和依賴組合也可能不同。 | 如果產品明確不釋出某個 ABI,可以將其標記不適用而不是強行測試。 |
| 保護強度與相容性應在同一候選包上閉合。 | 重新構建或替換 SO 會改變程式碼、符號、依賴和可能的執行行為。 | 這是產物治理原則,不代表任何特定保護級別已經透過。 |
工程常見問題
只有一個主 SO,需要畫依賴圖嗎?
需要。主 SO 仍可能依賴系統庫、C++ 執行時或第三方庫,並透過 JNI 與 Java 層互動。依賴圖用於確認最早裝載點和責任邊界。
arm64-v8a 透過後還要測 armeabi-v7a 嗎?
如果釋出包包含並支援 armeabi-v7a,就需要獨立驗證。兩個 ABI 的指令、呼叫約定和依賴產物不同,不能互相替代。
隱藏所有匯出符號是否更安全?
不一定。必要的靜態 JNI 入口、第三方呼叫和系統約定可能依賴匯出符號。應先確認註冊和呼叫方式,再最小化暴露。
CheckJNI 沒有報錯是否可以釋出?
不能單獨據此釋出。還要驗證實際目標配置、業務輸入、異常路徑、ABI、系統範圍、安裝升級和回滾。
SO 加固後崩潰應該先關閉全部保護嗎?
先固定候選身份並找最早差異。可以按保護組二分或回退,但每次只改變一個可記錄變數,避免得到無法歸因的新產物。