5

When I run a debug session using FreeDOS and DEBUG.EXE, I get a session like the following. I've added a description of what I saw as annotation on the image. Each step, I've printed out the stack contents. What's very confusing is why the stack is changing at all given the commands I am running – such as simple moves between registers.

Step 1: move AX into DS, and mysteriously, something starts eating the stack! what gives?

MOV DS, AX

Step 2: move a literal 0 value into DX, and weirdly a couple values on the stack start to change. Why on Earth is this happening?

MOV DX, 0000

... and so on. Look closely at the image to see how things change from step to step.

If anyone has a clue, I'd love to hear it.

debug session

8
  • 4
    This is probably off-topic here, even if you are using retro tooling.
    – tofro
    Commented May 28, 2018 at 14:20
  • 2
    What's retro about this is the reliance of interrupt service routines on the user's stack. Modern processors tend to have alternative hardware mechanisms for this, such a shadow stack pointer register that is switched to while entering in supervisor mode to service interrupts. From today's point of view, it would be bad form for the OS to have rely on the user program maintaining a proper stack pointer register and adequate stack space for servicing interrupts, given that a modern OS is protecting itself and other processes against malicious or accidental behaviors.
    – Erik Eidt
    Commented May 29, 2018 at 14:14
  • 2
    @ErikEidt I don't think that's enough to justify this as a "retrocomputing" question. The reason why this is happening is not "because you are using a retro OS", but because "you cannot rely on the memory below the stack pointer not changing". For example, I can imagine a "modern" OS where if those bytes are in a different page, they could go from "00" to "page not present" in between instructions (I don't know if that can happen in Windows; it would not occur to me to care because "I don't rely on the memory below the SP not changing"). Commented May 30, 2018 at 11:34
  • 1
    Sure, you don't want to rely on memory below the SP in general. However, the OP is not attempting to rely on memory below the SP, but rather understand this observed behavior relative to mere debugging. And I'm asserting that this particular behavior, due to its antiquated interrupt handling mechanism, is a "retro".
    – Erik Eidt
    Commented May 30, 2018 at 15:35
  • @EuroMicelli: Actually, modern PC platforms have made it "officially" safe for a function which doesn't use the stack or call other functions to use up to (IIRC) 512 bytes beyond the stack pointer for local variables without having to adjust SP before and after doing so, and so the fact that such usage isn't safe everywhere is verging on "retro". Further, on most modern debug platforms it is safe for code to abuse the stack pointer while interrupts are disabled, but the design of MS-DOS DEBUG isn't modern.
    – supercat
    Commented Jun 1, 2018 at 19:18

3 Answers 3

14

Note all your references to changing values are below the stack pointer, actually a free space. You are not expected to care about this area (stack grows towards lower addresses), as this is of no concern to your application.

Even if your computer is sitting at a debugger prompt and apparently is inactive, it isn't. It constantly runs through interrupt service routines, like the timer (or other peripherals) interrupt routines that constantly use stack memory, others will be the result of the debugger itself frequently looking for keypresses - What you see is simply the leftovers of these frequent activities in the free space of the stack (which is absolutely irrelevant for your application - The part of the stack your application is working with is above the stack pointer). Thus, the values you see are areas of free space that have been used and changed by such system routines, as you are sharing the stack with these routines. And, BTW, it is absolutely irrelevant what your appliction is doing in a debug step - These areas might also change if you are not even stepping through your own code.

The only thing you hopefully will never observe is values changing at and above the stack pointer or intermittent changes of the stack pointer itself because this is where your program's working memory is.

11
  • 1
    @ByronKatz: More modern debugging tools will often avoid disturbing anything near the application's stack, but doing that requires that either the processor has a separate stack pointer for the application and the debugger, or that the debugger emulates the behavior of a virtual CPU, which in the absence of hardware support would totally dog performance [probably by at least an order of magnitude].
    – supercat
    Commented Jun 1, 2018 at 21:02
  • 1
    @supercat this is not so much s function of the debugging tool, but rather of the OS, where the stack is, is normally not determined by the debugger.
    – tofro
    Commented Jun 7, 2018 at 18:52
  • 1
    @supercat I wouldn't want my debugger to put the stack somewhere else than the program stand-alone would put it - It stops being debugging once the debugger changes the behavior of the program. And I actually have never experienced one that did. Can you give a concrete example of one of those debuggers?
    – tofro
    Commented Jun 7, 2018 at 20:10
  • 1
    And how would you debug a program that accidentally runs the stack over its own data or even program segment with a debugger like that?
    – tofro
    Commented Jun 7, 2018 at 20:19
  • 2
    TD386 used virtual mode to be able to use memory above 1M for the debugger code itself (enabling debugging of programs that largely filled DOS memory space, with a close-to-zero footprint of the debugger in conventional memory) - To my knowledge, it had none of the features you mentioned. And, I would still be annoyed if it would move my stack around.
    – tofro
    Commented Jun 8, 2018 at 9:01
4

The cause of this change is the fact that you are using the 't' command to step through instructions. That command makes use of a CPU interrupt to regain control after the instruction completes. That interrupt stores state on the stack before transferring control to the interrupt handler. The state on the stack is what you are seeing.

0

According to the comment I recreated the same program by copying the source from page 243 of the PDF of Assembly Language Step by Step:

MyStack SEGMENT STACK
    db 64 dup ('STACK!!!')
MyStack ENDS

MyData SEGMENT
Eat1    db "Eat at Joe's...",'$'
Eat2    db "...ten million flies can't ALL be wrong!",'$'
CRLF    db 0Dh,0Ah,'$'
MyData ENDS

MyProg SEGMENT
    assume CS:MyProg,DS:MyData
Main    Proc
Start:
    mov ax, MyData
    mov ds, ax

    mov dx, offset Eat1
    call Writeln
    mov dx, offset Eat2
    call Writeln

    mov ah, 4Ch
    mov al, 0
    int 21h

Write   PROC
    mov ah, 09h
    int 21h
    ret
Write   ENDP

Writeln PROC
    call Write
    mov dx, OFFSET CRLF
    call Write
    ret
Writeln ENDP

Main    ENDP
MyProg  ENDS

    END Start

Note that I replaced the uses of lea dx, by mov dx, offset which older MASM versions may have done automatically. I then assembled this program using JWasm and linked it using WarpLink:

E:\>jwasm -Zm eat2.asm
JWasm v2.18, Apr  9 2023, Masm-compatible assembler.
Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved.
Source code is available under the Sybase Open Watcom Public License.

eat2.asm: 43 lines, 2 passes, 2 ms, 0 warnings, 0 errors
E:\>warplink eat2

WarpLink 2.61 (05/11/93 Alpha) Copyright 1989-93 Michael Devore.
All rights reserved.


E:\>load image size: 001K
E:\>

Next I ran this file in lDebug:

E:\>ldebug eat2.exe
lDebug (2024-03-29)
-re.append @dw ss:(sp - 11) & ~0F 200 - 1
-re
AX=0000 BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A2D ES=2A2D SS=2A3D CS=2A61 IP=0000 NV UP EI PL ZR NA PE NC
2A61:0000 B85D2A            mov     ax, 2A5D
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 214B 2121-5453 4341 214B 0000 STACK!!!STACK!..
-t
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A2D ES=2A2D SS=2A3D CS=2A61 IP=0003 NV UP EI PL ZR NA PE NC
2A61:0003 8ED8              mov     ds, ax
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0000 2A5D-0000 0003 2A61 3246 STAC..]*....a*F2
-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0005 NV UP EI PL ZR NA PE NC
2A61:0005 BA0000            mov     dx, 0000
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0003 2A5D-0000 0005 2A61 3246 STAC..]*....a*F2
-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0008 NV UP EI PL ZR NA PE NC
2A61:0008 E81100            call    001C
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0005 2A5D-0000 0008 2A61 3246 STAC..]*....a*F2
-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=01FE BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=001C NV UP EI PL ZR NA PE NC
2A61:001C E8F8FF            call    0017
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 2A5D 0000-001C 2A61 3246 000B STAC]*....a*F2..
-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0017 NV UP EI PL ZR NA PE NC
2A61:0017 B409              mov     ah, 09
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 2A5D 0000 0017-2A61 3246 001F 000B ST]*....a*F2....
-
AX=095D BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0019 NV UP EI PL ZR NA PE NC
2A61:0019 CD21              int     21
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  0017 095D 0000 0019-2A61 3246 001F 000B ..].....a*F2....
-
Eat at Joe's...AX=0924 BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=001B NV UP EI PL ZR NA PE NC
2A61:001B C3                retn
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 0924 0000-0266 0000 0000 0000 STAC$...f.......
2A3D:01F0  0000 2A5D 2A2D 001C-2A61 3246 001F 000B ..]*-*..a*F2....
-

Now let's examine each step in turn:

-re
AX=0000 BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A2D ES=2A2D SS=2A3D CS=2A61 IP=0000 NV UP EI PL ZR NA PE NC
2A61:0000 B85D2A            mov     ax, 2A5D
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 214B 2121-5453 4341 214B 0000 STACK!!!STACK!..

This is the initial state of the program. The zero just below ss:sp is from where DOS stores the value for ax during program loading. (The debugger automatically retrieves this value and uses it to initialise its reg_eax variable.)

-t
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A2D ES=2A2D SS=2A3D CS=2A61 IP=0003 NV UP EI PL ZR NA PE NC
2A61:0003 8ED8              mov     ds, ax
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0000 2A5D-0000 0003 2A61 3246 STAC..]*....a*F2

Let's examine the code that causes these stack writes. First, there's the interrupt 1 (single-step trace interrupt) entrypoint of the debugger:

        ; Interrupt 01h (single-step interrupt) handler.
iispentry intr1, 0, sharedentry1
    lframe int
    lenter
    push ax

The first three words are the interrupt stack frame's fl, cs, and ip in order, the 3246h is the flags, 2A61h is the code segment, and 0003h is the instruction pointer. This points at the next instruction, the mov ds, ax (same one as displayed by the register dump).

Then there's a 0000h. This is the bp value pushed by the lenter macro (from my macro collection lmacros2.mac). Then we get 2A5Dh which is the value of ax after the first instruction ran.

After the intr1 entrypoint is done, it branches to the shared intrtn function:

        ; Common interrupt routine.

        ; Housekeeping.
intrtn:
    cli             ; just in case
    pop word [cs:reg_eip]       ; recover things from stack
    pop word [cs:reg_cs]
    pop word [cs:reg_efl]
    mov word [cs:reg_ds], ds    ; ! word-aligned (AC flag)
    mov word [cs:reg_eax], ax   ; ! word-aligned (AC flag)
    mov ax, cs
    mov ds, ax          ; => lDEBUG_DATA_ENTRY
%ifn _RUN_ENTRY_SECTION
    ; (elided because this is code is not used)
%else
    jmp intrtn_entry
%endif

This eventually stores away the values of ss and esp then sets both to address different stacks in the debugger stack segment:

    mov word [reg_ss], ss   ; save stack position
_386    and word [reg_eip+2], byte 0    ; we're from real mode
    _386_o32        ; mov dword [reg_esp], esp
    mov word [reg_esp], sp
    mov ss, ax      ; mov ss, cs    ; (don't use the stack here)

; ...

    mov word [ss:reg_ds], ds    ; ! word-aligned (AC flag)
    mov word [ss:reg_eax], ax   ; ! word-aligned (AC flag)
    mov ax, ss
    mov ds, ax      ; mov ds, ss
@@:
%endif
    mov ax, 2
%ifn _ONLY386
    _386_jmps .386          ; -->
; ...

.386:
[cpu 386]
    mov esp, reg_gs+2

Then there's another zero, which is actually from when the debugger entered the application (before the application instruction ran). This is in the run function:

    mov byte [bInDbg], 0
    _386_o32        ; push dword [reg_efl]
    push word [reg_efl]
    _386_o32        ; push dword [reg_cs]   ; high word is zero
    push word [reg_cs]
    _386_o32        ; push dword [reg_eip]
    push word [reg_eip]
    test byte [reg_efl+1], 2    ; IF set?
    mov ds, word [reg_ds]       ; restore ds
    jz .di
    sti             ; required for ring3 protected mode if IOPL==0
.di:
%if _ONLYNON386
    iret
%else
..@patch_no386_iret_code_or_entry:
    o32         ; iretd
    iret                ; jump to program
%endif

On a 386 CPU, this actually restores efl, cs, and eip. The high word for cs is ignored and eip generally must be < 64 KiB in Real/Virtual 86 Mode. But the high word of efl should be preserved by the debugger so an o32 iret (also called iretd) is required.

Anyway, on running the instruction at address 0, the debugger pushes the efl dword to dword [ss:1FCh], the cs dword to dword [ss:1F8h], and the eip dword to dword [ss:1F4h]. So the word [ss:1F4h] gets the value of ip that the debugger returns to, which is 0000h for the very first trace step.

And on. Note that I entered an empty line to lDebug, which automatically repeats the prior T command. (Recent versions of FreeDOS Debug/X share this feature.)

-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0005 NV UP EI PL ZR NA PE NC
2A61:0005 BA0000            mov     dx, 0000
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0003 2A5D-0000 0005 2A61 3246 STAC..]*....a*F2

Looking at it from the top this is fl, cs, ip, bp, ax all from after running the prior instruction again. The next word is the ip of what was just executed at offset 0003h, set by run again.

Further on:

-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=0200 BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0008 NV UP EI PL ZR NA PE NC
2A61:0008 E81100            call    001C
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 0005 2A5D-0000 0008 2A61 3246 STAC..]*....a*F2

This again has fl, cs, ip, bp, ax, all from after running the prior instruction. And 0005h as the prior ip.

-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=01FE BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=001C NV UP EI PL ZR NA PE NC
2A61:001C E8F8FF            call    0017
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 4341 2A5D 0000-001C 2A61 3246 000B STAC]*....a*F2..

000Bh is the newly pushed near return address of the call 001C. After that the outgoing fl, cs, ip, bp, and ax. The low word of the incoming eip is not found here because the call pushed a word onto the stack, so the five register values written by lDebug's intr1 overwrote the entire iretd stack frame.

-
AX=2A5D BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0017 NV UP EI PL ZR NA PE NC
2A61:0017 B409              mov     ah, 09
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  5453 2A5D 0000 0017-2A61 3246 001F 000B ST]*....a*F2....

This shows another near return address word on the stack. Nothing else of note here.

-
AX=095D BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=0019 NV UP EI PL ZR NA PE NC
2A61:0019 CD21              int     21
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 214B 2121-5453 4341 214B 2121 STACK!!!STACK!!!
2A3D:01F0  0017 095D 0000 0019-2A61 3246 001F 000B ..].....a*F2....

Here we still have the two near return addresses on the stack, 000Bh then 001Fh. After that as usual fl, cs, ip, bp. Then we have a word reading 095Dh which is the new ax value also pushed by intr1, with the ah value now changed to 09h. After that the iretd frame's eip shows up again, as 0017h.

-
Eat at Joe's...AX=0924 BX=0000 CX=0266 DX=0000 SP=01FC BP=0000 SI=0000 DI=0000
DS=2A5D ES=2A2D SS=2A3D CS=2A61 IP=001B NV UP EI PL ZR NA PE NC
2A61:001B C3                retn
header     0    2    4    6    8    A    C    E    0123456789ABCDEF
2A3D:01E0  5453 4341 0924 0000-0266 0000 0000 0000 STAC$...f.......
2A3D:01F0  0000 2A5D 2A2D 001C-2A61 3246 001F 000B ..]*-*..a*F2....

For completeness: This still has 000Bh and 001Fh as the near return addresses. Then fl, cs, ip. A bp and ax value from intr1 are not on this stack because the int 21h instruction is proceeded past by writing an int3 instruction (single-byte opcode 0CCh). So the intr3 entrypoint is used.

After that there's some more values on the stack: the application's es, ds, bp, di, si, dx, cx, bx, ax.

This is from the kernel's interrupt 21h handler. It uses a macro defined in stacks.inc:

%macro  PUSH$ALL  0
                push    es
                push    ds
                push    bp
                push    di
                push    si
                push    dx
                push    cx
                push    bx
                push    ax
%endmacro

Note that the kernel returns al as 24h (the dollar sign terminator), as documented in the Interrupt List. You can also see here that the kernel uses this stack frame to return the ax value (and all other 16-bit 8086 registers) so we get the return value of the interrupt call, not the value on entry.


Finally, looking at your question's values:

7346h, 0D16h, 0003h are the interrupt stack frame. The next 0D16h is the low word of cs from the stack frame built for iretd. The 0000_0000h is the initial eip from the iretd frame.

Second step: 0005h is the returning ip, matching the offset of the disassembly at this point. 0000_0003h is the incoming eip of the iretd stack frame.

Subsequent steps work in the same way as I described for lDebug, except that like lDebug's intr3 the intr01 entry of FreeDOS Debug/X does not push anything to the user application stack before switching to the debugger's stacks.

I reviewed the source of v1.25 of FreeDOS Debug/X, which was released on 2011-08-07 so it is the latest version you could have used on 2018-05-28.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .