在 COM 程序中保存寄存器状态

Saving registers state in COM program

我反汇编了一个简单的 DOS .COM 程序,其中有一些代码可以保存和恢复寄存器值

PUSH AX ; this is the first instruction
PUSH CX
....
POP CX
POP AX
MOV AX, 0x00 0x4C
INT 21 // call DOS interrupt 21 => END

这与 C 程序中的函数序言和尾声非常相似。但是序言是由编译器自动添加的,上面的程序是用汇编程序手动编写的,因此程序员对这段代码中的值的保存和恢复负全部责任。

我的问题是如果我无意中忘记在我的程序中保存一些寄存器会发生什么?

如果我在 HEX 编辑器中故意将这些指令替换为 NOP 会怎样?这会导致程序崩溃吗?为什么被调用函数负责在堆栈上保存外部上下文?从我的角度来看,这应该以某种方式在调用函数时完成,以防止如果我使用第 3 方库和编写不当的代码可能会破坏我的程序执行的问题。

如果您不想防止忘记要 pushpop 的内容,我建议您坚持使用更高级的语言。

在汇编程序中,如果该函数是您自己的,那么您应该保存和恢复您在函数中使用的所有寄存器,除了那些 return 函数输出的寄存器。如果其他人编写了该函数,请查看其文档。如果有疑问,save/restore 注册 before/after 调用函数(除了那些应该 return 值的函数)。

使调用函数在调用另一个函数之前保存其所有工作寄存器的一个问题是,有时函数在其不知情的情况下被中断(即硬件中断)。例如,在 DOS 中,有一个讨厌的 54 毫秒计时器滴答声。每秒 18 次,硬件中断会将控制权从正在执行的任何代码转移到计时器滴答处理程序。这会自动发生,除非您的程序专门禁用了中断。

定时器节拍处理程序随后会保存它要使用的所有寄存器,完成它的工作,然后在返回之前恢复它保存的寄存器。

当然,您可以说中断处理程序很特殊,但为什么呢?即使 8086 上的寄存器很少(AX、BX、CX、DX、SI、DI、标志——我忘记了什么吗?我故意不包括段寄存器),使函数在传输之前保存其整个状态控制意味着您将使用大量不必要的堆栈 space 和执行周期来保存内容,因为它们 可能 被修改。但是如果被调用函数只负责保存它使用的寄存器,并且它只使用 AX 和 CX,那么它可以只保存这两个寄存器。它使代码更小、速度更快,堆栈 space 的使用也更少。

当您开始谈论深度很多的调用层次结构时,压入 8 个寄存器而不是 2 个寄存器之间的差异加起来很快。

考虑 x86-64,它有 64 个通用寄存器。你真的认为一个函数应该在调用另一个函数之前强制保存所有 64 个寄存器,即使被调用的函数只使用其中两个吗?保存64个64位寄存器需要512字节的栈space。与保存两个只需要 16 个字节的寄存器相反。

现在用汇编语言编写东西的主要目的是编写比编译器可以编写的代码更快、更小的代码。一个指导原则是不要做比你必须做的更多的工作。这意味着由您决定您的汇编语言函数正在使用哪些寄存器,并在进入时保存这些寄存器并在退出时恢复它们。

由于 DOS 终止功能不依赖于任何寄存器设置(AX 除外)进行操作 (*) pushes/pops 在您发布的代码中似乎 多余。但是,您应该知道,程序员可能为了在本地使用它们而推送了这些值!因此,在 HEX 编辑器中用 NOP 替换这两个推送肯定是个坏主意。但是,您可以用 NOP 替换两个 pops,因为此时在程序中不需要恢复 AX/CX 以及平衡堆栈,因为 (*).

由于您的问题是关于在程序级别上保存寄存器,答案必须是pushing/popping寄存器是为了保存它们 没用。如果您无意中忘记在程序中保存一些寄存器,也不会发生什么坏事。