Segmentation Fault在后台如何工作?


266

除了“ CPU的MMU发送信号”和“内核将其定向到有问题的程序,终止它”之外,我似乎找不到任何其他信息。

我以为它可能会将信号发送到外壳,外壳通过终止有问题的进程和打印来处理它"Segmentation fault"。因此,我通过编写一个极小的shell (称为crsh(废话外壳))测试了该假设。该外壳程序不执行任何操作,只是接受用户输入并将其输入给system()方法。

#include <stdio.h>
#include <stdlib.h>

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

因此,我在一个裸终端(没有bash在下面运行)中运行了这个shell 。然后,我继续运行一个产生段错误的程序。如果我的假设正确,则可能是a)崩溃crsh,关闭xterm,b)不打印"Segmentation fault"或c)两者。

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

回到正方形,我猜。我刚刚演示了执行此操作的不是外壳程序,而是其下的系统。甚至如何打印“细分故障”?“谁”在做?内核?还有吗 信号及其所有副作用如何从硬件传播到程序的最终终止?


43
crsh这种实验是个好主意。感谢您让我们所有人都知道它及其背后的想法。
Bruce Ediger

30
当我第一次看到时crsh,我以为它会被称为“崩溃”。我不确定这是否是同样合适的名称。
jpmc26 2016年

56
这是一个不错的实验...但是您应该知道system()引擎盖下的功能。事实证明,这system()将产生一个shell进程!因此,您的shell进程会生成另一个shell进程,而 shell进程(可能/bin/sh是类似的东西)就是运行该程序的那个进程。方式/bin/shbash工作方式是使用fork()exec()(或execve()家族中的其他功能)。
Dietrich Epp

4
@BradenBest:是的。阅读手册页man 2 wait,其中将包含宏WIFSIGNALED()WTERMSIG()
Dietrich Epp

4
就像你说的那样!我尝试添加支票(WIFSIGNALED(status) && WTERMSIG(status) == 11)以使其打印出一些高飞("YOU DUN GOOFED AND TRIGGERED A SEGFAULT")。当我segfault从内部运行程序时crsh,它完全打印出了该代码。同时,正常退出的命令不会产生错误消息。
Braden Best

Answers:


248

所有现代CPU都有能力中断当前正在执行的机器指令。它们保存了足够的状态(通常,但并非总是在堆栈上),以便以后可以恢复执行,就好像什么都没发生一样(被中断的指令通常会从头开始重新启动)。然后他们开始执行一个中断处理程序,该中断处理程序只是更多的机器代码,但是放置在特殊的位置,因此CPU可以提前知道它在哪里。中断处理程序始终是操作系统内核的一部分:该组件以最大的特权运行,并负责监督所有其他组件的执行。1,2

中断可以是同步的,这意味着它们是由CPU本身作为对当前正在执行的指令所做的直接响应而触发的,也可以是异步的,这意味着它们是由于外部事件(如数据到达网络)而在不可预测的时间发生的。港口。有些人将术语“中断”保留为异步中断,而将同步中断称为“陷阱”,“故障”或“异常”,但是这些词都具有其他含义,因此我将坚持使用“同步中断”。

现在,大多数现代操作系统都有进程的概念。从最基本的意义上说,这是一种机制,计算机可以同时运行多个程序,但这也是操作系统配置内存保护的关键方面,这是大多数功能(但是,仍然不是全部)现代CPU。它与虚拟内存一起,可以更改内存地址和RAM中实际位置之间的映射。内存保护允许操作系统为每个进程提供其自己的私有RAM块,只有它可以访问。它还允许操作系统(代表某个进程)将RAM的区域指定为只读,可执行,在一组协作进程之间共享等。还将有一部分内存只能由内存访问。核心。3

只要每个进程仅以CPU配置为允许的方式访问内存,就看不到内存保护。当进程违反规则时,CPU将生成一个同步中断,要求内核进行处理。通常情况下,进程并没有真正违反规则,只有内核需要做一些工作才能允许进程继续。例如,如果需要将进程内存的页面“移出”交换文件,以释放RAM中的空间用于其他操作,则内​​核将标记该页面不可访问。下次该进程尝试使用该进程时,CPU将生成一个内存保护中断。内核将从交换中检索页面,将其放回原处,再次标记为可访问,然后恢复执行。

但是,假设该过程确实违反了规则。它试图访问从未映射过任何RAM的页面,或者试图执行被标记为不包含机器代码的页面,等等。通常称为“ Unix”的操作系统家族都使用信号来处理这种情况。4信号类似于中断,但它们由内核生成并由进程处理,而不是由硬件生成并由内核处理。流程可以定义信号处理程序用自己的代码,并告诉内核它们在哪里。然后,这些信号处理程序将执行,并在必要时中断正常的控制流程。信号都有一个数字和两个名称,其中一个是隐喻的缩写,另一个是隐喻性稍低的短语。当一个进程违反内存保护规则时,生成的信号为(按惯例)数字11,其名称为SIGSEGV和“分段故障”。5,6

信号和中断之间的重要区别是每个信号都有默认行为。如果操作系统未能为所有中断定义处理程序,则这是OS中的错误,并且当CPU尝试调用缺少的处理程序时,整个计算机将崩溃。但是过程没有义务为所有信号定义信号处理程序。如果内核为进程生成信号,并且该信号保留其默认行为,则内核将继续执行默认操作,而不会打扰进程。大多数信号的默认行为是“不执行任何操作”或“终止此过程,并且可能还会产生核心转储”。SIGSEGV是后者之一。

因此,总而言之,我们有一个违反了内存保护规则的过程。CPU暂停了该进程并生成了同步中断。内核发现该中断并SIGSEGV为该过程生成了信号。让我们假设过程中并没有设立一个信号处理器SIGSEGV,所以内核执行的默认行为,这是终止进程。这与_exit系统调用具有相同的效果:关闭打开的文件,释放内存,等等。

到现在为止,还没有打印出任何人可以看到的消息,并且外壳(或更一般地说,刚终止的过程的父进程)根本没有涉及。SIGSEGV进入违反规则的流程,而不是其父流程。但是,序列的下一步是通知父进程其子进程已终止。这可以用几种不同的方法,其中最简单的是当父已经等待这个通知,使用的一个发生wait系统调用(waitwaitpidwait4等)。在这种情况下,内核只会导致系统调用返回,并向父进程提供一个称为退出状态的代码号7退出状态通知父级为何子进程被终止;在这种情况下,它将得知子级由于SIGSEGV信号的默认行为而被终止。

然后,父进程可以通过打印消息将事件报告给人类。shell程序几乎总是这样做。您的crsh代码不包含执行此操作的代码,但是无论如何都会发生,因为C库例程在“幕后” system运行了一个功能齐全的shell /bin/sh。在这种情况下crsh祖父母;父进程通知的字段为/bin/sh,它会打印其通常的消息。然后/bin/sh,它自身退出,因为它无所事事,并且C库的实现system接收到该退出通知。通过检查的返回值,您可以在代码中看到退出通知system; 但它不会告诉您孙子进程是在段错误中死亡的,因为中间shell进程消耗了它。


脚注

  1. 某些操作系统没有将设备驱动程序作为内核的一部分来实现;然而,所有的中断处理程序还是要内核的一部分,所以这是否配置内存保护的代码,因为硬件不允许任何东西,但内核做这些事情。

  2. 可能存在一个称为“管理程序”或“虚拟机管理器”的程序,该程序比内核具有更高的特权,但是出于此答案的目的,它可以被视为硬件的一部分。

  3. 内核是一个程序,但不是一个进程。它更像一个图书馆。除了它们自己的代码之外,所有进程还不时执行部分内核代码。可能有许多执行内核代码的“内核线程” ,但是在这里它们与我们无关。

  4. 当然,您可能不得不处理的又一个不能被认为是Unix实现的操作系统也是Windows。在这种情况下,它不使用信号。(事实上,它不具有信号;在Windows上的<signal.h>界面完全由C库伪造。)它使用一种叫做“ 结构化异常处理 ”代替。

  5. SIGBUS而不是生成了一些违反内存保护的行为(“总线错误”)SIGSEGV。两者之间的界限不明确,并且因系统而异。如果您编写了一个程序定义了一个处理程序SIGSEGV,则最好为定义一个相同的处理程序SIGBUS

  6. “分段错误”是运行原始Unix的其中一台计算机(可能是PDP-11)因违反内存保护而生成的中断的名称。“ 分段 ”是一个类型的内存保护的,但现在的术语“分段故障 ”一般是指任何类型的内存保护冲突。

  7. 可以通过其他所有方式通知父进程子进程已终止,直到父进程调用wait并接收退出状态。只是其他事情首先发生。


@zvol:广告2)我认为说CPU对进程一无所知是不对的。您应该说它调用了一个中断处理程序,该处理程序转移了控制权。
user323094 '16

9
@ user323094实际上,现代多核CPU确实对进程了解很多。足以在这种情况下,他们可以仅挂起触发内存保护错误的执行线程。另外,我试图不深入了解低级细节。从用户空间程序员的角度来看,有关步骤2的最重要的了解是,它是检测违反内存保护的硬件;它是检测内存保护违规的硬件。因此,在识别“有害进程”时,硬件,固件和操作系统之间的精确分工就更少了。
zwol

另一个可能使幼稚的读者感到困惑的微妙之处是“内核向有问题的进程发送了SIGSEGV信号。” 它使用通常的行话,但实际上意味着内核告诉自己处理进程栏上的信号foo(即,除非安装了信号处理程序,否则不会涉及用户级代码,这个问题将由内核解决)。因此,我有时更喜欢“在过程中引发SIGSEGV信号”
dmckee '16

2
SIGBUS(总线错误)和SIGSEGV(分段错误)之间的重要区别在于:当CPU 知道您不应该访问地址(因此它不发出任何外部存储器总线请求)时,就会发生SIGSEGV 。当CPU仅在将请求发送到其外部地址总线后才发现寻址问题时,就会发生SIGBUS。例如,要求一个总线上没有任何响应的物理地址,或者要求在未对齐的边界上读取数据(这将需要两个物理请求而不是一个)
Stuart Caie

2
@StuartCaie您正在描述中断的行为;确实,许多CPU都具有您所概述的区别(尽管有些区别不大,并且两者之间的界线有所不同)。的信号 SIGSEGV和SIGBUS,然而,可靠映射到这两个CPU级条件。POSIX要求SIGBUS而不是SIGSEGV的唯一条件是,mmap将文件放入比文件大的内存区域,然后访问文件末尾的“整个页面”。(否则,POSIX对于何时发生SIGSEGV / SIGBUS / SIGILL / etc还是很含糊的。)
zwol

42

shell确实与该消息有关,并crsh间接调用了shell,可能是bash

我写了一个小小的C程序,该程序始终会隔离故障:

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

当我从默认外壳运行时zsh,得到以下信息:

4 % ./segv
zsh: 13512 segmentation fault  ./segv

当我从运行时bash,我得到您在问题中指出的内容:

bediger@flq123:csrc % ./segv
Segmentation fault

我打算在我的代码中编写一个信号处理程序,然后我意识到,据exec system()所使用的库调用crsh是一个外壳/bin/sh程序man 3 system。这/bin/sh几乎可以肯定是打印出“分割故障”,因为crsh肯定不是。

如果您重写crsh以使用execve()系统调用来运行程序,则不会看到“分段错误”字符串。它来自调用的shell system()


5
我只是在与Dietrich Epp讨论这个问题。我一起破解了一个使用的crsh版本,execvp并再次进行了测试,以发现尽管shell仍然没有崩溃(这意味着SIGSEGV从未发送到shell),但它没有显示 “ Segmentation Fault”。什么都没打印。这似乎表明shell检测到其子进程何时被杀死,并负责打印“ Segmentation fault”(或其某些变体)。
Braden Best

2
@BradenBest-我做了同样的事情,我的代码比您的代码更草率。我什么也没收到,我什至更烂的外壳也没印任何东西。我waitpid()在每个fork / exec上使用过,对于有分段错误的进程,它返回的状态值与以0状态退出的进程的返回值是不同的。
Bruce Ediger

21

除了“ CPU的MMU发送信号”和“内核将其定向到有问题的程序,终止它”之外,我似乎找不到任何其他信息。

这是一个乱码的摘要。Unix信号机制与启动进程的特定于CPU的事件完全不同。

通常,当访问错误地址(或将其写入只读区域,尝试执行不可执行的段等)时,CPU会生成一些特定于CPU的事件(在传统的非VM架构上,这是之所以称为分段违规,是因为每个“段”(传统上是只读的可执行文件“文本”,可写且可变长度的“数据”以及堆栈通常位于内存的另一端)具有固定的地址范围-在现代体系结构上,很可能是页面错误(针对未映射的内存)或访问冲突(针对读取,写入和执行权限问题),我将在剩下的答案中重点介绍这一点。

现在,内核可以完成几件事。还为有效但未加载的内存生成页面错误(例如,换出内存或在映射文件中等),在这种情况下,内核将映射内存,然后从导致内存不足的指令中重新启动用户程序。错误。否则,它将发送信号。这并不完全是“将[原始事件]定向到有问题的程序”,因为安装信号处理程序的过程是不同的,并且大多与体系结构无关,与预期该程序模拟安装中断处理程序相比。

如果用户程序安装了信号处理程序,则意味着创建堆栈框架并将用户程序的执行位置设置为信号处理程序。对所有信号都执行相同的操作,但是在发生分段违规的情况下,通常会安排一些事情,以便如果信号处理程序返回,它将重新启动导致错误的指令。用户程序可能已经修复了错误,例如通过将内存映射到有问题的地址-这是否与体系结构有关。信号处理程序还可以跳转到程序中的其他位置(通常是通过longjmp或引发异常),以中止导致错误内存访问的任何操作。

如果用户程序未安装信号处理程序,则将其终止。在某些架构上,如果忽略信号,则可能会一遍又一遍地重启指令,从而导致无限循环。


+1,只有答案会添加任何东西到被接受的答案。很好地描述了“细分”历史。有趣的事实:x86实际上在32位保护模式下仍具有段限制(启用或不启用分页(虚拟内存)),因此访问内存的指令可以生成#PF(fault-code)(页面错误)或#GP(0)(“如果内存操作数有效地址在CS外部, DS,ES,FS或GS段限制。”)。64位模式删除了段限制检查,因为OS只是改用分页,而用户空间则使用平面内存模型。
彼得·科德斯

实际上,我相信x86上的大多数操作系统都使用分段分页:在平坦的页面地址空间内的一大堆分段。这就是保护内核内存并将其映射到每个地址空间的方式:环(保护级别)链接到段而不是页面上
LorenzoDematté16年

另外,在NT上(但我很想知道在大多数Unix上是否相同!)“分段错误”可能经常发生:在用户空间的开头有一个64k受保护的段,因此取消引用NULL指针会引发一个错误。 (正确?)分段错误
LorenzoDematté16年

1
@LorenzoDematté是的,所有或几乎所有的现代Unix都会在地址空间的开始处保留大量永久未映射的地址,以捕获NULL取消引用。它可能非常大-实际上,在64位系统上,它可能是4 GB,因此将迅速捕获到32位指针的意外截断。但是,从严格意义上讲x86上的分段几乎没有使用。有一个用于用户空间的平面部分,一个用于内核的平面部分,也许还有几个用于特殊技巧的部分,例如从FS和GS中获得一些使用。
zwol

1
@LorenzoDemattéNT使用异常而不是信号;在这种情况下为STATUS_ACCESS_VIOLATION。
Random832 '16

18

分段错误是对不允许的内存地址的访问(不是进程的一部分,或者试图写入只读数据,或者执行不可执行的数据,...)。这被MMU(内存管理单元,今天是CPU的一部分)捕获,导致中断。中断由内核处理,内核将SIGSEGFAULT信号(signal(2)例如)发送到有问题的进程。该信号的默认处理程序转储核心(请参阅参考资料core(5))并终止该过程。

外壳绝对没有帮助。


3
因此,您的C库(如台式机上的glibc)定义了字符串吗?
drewbenn

7
还值得注意的是,SIGSEGV 可以被处理/忽略。因此,可以编写不会被其终止的程序。Java虚拟机是使用SIGSEGV内部出于不同的目的,这里提到一个显着的例子:stackoverflow.com/questions/3731784/...
卡罗尔诺瓦克

2
同样,在Windows上,.NET在大多数情况下也不会添加空指针检查-它只会捕获访问冲突(相当于segfaults)。
immibis
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.