背景

熟悉我的老朋友可能都知道,之前为了应对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 **) &currentEnv, 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 8192KBat 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.只是打印了一些logart_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!

“Signal”背后的bug与解决相关推荐

  1. Visual Studio2005奇怪的bug及解决【月儿原创】

    Visual Studio2005查看设计器打开失败的bug及解决 作者:清清月儿 主页:http://blog.csdn.net/21aspnet/           时间:2007.3.23 在 ...

  2. 浅谈Android Fragment嵌套使用存在的一些BUG以及解决方法

    自从Android3.0引入了Fragment之后,使用Activity去嵌套一些Fragment的做法也变得更加流行,这确实是Fragment带来的一些优点,比如说:Fragment可以使你能够将a ...

  3. js中hover事件时候的BUG以及解决方法

    js中hover事件时候的BUG以及解决方法 参考文章: (1)js中hover事件时候的BUG以及解决方法 (2)https://www.cnblogs.com/mmykdbc/p/7464050. ...

  4. Dumpzilla工具第615行bug的解决办法

    Dumpzilla工具第615行bug的解决办法 在Dumpzilla使用选项frequency时,会提示SQL语法错误.这是由于其中SQL语句编写错误.需要将615行中: where url lik ...

  5. 记一次使用 android 自带 WebView 做富文本编辑器之API、机型的兼容及各种奇葩bug的解决...

    转载请声明出处(http://www.cnblogs.com/linguanh/) 目录 1,测试设备介绍 2,开源项目richeditor及CrossWalk的选择 3,遇到的bug及其解决方法 4 ...

  6. [css] 写出你遇到过IE6/7/8/9的BUG及解决方法

    [css] 写出你遇到过IE6/7/8/9的BUG及解决方法 把以前兼容IE6.7学习的东西搬出来了,还以为不见了.兼容性问题 1.IE6margin双边距问题 2.IE67 li间隙问题 3.图片间 ...

  7. css文本省略(······)行高错位(bug)- 解决办法

    应用css文本省略(······)属性:-webkit-line-clamp: 3; 导致:行高错位.(F12查看发现css属性line-height的值并没变,但为什么浏览器显示文本的实际行距却变小 ...

  8. ie6,ie7,ie8 css bug兼容解决记录

    断断续续的在开发过程中收集了好多的bug以及其解决的办法,都在这个文章里面记录下来了!希望以后解决类似问题的时候能够快速解决 ,也希望大家能在留言里面跟进自己发现的ie6 7 8bug和解决办法! 1 ...

  9. php is_subclass_of,PHP_PHP is_subclass_of函数的一个BUG和解决方法,is_subclass_of的作用: 复制代码 - phpStudy...

    PHP is_subclass_of函数的一个BUG和解决方法 is_subclass_of的作用: bool is_subclass_of ( object object, string class ...

最新文章

  1. python编程有用吗-分享8点超级有用的Python编程建议
  2. Centos 下安装redmine及设置发送邮件功能
  3. python time 语句_python的time模块总结
  4. html5新特性:异步上传文件
  5. git 命令git 地址_这是我上周使用的所有Git命令及其作用。
  6. 在线学习新编程 技巧全攻略
  7. java环形队列测试,JAVA数据结构之循环队列的实现
  8. npm以及gulp相关操作
  9. ARMV8体系结构简介
  10. SqlServer死锁com.microsoft.sqlserver.jdbc.SQLServerException: Transaction (Process ID 52) was deadlock
  11. TensorFlow保存或加载训练的模型
  12. 小米球ngrok如何后台启动
  13. Unity 制作360全景视频 全景图片流程
  14. 理解JDBC/JPA/Mybatis/Hibernate
  15. Linux与ISCSI
  16. 蓝桥杯-迷宫(DFS)
  17. 长沙十大情调情侣约会餐厅,你们去过哪几家?
  18. 我天!中国科技原来有桎梏!道翰天琼认知智能机器人API平台接口为您揭秘-1。
  19. C. Ehab and Path-etic MEXs
  20. 设备管理器如何改成锁定计算机,如何设置电脑不锁屏幕

热门文章

  1. C语言中的结构体(struct)
  2. 最近分享一款抖音上很火的七夕节程序员表白页面_html5七夕表白放烟花动画特效
  3. 邮件中html内嵌图片,email - 在HTML电子邮件中嵌入附加图像 - SO中文参考 - www.soinside.com...
  4. ThinkPad t440p 左侧滋滋滋滋响
  5. C语言 —— 菜鸟教程【C练习实例3】
  6. 基于Java毕业设计材料提交管理系统源码+系统+mysql+lw文档+部署软件
  7. 在网页加载完毕时自动触发某个按钮的点击事件
  8. GIS 中矢量多边形网格化问题研究
  9. 家庭宽带上行下行是什么意思带宽的上行下行速率对网速有什么影响?
  10. 中国移动颓势再现,用户大量流失,5G用户净增数最少