为什么我的信号处理器不生效

为什么我的信号处理器不生效?

之前写了几篇关于Linux信号的文章,有很多小读者找到我后台留言,说对他们帮助很大。同时也有小伙伴用了我之前写的一个框架,Signal把公司的Crash上报机制给“玩坏”了。

事情是这样的,有小伙伴用了Signal,本意是想发生异常的时候进行了一次兜底重启,结果他把所有的新号都注册进去了,导致他们的异常上报直接变成了 0 native crash。当然,这个是符合Signal的机制的,因为我们偷偷拦截了信号的分发过程进行重启,所以信号就不会被传递到Crash SDK了。

下面我们来看一下具体的经过,已经“始作俑者”的我究竟干了什么。

常见的Crash上报

我们都知道,常见的crash上报框架,比如xCrash,或者由xCrash衍生的各个上报Crash SDK,其实都采取了同样一种方式上报native crash,就是通过诸如sigaction函数,注册了一个信号处理器,当一场信号来临时,比如SIGSEGV(11 内存错误相关),开始dump堆栈,之后就上报为一次crash。

image.png

为了不要混淆我们接下来说的东西,我们Signal指的是框架,而不是信号。

image.png 当Signal最后初始化的时候,其实会把old_action给设置为NULL,没错,我们故意不把信号往下面传递了。

int sigaction(int __signal, const struct sigaction* __new_action, struct sigaction* __old_action);

对于不了解的读者,我们看一下sigaction的定义,其中第二个参数就是当前注册的新号处理函数,第三个参数就是上一次注册的新号处理函数,而这个信号处理器是可以被覆盖的,比如我们注册两个sigaction


struct sigaction sigc;
sigc.sa_sigaction = sig_func;
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;

struct sigaction normal_sig;
normal_sig.sa_sigaction = normal_func;
sigfillset(&normal_sig.sa_mask);
normal_sig.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;

我们注册两个sigaction
sigaction(SIGSEGV, &sigc, NULL);
sigaction(SIGSEGV, &normal_sig, NULL);


那么最终SIGSEGV来的时候,只会被normal_sig里面的信号处理器处理,因为会互相覆盖(如果后者没有保存上一个信号处理器并主动把信号传递下去的话)

可以看到,信号处理的这条“链”,它是可以被人为破坏掉的,因为旧的信号处理器可以不被保留,就算被保留了,也可以不调用。

再探sigaction

如果我是一个业务方同学,那么我肯定是笑嘻嘻的,因为我们完全可以打断调用链重启对吧,这样虽然我对用户的体验比较差,但是我指标好看呀!作为APM的同学,就头疼了,有的时候,用户重启也想记录为一次异常,但是诸如xCrash等上报框架,是无法避免信号处理链断裂的问题。

作为APM一方,我们诉求是,最先拿到信号并进行上报处理。下面,我们就来看,这个究竟怎么做到!

我们的标题也看到了,是再探sigaction。那么我们再来反思一下,在jni调用的sigaction,有很多小伙伴就被误导进去了,它其实并不是Linux中的信号注册处理函数,而是一个同名的Android 系统的sigaction函数!


extern "C" int sigaction(int signal, const struct sigaction* new_action,
                         struct sigaction* old_action) {
    InitializeSignalChain();
    return __sigaction(signal, new_action, old_action, linked_sigaction);
}

惊喜嘛!它居然是一个壳!最终调用的是通过Android SignalChain机制调用真正的Linux sigaction。

SignalChain机制

SignalChain是Android特有的一种对信号处理的一种机制,我们来看一下InitializeSignalChain做了什么

__attribute__((constructor)) static void InitializeSignalChain() {
    static std::once_flag once;
    std::call_once(once, []() {
        lookup_libc_symbol(&linked_sigaction, sigaction, "sigaction");
        lookup_libc_symbol(&linked_sigprocmask, sigprocmask, "sigprocmask");

#if defined(__BIONIC__)
        lookup_libc_symbol(&linked_sigaction64, sigaction64, "sigaction64");
        lookup_libc_symbol(&linked_sigprocmask64, sigprocmask64, "sigprocmask64");
#endif
    });
}

它其实是找到了Linux中真正的sigaction函数与sigprocmask,前者作用是为了注册信号处理器,后者是为了添加信号掩码,而我们调用sigaction,都会在这里被拦截,比如Android会在进程创建的时候,统一将SIGSEGV进行了特殊处理,在FaultManager::Init方法中,目的就是为了当信号来临时,最先回调的是系统的信号处理器art_sigsegv_handler

void FaultManager::Init(bool use_sig_chain) {
    CHECK(!initialized_);
    if (use_sig_chain) {
        sigset_t mask;
        sigfillset(&mask);
        sigdelset(&mask, SIGABRT);
        sigdelset(&mask, SIGBUS);
        sigdelset(&mask, SIGFPE);
        sigdelset(&mask, SIGILL);
        sigdelset(&mask, SIGSEGV);

        SigchainAction sa = {
                .sc_sigaction = art_sigsegv_handler,
                .sc_mask = mask,
                .sc_flags = 0UL,
        };

        AddSpecialSignalHandlerFn(SIGSEGV, &sa);

它的定义如下

static bool art_sigsegv_handler(int sig, siginfo_t* info, void* context) {
    return fault_manager.HandleSigsegvFault(sig, info, context);
}

关键的AddSpecialSignalHandlerFn函数,其实做的事情就是,第一,保证SignalChain存在,其次就是维护一个列表,保证art_sigsegv_handler在最先处理的位置,位置0,接着就是我们应用层调用sigaction加进去的其他handler

extern "C" void AddSpecialSignalHandlerFn(int signal, SigchainAction* sa) {
    InitializeSignalChain();

    if (signal <= 0 || signal >= _NSIG) {
        fatal("Invalid signal %d", signal);
    }

  
    chains其实是一个数组,最先加入的元素,也就是Chain头,就是一开始通过FaultManager Init方法加入的处理器
    chains[signal].AddSpecialHandler(sa);
    chains[signal].Claim(signal);
}

其实过程就是这样

image.png

到这里,我们就明白了,原来最先处理信号的这个需求,并不是APM特有的,我们Android系统也需要,而这个处理的方式,就是直接调用libc里面的sigaction注册真正的信号处理函数,其他通过jni层调用的sigaction,其实被包装进了SignalChain机制,即自主维护了一条信号处理器调用链,而这个调用链无论中间的元素怎么变(即使中间断裂了),都不会影响首先处理的信号处理函数art_sigsegv_handler。

直接调用libc sigaction

实现SignalChain最重要的一点,就是我们不直接调用Android系统的sigaction去信号处理器就可以了,而是我们直接调用libc的sigaction,怎么做到呢?很简单,我们之间从so找到符号调用就可以了

获取libc.so句柄
void *libc = dlopen("libc.so", RTLD_LOCAL);
if (__predict_true(NULL != libc)) {

    // sigaction64() / sigaction()
    libc_sigaction64 = (libc_sigaction64_t)dlsym(libc, "sigaction64");
    if (NULL == libc_sigaction64)
        libc_sigaction = (libc_sigaction_t)dlsym(libc, "sigaction");

    dlclose(libc);
}

struct sigaction sigc;
sigc.sa_sigaction = sig_func;
// 信号处理时,先阻塞所有的其他信号,避免干扰正常的信号处理程序
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;

struct sigaction *ac  = calloc(1,sizeof (struct sigaction));
prev_action = ac;

libc_sigaction64(SIGSEGV, &sigc, prev_action);

我们之间调用libc的sigaction,即使后续再调用Android的sigaction,也是先回调libc的sigaction的,因为我们覆盖了AndroidSignalChain的信号处理器!完整代码如下

typedef int (*libc_sigaction64_t)(int, const struct sigaction64 *, struct sigaction64 *);
typedef int (*libc_sigaction_t)(int, const struct sigaction *, struct sigaction *);
static libc_sigaction64_t libc_sigaction64 = NULL;
static libc_sigaction_t libc_sigaction = NULL;
struct sigaction * prev_action;
static void sig_func(int sig_num, struct siginfo *info, void *ptr) {

    __android_log_print(ANDROID_LOG_ERROR, "hello", "sig fun ");
    prev_action ->sa_sigaction(sig_num,info,ptr);
}

static void normal_func(int sig_num, struct siginfo *info, void *ptr) {

    __android_log_print(ANDROID_LOG_ERROR, "hello", "normal_func ");
}

JNIEXPORT void JNICALL
Java_com_pika_mooner_MainActivity_test(JNIEnv *env, jobject thiz) {
    void *libc = dlopen("libc.so", RTLD_LOCAL);
    if (__predict_true(NULL != libc)) {

        // sigaction64() / sigaction()
        需要区分64位就调用sigaction64 32就调用sigaction
        libc_sigaction64 = (libc_sigaction64_t)dlsym(libc, "sigaction64");
        
        libc_sigaction = (libc_sigaction_t)dlsym(libc, "sigaction");

        dlclose(libc);
    }

    struct sigaction sigc;
    sigc.sa_sigaction = sig_func;
    // 信号处理时,先阻塞所有的其他信号,避免干扰正常的信号处理程序
    sigfillset(&sigc.sa_mask);
    sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;

    struct sigaction *ac  = calloc(1,sizeof (struct sigaction));
    prev_action = ac;


    这里简单直接调用libc_sigaction64
    libc_sigaction64(SIGSEGV, &sigc, prev_action);


    struct sigaction normal_sig;
    normal_sig.sa_sigaction = normal_func;
    // 信号处理时,先阻塞所有的其他信号,避免干扰正常的信号处理程序
    sigfillset(&normal_sig.sa_mask);
    normal_sig.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;

    sigaction(SIGSEGV, &normal_sig, NULL);

}

此时当SIGSEGV来的时候,最终输出的是log是

image.png

通过我们的改造,就从原本的流程变成了下图

image.png

之后我们采取这种方式进行APM改造,就能最先获取信号并处理了!

总结

信号作为Linux重要的一环,Android并没有完全照搬Linux的信号处理,而是在上层构建了SignalChain机制合理调度信号处理,我们一定要注意Linux内核处理与Android系统这里面的区别!看到这里,还不点个赞!!!

作者:Pika
原文链接:https://juejin.cn/post/7248513044917682231

猜你喜欢

转载自blog.csdn.net/Code1994/article/details/131387081