linux SIGSEGV 信号捕捉,保证发生段错误后程序不崩溃

在linux中编程的时候 有时候 try catch 可能满足不了我们的需求。因为碰到类似数组越界 ,非法内存访问之类的 ,这样的错误无法捕获。下面我们介绍一种使用捕获信号实现的异常 用来保证诸如段错误之类的错误发生时程序不会崩溃,而是跳过代码继续执行。首先我们来看看发生段错误之后系统的处理。

发生段错误后系统会抛出 SIGSEGV 信号 ,之后 调用默认的信号处理函数 ,产生core文件 ,然后关闭程序 。

那有没有一种办法可以保证程序不会死掉呢,当然是有的 。首先我们想到的是 截获改信号,调用自己的信号处理函数 。

让我们来看看signal 这个函数 。

 #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

        第一个参数 的意思表示你要绑定的信号 (可以使用在控制台使用 kill -l 查看都有哪些信号 ,这些就不讲了,有兴趣的可以上网查)

        第二个参数 是表示信号处理的函数 指针 ,返回值为void* 参数为int ,如上 ,另外 系统也定义了一些宏 

                           (SIG_IGN,和 SIG_DFL) 第一个表示忽略这个信号 ,第二个表示 使用默认的信号处理函数 如果我们处理的       是SIGSEGV信号 ,那么它就会产生core文件 等等操作  

        返回值是一个信号处理函数的指针 ,如果发生错误 返回 SIG_ERR 这个宏 ,事实上 也是定义的一个函数 产生错误的原因 主要是因为给定的信号不正确 

另外这个使用函数 有两点要注意 

   1. 进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数 返回 这点非常重要 ,后面会讲到。

   2. 信号函数处理完之后,会将该信号恢复为默认处理状态 ,即重新与产生core文件...函数绑定,所以在下一次用到的时候要重新调用signal这个函数绑定

       自定义的信号处理函数

那么我们就可以开始尝试使用它了

 

#include <signal.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
//信号处理函数
void recvSignal(int sig)
{
	printf("received signal %d !!!\n",sig);
}
int main(int argc,char** argv)
{
  //给信号注册一个处理函数 
  signal(SIGSEGV, recvSignal);
  int* s = 0;
  (*s) = 1;
 //以上两句用来产生 一个 传说中的段错误
  while(1)
  {
    sleep(1);
    printf("sleep 1 \n");
  }
  return 0;
}



 

编译运行  一直打印收到信号 11 (SIGSEGV),为什么呢 ,

上面代码给SIGSEGV 这个信号注册了一个处理函数 ,替代了系统默认的产生core文件的处理函数 ,当错误发生后 ,系统 发送 SIGSEGV ,然后 中断了程序 跳到 recvSignal 处理函数中去 ,处理完成后 ,再跳回来错误发生的地方 ,然后继续产生错误 ,继续发送 SIGSEGV  信号 ... 

使用 setjmp 和longjmp 尝试跳过错误堆栈   

#include <setjmp.h>

 int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val);

系统跳转函数 ,可以直接在函数之间跳转 (比goto 强大多了) 

int setjmp(jmp_buf env);  这个函数 将上下文 ,就是cpu和内存的信息保存到env中 (不用去理解 jmp_buf,就当我们平时用的buff好了),然后调用 void longjmp(jmp_buf env, int val); 的时候 跳转到使用env中的信息 ,恢复上下文 。如果是第一回调用setjmp 它会返回 0,如果是在 从longjmp 跳转过来的 ,那就返回 longjmp的参数 val,根据setjmp的返回值 我们就可以决定执行可能发生错误的代码还是直接跳过这段代码 。知道了原理之后 我们可能就会这样写 


  1. #include <signal.h>

  2. #include <setjmp.h>

  3. #include <stdarg.h>

  4. #include <stdlib.h>

  5. #include <stdio.h>

  6. jmp_buf env;

  7. //信号处理函数

  8. void recvSignal(int sig)

  9. {

  10. printf("received signal %d !!!\n",sig);

  11. longjmp(env,1);

  12. }

  13. int main(int argc,char** argv)

  14. {

  15. //保存一下上下文

  16. int r = setjmp(env);

  17. if( r == 0)

  18. {

  19. //初次执行 ,那么可以执行 可能会发生错误的代码

  20. //给信号注册一个处理函数

  21. signal(SIGSEGV, recvSignal);

  22. printf("excute this code!!");

  23. int* s = 0;

  24. (*s) = 1;

  25. }

  26. else

  27. {

  28. //是由longjmp 跳转回来的

  29. printf("jump this code !!");

  30. }

  31. while(1)

  32. {

  33. sleep(1);

  34. printf("sleep 1 \n");

  35. }

  36. return 0;

  37. }


 

编译 ,执行  产生 SIGSEGV 信号 ,然后在信号函数 里边跳转 到  int r = setjmp(env); 这一行 ,之后 直接略过了 可能发生错误的这段代码 ,跳转生效,可是这种方式还有一个bug,我们看看下面的代码 


  1. #include <signal.h>

  2. #include <setjmp.h>

  3. #include <stdarg.h>

  4. #include <stdlib.h>

  5. #include <stdio.h>

  6. jmp_buf env;

  7. //信号处理函数

  8. void recvSignal(int sig)

  9. {

  10. printf("received signal %d !!!\n",sig);

  11. longjmp(env,1);

  12. }

  13. int main(int argc,char** argv)

  14. {

  15. for(int i = 0; i < 2; i++)

  16. {

  17. //保存一下上下文

  18. int r = setjmp(env);

  19. if( r == 0)

  20. {

  21. //初次执行 ,那么可以执行 可能会发生错误的代码

  22. //给信号注册一个处理函数

  23. signal(SIGSEGV, recvSignal);

  24. printf("excute this code!!");

  25. int* s = 0;

  26. (*s) = 1;

  27. }

  28. else

  29. {

  30. //是由longjmp 跳转回来的

  31. printf("jump this code !!");

  32. }

  33. sleep(5);

  34. }

  35. while(1)

  36. {

  37. sleep(1);

  38. printf("sleep 1 \n");

  39. }

  40. return 0;

  41. }


 

当for循环第二次执行的时候 ,程序依然产生了 SIGSEGV,系统仍然调用了默认的处理函数产生了core文件 ,分析下原因 上面我们说过“进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数返回”,在进入到信号处理函数之后 ,这个时候 系统阻塞了 SIGSEGV 这个信号 ,当跳回到 int r = setjmp(env); 这行代码的时候  SIGSEGV 信号依然是阻塞的 ,那以后 再给他绑定信号处理函数 自然没有作用 。

好在系统给我们提供了int sigsetjmp(sigjmp_buf env, int savesigs);和  void siglongjmp(sigjmp_buf env, int val);这两个函数 ,这两个函数 和上面的 int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val); 大同小异 ,唯一的不同 是sigsetjmp 函数 多了 一个参数 ,savesigs,查看这函数的说明可以知道 ,当 savesigs 不为 0时,会保存当前的信号屏蔽表 (signal mask),然后在使用siglongjmp 跳转的时候 会恢复 线程的 屏蔽表。

于是我们把上面的代码修改 后如下:


  1. #include <signal.h>

  2. #include <setjmp.h>

  3. #include <stdarg.h>

  4. #include <stdlib.h>

  5. #include <stdio.h>

  6. // jmp_buf env;

  7. //信号处理函数

  8. void recvSignal(int sig)

  9. {

  10. printf("received signal %d !!!\n",sig);

  11. siglongjmp(env,1);

  12. }

  13. int main(int argc,char** argv)

  14. {

  15. for(int i = 0; i < 2; i++)

  16. {

  17. //保存一下上下文

  18. int r = sigsetjmp(env,1);

  19. if( r == 0)

  20. {

  21. //初次执行 ,那么可以执行 可能会发生错误的代码

  22. //给信号注册一个处理函数

  23. signal(SIGSEGV, recvSignal);

  24. printf("excute this code!!");

  25. int* s = 0;

  26. (*s) = 1;

  27. }

  28. else

  29. {

  30. //是由longjmp 跳转回来的

  31. printf("jump this code !!");

  32. }

  33. sleep(5);

  34. }

  35. while(1)

  36. {

  37. sleep(1);

  38. printf("sleep 1 \n");

  39. }

  40. return 0;

  41. }


 

编译后 运行 。按照我们的需求 第二次进入for循环时, 发生段错误后程序不会死掉 ,而是会跳过这段代码了继续往下走 。下面我做了一个简单的封装 ,在错误发生时,我打印出了 错误信息 ,然后跳过错误的代码 


  1. /*

  2. ** file name CException.h

  3. */

  4. #ifndef _CEXCEPTION_H_

  5. #define _CEXCEPTION_H_

  6. #include <setjmp.h>

  7. #include <stdlib.h>

  8. #include <stdarg.h>

  9. #include <execinfo.h>

  10. #include <stdio.h>

  11. #include <signal.h>

  12. #include <iostream>

  13. #include <string.h>

  14. typedef struct Except_frame

  15. {

  16. jmp_buf env;

  17. int flag;

  18. void clear()

  19. {

  20. flag = 0;

  21. bzero(env,sizeof(env));

  22. }

  23. bool isDef()

  24. {

  25. return flag;

  26. }

  27. Except_frame()

  28. {

  29. clear();

  30. }

  31. }Except_frame;

  32. extern Except_frame* except_stack;

  33. extern void errorDump();

  34. extern void recvSignal(int sig);

  35. Except_frame* except_stack = new Except_frame;

  36. void errorDump()

  37. {

  38. const int maxLevel = 200;

  39. void* buffer[maxLevel];

  40. int level = backtrace(buffer, maxLevel);

  41. const int SIZE_T = 1024;

  42. char cmd[SIZE_T] = "addr2line -C -f -e ";

  43. char* prog = cmd + strlen(cmd);

  44. readlink("/proc/self/exe", prog, sizeof(cmd) - (prog-cmd)-1);

  45. FILE* fp = popen(cmd, "w");

  46. if (!fp)

  47. {

  48. perror("popen");

  49. return;

  50. }

  51. for (int i = 0; i < level; ++i)

  52. {

  53. fprintf(fp, "%p\n", buffer[i]);

  54. }

  55. fclose(fp);

  56. }

  57. void recvSignal(int sig)

  58. {

  59. printf("received signal %d !!!\n",sig);

  60. errorDump();

  61. siglongjmp(except_stack->env,1);

  62. }

  63. #define TRY \

  64. except_stack->flag = sigsetjmp(except_stack->env,1);\

  65. if(!except_stack->isDef()) \

  66. { \

  67. signal(SIGSEGV,recvSignal); \

  68. printf("start use TRY\n");

  69. #define END_TRY \

  70. }\

  71. else\

  72. {\

  73. except_stack->clear();\

  74. }\

  75. printf("stop use TRY\n");

  76. #define RETURN_NULL \

  77. } \

  78. else \

  79. { \

  80. except_stack->clear();\

  81. }\

  82. return NULL;

  83. #define RETURN_PARAM { \

  84. except_stack->clear();\

  85. }\

  86. return x;

  87. #define EXIT_ZERO \

  88. }\

  89. else \

  90. { \

  91. except_stack->clear();\

  92. }\

  93. exit(0);

  94. #endif



另外建一个文件 ,


  1. #include "CException.h"

  2. int main(int argc,char** argv)

  3. {

  4. //可以如下使用

  5. TRY

  6. int*s = 0;

  7. (int*s) = 1;

  8. END_TRY

  9. //使用这两个宏包含可能发生的错误代码 ,当然可以根据需求 使用

  10. //RETURN_NULL

  11. //RETURN_PARAM(0)

  12. //EXIT_ZERO 这三个宏

  13. return 0;

  14. }



这个时候我们就能使用TRY 和 END_TRY,RETURM_NULL,RETURN_PARAM(param) 来实现程序发生段错误后跳过错误代码继续运行了 ,不过此代码仅限于单线程使用

<think>嗯,用户问的是Linux应用程序发生段错误时是否会发送SIGSEGV信号,以及相关的处理机制。首先,我需要确认段错误SIGSEGV的关系。根据之前的知识,段错误通常是由于访问了非法内存地址,比如空指针、未映射的区域或者只读区域写入数据。这时候操作系统会检测到并发送SIGSEGV信号给进程。 然后,用户提供的引用内容也支持这一点。引用[1]和[2]提到注册处理函数来捕捉SIGSEGV,说明段错误确实会触发这个信号。引用[3]和[4]也讨论了如何捕获和处理这个信号,进一步验证了这一点。所以可以确定,当段错误发生时,系统会发送SIGSEGV信号。 接下来需要解释处理机制。默认情况下,SIGSEGV会导致进程终止并生成core dump。但用户可以通过signal或sigaction函数来注册自定义处理函数,改变默认行为。例如,引用[1]中的代码展示了如何注册处理函数,使得程序段错误崩溃,继续执行。但要注意,处理函数返回后,程序可能会再次触发同样的错误,导致无限循环,如引用[1]提到的“继续产生错误,继续发送SIGSEGV信号”。 另外,处理段错误时需要注意的问题,比如异步信号安全和调试方法。比如引用[3]提到的backtrace和objdump工具,可以用来定位错误位置。这部分可能需要详细说明,帮助用户理解如何处理和调试段错误。 用户可能还想知道如何处理SIGSEGV信号,比如在信号处理函数中记录信息、恢复执行或优雅退出。同时,他们可能关心为什么有时候即使处理了信号程序仍然无法继续正常运行,比如因为内存状态已经被破坏。 最后,生成相关问题需要考虑用户可能的后续疑问,比如如何调试段错误信号处理的安全函数,或者如何生成core文件进行分析。这些问题应该基于现有的回答内容,引导用户进一步学习。</think>根据Linux系统的内存管理机制,应用程序发生段错误时**必然会产生SIGSEGV信号**。这是由内核MMU(内存管理单元)检测到非法内存访问时触发的标准处理流程[^1][^2]。 ### 一、段错误信号触发条件 当进程出现以下行为时会收到SIGSEGV: 1. **非法地址访问**:访问未映射的虚拟地址(如空指针解引用) 2. **权限违规**:尝试写入只读内存区域(如代码段) 3. **栈溢出**:超过栈内存限制(如无限递归) 4. **对齐错误**:在要求对齐的架构上未对齐访问(如ARM架构的未对齐内存访问) ### 二、信号处理机制流程 ``` 进程触发段错误 → 内核发送SIGSEGV信号处理器判断: ↓ ↳ 未注册处理函数 → 终止进程并生成core dump(默认行为) ↳ 已注册处理函数 → 执行自定义处理逻辑 → 返回原执行点(可能再次触发错误循环)[^4] ``` ### 三、关键处理示例 ```c #include <signal.h> #include <stdio.h> void handler(int sig) { fprintf(stderr, "捕获到段错误,地址: %p\n", __builtin_return_address(0)); _exit(1); // 必须立即退出,避免无限循环 } int main() { struct sigaction sa; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sigaction(SIGSEGV, &sa, NULL); // 注册信号处理器[^2][^3] // 触发段错误的示例代码 int* p = NULL; *p = 42; // 空指针解引用 return 0; } ``` ### 四、重要注意事项 1. **信号处理局限性**: - 能真正"修复"内存错误,只能进行错误记录/资源清理 - 处理函数返回后仍可能再次触发错误(形成循环)[^4] - 必须使用异步信号安全函数(如`write()`替代`printf()`) 2. **调试建议**: ```bash # 生成带调试信息的可执行文件 gcc -g -rdynamic program.c -o program # 捕获堆栈信息 catchsegv ./program # 反汇编定位错误 objdump -dS program > disassembly.s ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值