本文正在参加「金石方案 . 瓜分6万现金大奖」
布景
熟悉我的老朋友或许都知道,之前为了应对crash与anr,开源过一个“民间偏方”的库Signal,用于解决在发生crash或许anr时进行运用的重启,然后最大程度减少其坏影响。
在维护的进程中,发生过这样一件趣事,便是有位朋友发现在遇到信号为SIGSEGV时,再调用信号处理函数的时分
void SigFunc(int sig_num, siginfo *info, void *ptr) {
// 这儿判空并不代表这个对象便是安全的,由于有或许是脏内存
if (currentEnv == nullptr || currentObj == nullptr) {
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
__android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(ILjava/lang/String;)V");
if (!id) {
__android_log_print(ANDROID_LOG_INFO, TAG, "%d !id!id!id!id!id!id!id", sig_num);
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d 11111111111111111111", sig_num);
jstring nativeStackTrace = currentEnv->NewStringUTF(backtraceToLogcat().c_str());
__android_log_print(ANDROID_LOG_INFO, TAG, "%d 22222222222222222222", sig_num);
currentEnv->CallVoidMethod(currentObj, id, sig_num, nativeStackTrace);
__android_log_print(ANDROID_LOG_INFO, TAG, "%d 33333333333333333333", sig_num);
// 开释资源
currentEnv->DeleteGlobalRef(currentObj);
currentEnv->DeleteLocalRef(nativeStackTrace);
}
会遇到
从上文打印中看到,SIGSEGV被抛出,之后被咱们的信号处理函数抓到了,可是却没有被回调到java层,反而变成了SIGABRT。还有便是SIGSEGV被捕获后,却无法经过jni回调给java层的重启处理。本文将从这个例子动身,从踩坑的进程中去学习更多jni知识。
呈现SIGABRT的原因
首要呢,currentEnv是一个大局的变量,咱们一般jni开发的时分,都习惯于保存一个JNIEnv大局的引用,用于后续的调用处理!可是!这样其实是一个危险的操作,比方咱们在sigaction注册一个信号处理函数的时分,那么当信号来的时分,咱们的信号处理运转在哪个线程呢?
答案是:不确定。当信号处理时,会根据当时内核的调度,或许会在当时宣布信号的线程中进行处理,一起也或许会别的开出一个线程进行处理。而咱们的JNIEnv,它其实是一个线程相关的资源,或许说是线程本地资源(TLS),假如咱们在其他线程中调用到这个JNIEnv,那么会怎样样呢?比方上面例子中的currentEnv,创建在咱们的java层的main线程,此刻在信号处理函数中调用currentEnv->FindClass,那么不好意思,这个可不属于当时线程的资源,因而linux内核就会宣布一个SIGABRT信号,提示着这个操作将被阻断
java_vm_ext.cc
void JavaVMExt::JniAbort(const char* jni_function_name, const char* msg) {
Thread* self = Thread::Current();
ScopedObjectAccess soa(self);
ArtMethod* current_method = self->GetCurrentMethod(nullptr);
std::ostringstream os;
os << "JNI DETECTED ERROR IN APPLICATION: " << msg;
if (jni_function_name != nullptr) {
os << "\n in call to " << jni_function_name;
}
// TODO: is this useful given that we're about to dump the calling thread's stack?
if (current_method != nullptr) {
os << "\n from " << current_method->PrettyMethod();
}
if (check_jni_abort_hook_ != nullptr) {
check_jni_abort_hook_(check_jni_abort_hook_data_, os.str());
} else {
// Ensure that we get a native stack trace for this thread.
ScopedThreadSuspension sts(self, ThreadState::kNative);
LOG(FATAL) << os.str();
UNREACHABLE();
}
}
JniAbort 调用会在一切的办法调用行进行检测,假如运用到了其他线程的JNIEnv,就会宣布SIGABRT信号并打印堆栈信息,用于排查
java_vm_ext.cc:578] JNI DETECTED ERROR IN APPLICATION: thread Thread[3,tid=22651,Native,Thread*=0xb400007c96340270,peer=0x12c4d1a0,"Thread-3"] using JNIEnv* from thread Thread[1,tid=22160,Runnable,Thread*=0xb400007c9630dbe0,peer=0x73467b00,"main"]
那么假如咱们真的有场景需求经过在信号处理函数中调用到JNIEnv怎样办,其实也很简单,经过javaVm重新获取一个JNIEnv即可,javaVm确保是虚拟机中唯一的,因而能够放在大局变量中,当咱们想要在信号处理函数时调用到jni办法,可重新获取当时线程的环境
信号处理函数中
if (javaVm->GetEnv((void **) ¤tEnv, JNI_VERSION_1_4) != JNI_OK) {
return ;
}
SIGSEGV被捕获可是调用jni无法进行
咱们的例子是这样的,在java层调用一个jni函数,这个函数经过raise调用向本身发送一个SIGSEGV信号
raise(SIGSEGV);
此刻咱们的信号处理函数能够捕获到这个事情,可是经过currentEnv->CallVoidMethod却无法调用相应的java层办法了,一起log中呈现一个StackOverflowError
Process: com.example.signal, PID: 24575
java.lang.StackOverflowError: stack size 8192KB
at com.example.signal.MainActivity.throwNativeCrash(Native Method)
at com.example.signal.MainActivity.onCreate$lambda-0(MainActivity.kt:23)
at com.example.signal.MainActivity.$r8$lambda$__atZomnwlT46HKNaZgatRAAqwU(Unknown Source:0)
at com.example.signal.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:8160)
那么这个究竟是怎样一回事呢?
首要咱们要明白,咱们真的是由于栈内存耗尽了呈现StackOverflowError了吗?当然不是!咱们只是在jni向自己线程宣布了一个SIGSEGV信号算了,怎样跟栈溢出扯上关系了?咱们从art虚拟机开端说起
在art虚拟机中,呈现SIGSEGV时,会默许先回调这个办法
# fault_handler.cc
// Signal handler called on SIGSEGV.
static bool art_fault_handler(int sig, siginfo_t* info, void* context) {
return fault_manager.HandleFault(sig, info, context);
}
核心是办法
bool FaultManager::HandleFault(int sig, siginfo_t* info, void* context) {
if (VLOG_IS_ON(signals)) {
PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info);
}
#ifdef TEST_NESTED_SIGNAL
// Simulate a crash in a handler.
raise(SIGSEGV);
#endif
针对生成机器码处理
if (IsInGeneratedCode(info, context, true)) {
VLOG(signals) << "in generated code, looking for handler";
for (const auto& handler : generated_code_handlers_) {
VLOG(signals) << "invoking Action on handler " << handler;
if (handler->Action(sig, info, context)) {
// We have handled a signal so it's time to return from the
// signal handler to the appropriate place.
return true;
}
}
}
// We hit a signal we didn't handle. This might be something for which
// we can give more information about so call all registered handlers to
// see if it is.
其他非机器码处理
if (HandleFaultByOtherHandlers(sig, info, context)) {
return true;
}
// Set a breakpoint in this function to catch unhandled signals.
只是打印了一些log
art_sigsegv_fault();
return false;
}
咱们能够留意到,在上面有这么一个判断IsInGeneratedCode,假如是则测验遍历generated_code_handlers_里边的handler对信号处理,那么IsInGeneratedCode是个啥?其实它是指dex字节码编译成机器码这些代码,art虚拟会在编译成机器码的时分,生成一些虚拟机相关的指令,因而假如SIGSEGV是在这些机器码中生成的,那么就要经过generated_code_handlers_里边的处理器去处理,一起假如对错机器码生成的,则走到HandleFaultByOtherHandlers办法中进行处理
bool FaultManager::HandleFaultByOtherHandlers(int sig, siginfo_t* info, void* context) {
if (other_handlers_.empty()) {
return false;
}
Thread* self = Thread::Current();
DCHECK(self != nullptr);
DCHECK(Runtime::Current() != nullptr);
DCHECK(Runtime::Current()->IsStarted());
for (const auto& handler : other_handlers_) {
if (handler->Action(sig, info, context)) {
return true;
}
}
return false;
}
因而咱们特别关注一下generated_code_handlers_,other_handlers_(针对默许处理),它们都是一个集合std::vector<FaultHandler*> 咱们看到它的增加元素办法,在FaultManager::AddHandler中
void FaultManager::AddHandler(FaultHandler* handler, bool generated_code) {
DCHECK(initialized_);
if (generated_code) {
generated_code_handlers_.push_back(handler);
} else {
other_handlers_.push_back(handler);
}
}
这儿边增加的handler都是FaultHandler的子类,分别是NullPointerHandler,SuspensionHandler,StackOverflowHandler,JavaStackTraceHandler
虽然JavaStackTraceHandler被加入到了other_handlers_,可是依旧会判断是否处于虚拟机code中
在这儿咱们明白了SIGSEGV虚拟机的默许处理,一般SIGSEGV都会进入上述handler的判断,假如满意了条件就会先履行(之后才履行到咱们的信号处理函数,假如系统栈溢出,那么有或许履行不到自己的信号处理器)。本例子中raise(SIGSEGV)向自己的线程抛出了SIGSEGV,假如信号处理器中没有选用Call系列调用到java层的话,那也不会有问题。
假如调用到了java层,那么就以栈溢出的方式打印log并重新发一个信号值为SIGKILL的信号杀死当时进程。(这儿一向有个疑惑点,目前还没在art源码上看到为什么会这样,假如有知道的大佬可劳烦奉告)
Sending signal. PID: 29066 SIG: 9
解决办法也比较简单,当咱们异常处理器无法在栈异常情况下,咱们能够事先选用sigaltstack分配一块栈空间
stack_t ss;
if(NULL == (ss.ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE))){
Handle_Exception();
break;
}
ss.ss_size = SIGNAL_CRASH_STACK_SIZE;
ss.ss_flags = 0;
if(0 != sigaltstack(&ss, NULL)) {
Handle_Exception();
break;
}
一起设置flag为SA_ONSTACK即可,让信号处理函数有一个安全的栈空间,得以进行后续调用
sigc.sa_flags = SA_SIGINFO|SA_ONSTACK;
小结
本次算是一个记载,以一个现象例子,更深入了解jni调用,期望读者有所收获,最终继续贴一下项目地址,假如有更多好点子的话,请多多pr!
github.com/TestPlanB/S…