关于c:深入剖析Sgementation-fault原理

8次阅读

共计 4443 个字符,预计需要花费 12 分钟才能阅读完成。

深刻分析 Sgementation fault 原理

前言

咱们在日常的编程当中,咱们很容易遇到的一个程序解体的谬误就是segmentation fault,在本篇文章当中将次要剖析段谬误产生的起因!

Sgementation fault 产生的起因

产生 Sgementation fault 的间接起因是,程序收到一个来自内核的 SIGSEGV 信号,如果是你的程序导致的内核给过程发送这个信号的话,那么就是你的程序正在读或者写一个没有调配的页面或者你没有读或者写的权限。这个信号的起源有两个:

  • 程序的非法拜访,本身程序的指令导致的 Sgementation fault。
  • 另外一种是由别的程序间接发送 SIGSEGV 信号给这个过程。

在类 Linux 零碎中,内核给过程发送的信号为 SIGGEV,信号对应数字为 11,在 Linux 当中信号对应的数字状况大抵如下所示:

 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

当一个程序产生 segmentation fault 的时候,这个程序的退出码 exitcode 等于 139!

产生 segmentation fault 的一个次要的起因是咱们本人的程序产生非法拜访内存,同时别的程序给这个过程发送 SIGSGEV 信号也会导致咱们的程序产生 segmentation fault 谬误。

比方上面的程序就是本人产生的段谬误(产生了越界拜访):

#include <stdio.h>

int main() {int arr[10];
  arr[1 << 20] = 100; // 会导致 segmentation fault
  printf("arr[12] = %d\n", arr[1 << 20]); // 会导致 segmentation fault
  return 0;
}

上面是一个别的程序给其余程序发送 SIGSGEV 信号会导致其余过程呈现段谬误(上面的终端给下面终端的过程号等于 504092 的程序发送了一个信号值等于 11(就是 SIGGSGEV)信号,让他产生段谬误):

自定义信号处理函数

操作系统容许咱们本人定义函数,当某些信号被发送到过程之后,过程就会去执行这些函数,而不是零碎默认的程序(比如说 SIGSEGV 默认函数是退出程序)。上面来看咱们重写 SIGINT 信号的处理函数,当一个程序在终端执行的时候咱们按下 ctrl+c,这个正在执行的程序就会收到一个来自内核的 SIGINT 信号:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

void sig(int n) { // 参数 n 示意代表信号的数值
  char* str = "signal number = %d\n";
  char* out = malloc(128);
  sprintf(out, str, n);
  write(STDOUT_FILENO, out, strlen(out));
  free(out);
}

int main() {signal(SIGINT, sig); // 这行代码就是注册函数 当过程收到 SIGINT 信号的时候就执行 sig 函数
  printf("pid = %d\n", getpid());
  while (1)
  {sleep(1);
  }
  
  return 0;
}

首先咱们须要晓得,当咱们在终端启动一个程序之后,如果咱们在终端按下 ctrl+ c 终端会给以后正在运行的过程以及他的子过程发送 SIGINT 信号,SIGINT 信号的默认处理函数就是退出程序,然而咱们能够捕捉这个信号,重写处理函数。在下面的程序当中咱们就本人重写了 SIGINT 的处理函数,当过程接管到 SIGINT 信号的时候就会触发函数 sig。下面程序的输入印证了咱们的后果。

咱们在终端当中最罕用的就是 ctrl+c 和 ctrl + z 去中断以后终端正在执行的程序,其实这些也是给咱们的程序发送信号,ctrl+ c 发送 SIGINT 信号 ctrl+ z 发送 SIGTSTP 信号。因而和下面的机制相似,咱们能够应用处理函数重写的形式,笼罩对应的信号的行为,比方上面的程序就是应用处理函数重写的形式进行信号处理:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

void sig(int no) {char out[128];
  switch(no) {
    case SIGINT:
      sprintf(out, "received SIGINT signal\n");
      break;
    case SIGTSTP:
      sprintf(out, "received SIGSTOP signal\n");
      break;
  }
  write(STDOUT_FILENO, out, strlen(out));
}

int main() {signal(SIGINT, sig);
  signal(SIGTSTP, sig);
  while(1) {sleep(1);}
  return 0;
}

当初咱们执行这个程序而后看看当咱们输出 ctrl+ z 和 ctrl+ c 会呈现有什么输入。

从下面的输入咱们能够看到实现了咱们想要的输入后果,阐明咱们的函数重写失效了。

段谬误的魔幻

这里有另外一个会产生 SIGSEGV 信号的程序,咱们看看这个程序的输入是什么:

#include <stdio.h>
#include <unistd.h> 
#include <signal.h>

void sig(int n) {write(STDOUT_FILENO, "a", 1); // 这个函数就是向规范输入输入一个字符 a 
}

int main() {signal(SIGSEGV, sig); // 这个是注册一个 SIGSEGV 谬误的处理函数 当操作系统给过程发送一个 SIGSEGV 信号之后这个函数就会被执行
  int* p; 
  printf("%d\n", *p); // 解援用一个没有定义的指针 造成 segementation fault
  return 0;
}

咱们晓得下面的程序必定会产生 segmentation fault 谬误,会收到 SIGSGEV 信号,必定会执行到函数 sig。然而下面的程序会一直的输入a 产生死循环。

下面程序的后果是不是有点难以了解,如果想要理解这个程序的行为,咱们就须要理解操作系统是如何解决 segmentation fault 的,理解这个处理过程之后对下面程序的输入就很容易了解了。

信号处理函数的执行过程

当咱们的过程接管到信号会去执行咱们重写的信号处理函数,如果在咱们的信号处理函数当中没有退出程序或者转移程序的执行流(能够应用 setjmp 和 longjmp 实现),即调用函数失常返回。信号处理函数返回之后会从新执行信号产生地位的指令,也就是说哪条指令导致操作系统给过程发送信号,那条条指令在信号处理函数返回的时候依然会被执行,因而咱们才看到了下面的输入后果,因为零碎会一直的执行那条产生了 segmentation fault 的指令。

那么咱们如何修改咱们的代码,让程序不进入死循环,让程序可能失去咱们的接管呢。有两种方法:

  • 一种是在信号处理函数当中进行一些逻辑解决之后而后,应用零碎调用_exit 间接退出。
  • 另外一种应用 setjmp 和 longjmp 进行执行流的跳转。

间接应用_exit 退出

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig(int n) {printf("间接在这里退出 \n");
  _exit(1); // 应用零碎调用间接退出
}

int main() {signal(SIGSEGV, sig);
  *(int*) NULL = 0;
  printf("完结 \n"); // 这个打印不会输入
  return 0;
}

应用控制流跳转

#include <stdio.h>
#include <signal.h>
#include <setjmp.h>

jmp_buf env;

void sig(int n) {printf("筹备回到主函数 \n");
  longjmp(env, 1);
}

int main() {signal(SIGSEGV, sig);
  if(!setjmp(env)) {printf("产生段谬误 \n");
    *(int*) NULL = 0;
  }else {printf("回到了主函数 \n");
  }
  return 0;
}

总结

在本篇文章当中次要给大家介绍了 Sgementation fault 的原理,并且本人入手写了他的信号处理函数,在信号处理函数当中发现如果信号处理函数失常退出的话,那么程序会进入一个死循环,永远不会进行,会一直的产生 Sgementation fault,因而咱们应用了两种形式让程序完结,一种是在信号处理函数当中不进行返回间接退出,然而这种状况会有一个弊病,如果咱们原来的程序在前面还有一些操作的话就不可能执行了,如果有些程序很重要的,这就可能会造成很多谬误。第二种形式是咱们能够应用 setjmp 和 longjmp 转移控制流,再次回到主函数执行。

以上就是本篇文章的所有内容了,我是LeHung,咱们下期再见!!!更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0