【问题标题】:Stack write order and code execution order堆栈写入顺序和代码执行顺序
【发布时间】:2020-12-25 14:36:33
【问题描述】:

我正在阅读 1980 年代彼得·诺顿和约翰·索纳 (Peter Norton) 和约翰·索纳 (John Sohna) 的名著,他们至少在意大利语版本中说:

给定一个没有定义为 STACK 保留的空间的汇编代码(因此代码中没有 .STACK 指令),通过汇编它,链接它,然后用 DEBUG 观察它的寄存器状态(直接来自 .EXE 文件) ,我们有以下内容:

A> DEBUG TEST_SEG.EXE
-R
AX = OOOO BX = OOOO CX = 0004 DX = OOOO SP = OOOO BP = OOOO SI = OOOO DI = OOOO
DS = 3985 ES = 3985 SS = 3995 CS = 3995 IP = OOOO NV UP EI PL NZ NA PO NC
3995:0000 B44C           MOV       AH, 4C
-

这本书还说: 堆栈现在位于 3995:0,这是程序的开始 (CS:O)。这绝对不好。堆栈绝不能靠近程序代码。此外,由于堆栈指针位于 SS:O 中,因此它没有增长空间(随着堆栈向下增长)。由于这些原因,您必须为 .EXE 程序定义一个堆栈段。

现在,我做了一些测试,我了解到堆栈是向下增长的(例如,从 0000h、FFFEh、FFFFh、FFFA 等),然后从最高地址到底部(最低地址)。相反,指令指针 (IP) 从最低地址(在示例中从 0000h)向更高地址增长。 通过将数据插入堆栈并在程序中添加代码,这两者将不会相遇(至少在一段时间内),因为有 64K 的内存余量。

所以,在我看来,这个 .EXE 程序的行为或多或少就好像它是一个 .COM。

书上写的是正确的(在这种情况下我遗漏了一些东西),还是我所经历的实际上符合事实,因此在书中(至少在意大利语版本中)有错误?

【问题讨论】:

  • 你是对的:堆栈就在代码段的下方。我不记得 DOS 是如何初始化 EXE 的,堆栈可能靠近 PSP 或在“未保留”区域。作为旁注,请注意非常古老的书籍及其翻译。
  • @MargaretBloom 与 .COM 文件(没有标题)不同,堆栈指针的初始位置在每个 .EXE 文件的标题中指定。
  • 堆栈增长到较低的地址,并收缩到较高的地址,随着程序的运行而动态。然而,代码只在编译/链接时“增长”,而不是在运行时:一旦运行,程序代码的大小是固定的。 IP 寄存器动态地跟踪处理器正在运行的指令。有时我们将代码的当前结尾称为位置计数器,以区分增长代码的编译时间概念和指令指针的动态概念。
  • 我不记得DOS内存分配的细节了,但是我们能确定3995:FFFE确实可供这个程序使用,而不是分配给另一个程序吗?如果没有,您可能刚刚覆盖了其他人的代码或数据。
  • @NateEldredge EXE 标头中的字段可以保证空间已分配给程序。如果链接器按照 rcgldr 的回答建议为堆栈分配空间,那么这些字段可能会设置为分配该空间。但是,我不确定这是否真的是 MS-DOS 链接器实际所做的。

标签: assembly stack dos x86-16 real-mode


【解决方案1】:

TL;DR:彼得诺顿的书是正确的。您应该在创建 DOS EXE (MZ) 程序时使用 .STACK 指令定义堆栈以避免未定义的行为。或者,您可以使用SEGMENT/ENDS 指令和适当的BYTE ### DUP(?) 语句创建一个名为STACK 的段和一个名为STACK 的类,其中### 是堆栈大小(以字节为单位)。


我有这本书的英文版 (3rd Edition),似乎与您引用的内容相当:

DOS 总是将堆栈指针设置到段的最后 它将 COM 文件加载到内存中。因此,您无需 为 COM 文件声明一个堆栈段(带有 .STACK)。会发生什么 如果您从 TEST_SEG.ASM 中删除了 .STACK 指令?

C>DEBUG TEST_SEG.EXE
R
AX=0000 BX=0000 CX=0004 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=3985 ES=3985 SS=3995 CS=3995 IP=0000 NV UP EI PL NZ NA PO NC 
3090:0000 B44C MOV AH,4C

堆栈现在位于 3995:0,这是您的程序的开始 (CS:0)。 这是一个非常坏的消息。您不希望堆栈靠近您的 程序的代码。由于堆栈指针位于 SS:0,它没有空间 增长(因为堆栈在内存中增长)。由于这些原因,您 必须为 EXE 程序声明一个堆栈段。

DOS EXE (MZ) 程序格式包括一个header,其中包括指定起始堆栈值的字段SS:SPSP 的值将是使用.STACK 指令请求的堆栈大小。如果.STACK 指令没有指定值,对于大多数 MASM 和兼容的汇编程序,它通常默认为 1024 字节(0x400 字节)。

当您指定.STACK 指令时,链接器将被引导生成一个不与 EXE 程序中的其他段冲突的 SS:SP 值。

DOS EXE 文件格式允许在程序加载到内存时重新定位段。 SS 的值写入堆栈指针的头部是一个相对于初始设置为零的 CS 的值。当 DOS 加载程序将 EXE 程序读入内存时,它将对程序进行修复,包括标题中的 SS:SP 值。例如,如果您有一个 DOS EXE 程序,其中标头中的 SS:SP 由链接器设置为 0x0002:0x0400 并且 DOS 加载程序将您的程序加载到段 0x3995 然后堆栈(SS :SP) 将设置为 0x3995+2:0x0400 = 0x3997:0x0400。

链接器指定程序需要多少内存并将适当的信息写入标头。当 DOS 加载器读取头部时,它会检查是否有足够的内存,这包括堆栈段。


在 EXE 程序中不使用 .STACK 会发生什么?

当您的汇编代码中未指定.STACK 时,链接器写入标头中SS:SP 字段的值将设置为0x0000:0x0000。这意味着当 DOS 加载程序重新定位段并设置 SS:SP 时,有效的内存位置将与 CS:0x0000 相同。这意味着如果您的 CS 段中有 65536 字节的代码(足以填满整个段),您的堆栈将在它之上向下增长。例如,如果您将 16 位值压入堆栈,堆栈指针将从其中减去 2,并将值写入该位置。那将是 CS:0xFFFE。同样,实际上并不能保证 CS:0xFFFECS:0xFFFF 甚至是您的程序可用的内存!当您不指定堆栈时,它不会影响链接器写入标题字段的程序的大小。当 DOS 加载器将其读入内存时,它不知道是否有足够的内存用于您的堆栈。

这就是为什么大多数链接器会警告您正在生成一个没有定义堆栈的 EXE。如果您没有指定堆栈,那么当堆栈的位置在您的程序空间内的某个位置或位于 DOS 或其他应用程序未使用的未使用空间区域时,当加载到内存中时,它可能会正常工作。你不应该依赖运气。

Peter Norton 的建议是,您应该始终使用.STACK 指令或使用SEGMENT/ENDS 指令显式定义您自己的堆栈段,并为其分配适当数量的字节。这样你的程序就可以按预期被 DOS 加载并按预期运行。


使用早期开发工具将 EXE 转换为 COM 程序

您可能希望生成没有堆栈的 EXE 的一个原因是当您使用旧版本的 MASM 和无法直接生成 COM 程序的链接器时。在 MASM 的早期版本中,没有 TINY 模型。要生成 COM 程序,您创建了一个没有指定堆栈和段重定位的 SMALL 模型程序,然后您将使用链接器生成 EXE 程序。如果 EXE 程序满足 COM 程序的要求,它可以从 EXE 转换。 EXE2BIN 是一个可以尝试进行这种转换的程序。最初加载的 DOS COM 程序将堆栈 (SS:SP) 设置为代码段末尾可用的最后一段对齐内存地址。然后它将 0x0000 压入堆栈。这是为了保持与 CP/M 的兼容性。 DOS 加载程序将值 0x0000 压入堆栈,以便您可以执行RET 来终止程序。地址 CS:0x0000 在 DOS PSP 中,包含一个 Int 0x20 指令来终止程序。

加载 DOS COM 程序时:如果 DOS 加载器发现有完整的 64KiB 可用内存,则 SS 的值将设置为 DOS Program Segment Prefix (PSP) 所在的段,并且它将 SP 设置为 0x0000 并将 0x0000 压入堆栈。这就是为什么当您在调试器中查看时,您经常会在 DOS COM 程序中看到初始 SS:SPCS:0xFFFE 开始。

我不知道为什么在 Peter Norton 书中,调试跟踪输出中 SP 的值是 0xFFEE (SP=FFEE)。它看起来不寻常,但仍然有效。可能是他使用的 DOS 版本;可用内存量;或者他的调试器在 0x0000 返回地址上方的前 16 个字节中放置了其他内容。

【讨论】:

    【解决方案2】:

    堆栈现在位于 3995:0,这是程序的开始 (CS:0)。这绝对不好。堆栈绝不能靠近程序代码。

    只要堆栈在代码之前,这绝对不是问题:

    栈向下增长,栈指针先递减。 (有些 CPU 类型是 push 先写入值然后更改 SP;在这样的 CPU 上会出现问题。)

    所以如果初始的 SS:SP 和初始的 CS:IP 都是 0:7C00 则不会有问题。 (这是引导扇区的典型组合。)

    另外,由于堆栈指针在 SS:0,它没有增长空间。

    这是正确的:

    在某些操作模式下,如果 SP 为 0,x86 CPU 不允许push(或call ...)。

    程序可能会简单地崩溃,而不是包装到 0xFFFE。

    当然还有另一个问题:如果程序长度超过 64K 且 CS=SS,如果 SP 从 SS:0 回绕到 SS:0xFFFE,push 操作将覆盖程序代码。

    【讨论】:

    • 问题提到了 16 位,我认为这意味着实模式(与 286 保护模式相反,即使这样,sp == 0 在推送之前也会减少到 fffe)。如果程序长于 64K,则需要多个段,如果代码或命令行未指定堆栈大小,链接器仍将提供默认堆栈。
    【解决方案3】:

    由于这是 16 位实模式代码,如果 ss:sp = 3395:0000,它实际上是在代码段的末尾,因为在进行任何推送之前 sp 会递减到 fffe。

    由于源代码或命令行没有指定堆栈大小,链接器默认将堆栈放在代码段的末尾。

    【讨论】:

      猜你喜欢
      • 2018-10-02
      • 2012-04-21
      • 1970-01-01
      • 2014-05-26
      • 1970-01-01
      • 1970-01-01
      • 2021-09-01
      • 1970-01-01
      相关资源
      最近更新 更多