這個(gè)示例在主界面底部新增了一個(gè) JNI Crash 按鈕。點(diǎn)擊后 Kotlin 調(diào)用
NativeCrash.crashFromJni(),JNI 進(jìn)入 native-crash.cpp,最終在
crashOnPurpose() 中寫空指針并觸發(fā) SIGSEGV。
關(guān)鍵代碼位置:
- Kotlin 入口:
app/src/main/java/com/mobilefiction/svgexample/NativeCrash.kt - JNI 實(shí)現(xiàn):
app/src/main/cpp/native-crash.cpp - CMake 配置:
app/src/main/cpp/CMakeLists.txt - Gradle 接入:
app/build.gradle.kts
1. 構(gòu)建 Debug 包
.\gradlew.bat :app:assembleDebug
Debug 包會(huì)保留未剝離的 native so,后面定位時(shí)優(yōu)先使用這個(gè)目錄里的文件:
app/build/intermediates/cxx/Debug/<hash>/obj/<abi>/libnative-crash.so
不要用 APK 中解出來的 so 或 merged_native_libs 中的 so 做精確行號(hào)定位,它們可能已經(jīng)被 strip。
2. 安裝并觸發(fā)崩潰
adb install -r .\app\build\outputs\apk\debug\app-debug.apk
adb logcat -c
adb shell am start -n com.mobilefiction.svgexample/.MainActivity
打開另一個(gè)終端開始收集日志:
adb logcat -v threadtime > crash.log
回到手機(jī)或模擬器點(diǎn)擊底部的 JNI Crash 按鈕。應(yīng)用閃退后,停止 logcat。
3. 從日志里找到 native backtrace
在 crash.log 中搜索 libnative-crash.so、SIGSEGV 或 backtrace。你會(huì)看到類似:
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)
backtrace:
#00 pc 000000000000xxxx .../libnative-crash.so (crashOnPurpose()+...)
#01 pc 000000000000yyyy .../libnative-crash.so (Java_com_mobilefiction_svgexample_NativeCrash_crashFromJni+...)
記下 #00 行的 pc 地址,例如 000000000000xxxx。Android tombstone 中這類
pc 通??梢灾苯幼鳛?so 內(nèi)偏移傳給 addr2line。
4. 找到 ABI 與未剝離 so
$abi = (adb shell getprop ro.product.cpu.abi).Trim()
$so = Get-ChildItem .\app\build\intermediates\cxx\Debug -Recurse -Filter libnative-crash.so |
Where-Object { $_.FullName -like "*\obj\$abi\*" } |
Select-Object -First 1
$so.FullName
如果設(shè)備是 arm64-v8a,$so.FullName 應(yīng)該指向類似:
D:\project\ComposeSVGExample\app\build\intermediates\cxx\Debug\<hash>\obj\arm64-v8a\libnative-crash.so
5. 用 addr2line 定位源文件行
先找到 NDK:
$sdk = (Get-Content .\local.properties | Where-Object { $_ -like "sdk.dir=*" }).Substring(8).Replace("\:", ":").Replace("\\", "\")
$ndk = (Get-ChildItem "$sdk\ndk" | Sort-Object Name -Descending | Select-Object -First 1).FullName
$addr2line = "$ndk\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-addr2line.exe"
把第 3 步拿到的 pc 地址傳給 llvm-addr2line:
& $addr2line -C -f -e $so.FullName 000000000000xxxx
預(yù)期輸出會(huì)落到 JNI 崩潰函數(shù)附近:
crashOnPurpose()
D:/project/ComposeSVGExample/app/src/main/cpp/native-crash.cpp:10
其中第 10 行就是:
*nullPointer = 42;
6. 用符號(hào)表粗定位函數(shù)
如果只有 symbol table,通常只能定位到函數(shù)名和函數(shù)內(nèi)偏移,不能穩(wěn)定給出源文件行??梢杂?br>
llvm-nm 或 llvm-readelf 看符號(hào)地址:
$nm = "$ndk\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-nm.exe"
& $nm -n -C $so.FullName | Select-String "crashOnPurpose|NativeCrash"
示例輸出形態(tài):
000000000000aaaa t crashOnPurpose()
000000000000bbbb T Java_com_mobilefiction_svgexample_NativeCrash_crashFromJni
把 tombstone 的 pc 地址和符號(hào)地址對(duì)比:
- 如果
pc落在crashOnPurpose()起始地址之后、下一個(gè)函數(shù)地址之前,崩潰就在這個(gè)函數(shù)內(nèi)。 -
pc - crashOnPurpose 起始地址就是函數(shù)內(nèi)偏移;tombstone 有時(shí)也會(huì)直接顯示成crashOnPurpose()+偏移。 - 若要精確到
native-crash.cpp:10,使用未剝離 so 加llvm-addr2line。
7. 用 ndk-stack 一次性符號(hào)化
ndk-stack 會(huì)讀取 logcat 中的 backtrace,并用 -sym 指向的未剝離 so 目錄批量符號(hào)化:
$symDir = $so.Directory.FullName
& "$ndk\ndk-stack.cmd" -sym $symDir -dump .\crash.log
如果符號(hào)匹配,輸出中會(huì)顯示 crashOnPurpose() 和
app/src/main/cpp/native-crash.cpp:10。
8. Release 符號(hào)說明
app/build.gradle.kts 里已經(jīng)給 debug 和 release 配置了:
ndk {
debugSymbolLevel = "FULL"
}
FULL 能保留文件名和行號(hào)信息,適合配合 addr2line 精確定位。若只配置
SYMBOL_TABLE,通常只能還原函數(shù)名,適合確認(rèn)崩潰大致落在哪個(gè) native 函數(shù)。
這段 JNI 崩潰代碼只用于演示 native crash 定位,請(qǐng)不要保留在生產(chǎn)版本中。