堆栈写入顺序和代码执行顺序

问题描述

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

给出了一个汇编代码,没有定义为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,FFFCh,FFFA等)向下增长,然后从最高地址到最低地址(最低地址)。相反,指令指针(IP)从最低地址(在示例中为0000h)向较高地址增长。 通过将数据插入堆栈并向程序中添加代码,由于存在64K的内存余量,所以这两个对象(至少有一会儿)将不满足。

因此,我认为此.EXE程序的行为或多或少就像是.COM一样。

书中写的是正确的(在这种情况下,我缺少什么),还是我实际经历的与事实相符,因此书中(至少在意大利语中)存在错误

解决方法

堆栈现在位于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操作将覆盖程序代码。

,

TL; DR :Peter Norton的书是正确的。创建DOS EXE(MZ)程序时,应使用.STACK指令定义堆栈,以避免未定义的行为。另外,您可以使用STACK / STACK伪指令和适当的SEGMENT语句(其中ENDS是堆栈大小(以字节为单位)。


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

在以下情况下,DOS始终将堆栈指针设置为该段的末尾 它将COM文件加载到内存中。因此,您不需要 声明COM文件的堆栈段(带有.STACK)。会发生什么 是否从TEST_SEG.ASM中删除了.STACK指令?

BYTE ### DUP(?)

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

DOS EXE(MZ)程序格式包括一个header,其中包含指定起始堆栈值 SS:SP 的字段。 SP 的值将是使用###指令请求的堆栈的大小。如果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 指令未指定值,则对于大多数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:0xFFFE CS:0xFFFF 甚至是程序的可用内存!如果不指定堆栈,则不会增加链接程序写入标头字段的程序大小。当DOS加载程序将其读入内存时,它不会知道堆栈是否有足够的内存。

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

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


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

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

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

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

,

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

由于源代码或命令行未指定堆栈大小,因此链接程序默认将堆栈放在代码段的末尾。