Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x72a2ab8f40 in tid 20224...
我們收到一個(gè)來自于Android11 的crash error??注盈,很少做NDK相關(guān)自晰,看得我一臉懵灶平。
11-17 22:25:55.604 10114 14417 20224 F libc : Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x72a2ab8f40 in tid 20224 (...), pid 14417 (roid.app.camera)
11-17 22:26:11.921 10114 20280 20280 F DEBUG : backtrace:
11-17 22:26:11.921 10114 20280 20280 F DEBUG : #00 pc 000000000036243c /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::GuardedCopy::Check(char const, void const, bool)+20) (BuildId: 4282922bc58d6d4ba18963d4940dba75)
11-17 22:26:11.921 10114 20280 20280 F DEBUG : #01 pc 0000000000364bd4 /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::GuardedCopy::ReleaseGuardedPACopy(char const, _JNIEnv, _jarray, void, int)+632) (BuildId: 4282922bc58d6d4ba18963d4940dba75)
11-17 22:26:11.921 10114 20280 20280 F DEBUG : #02 pc 000000000036438c /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::ReleasePrimitiveArrayElements(char const, art::Primitive::Type, _JNIEnv, _jarray, void, int)+712) (BuildId: 4282922bc58d6d4ba18963d4940dba75)
看trace是發(fā)生在了ReleasePrimitiveArrayElements砂客。Code 差不多是這樣的偿乖, 也就是ReleasePrimitiveArrayElements發(fā)現(xiàn)數(shù)組越界了僚焦。
{
// jbyteArray data
unsigned char *input = new unsigned char[len];
env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte *>(input));
... // actions
env->ReleaseByteArrayElements(data, reinterpret_cast<jbyte *>(input), 0);
}
當(dāng)然最開始我并不理解什么叫數(shù)組越界茶凳,本能先考慮的是择示,數(shù)組出問題了吧陨簇?是不是空的吐绵?
{
if (data == nullptr || input == nullptr) {
LOG("data == null or input == null");
return;
}
}
那顯然呢一定是完全沒用啊??
其實(shí)如果你故意把它設(shè)置為null的時(shí)候你就會(huì)發(fā)現(xiàn)空指針的報(bào)錯(cuò)是signal 6,而且會(huì)直白地告訴你non-nullable argument was NULL河绽。
A/DEBUG: signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
A/DEBUG: Abort message: 'JNI DETECTED ERROR IN APPLICATION: non-nullable argument was NULL
in call to ReleaseByteArrayElements
from boolean ...(byte[], double, double, double, double)'
A/DEBUG: backtrace:
A/DEBUG: #00 pc 00000000000832a8 /apex/com.android.runtime/lib64/bionic/libc.so (abort+160) (BuildId: b0750023d0cf44584c064da02400c159)
A/DEBUG: #01 pc 00000000004ba634 /apex/com.android.runtime/lib64/libart.so (art::Runtime::Abort(char const)+2388) (BuildId: c79d8488d870b3079640a498165bbfd0)
A/DEBUG: #02 pc 000000000000b458 /system/lib64/libbase.so (android::base::LogMessage::~LogMessage()+580) (BuildId: 36cd125456a5320dd3dcb8cfbd889a1a)
A/DEBUG: #03 pc 0000000000378c18 /apex/com.android.runtime/lib64/libart.so (art::JavaVMExt::JniAbort(char const, char const)+1584) (BuildId: c79d8488d870b3079640a498165bbfd0)
A/DEBUG: #04 pc 0000000000378e3c /apex/com.android.runtime/lib64/libart.so (art::JavaVMExt::JniAbortV(char const, char const, std::__va_list)+108) (BuildId: c79d8488d870b3079640a498165bbfd0)
A/DEBUG: #05 pc 000000000036b264 /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::AbortF(char const, ...)+136) (BuildId: c79d8488d870b3079640a498165bbfd0)
A/DEBUG: #06 pc 00000000003754a8 /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::CheckJNI::ReleasePrimitiveArrayElements(char const, art::Primitive::Type, _JNIEnv, _jarray, void, int)+1908) (BuildId: c79d8488d870b3079640a498165bbfd0)
所以排除了空指針的可能性
所以己单,好了,別猜了耙饰,來理解一下吧纹笼。
https://source.android.com/devices/tech/debug/native-crash
這個(gè)錯(cuò)誤叫:Execute-only memory violation (Android 10 only) 只執(zhí)行內(nèi)存違規(guī)
對于 Android 10 及更高版本中的 arm64,二進(jìn)制文件和庫的可執(zhí)行部分會(huì)映射到只執(zhí)行(不可讀裙豆颉)內(nèi)存廷痘,作為防范代碼重用攻擊的一種安全強(qiáng)化技術(shù)。通過在可執(zhí)行內(nèi)存中搜索已知函數(shù)件已,或通過在事先不了解內(nèi)存布局的情況下構(gòu)造小工具鏈笋额,可以使用讀取基元繞過地址空間布局隨機(jī)化 (ASLR)。通過將可執(zhí)行代碼標(biāo)記為不可讀篷扩,讀取基元將無法訪問可執(zhí)行內(nèi)存兄猩,從而使各種攻擊技術(shù)無法圖謀不軌。這不僅可以提高 ASLR 的有效性,而且可以提高控制流完整性 (CFI)枢冤,因?yàn)椴粫?huì)再向使用讀取基元的攻擊者透露有效目的地鸠姨。
將代碼設(shè)為不可讀會(huì)導(dǎo)致故意或意外讀入已標(biāo)記為只執(zhí)行的內(nèi)存段拋出SIGSEGV
,并且代碼為SEGV_ACCERR
淹真。這可能是因?yàn)殄e(cuò)誤讶迁、漏洞、混合了代碼的數(shù)據(jù)(例如文字池)或故意進(jìn)行的內(nèi)存自省導(dǎo)致的核蘸。
編譯器會(huì)假設(shè)代碼和數(shù)據(jù)不是混合在一起的巍糯,但是手寫程序集會(huì)導(dǎo)致出現(xiàn)問題。在許多情況下客扎,只需將常量移動(dòng)到.data
部分鳞贷,即可解決這些問題。如果可執(zhí)行代碼段絕對有必要進(jìn)行代碼內(nèi)省虐唠,則應(yīng)首先調(diào)用mprotect(2)
以將該代碼標(biāo)記為可讀,然后在操作完成后重新將該代碼標(biāo)記為不可讀惰聂。
Cause: execute-only (no-read) memory access error; likely due to data in .text.
您可以通過原因行將只執(zhí)行內(nèi)存違規(guī)與其他崩潰區(qū)分開來疆偿。
您可以使用 crasher xom 重現(xiàn)此類崩潰的實(shí)例。
我想你根本沒看懂吧搓幌?沒錯(cuò)因?yàn)槲易x了兩遍都覺得啥也沒懂杆故。
然后我接下來看見了這個(gè)
https://source.android.com/devices/tech/debug/execute-only-memory
本來好像看到了希望,但是它有個(gè)warning溉愁。說這個(gè)在Android 11上移除了处铛。
Important: XOM support has been removed in the upstream Linux kernel. XOM is only supported in Android 10 and has been removed in Android 11 and kernel changes removing it have been backported to 4.9, so the common kernel no longer supports XOM. More details on why XOM support was removed upstream can be found at PAN mitigation bypass
不出所料,即使disable execute-only at a module level 也完全不work拐揭。
// Android.mk
LOCAL_XOM := false
// Android.bp
cc_binary { // or other module types
...
xom: false,
}
由于一開始沒有可復(fù)現(xiàn)的設(shè)備撤蟆,所以大多數(shù)情況都是根據(jù)log來猜測。終于在周末我拿到了可復(fù)現(xiàn)的設(shè)備堂污。才發(fā)現(xiàn)這個(gè)問題最直接的復(fù)現(xiàn)方法是:
$adb root
$adb remount
$adb shell setenforce 0
——> 表示關(guān)閉selinux防火墻, 權(quán)限問題家肯。
$adb shell
In the adb shell,
# stop
# setprop dalvik.vm.checkjni true
# setprop dalvik.vm.jniopts forcecopy
# start
(Device is rebooted)
$ adb shell getprop | grep "dalvik.vm.checkjni"[dalvik.vm.checkjni]: [true] (Check true)
——>啟動(dòng)JNI檢查,調(diào)試用
$ adb shell getprop | grep "dalvik.vm.jniopts"[dalvik.vm.jniopts]: [forcecopy] (Check forcecopy)
——>檢查數(shù)組越界
打開了CheckJNI來檢查JNI error
https://android-developers.googleblog.com/2011/07/debugging-android-jni-with-checkjni.html
當(dāng)然最重要的能夠復(fù)現(xiàn)問題的點(diǎn)是dalvik.vm.jniopts盟猖。
如果是warnonly實(shí)際上你是get不到crash的讨衣。所以要改成forcecopy。
setprop dalvik.vm.jniopts warnonly
這個(gè)點(diǎn)就涉及到ART 垃圾回收(GC)了式镐。https://source.android.com/devices/tech/dalvik/gc-debug
ART 有多個(gè)不同的 GC 方案反镇,涉及運(yùn)行不同的垃圾回收器。從 Android 8 (Oreo) 開始娘汞,默認(rèn)方案是并發(fā)復(fù)制 (CC)歹茶。另一個(gè) GC 方案是并發(fā)標(biāo)記清除 (CMS)。
并發(fā)復(fù)制 (CC):
并發(fā)復(fù)制 GC 的一些主要特性包括:
- CC 支持使用名為“RegionTLAB”的觸碰指針分配器。此分配器可以向每個(gè)應(yīng)用線程分配一個(gè)線程本地分配緩沖區(qū) (TLAB)辆亏,這樣风秤,應(yīng)用線程只需觸碰“棧頂”指針,而無需任何同步操作扮叨,即可從其 TLAB 中將對象分配出去缤弦。
- CC 通過在不暫停應(yīng)用線程的情況下并發(fā)復(fù)制對象來執(zhí)行堆碎片整理。這是在讀取屏障的幫助下實(shí)現(xiàn)的彻磁,讀取屏障會(huì)攔截來自堆的引用讀取碍沐,無需應(yīng)用開發(fā)者進(jìn)行任何干預(yù)。
- GC 只有一次很短的暫停衷蜓,對于堆大小而言累提,該次暫停在時(shí)間上是一個(gè)常量。
- 在 Android 10 及更高版本中磁浇,CC 會(huì)擴(kuò)展為分代 GC斋陪。它支持輕松回收存留期較短的對象,這類對象通常很快便會(huì)無法訪問置吓。這有助于提高 GC 吞吐量无虚,并顯著延遲執(zhí)行全堆 GC 的需要。
這里有提到
使用 SIGQUIT 獲取 GC 性能信息
如需獲得應(yīng)用的 GC 性能時(shí)序衍锚,請將
SIGQUIT
發(fā)送到已在運(yùn)行的應(yīng)用友题,或者在啟動(dòng)命令行程序時(shí)將-XX:DumpGCPerformanceOnShutdown
傳遞給dalvikvm
。當(dāng)應(yīng)用獲得 ANR 請求信號(hào) (SIGQUIT
) 時(shí)戴质,會(huì)轉(zhuǎn)儲(chǔ)與其鎖定度宦、線程堆棧和 GC 性能相關(guān)的信息告匠。
如需獲得 GC 時(shí)序轉(zhuǎn)儲(chǔ),請使用以下命令:
adb shell kill -S QUIT PID
這會(huì)在/data/anr/
中創(chuàng)建一個(gè)文件(名稱中會(huì)包含日期和時(shí)間后专,例如 anr_2020-07-13-19-23-39-817)。此文件包含一些 ANR 轉(zhuǎn)儲(chǔ)信息以及 GC 時(shí)序漾稀。您可以通過搜索“Dumping cumulative Gc timings”(轉(zhuǎn)儲(chǔ)累計(jì) GC 時(shí)序)來確定 GC 時(shí)序。這些時(shí)序會(huì)顯示一些需要關(guān)注的內(nèi)容崭捍,包括每個(gè) GC 類型的階段和暫停時(shí)間的直方圖信息啰脚。暫停信息通常比較重要实夹。
例如(本示例中顯示平均暫停時(shí)間為 1.83 毫秒粒梦,該值應(yīng)該足夠低,在大多數(shù)應(yīng)用中不會(huì)導(dǎo)致丟幀):
young concurrent copying paused: Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms
掛起時(shí)間:
suspend all histogram: Sum: 1.513ms 99% C.I. 3us-546.560us Avg: 47.281us Max: 601us
總耗時(shí)和 GC 吞吐量:
Total time spent in GC: 502.251ms
Mean GC size throughput: 92MB/s
Mean GC object throughput: 1.54702e+06 objects/s
所以要查閱的話只要把/data/anr/anr_2020-07-13-19-23-39-81 pull出來就可以匀们。
分析GC正確性問題的工具
造成 ART 內(nèi)部崩潰的原因多種多樣缴淋。讀取或?qū)懭雽ο笞侄螘r(shí)發(fā)生崩潰可能表明堆損壞。如果 GC 在運(yùn)行時(shí)崩潰泄朴,也可能是由堆損壞造成的重抖。造成堆損壞的最常見原因是應(yīng)用代碼不正確。那CheckJNI就是用來調(diào)試的工具之一祖灰。
CheckJNI 是一種添加 JNI 檢查來驗(yàn)證應(yīng)用行為的模式钟沛;出于性能方面的原因,默認(rèn)情況下不啟用此類檢查局扶。此類檢查將捕獲一些可能會(huì)導(dǎo)致堆損壞的錯(cuò)誤恨统,如使用無效/過時(shí)的局部和全局引用。
adb shell setprop dalvik.vm.checkjni true
CheckJNI 的 forcecopy 模式對于檢測超出數(shù)組區(qū)域末端的寫入很有用三妈。啟用后延欠,forcecopy 會(huì)促使數(shù)組訪問 JNI 函數(shù)返回帶有紅色區(qū)域的副本。紅色區(qū)域是返回的指針末端/始端的一個(gè)區(qū)域沈跨,該區(qū)域具有一個(gè)特殊值,該值在數(shù)組釋放時(shí)得到驗(yàn)證兔综。如果紅色區(qū)域中的值與預(yù)期值不匹配饿凛,表明發(fā)生了緩沖區(qū)溢出或欠載。這會(huì)導(dǎo)致 CheckJNI 中止软驰。
adb shell setprop dalvik.vm.jniopts forcecopy
舉例來說涧窒,當(dāng)寫入超出從 GetPrimitiveArrayCritical 獲取的數(shù)組的末端時(shí),這就是 CheckJNI 應(yīng)捕獲的一個(gè)錯(cuò)誤锭亏。此操作可能會(huì)損壞 Java 堆纠吴。如果寫入發(fā)生在 CheckJNI 紅色區(qū)域內(nèi),則在調(diào)用相應(yīng)的 ReleasePrimitiveArrayCritical 時(shí)慧瘤,CheckJNI 會(huì)捕獲該問題戴已。否則,寫入會(huì)損壞 Java 堆中的某個(gè)隨機(jī)對象锅减,并且可能會(huì)導(dǎo)致將來發(fā)生 GC 崩潰糖儡。如果損壞的內(nèi)存是引用字段,則 GC 可能會(huì)捕獲錯(cuò)誤并輸出錯(cuò)誤消息“Tried to mark <ptr> not contained by any spaces”怔匣。
當(dāng) GC 嘗試標(biāo)記一個(gè)對象但無法找到其空間時(shí)握联,就會(huì)發(fā)生此錯(cuò)誤。此檢查失敗后,GC 會(huì)遍歷根金闽,并嘗試查看無效的對象是否為根代芜。結(jié)果共有兩個(gè)選項(xiàng):對象為根或非根。
???♀?不知道你看懂沒组橄。反正我看翻譯是云里霧里玉工。建議你們看英文去遵班。
CheckJNI's forcecopy mode is useful for detecting writes past the end of array regions. When enabled, forcecopy causes the array access JNI functions to return copies with red zones. A red zone is a region at the end/start of the returned pointer that has a special value, which is verified when the array is released. If the values in the red zone don’t match what's expected, a buffer overrun or underrun occurred. This causes CheckJNI to abort.
這意思就是說forcecopy會(huì)標(biāo)記數(shù)組狭郑,如果使用了被標(biāo)記為release的數(shù)組則就會(huì)報(bào)錯(cuò)翰萨。
An example of an error that CheckJNI should catch is writing past the end of an array obtained from GetPrimitiveArrayCritical. This operation can corrupt the Java heap. If the write is within the CheckJNI red zone area, then CheckJNI catches the issue when the corresponding ReleasePrimitiveArrayCritical is called. Otherwise, the write corrupts some random object in the Java heap and can cause a future GC crash. If the corrupted memory is a reference field, then the GC may catch the error and print the error Tried to mark <ptr> not contained by any spaces.
This error occurs when the GC attempts to mark an object that it can’t find a space for. After this check fails, the GC traverses the roots and tries to see if the invalid object is a root. From here, there are two options: The object is a root or a nonroot object.
盡管如此,我依然還是沒有太理解的雳锋。
但是有一點(diǎn)很奇怪玷过,參考JNI tips
https://developer.android.com/training/articles/perf-jni
jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
This grabs the array, copies the first len byte elements out of it, and then releases the array. Depending upon the implementation, the Get call will either pin or copy the array contents. The code copies the data (for perhaps a second time), then calls Release; in this case JNI_ABORT ensures there's no chance of a third copy.
One can accomplish the same thing more simply:
env->GetByteArrayRegion(array, 0, len, buffer);
This has several advantages:
- Requires one JNI call instead of 2, reducing overhead.
- Doesn't require pinning or extra data copies.
- Reduces the risk of programmer error — no risk of forgetting + to call Release after something fails.
理論上因?yàn)槲覀兪褂肎etByteArrayRegion方法其實(shí)是不需要ReleaseByteArrayElements的,但是我們?nèi)サ鬜elease方法又會(huì)發(fā)生泄漏真仲。
(這里面要穿插一個(gè)知識(shí)點(diǎn)袒餐,是如果release的最后一位參數(shù)使用了JNI_COMMIT而不是0的話,那之前??的code是會(huì)又memory leak的墓懂。)
吶捕仔,然后最后終于在這里找到了答案
https://zhuanlan.zhihu.com/p/148158311
作者舉了個(gè)特別棒的int數(shù)組求和的例子
public native int sumArray(int[] array);
extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
//數(shù)組求和
int result = 0;
//方式1 推薦使用
jint arr_len = env->GetArrayLength(array);
//動(dòng)態(tài)申請數(shù)組
jint *c_array = (jint *) malloc(arr_len * sizeof(jint));
//初始化數(shù)組元素內(nèi)容為0
memset(c_array, 0, sizeof(jint) * arr_len);
//將java數(shù)組的[0-arr_len)位置的元素拷貝到c_array數(shù)組中
env->GetIntArrayRegion(array, 0, arr_len, c_array);
for (int i = 0; i < arr_len; ++i) {
result += c_array[i];
}
//動(dòng)態(tài)申請的內(nèi)存 必須釋放
free(c_array);
return result;
}
C層拿到j(luò)intArray之后首先需要獲取它的長度,然后動(dòng)態(tài)申請一個(gè)數(shù)組(因?yàn)镴ava層傳遞過來的數(shù)組長度是不定的,所以這里需要?jiǎng)討B(tài)申請C層數(shù)組),這個(gè)數(shù)組的元素是jint類型的.malloc是一個(gè)經(jīng)常使用的拿來申請一塊連續(xù)內(nèi)存的函數(shù),申請之后的內(nèi)存是需要手動(dòng)調(diào)用free釋放的.然后就是調(diào)用GetIntArrayRegion函數(shù)將Java層數(shù)組拷貝到C層數(shù)組中,然后求和。
對應(yīng)的需要release的:
extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
//數(shù)組求和
int result = 0;
//方式2
//此種方式比較危險(xiǎn),GetIntArrayElements會(huì)直接獲取數(shù)組元素指針,是可以直接對該數(shù)組元素進(jìn)行修改的.
jint *c_arr = env->GetIntArrayElements(array, NULL);
if (c_arr == NULL) {
return 0;
}
c_arr[0] = 15;
jint len = env->GetArrayLength(array);
for (int i = 0; i < len; ++i) {
//result += *(c_arr + i); 寫成這種形式,或者下面一行那種都行
result += c_arr[i];
}
//有Get,一般就有Release
env->ReleaseIntArrayElements(array, c_arr, 0);
return result;
}
綜上钓葫,就是用free(*ptr)础浮。
{
// jbyteArray data
unsigned char *input = new unsigned char[len];
env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte *>(input));
... // actions
free(input);
// env->ReleaseByteArrayElements(data, reinterpret_cast<jbyte *>(input), 0);
}
可謂艱苦卓絕地找到了解決方案豆同。
然而其實(shí)這并不是很難的一個(gè)問題對吧影锈。還是對NDK開發(fā)不夠熟悉鸭廷,對debug的方式也不太熟悉。
好在拿到了必現(xiàn)路徑磁滚。
這些年的經(jīng)驗(yàn)只證明了一件事垂攘。
只要問題能給我必現(xiàn)路徑晒他,那么我一定能給你解決方案陨仅。假使不能很快,則只能說還沒太理解触徐。給自己點(diǎn)時(shí)間撞鹉。
對了鸟雏,之前對于越界有提到一個(gè)解決方案是mprotect孝鹊,不過我的嘗試一直以失敗告終惶室。mprotect((inputBytes+allosize-pagesize), pagesize, PROT_READ) 總是返回-1夹界。這讓我百思不得其解可柿。https://cs.android.com/ 源碼看了看也還是沒太get复斥。就當(dāng)成遺留問題先吧目锭。
#include <sys/mman.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
{
int pagesize = sysconf(_SC_PAGE_SIZE);
if (pagesize == -1) {
LOG("sysconf");
}
int allosize = env->GetArrayLength(data);
if (mprotect((input+allosize-pagesize), pagesize, PROT_READ) == -1) {
LOG("mprotect");
return hasObject;
}
}