分段错误如何在后台工作?

Bra*_*est 272 shell kernel signals segmentation-fault

除了“CPU 的 MMU 发送信号”和“内核将其定向到违规程序并终止它”之外,我似乎找不到任何关于此的信息。

我认为它可能将信号发送到外壳,外壳通过终止违规进程和打印来处理它"Segmentation fault"。所以我通过编写一个我称之为crsh(废话 shell)的极简 shell 来测试这个假设。除了获取用户输入并将其提供给system()方法之外,此外壳不执行任何操作。

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

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

所以我在一个裸终端中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]
Run Code Online (Sandbox Code Playgroud)

回到第一个,我猜。我刚刚证明了不是外壳执行此操作,而是下面的系统。“分段错误”是如何打印出来的?“谁”在做?内核?还有什么?信号及其所有副作用如何从硬件传播到程序的最终终止?

zwo*_*wol 257

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

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

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

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

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

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

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

到目前为止,没有任何东西打印出人类可以看到的任何消息,并且 shell(或者,更一般地说,刚刚终止的进程的父进程)根本没有涉及。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并接收退出状态。只是其他事情先发生。

  • @user323094 现代多核 CPU 实际上对进程有很多了解;足够了,在这种情况下,他们只能挂起触发内存保护故障的执行线程。此外,我试图不进入低级细节。从用户空间程序员的角度来看,理解第 2 步的最重要的事情是检测内存保护违规的是*硬件*;在识别“违规进程”时,硬件、固件和操作系统之间的精确分工更是如此。 (9认同)
  • SIGBUS(总线错误)和 SIGSEGV(分段错误)之间的显着区别在于:当 CPU *知道*您不应该访问地址(因此它不会发出任何外部存储器总线请求)时,就会发生 SIGSEGV。当 CPU 仅在将您的请求置于其外部地址总线上后才发现寻址问题时,就会发生 SIGBUS。例如,请求总线上没有任何响应的物理地址,或请求读取未对齐边界上的数据(这将需要两个物理请求而不是一个) (3认同)
  • @StuartCaie 你在描述 _interrupts_ 的行为;实际上,许多 CPU 会区分您概述的区别(尽管有些没有,并且两者之间的界限各不相同)。然而,_signals_ SIGSEGV 和 SIGBUS _不_可靠地映射到这两个 CPU 级条件。POSIX 需要 SIGBUS 而不是 SIGSEGV 的唯一条件是当你将一个文件 `mmap` 到一个比文件大的内存区域,然后访问文件末尾之外的“整个页面”。(否则 POSIX 对 SIGSEGV/SIGBUS/SIGILL/etc 何时发生非常模糊。) (3认同)

Bru*_*ger 43

外壳确实与该消息有关,并crsh间接调用外壳,这可能是bash.

我写了一个小 C 程序,总是出现段错误:

#include <stdio.h>

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

        *i = 12;

        return 0;
}
Run Code Online (Sandbox Code Playgroud)

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

4 % ./segv
zsh: 13512 segmentation fault  ./segv
Run Code Online (Sandbox Code Playgroud)

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

bediger@flq123:csrc % ./segv
Segmentation fault
Run Code Online (Sandbox Code Playgroud)

我打算在我的代码中编写一个信号处理程序,然后我意识到execsystem()使用的库调用crsh是一个 shell,/bin/sh根据man 3 system. 这/bin/sh几乎可以肯定是打印出“Segmentation fault”,因为crsh肯定不是。

如果重新编写crsh使用execve()系统调用来运行程序,则不会看到“Segmentation fault”字符串。它来自由system().

  • 我刚刚和迪特里希·埃普讨论了这个问题。我 [hacked together](http://pastebin.com/mtgrc5Ri) 一个使用 `execvp` 的 crsh 版本并再次进行测试以发现当 shell 仍然没有崩溃时(意味着 SIGSEGV 永远不会发送到 shell ),它*不*打印“分段错误”。根本没有打印任何内容。这似乎表明 shell 检测到它的子进程何时被杀死并负责打印“分段错误”(或其某些变体)。 (5认同)
  • @BradenBest - 我做了同样的事情,我的代码比你的代码更草率。我根本没有收到任何消息,我更糟糕的外壳也没有打印任何东西。我在每个 fork/exec 上使用了 `waitpid()`,它为有分段错误的进程返回一个不同的值,而不是以 0 状态退出的进程。 (2认同)

Ran*_*832 21

除了“CPU 的 MMU 发送信号”和“内核将其定向到违规程序并终止它”之外,我似乎找不到任何关于此的信息。

这是一个有点乱的总结。Unix 信号机制与启动进程的特定于 CPU 的事件完全不同。

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

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

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

如果用户程序没有安装信号处理程序,它会被简单地终止。在某些架构上,如果忽略该信号,它可能会一遍又一遍地重新启动指令,从而导致无限循环。


von*_*and 18

分段错误是对不允许的内存地址的访问(不是进程的一部分,或尝试写入只读数据,或执行不可执行的数据,...)。这会被 MMU(Memory Management Unit,今天是 CPU 的一部分)捕获,导致中断。中断由内核处理,内核向违规进程发送SIGSEGFAULT信号(参见signal(2)示例)。此信号的默认处理程序转储核心(请参阅 参考资料core(5))并终止进程。

外壳在这方面完全没有参与。

  • 还值得注意的是 SIGSEGV *可以* 被处理/忽略。所以可以编写一个不会被它终止的程序。Java 虚拟机是一个值得注意的例子,它在内部将 SIGSEGV 用于不同的目的,如下所述:http://stackoverflow.com/questions/3731784/a-lot-of-sigsegv-while-straceing-java-process (7认同)
  • 同样,在 Windows 上,.NET 在大多数情况下不会费心添加空指针检查 - 它只是捕获访问冲突(相当于段错误)。 (2认同)