Search code examples
assemblystackdosx86-16real-mode

Stack write order and code execution order


I was reading a famous book from the 1980s by Peter Norton and John Sohna, who state, at least in the Italian version, that:

given an assembly code without the definition of the space reserved for STACK (therefore without the .STACK directive in the code), by assembling it, linking it and then observing its register states with DEBUG (directly from the .EXE file), we have the following:

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
-

the book also says: The stack is now at 3995: 0, which is the start of the program (CS: O). This is absolutely not good. The stack must never be near the program code. Also, since the stack pointer is in SS: O, it has no room to grow (as the stack grows down). For these reasons, you must define a stack segment for .EXE programs.

Now, I did some tests, and I understand that the stack grows downwards (ie for example, from 0000h, FFFEh, FFFCh, FFFA, etc.), then from the highest address to the bottom (lowest address). The Instruction Pointer (IP) instead grows from the lowest address (in the example from 0000h), towards higher addresses. By inserting data into the stack and adding code into the program, these two will not meet (at least for a while) since there is a 64K memory margin.

So, this .EXE program behaves more or less as if it were a .COM in my opinion.

Is what is written in the book correct (in this case I am missing something), or what I experienced actually corresponds to the truth and therefore in the book (at least in the Italian version) there is an error?


Solution

  • TL;DR: Peter Norton's book is correct. You should define a stack using the .STACK directive when creating a DOS EXE (MZ) program to avoid undefined behaviour. Alternatively you can create a segment named STACK with a class called STACK using the SEGMENT/ENDS directives with an appropriate BYTE ### DUP(?) statement where ### is the stack size in bytes.


    I have the English version of the book (3rd Edition) that seems to be the equivalent of what you are quoting:

    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.

    The DOS EXE (MZ) program format includes a header that includes fields specifying the starting stack value SS:SP. The value of SP will be the size of the stack requested with the .STACK directive. If the .STACK directive doesn't have a value specified it is generally defaulted to 1024 bytes (0x400 bytes) for most MASM and compatible assemblers.

    When you specify the .STACK directive the linker will be directed to generate an SS:SP value that doesn't conflict with other segments in the EXE program.

    The DOS EXE file format allows the segments to be relocated when the program is loaded in memory. The value of SS written into the header for the stack pointer is a value relative to a CS that is initially set to zero. When the DOS loader reads the EXE program into memory it will perform fixups to the program including SS:SP value in the header. If for example you have a DOS EXE program where SS:SP in the header is set by the linker to 0x0002:0x0400 and the DOS loader loads your program to segment 0x3995 then the stack (SS:SP) will be set to 0x3995+2:0x0400 = 0x3997:0x0400.

    The linker specifies how much memory a program needs and writes the appropriate information into the header. When the DOS loader reads the header it checks to determine if there is enough memory, and this includes the stack segment.


    What Happens when you Don't Use .STACK in an EXE Program?

    When .STACK isn't specified in your assembly code the value written by the linker to the SS:SP fields in the header will be set to 0x0000:0x0000. This means that when the DOS loader relocates the segment and sets up SS:SP the effective memory location will be the same as CS:0x0000. This means if your CS segment has 65536 bytes of code in it (enough to fill the entire segment) your stack will grow down over top of it. As an example if you push a 16 bit value on the stack the stack pointer will have 2 subtracted from it and the value written to that location. That would be CS:0xFFFE. As well, there isn't actually guarantee that CS:0xFFFE and CS:0xFFFF are even available memory to your program! When you don't specify a stack it doesn't contribute to the size of the program written by the linker to the header fields. When the DOS loader reads it into memory it will have no idea if there is enough memory for your stack or not.

    This is why most linkers will warn you that you are generating an EXE without a defined stack. If you don't specify a stack, when loaded into memory it will probably work just fine if the location of the stack is somewhere within your program space or is in an unused area of space not being used by DOS or another application. You shouldn't rely on getting lucky.

    What Peter Norton is suggesting is that you should always use a .STACK directive or define your own stack segment explicitly using the SEGMENT/ENDS directive with an appropriate number of bytes allocated to it. This is so your program will be loaded by DOS as you expect and run as you expect.


    Converting EXE to COM Programs with Early Development Tools

    One reason that you may wish to produce an EXE without a stack is when you are using old versions of MASM and a linker that couldn't generate a COM program directly. In early versions of MASM there was no TINY model. To generate a COM program you created a SMALL model program that had no stack specified and no segment relocations and then you'd use the linker to generate the EXE program. If the EXE program meets the requirements of a COM program it can be converted from an 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 instruction to terminate the program.

    When loading a DOS COM program: If the DOS loader finds that there is a full 64KiB of memory available the value of SS will be set to the segment the DOS Program Segment Prefix (PSP) is in and it will set SP to 0x0000 and push 0x0000 on the stack. This is why you will often see an initial SS:SP in a DOS COM program start at CS:0xFFFE when viewed in the debugger.

    I do not know why in the Peter Norton book the value for SP is 0xFFEE (SP=FFEE) in the debug trace output. It looks unusual but is still valid. It may be the version of DOS he was using; the amount of memory available; or his debugger placed something else in the top 16 bytes above the 0x0000 return address.