Search code examples
assemblystackx86-16tasmdosbox

Pop inside procedure does not work as intended


I have next code in TASM:

.MODEL SMALL
.STACK 128
.DATA
    msg        DB 'Message$'
    crlf       DB 0Dh, 0Ah, '$'
.CODE

print_string proc
    pop dx
    mov ah, 09h
    int 21h
    ret
print_string endp

Entry:
    mov ax, @data
    mov ds, ax
    
    push offset msg
    call print_string

    mov ax, 4c00h
    int 21h

END Entry

I have a procedure that retrieves a message pointer from the stack for printing, but it prints weird symbols into console.
If I inline my procedure into code like this:

push offset msg
pop dx
mov ah, 09h
int 21h

it will work correctly and output my message into console.
Can someone explain why popping the same value from stack inside procedure results in non-expected behaviour?


Solution

  • push, call, pop, ret all impact the stack

    push offset msg
    call print_string
    

    After these 2 instructions have been executed the stack looks like:

    R, R, M, M, ...          R = Return address (2 bytes)
    ^                        M = Message pointer (2 bytes)
    sp
    

    The instruction that runs next is that pop dx. The stack will now look like:

          M, M, ...
          ^
          sp
    

    Contrary to what you intended, the DX register now contains the return address for the call/ret instructions. That's not a valid pointer to a message and so you see garbage appear on the screen.
    And when then the ret instruction runs, it will pop off the stack some word that isn't a return address at all.

    Solution 1

    Move the return address temporarily away. Next code pops it into AX that we're going to clobber anyway:

    print_string proc
      pop  ax            ; return address
      pop  dx            ; message pointer (removes the argument from the stack)
      push ax            ; return address
      mov  ah, 09h
      int  21h
      ret
    print_string endp
    

    Solution 2

    Use a stackframe pointer.

    print_string proc
      push bp            ; preserving BP
      mov  bp, sp
      mov  dx, [bp+4]    ; message pointer
      mov  ah, 09h
      int  21h
      pop  bp            ; restoring BP
      ret  2             ; remove the argument from the stack
    print_string endp
    

    Because we want to preserve BP, we used the push bp instruction which made the stack look like:

    B, B, R, R, M, M, ...
    ^           ^
    sp          |
    bp          |
    <--- +4 --->|
    

    We retrieve the message pointer at offset +4 from the new stackpointer.

    Solution 3

    Use a stackframe pointer.

    print_string proc
      xchg bp, ax        ; preserving BP in the AX register
      mov  bp, sp
      mov  dx, [bp+2]    ; message pointer
      xchg bp, ax        ; restoring BP from the AX register
      mov  ah, 09h
      int  21h
      ret  2             ; remove the argument from the stack
    print_string endp
    

    Here we preserve BP in the AX register that the code is going to clobber anyway. The stack looks like:

    R, R, M, M, ...
    ^     ^
    sp    |
    bp    |
    < +2 >|
    

    We retrieve the message pointer at offset +2 from the new stackpointer.

    The last examples used ret 2 to remove the stacked argument. Alternatively, you could remove the argument once returned to the caller:

    push offset msg
    call print_string
    add  sp, 2