Search code examples
assemblyx86nasmx86-16calling-convention

Accessing pushed args after calling a routine


I am learning x86 assembly using FreeDOS and nasm. I have this little test program that all it does is print A to the screen and exits.

It works fine if I don't use the Write routine.

But what seems to be happening is when I push A on to the stack then call Write it places the next IP on to the stack and when I pop off A in the routine I get the IP not the value I pushed.

I am sure it is something simple but I don't see the issue.

segment data                    ; start of data segment

segment code                    ; start of code segment
..start:

label1:
top:
        push 'A'
        call Write
        mov ah, 4ch
        mov al, 0
        int 21h

Write:
        pop dx
        mov ah, 02h
        int 21h
        ret
end:
        mov ah, 4ch             ;exit
        mov al, 0               ;exit code 0
        int 21h                 ;call intr

segment stack class=stack       ; start of stack segment
        resb 512

Solution

  • This is how it is supposed to work. Calling into a function pushes the return address on the stack. So, when your function is entered, the top of the stack will be return address and not what you previously pushed.

    In 32-bit code you could now just use the stack pointer directly to access the previously pushed value (something like [esp+4] or [esp+2] in 16-bit mode), but this is not possible with pure 16-bit assembly with only 16-bit addressing modes and their limited choice of registers (not including [sp]).

    The normal way is to set up bp as a frame pointer from which you have random access to your stack frame, including stack args or any local vars you reserve space for.

    Write:
        push bp            ; Save previous value of bp so it won't get lost
        mov bp, sp         ; Set bp ("base pointer") to current stack pointer position
    
        mov dx, [bp+4]     ; Get argument from stack
        mov ah, 02h
        int 21h
    
        mov sp, bp         ; Restore stack pointer
        pop bp             ; Restore value of base pointer
    
        ret 2              ; Indicate how many bytes should be popped from stack after return
    

    Instead of pop dx, we use mov dx, [bp+4] here. At this point, [bp] would be the previous bp value (since it was last pushed before bp was assigned to sp), [bp+2] would be the return address, and [bp+4] your first argument.

    (Remember that the stack grows downwards, that's why you need +4 and not -4 here.)

    Also, when you return, you have to be sure that the argument is removed from the stack. You can either let the caller clean up or use ret with the number of bytes to remove as argument. It's an extra sp += n after popping the return address. In your case, ret 2 would implement callee-pops for this function.