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

文章讲述了在Android系统中,由于SignalChain机制的存在,信号处理器的注册和处理顺序变得复杂。作者通过分析一个使用Signal框架导致Crash上报失效的案例,解释了如何利用libc的sigaction绕过SignalChain,确保APM能最先捕获和处理信号,特别是SIGSEGV信号,从而实现异常的正确上报。

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

之前写了几篇关于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

你问得非常好! > **“为什么 SWD 一定要保留?”** 这个问题触及了 STM32 开发中最基础但又最关键的环节:**程序如何烧录和调试?** 我们来从零开始,彻底讲清楚: --- ## 🔌 一、SWD 是什么? **SWD(Serial Wire Debug)** 是 ARM 公司为 Cortex-M 系列处理器设计的一种 **标准调试接口**。 它用于: - ✅ 下载程序(把你的代码写入 Flash) - ✅ 调试程序(单步运行、设置断点、查看变量) - ✅ 实时监控(通过 ITM/SWO 输出日志) - ✅ 读取芯片状态、复位、擦除等 📌 它就像电脑的 USB 接口一样——没有它,你就没法把文件拷贝进去或与设备通信。 --- ## 📦 二、SWD 需要哪几个引脚? 在 STM32 上,SWD 使用两个关键引脚: | 引脚 | 名称 | 功能 | |------|------|------| | PA13 | SWDIO | Serial Wire Data I/O(双向数据线) | | PA14 | SWCLK | Serial Wire Clock(时钟线) | ✅ 只需要这两根线,ST-Link 或其他调试器就能完成所有下载和调试任务! > 💡 对比 JTAG 模式需要 5 根线(TCK/TMS/TDI/TDO/nTRST),SWD 更省引脚、更高效。 --- ## ❓ 三、如果保留 SWD,会发生什么? 如果你把 **PA13 或 PA14** 当作普通 GPIO、UART、PWM 来用,而没有正确配置,就会出现以下问题: ### ⚠️ 问题 1:无法下载程序(Programmer not connected) 当你编译完代码点击 “Download” 时,IDE(如 STM32CubeIDE、Keil)会提示: ``` Error: No ST-Link detected Unable to connect to target ``` 原因:调试器试图通过 PA13/PA14 与芯片通信,但这些引脚已经被占用或电平被拉低/高,导致信号冲突。 ### ⚠️ 问题 2:可以下载一次,但下次就失败了 有些情况下,第一次能下载成功(因为芯片刚上电,还没运行用户代码),但一旦程序开始运行并把 PA13/PA14 设置为输出模式,下次再连接就会失败。 比如这段危险代码: ```c HAL_GPIO_WritePin(GPIOA, GPIO_PIN_13, GPIO_PIN_SET); // 把 SWDIO 拉高 ``` ➡️ 直接导致调试器无法驱动该线路! ### ⚠️ 问题 3:无法调试(能设断点、能暂停) 即使勉强下载成功,你也无法使用调试功能: - 断点不生效 - 无法查看变量值 - 无法单步执行 因为你已经破坏了调试通道。 --- ## ✅ 四、那是是完全能用 PA13 和 PA14? 是!你可以使用它们,但必须满足一个前提: > ✅ **确保在系统启动初期,这些引脚会被错误地初始化为普通输出。** ### 正确做法(推荐): #### 方法 1:永远要将 PA13/PA14 用作通用输出 - PA13 → 仅用于 SWDIO(保留) - PA14 → 仅用于 SWCLK(保留) 👉 这是最安全的做法,尤其适合初学者。 #### 方法 2:只在需要调试时才复用(高级用法) 某些项目要求节省引脚,可以在以下条件下复用: - 程序已稳定,再需要频繁调试 - 使用外部 bootloader 或串口 ISP 更新程序 - 在 `main()` 中延时后再初始化 PA13/PA14 为 GPIO 示例(风险操作,慎用): ```c int main(void) { HAL_Init(); SystemClock_Config(); // 延迟一段时间,让调试器完成连接 HAL_Delay(100); // 给 ST-Link 时间建立连接 // 再初始化 PA13 为输出(LED 控制) __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_13); HAL_Delay(500); } } ``` ⚠️ 危险点: - 如果你在调试过程中重启程序,可能会断开连接 - 某些 IDE 会在每次运行前尝试 halt CPU,此时可能失败 --- ## 🔄 五、有没有替代方案?用 SWD 也能烧录吗? 有!以下是几种绕过 SWD 的方法(各有优劣): | 方法 | 是否可行 | 说明 | |------|----------|------| | ✅ 使用 USART1 + Boot0 引脚进入系统自举模式(System Memory Boot) | ✔️ 可行 | 通过串口下载程序(如 STM32CubeProgrammer) | | ✅ 使用 USB DFU(如果支持) | ✔️ 部分型号支持 | 需要外接 USB 并实现 DFU 协议 | | ✅ 使用 CAN、I2C、SPI 自定义 bootloader | ✔️ 复杂 | 需自己开发升级协议 | | ❌ 完全依赖 SWD | ✘ 推荐 | 一旦锁死,只能用“强制擦除”恢复 | 📌 所以:**保留 SWD 是最简单、最可靠的开发方式。** --- ## 🧩 六、总结:为什么 SWD 必须保留? | 角度 | 解释 | |------|------| | 🔧 开发效率 | 没有 SWD 就等于失去了实时调试能力,开发成本大幅上升 | | 🛠️ 故障排查 | 断点、变量监视、调用栈都依赖 SWD | | 💾 程序更新 | 每次改代码都要拔插 Boot0?太麻烦!SWD 一键下载 | | 🧯 安全性 | 错误配置可能导致芯片“变砖”(locked),需特殊手段解锁 | > ✅ 所以结论是: > > **在开发阶段,必须保留 PA13(SWDIO) 和 PA14(SWCLK),得将其随意用作普通 IO 输出。** --- ## ✅ 最佳实践建议 | 场景 | 推荐做法 | |------|----------| | 初学者 / 开发阶段 | ❌ 要用 PA13/PA14 做任何功能;保留给 SWD | | 引脚紧张项目 | ✅ 可在程序后期启用 PA13/PA14 作为输入或低频输出(避免干扰) | | 量产阶段 | ✅ 可考虑关闭调试功能,启用 I/O 复用(通过选项字节) | | 特殊需求 | ✅ 使用串口 ISP 或 USB DFU 替代 SWD,释放调试引脚 | --- ## 🔐 补充知识:如何永久关闭 SWD 功能? 如果你确定再需要调试,可以通过 **选项字节(Option Bytes)** 关闭: ```c // 示例:禁用 JTAG 和 SWD,释放 PA13~PA15 __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 启用备份域访问 FLASH_OBProgramInitTypeDef ob; ob.OptionType = OPTIONBYTE_USER | OPTIONBYTE_DATA; ob.UserType = OB_USER_nRST_STOP | OB_USER_nRST_STDBY; ob.UserConfig = OB_RESET_ON_STOP_ENTRY | OB_RESET_ON_STANDBY_ENTRY; // 关闭调试接口 ob.OptionType |= OPTIONBYTE_USER; ob.UserType |= OB_USER_DEBUG; ob.UserConfig |= OB_DEBUG_DISABLE; // 禁用所有调试功能 HAL_FLASH_OB_Launch(); // 应用设置 ``` ⚠️ 警告:这会导致你再也无法通过 SWD 下载程序,除非重新启用或使用 Boot0 进入 ISP 模式。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值