“Signal”背后的bug与解决

news2024/11/28 4:47:54

背景

熟悉我的老朋友可能都知道,之前为了应对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 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!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/41985.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python合集1

我的首个python的合集啊~~ 完全给自己看啊 不喜喷了也不里你 一、一维插值 对现有数据进行拟合或插值是数学分析中常见的方式。 通过分析现有数据&#xff0c;得到一个连续的函数&#xff08;也就是曲线&#xff09;&#xff1b;或者更密集的离散方程与已知数据互相吻合&…

HTML+CSS详细知识点(下)

&#x1f525;上一篇&#x1f525;HTMLCSS详细知识点复习&#xff08;上&#xff09; 文章目录五、列表和超链接1、列表标签2、CSS控制列表样式3、超链接六、表格和表单1、表格2、表单七、浮动与定位1、元素的浮动2、清除浮动3、overflow属性4、元素的定位属性5、position属性五…

[附源码]计算机毕业设计springboot安防管理平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【吴恩达机器学习笔记】五、逻辑回归

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为学习吴恩达机器学习视频的同学提供的随堂笔记。 &#x1f4da;专栏简介&#xff1a;在这个专栏&#xff0c;我将整理吴恩达机器学习视频的所有内容的笔记&…

【Hack The Box】linux练习-- Horizontall

HTB 学习笔记 【Hack The Box】linux练习-- Horizontall &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &…

Spring Cloud和Dubbo有哪些区别?

Spring Cloud Spring Cloud是⼀个微服务框架&#xff0c;提供了微服务领域中的很多功能组件&#xff0c;并且Spring Cloud是⼀个⼤⽽全的框架 Dubbo Dubbo⼀开始是⼀个RPC调⽤框架&#xff0c;核⼼是解决服务调⽤间的问题 对比&#xff1a; Dubbo则更侧重于服务调⽤&#x…

Nuxt 3.0.0正式发布,集成Element Plus、Ant Design Vue和Arco Design Vue脚手架

发布说明 Nuxt 是使用简便的 Web 框架&#xff0c;用于构建现代和高性能的 Web 应用&#xff0c;可以部署在任何运行 JavaScript 的平台上。 Nuxt 3.0 11天前正式发布了稳定版&#xff0c;3.0 基于 Vue 3&#xff0c;为 TypeScript 提供了 “一等公民” 支持&#xff0c;并进行…

java面试强基(13)

前文链接(61条消息) java面试强基&#xff08;12&#xff09;_一个风轻云淡的博客-CSDN博客https://blog.csdn.net/m0_62436868/article/details/128047427?spm1001.2014.3001.5501 何为反射&#xff1f;反射机制优缺点&#xff1f; ​ 它赋予了我们在运行时分析类以及执行类…

Jenkins部署与基础配置(1)

5 Jenkins 部署与基础配置 IP地址角色172.18.8.19jenkins-master172.18.8.29jenkins-node1172.18.8.39jenkins-node2 [rootjenkins-master ~]# tail -n1 .bashrc PS1\[\e[1;32m\][\[\e[0m\]\[\e[1;32m\]\[\e[1;33m\]\u\[\e[34m\]\h\[\e[1;31m\] \w\[\e[1;32m\]]\[\e[0m\]# [r…

ISCTF新生赛(引用传递简单社工)

猫和老鼠 反序列化题目&#xff1a; <?php //flag is in flag.php highlight_file(__FILE__); error_reporting(0);class mouse { public $v;public function __toString(){echo "Good. You caught the mouse:";include($this->v);}}class cat {public $a;p…

05 Pod:如何理解这个Kubernetes里最核心的概念?

文章目录1 为什么要有pod?2. 为什么Pod 是 Kubernetes 的核心对象&#xff1f;3. 如何用YAML描述Pod?3.1 Pod的基本组成部分3.1.1 最重要的 spec.containers 字段使用3.1.1.1为什么要定义容器启动时要执行的命令&#xff1f;4. 如何使用kubectl 操作Pod?4.1 创建pod4.2 删除…

数据结构与算法之查找算法

数据结构与算法——查找算法 本文将不断更新查找有关算法&#xff0c;由于精力有限&#xff0c;因此本博文将分多次更新&#xff0c;感谢您的关注 文章目录数据结构与算法——查找算法1. 二分法查找&#xff08;折半查找&#xff09;1.1 算法叙述1.2 实例说明2. 插值查找&#…

【ML特征工程】第 8 章 :自动化特征化器:图像特征提取和深度学习

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

[2022-11-26]神经网络与深度学习第5章 - 循环神经网络(part 2)

contents循环神经网络(part 2) - 梯度爆炸实验写在开头解决方式概览梯度爆炸实验梯度打印函数思考&#xff1a;什么是范数、L2范数、为什么要打印梯度范数复现梯度爆炸现象使用梯度截断解决梯度爆炸问题思考&#xff1a;梯度截断解决梯度爆炸问题的原理&#xff1f;写在最后循环…

搭建MinIO容器

文章目录1 问题背景2 资源准备3 安装Docker服务4 关闭防火墙5 以Docker方式安装MinIO6 访问MinIO1 问题背景 玩一个前后端的项目&#xff0c;需要用到对象存储器&#xff0c;于是使用开源的MinIO。期间以Docker方式搭建遇到某些坑&#xff0c;此处仅以博客的方式记录下来 2 资源…

【通信原理课设--基于MATLAB/Simulink的2ASK数字带通传输系统建模与仿真】Simulink的使用介绍以及在本实验中的使用

目录 Simulink的简要介绍 Simulink的使用流程 进入Simulink 进入模型编辑窗口 ​ 建立一个新的文件 根据求需建立模型 对选择的模块进行参数设置 本次课程设计需要使用Simulink做ASK的仿真处理&#xff0c;那么下面就一起学习了解一下Simulink吧&#xff01; Simuli…

全球经济自由度1995-2021最新版绿色金融指数2001-2020

&#xff08;1&#xff09;全球经济自由度指数 1995-2021 1、数据来源&#xff1a;美国传统基金会 2、时间跨度&#xff1a;1995-2021 3、区域范围&#xff1a;全球 4、指标说明&#xff1a; 经济自由度指数&#xff0c;是由《华尔街日报》和美国传统基金会发布的年度报告…

爱站网关键词挖掘工具-长尾关键词挖掘站长工具

长尾词挖掘免费工具&#xff0c;为什么我们要使用长尾词挖掘免费工具&#xff0c;我们只要找准关键词就等于掌握了流量。 关键词可应用于任何平台&#xff1a;不管是网站、短视频、自媒体等&#xff01; 比如说用户A经常看体育领域的内容&#xff0c;平台就会给A打上体育领域标…

LeetCode刷题---19. 删除链表的倒数第 N 个结点(双指针-快慢指针)

文章目录一、编程题&#xff1a;142. 环形链表 II&#xff08;双指针-快慢指针&#xff09;1.题目描述2.示例1&#xff1a;3.示例2&#xff1a;4.示例3&#xff1a;5.提示&#xff1a;6.进阶&#xff1a;二、解题思路1.思路2.复杂度分析&#xff1a;3.算法图解三、代码实现总结…

[附源码]计算机毕业设计springboot班级事务管理论文2022

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…