【问题标题】:OS development: How to avoid an infinite loop after an exception routine操作系统开发:如何避免异常例程后的无限循环
【发布时间】:2012-02-27 09:58:16
【问题描述】:

几个月来,我一直在研究“自制”操作系统。 目前,它启动并进入 32 位保护模式。 我已经加载了中断表,但还没有设置分页。

现在在编写我的异常例程时,我注意到当一条指令引发异常时,异常例程会被执行,但随后 CPU 会跳回引发异常的指令!这并不适用于所有异常(例如,除零异常将跳回到除法指令之后的指令),但让我们考虑以下一般保护异常:

MOV EAX, 0x8
MOV CS, EAX

我的例程很简单:它调用一个显示红色错误消息的函数。

结果:MOV CS,EAX 失败 -> 显示我的错误消息 -> CPU 跳回 MOV CS -> 无限循环发送错误消息。

我已经和一位操作系统和 unix 安全方面的老师讨论过这个问题。 他告诉我他知道 Linux 有办法绕过它,但他不知道是哪一种。

天真的解决方案是从例程中解析抛出指令,以获得该指令的长度。 该解决方案相当复杂,我觉得在每个受影响的异常例程中添加对相对繁重的函数的调用有点不舒服......

因此,我想知道这是否是解决问题的另一种方法。也许有一个“魔法”寄存器包含可以改变这种行为的位?

--

非常感谢您提供任何建议/信息。

--

编辑:似乎很多人想知道为什么我要跳过有问题的指令并恢复正常执行。

我有两个原因:

  1. 首先,杀死一个进程是一种可能的解决方案,但不是一个干净的解决方案。这不是它在 Linux 中的做法,例如,(AFAIK)内核发送信号(我认为是 SIGSEGV)但不会立即中断执行。这是有道理的,因为应用程序可以阻止或忽略信号并恢复自己的执行。这是一种非常优雅的方式来告诉应用程序它做错了 IMO。

  2. 另一个原因:如果内核本身执行了非法操作怎么办?可能是由于错误,但也可能是由于内核扩展。正如我在评论中所说:在这种情况下我该怎么办?我应该直接杀死内核并显示一个带有笑脸的漂亮蓝屏吗?

这就是为什么我希望能够跳过指令。 “猜测”指令大小显然不是一种选择,并且解析指令似乎相当复杂(我不介意实现这样的例程,但我需要确定没有更好的方法)。

【问题讨论】:

    标签: exception assembly operating-system x86 interruption


    【解决方案1】:

    不同的异常有不同的原因。有些异常是正常的,异常只是告诉内核在允许软件继续运行之前需要做什么。这方面的例子包括一个页面错误告诉内核它需要从交换空间加载数据,一个未定义的指令异常告诉内核它需要模拟一条 CPU 不支持的指令,或者一个调试/断点异常告诉内核它需要通知调试器。对于这些,内核修复并静默继续是正常的。

    一些异常表示异常情况(例如软件崩溃)。处理这些类型的异常的唯一明智的方法是停止运行软件。您可以保存信息(例如核心转储)或显示信息(例如“蓝屏死机”)以帮助调试,但最终软件停止(进程终止,或者内核进入“什么都不做,直到用户重置计算机”状态)。

    忽略异常情况只会让人们更难找出问题所在。例如,想象一下上厕所的指令:

    • 进入浴室
    • 脱裤子
    • 坐下
    • 开始生成输出

    现在假设第 2 步失败,因为您穿着短裤(“找不到裤子”例外)。您是想在这一点停下来(带有易于理解的错误消息或其他内容),还是在所有有用的诊断信息都消失后忽略该步骤并尝试找出问题所在?

    【讨论】:

    • 我明白你的意思。我知道继续执行产生段错误的进程是没有意义的,但我只是想要这样做,主要是出于好奇。据我到目前为止所了解的,不可能以简单的方式做到这一点(例如,没有定义中断行为的魔法寄存器)。
    • 如果你真的想这样做;那么你必须实现一个解码器来从 RAM 中的原始字节确定指令的长度。对于 80x86(可变长度指令、前缀等)来说,这将是大量工作。一旦你让它“工作”,它在某些情况下仍然无法工作。例如,想象一个由“页面不存在”引起的页面错误——你不能尝试解码不存在的字节。
    • 我知道工作量很大。还没有考虑到页面错误,但这不是什么大问题(至少在我的情况下还没有)。
    • PantsNotFoundException。我希望有一天能在真正的代码中看到这一点。
    【解决方案2】:

    如果我理解正确,您想跳过导致异常的指令(例如mov cs, eax)并在下一条指令处继续执行程序。

    您为什么要这样做?通常,程序的其余部分不应该依赖于该指令成功执行的效果吗?

    一般来说,异常处理有三种方式:

    • 将异常视为不可修复的情况并终止进程。例如,除以零通常就是这样处理的。

    • 修复环境,然后再次执行指令。例如,有时会以这种方式处理页面错误。

    • 使用软件模拟指令并在指令流中跳过它。例如,有时会以这种方式处理复杂的算术指令。

    【讨论】:

    • 如果指令出现在进程中,那么确实,杀死进程将是最简单的解决方案。但是,如果指令出现在内核中怎么办?不一定是基本内核代码,也可能是内核扩展......我不能只是杀死内核! (我不是要重新发明 Windows);)我不确定我是否理解您关于软件仿真的第三点的意思,但它似乎比简单地解码指令更复杂!
    • 关于模拟的第三点是针对CPU不支持浮点的情况,但您想在软件中模拟这些指令。
    • 你能回答这些问题吗? “你为什么要这样做?通常情况下,程序的其余部分不应该依赖于该指令成功执行的效果吗?”
    • 我编辑了我的原始帖子,希望我能回答您的问题。
    • 我了解到您担心内核扩展可能会运行错误代码。如果是这种情况,您是否考虑过生成单独的进程来运行此类代码,这样您就可以在不关闭主内核的情况下杀死有问题的内核进程?
    【解决方案3】:

    您看到的是一般保护例外的特征。英特尔系统编程指南明确指出(6.15 异常和中断参考/中断 13 - 一般保护异常 (#GP)):

    Saved Instruction Pointer
    The saved contents of CS and EIP registers point to the instruction that generated the
    exception.
    

    因此,您需要编写一个异常处理程序来跳过该指令(这有点奇怪),或者只是简单地使用“$SAVED_EIP 的一般保护异常”或类似消息来终止违规进程。

    【讨论】:

      【解决方案4】:

      我可以想象一些情况,在这种情况下,人们希望通过解析失败的指令、模拟其操作,然后返回到该指令来响应 GPF。正常的模式是设置一些东西,以便指令,如果重试,会成功,但一个可能,例如有一些代码希望访问地址为 0x000A0000-0x000AFFFF 的某些硬件,并希望在缺少此类硬件的机器上运行它。在这种情况下,人们可能不想在那个空间中存储“真实”内存,因为每个单独的访问都必须被单独捕获和处理。我不确定是否有任何方法可以处理这个问题,而不必解码任何试图访问该内存的指令,尽管我知道一些虚拟 PC 程序似乎可以很好地管理它。

      否则,我建议您应该为每个线程设置一个跳转向量,当系统遇到 GPF 时应该使用该跳转向量。通常,该向量应该指向一个线程退出例程,但是将要使用指针执行“可疑”操作的代码可以将其设置为适合该代码的错误处理程序(代码应该在使用区域时取消设置向量错误处理程序是合适的)。

      我可以想象一个人可能想模拟一条指令而不执行它的情况,以及一个人可能想将控制权转移到错误处理程序例程的情况,但我无法想象任何人想简单地跳过的情况一条会导致 GPF 的指令。

      【讨论】:

        猜你喜欢
        • 2019-05-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多