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

我认为它可能会发送向shell发出信号,然后shell通过终止冒犯的过程并打印"Segmentation fault"来处理它。因此,我通过编写一个极小的shell(称为crsh(废话外壳))测试了该假设。除了需要用户输入并将其输入到system()方法中之外,此shell不会执行任何操作。 。然后,我继续运行一个产生段错误的程序。如果我的假设正确,则可能是a)崩溃bash,关闭xterm,b)不打印crsh,或c)两者都。

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

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


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

评论

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

这是一个不错的实验...但是您应该知道system()在后台进行的工作。事实证明system()会生成一个shell进程!因此,您的shell进程会生成另一个shell进程,并且该shell进程(可能是/ bin / sh或类似的东西)是运行程序的那个进程。 / bin / sh或bash的工作方式是使用fork()和exec()(或execve()系列中的另一个函数)。

@DietrichEpp我有一种使用system()的警告。我一起整理了一个使用execvp的crsh重写(这很困难,但是教会了我很多关于分叉的工作原理),然后重新进行了实验。得到了一些有趣的结果:1. Shell仍然没有崩溃。 2.它没有打印“ Segmentation Fault”。我知道这还没有定论,因为我可能犯了一个愚蠢的错误,但是从我所看到的来看,它看起来像是由外壳负责打印“ Segmentation Fault”。有想法吗?

@BradenBest:是的。阅读手册页man 2 wait,它将包含宏WIFSIGNALED()和WTERMSIG()。

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

#1 楼

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

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

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

,只要每个进程仅以配置为允许的方式访问内存,就看不到内存保护。当进程违反规则时,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运行“功能强大”的全功能外壳/bin/sh。在这种情况下,crsh是祖父母;父进程通知由/bin/sh进行字段处理,它会打印其通常的消息。然后/bin/sh本身退出,因为它无事可做,并且system的C库实现收到该退出通知。通过检查system的返回值,可以在代码中看到该退出通知。但它不会告诉您子进程死于段错误,因为它被中间shell进程消耗了。


脚注


某些操作系统未将设备驱动程序实现为内核的一部分;但是,所有中断处理程序仍必须是内核的一部分,因此配置内存保护的代码也必须是内核的一部分,因为硬件除了内核不允许这些操作外,其他任何操作都不能执行。比内核更具有特权的“管理程序”或“虚拟机管理器”,但出于此答案的目的,它可以被视为硬件的一部分。
内核是程序,但不是进程;它更像一个图书馆。除了自己的代码外,所有进程还会不时执行部分内核代码。可能有许多只执行内核代码的“内核线程”,但在这里它们与我们无关。
您现在可能不得不处理的唯一一个操作系统,也不能视为实现。当然,Unix是Windows。在这种情况下,它不使用信号。 (实际上,它没有信号;在Windows上,C库完全伪造了<signal.h>接口。)它改用“结构化异常处理”。
某些内存保护冲突会生成SIGBUS(“总线错误”)而不是SIGSEGV。两者之间的界限不明确,并且因系统而异。如果您编写了一个为SIGSEGV定义处理程序的程序,那么为SIGBUS定义相同的处理程序可能是一个好主意。wait并收到退出状态。只是其他事情首先发生。


评论


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

–user323094
16年1月29日在16:13

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

– zwol
16年1月29日在16:25

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

– dmckee ---前主持人小猫
16年1月31日在14:54

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

– Stuart Caie
16 Jan 31'23:58



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

– zwol
16-2-1在12:55



#2 楼

该外壳确实与该消息有关,并且crsh间接调用了一个外壳,可能是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


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

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

评论


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

– Braden Best
16 Jan 26'4:12



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

–布鲁斯·埃迪格(Bruce Ediger)
16年1月26日在14:07

#3 楼


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


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

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

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

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

如果用户程序没有信号处理程序已安装,只需终止即可。在某些体系结构上,如果忽略信号,则可能会一遍又一遍地重新启动指令,从而导致无限循环。

评论


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

– Peter Cordes
16 Jan 28'在6:44



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

–洛伦佐·德马特(LorenzoDematté)
16年1月28日在14:22

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

–洛伦佐·德马特(LorenzoDematté)
16年1月28日在14:24

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

– zwol
16年1月28日在16:37

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

–Random832
16年1月28日在16:39

#4 楼

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

shell对此毫无帮助。

评论


还值得注意的是,SIGSEGV可以被处理/忽略。因此,可以编写不会被其终止的程序。 Java虚拟机是一个值得注意的示例,它在内部出于不同的目的使用SIGSEGV,如下所述:stackoverflow.com/questions/3731784/…

– Karol Nowak
16年1月25日在22:53

同样,在Windows上,.NET在大多数情况下也无需添加空指针检查-它只会捕获访问冲突(相当于segfaults)。

–user253751
16年1月25日在23:44