堆栈写入顺序和代码执行顺序
Stack write order and code execution order
我正在阅读 80 年代彼得·诺顿和约翰·索纳所著的一本名著,至少在意大利语版本中,他们说:
给定一个没有为 STACK 保留的 space 定义的汇编代码(因此代码中没有 .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、FFFCh、FFFA等),然后从最高地址到底部(最低地址) .相反,指令指针 (IP) 从最低地址(在示例中从 0000h 开始)向更高地址增长。
通过向栈中插入数据和向程序中添加代码,由于有 64K 的内存余量,这两者不会相遇(至少暂时)。
所以,在我看来,这个 .EXE 程序的行为或多或少就像一个 .COM。
书上写的是正确的(在这种情况下我遗漏了什么),还是我所经历的实际上符合事实,因此书中(至少在意大利语版本中)有错误?
The stack is now at 3995:0, which is the start of the program (CS:0). This is absolutely not good. The stack must never be near the program code.
只要堆栈在代码之前,这绝对不是问题:
栈向下增长,栈指针先递减。 (有 CPU 类型,其中 push
首先写入值,然后更改 SP;在这样的 CPU 上,这将是一个问题。)
所以如果首字母SS:SP和首字母CS:IP都是0:7C00就没有问题了。 (这是引导扇区的典型组合。)
Also, since the stack pointer is in SS:0, it has no room to grow.
这是正确的:
在某些操作模式下,如果 SP 为 0,x86 CPUs 不允许 push
(或 call
...)。
程序可能会崩溃,而不是回绕到 0xFFFE。
当然你还有另一个问题:如果程序长于 64K 且 CS=SS,如果 SP 从 SS:0 换行到 [=33=,push
操作将覆盖程序代码]xFFFE.
由于这是 16 位实模式代码,如果 ss:sp = 3395:0000,它实际上在代码段的末尾,因为 sp 在执行任何推送之前递减为 fffe。
由于源代码或命令行没有指定堆栈大小,链接器默认将堆栈放在代码段的末尾。
TL;DR:Peter Norton 的书是正确的。在创建 DOS EXE (MZ) 程序时,您应该使用 .STACK
指令定义堆栈以避免未定义的行为。或者,您可以使用 SEGMENT
/ENDS
指令和适当的 BYTE ### DUP(?)
语句创建一个名为 STACK
的段和一个名为 STACK
的 class,其中 ###
是以字节为单位的堆栈大小。
我有这本书的英文版 (3rd Edition),它似乎与您引用的内容相同:
DOS always sets the stack pointer to the very end of the segment when
it loads a COM file into memory. For this reason, you do not need to
declare a stack segment (with .STACK) for COM files. What would happen
if you removed the .STACK directive from TEST_SEG.ASM?
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
The stack is now at 3995:0, which is the start of your program (CS:0).
This is very bad news. You do not want the stack anywhere near your
program's code. Since the stack pointer is at SS:0, it has no room to
grow (since the stack grows down in memory). For these reasons, you
must declare a stack segment for EXE programs.
DOS EXE (MZ) 程序格式包括一个 header,其中包含指定起始堆栈值的字段 SS:SP。 SP 的值将是使用 .STACK
指令请求的堆栈大小。如果 .STACK
指令没有指定值,对于大多数 MASM 和兼容的汇编程序,它通常默认为 1024 字节(0x400 字节)。
当您指定 .STACK
指令时,链接器将被引导生成一个 SS:SP 值,该值不会与 EXE 程序中的其他段冲突。
DOS EXE 文件格式允许在程序加载到内存时重定位段。写入堆栈指针 header 的 SS 的值是相对于初始设置为零的 CS 的值。当 DOS 加载程序将 EXE 程序读入内存时,它将对程序执行修正,包括 header 中的 SS:SP 值。例如,如果你有一个 DOS EXE 程序,其中 header 中的 SS:SP 被链接器设置为 0x0002:0x0400 并且 DOS 加载程序将你的程序加载到段0x3995 然后堆栈 (SS:SP) 将被设置为 0x3995+2:0x0400 = 0x3997:0x0400.
链接器指定程序需要多少内存并将适当的信息写入 header。当 DOS 加载程序读取 header 时,它会检查以确定是否有足够的内存,这包括堆栈段。
当您不在 EXE 程序中使用 .STACK 时会发生什么?
当您的汇编代码中未指定 .STACK
时,链接器写入 header 中的 SS:SP 字段的值将是设置为 0x0000:0x0000。这意味着当 DOS 加载程序重新定位段并设置 SS:SP 时,有效内存位置将与 CS:0x0000 相同。这意味着如果您的 CS 段中有 65536 字节的代码(足以填满整个段),您的堆栈将在其顶部向下增长。例如,如果您将一个 16 位值压入堆栈,堆栈指针将从中减去 2,并将值写入该位置。那将是 CS:0xFFFE。同样,实际上并不能保证 CS:0xFFFE 和 CS:0xFFFF 甚至是您程序的可用内存!当您不指定堆栈时,它不会影响链接器写入 header 字段的程序的大小。当 DOS 加载程序将它读入内存时,它不知道是否有足够的内存用于您的堆栈。
这就是为什么大多数链接器会警告您正在生成没有定义堆栈的 EXE。如果你没有指定一个堆栈,当它被加载到内存中时,如果堆栈的位置在你的程序中的某个地方 space 或在 space 的未使用区域中,它可能会正常工作 not being used通过 DOS 或其他应用程序。你不应该依赖运气。
Peter Norton 建议您始终使用 .STACK
指令或使用具有适当字节数的 SEGMENT
/ENDS
指令显式定义您自己的堆栈段分配给它。这样一来,您的程序将按预期由 DOS 加载,并按预期 运行 加载。
使用早期开发工具将 EXE 程序转换为 COM 程序
您可能希望生成没有堆栈的 EXE 的一个原因是当您使用旧版本的 MASM 和无法直接生成 COM 程序的链接器时。在 MASM 的早期版本中,没有 TINY
模型。要生成 COM 程序,您创建了一个 SMALL
模型程序,它没有指定堆栈,也没有段重定位,然后您将使用链接器生成 EXE 程序。如果 EXE 程序满足 COM 程序的要求,则可以从 EXE 转换。 EXE2BIN
was a program that could attempt to do such a conversion. A DOS COM program when initially loaded has the stack (SS:SP) set to the last paragraph aligned memory address available at the end of the code segment. It then pushes 0x0000 onto the stack. This was to retain compatibility with CP/M. The DOS loader pushes the value 0x0000 on the stack so that you can do a RET
to terminate the program. The address CS:0x0000 is in the DOS PSP and contain an Int 0x20
终止程序的指令。
加载 DOS COM 程序时:如果 DOS 加载程序发现有完整的 64KiB 内存可用,SS 的值将被设置为 DOS Program Segment Prefix (PSP) 在里面,它会将 SP 设置为 0x0000 并将 0x0000 压入堆栈。这就是为什么当在调试器中查看时,您经常会在 DOS COM 程序中看到初始 SS:SP 开始于 CS:0xFFFE。
我不知道为什么在 Peter Norton 的书中,SP 的值在调试跟踪输出中是 0xFFEE (SP=FFEE
)。它看起来不寻常但仍然有效。这可能是他使用的 DOS 版本;可用内存量;或s 调试器在 0x0000 return 地址上方的前 16 个字节中放置了其他内容。
我正在阅读 80 年代彼得·诺顿和约翰·索纳所著的一本名著,至少在意大利语版本中,他们说:
给定一个没有为 STACK 保留的 space 定义的汇编代码(因此代码中没有 .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、FFFCh、FFFA等),然后从最高地址到底部(最低地址) .相反,指令指针 (IP) 从最低地址(在示例中从 0000h 开始)向更高地址增长。 通过向栈中插入数据和向程序中添加代码,由于有 64K 的内存余量,这两者不会相遇(至少暂时)。
所以,在我看来,这个 .EXE 程序的行为或多或少就像一个 .COM。
书上写的是正确的(在这种情况下我遗漏了什么),还是我所经历的实际上符合事实,因此书中(至少在意大利语版本中)有错误?
The stack is now at 3995:0, which is the start of the program (CS:0). This is absolutely not good. The stack must never be near the program code.
只要堆栈在代码之前,这绝对不是问题:
栈向下增长,栈指针先递减。 (有 CPU 类型,其中 push
首先写入值,然后更改 SP;在这样的 CPU 上,这将是一个问题。)
所以如果首字母SS:SP和首字母CS:IP都是0:7C00就没有问题了。 (这是引导扇区的典型组合。)
Also, since the stack pointer is in SS:0, it has no room to grow.
这是正确的:
在某些操作模式下,如果 SP 为 0,x86 CPUs 不允许 push
(或 call
...)。
程序可能会崩溃,而不是回绕到 0xFFFE。
当然你还有另一个问题:如果程序长于 64K 且 CS=SS,如果 SP 从 SS:0 换行到 [=33=,push
操作将覆盖程序代码]xFFFE.
由于这是 16 位实模式代码,如果 ss:sp = 3395:0000,它实际上在代码段的末尾,因为 sp 在执行任何推送之前递减为 fffe。
由于源代码或命令行没有指定堆栈大小,链接器默认将堆栈放在代码段的末尾。
TL;DR:Peter Norton 的书是正确的。在创建 DOS EXE (MZ) 程序时,您应该使用 .STACK
指令定义堆栈以避免未定义的行为。或者,您可以使用 SEGMENT
/ENDS
指令和适当的 BYTE ### DUP(?)
语句创建一个名为 STACK
的段和一个名为 STACK
的 class,其中 ###
是以字节为单位的堆栈大小。
我有这本书的英文版 (3rd Edition),它似乎与您引用的内容相同:
DOS always sets the stack pointer to the very end of the segment when it loads a COM file into memory. For this reason, you do not need to declare a stack segment (with .STACK) for COM files. What would happen if you removed the .STACK directive from TEST_SEG.ASM?
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
The stack is now at 3995:0, which is the start of your program (CS:0). This is very bad news. You do not want the stack anywhere near your program's code. Since the stack pointer is at SS:0, it has no room to grow (since the stack grows down in memory). For these reasons, you must declare a stack segment for EXE programs.
DOS EXE (MZ) 程序格式包括一个 header,其中包含指定起始堆栈值的字段 SS:SP。 SP 的值将是使用 .STACK
指令请求的堆栈大小。如果 .STACK
指令没有指定值,对于大多数 MASM 和兼容的汇编程序,它通常默认为 1024 字节(0x400 字节)。
当您指定 .STACK
指令时,链接器将被引导生成一个 SS:SP 值,该值不会与 EXE 程序中的其他段冲突。
DOS EXE 文件格式允许在程序加载到内存时重定位段。写入堆栈指针 header 的 SS 的值是相对于初始设置为零的 CS 的值。当 DOS 加载程序将 EXE 程序读入内存时,它将对程序执行修正,包括 header 中的 SS:SP 值。例如,如果你有一个 DOS EXE 程序,其中 header 中的 SS:SP 被链接器设置为 0x0002:0x0400 并且 DOS 加载程序将你的程序加载到段0x3995 然后堆栈 (SS:SP) 将被设置为 0x3995+2:0x0400 = 0x3997:0x0400.
链接器指定程序需要多少内存并将适当的信息写入 header。当 DOS 加载程序读取 header 时,它会检查以确定是否有足够的内存,这包括堆栈段。
当您不在 EXE 程序中使用 .STACK 时会发生什么?
当您的汇编代码中未指定 .STACK
时,链接器写入 header 中的 SS:SP 字段的值将是设置为 0x0000:0x0000。这意味着当 DOS 加载程序重新定位段并设置 SS:SP 时,有效内存位置将与 CS:0x0000 相同。这意味着如果您的 CS 段中有 65536 字节的代码(足以填满整个段),您的堆栈将在其顶部向下增长。例如,如果您将一个 16 位值压入堆栈,堆栈指针将从中减去 2,并将值写入该位置。那将是 CS:0xFFFE。同样,实际上并不能保证 CS:0xFFFE 和 CS:0xFFFF 甚至是您程序的可用内存!当您不指定堆栈时,它不会影响链接器写入 header 字段的程序的大小。当 DOS 加载程序将它读入内存时,它不知道是否有足够的内存用于您的堆栈。
这就是为什么大多数链接器会警告您正在生成没有定义堆栈的 EXE。如果你没有指定一个堆栈,当它被加载到内存中时,如果堆栈的位置在你的程序中的某个地方 space 或在 space 的未使用区域中,它可能会正常工作 not being used通过 DOS 或其他应用程序。你不应该依赖运气。
Peter Norton 建议您始终使用 .STACK
指令或使用具有适当字节数的 SEGMENT
/ENDS
指令显式定义您自己的堆栈段分配给它。这样一来,您的程序将按预期由 DOS 加载,并按预期 运行 加载。
使用早期开发工具将 EXE 程序转换为 COM 程序
您可能希望生成没有堆栈的 EXE 的一个原因是当您使用旧版本的 MASM 和无法直接生成 COM 程序的链接器时。在 MASM 的早期版本中,没有 TINY
模型。要生成 COM 程序,您创建了一个 SMALL
模型程序,它没有指定堆栈,也没有段重定位,然后您将使用链接器生成 EXE 程序。如果 EXE 程序满足 COM 程序的要求,则可以从 EXE 转换。 EXE2BIN
was a program that could attempt to do such a conversion. A DOS COM program when initially loaded has the stack (SS:SP) set to the last paragraph aligned memory address available at the end of the code segment. It then pushes 0x0000 onto the stack. This was to retain compatibility with CP/M. The DOS loader pushes the value 0x0000 on the stack so that you can do a RET
to terminate the program. The address CS:0x0000 is in the DOS PSP and contain an Int 0x20
终止程序的指令。
加载 DOS COM 程序时:如果 DOS 加载程序发现有完整的 64KiB 内存可用,SS 的值将被设置为 DOS Program Segment Prefix (PSP) 在里面,它会将 SP 设置为 0x0000 并将 0x0000 压入堆栈。这就是为什么当在调试器中查看时,您经常会在 DOS COM 程序中看到初始 SS:SP 开始于 CS:0xFFFE。
我不知道为什么在 Peter Norton 的书中,SP 的值在调试跟踪输出中是 0xFFEE (SP=FFEE
)。它看起来不寻常但仍然有效。这可能是他使用的 DOS 版本;可用内存量;或s 调试器在 0x0000 return 地址上方的前 16 个字节中放置了其他内容。