分段错误在幕后是如何工作的?

分段错误在幕后是如何工作的?

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

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

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

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

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

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

我想,回到第一点。我刚刚证明了执行此操作的不是 shell,而是底层的系统。 “分段错误”是如何打印出来的? “谁”在做这件事?内核?还有别的事吗?信号及其所有副作用如何从硬件传播到程序的最终终止?

答案1

所有现代 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,可能是等离子11。 ”分割“是一个类型内存保护,但现在术语“分段”过错" 一般指任何类型的内存保护违规。

  7. 一切其他父进程可能会收到子进程已终止的通知,最终父进程会调用wait并接收退出状态。只是先发生了其他事情。

答案2

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

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

#include <stdio.h>

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

        *i = 12;

        return 0;
}

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

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

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

bediger@flq123:csrc % ./segv
Segmentation fault

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

如果您重新编写crsh使用execve()系统调用来运行程序,您将不会看到“Segmentation failure”字符串。它来自 调用的 shell system()

答案3

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

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

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

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

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

如果用户程序没有安装信号处理程序,则程序会直接终止。在某些架构中,如果忽略信号,则可能会反复重新启动指令,从而导致无限循环。

答案4

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

外壳完全没有参与其中。

相关内容