sergiu reznicencu
sergiu reznicencu

Reputation: 1039

x86 assembly: Pass parameter to a function through stack

I'm trying to make a subprogram in assembly which will draw a square on the screen. I don't think I can pass parameters to the subprogram like I would do in C++, so I figured that I could use stack to store and access the parameters (I can't use the common data registers because there are too many variables to pass).

The problem is (I remember reading somewhere) that when I use the call command to the address of the current "program" it is saved on the stack, so that when it's used the "ret" command it would know where to return. But if I store something on the stack and then call the function, I will have to save somewhere the address (that is on the top of stack) and then safely pop the parameters. Then after the code has finished and before calling "ret", I would have to push back the address.

Am I right? And, if yes, where can I store the address (I don't think the address is only 1 byte long so that it would fit in AX or BX or any other data register). Can I use IP to do this (although I know this is used for something else)?

This is what I imagine:

[BITS 16]
....
main:
  mov ax,100b
  push ax
  call rectangle ;??--pushes on the stack the current address?

jml $

rectangle:
  pop ax ;??--this is the addres of main right(where the call was made)?
  pop bx ;??--this is the real 100b, right?
  ....
  push ax
ret ;-uses the address saved in stack

Upvotes: 7

Views: 24024

Answers (3)

cogitator
cogitator

Reputation: 17

I don't think I can pass parameters to the subprogram like I would do in C++ [...]

To pass parameters to a subroutine you can do the following trick as seen in the example below:

.486
assume cs:code, ds:data, ss:stack

macro_for_subroutine macro parameter1, parameter2
  push parameter1 ; [bp+6]
  push parameter2 ; [bp+4]
  call subroutine ; [bp+2] (return address pushed onto the stack)
endm

stack segment use16 para stack
  db 256 dup(' ')
stack ends

data segment use16
  value1 dw 0
  value2 dw 0
data ends

code segment use16 para public 'code'
start:
main proc far
  ; set up stack for return
  push ds
  mov ax, 0
  push ax
  ; ----

  ; set DS register to data segment
  mov ax, data
  mov ds, ax

  macro_for_subroutine 1111h, 2222h

  ret ; return to DOS
main endp

subroutine proc near
  push bp ; [bp+0]
  mov bp, sp

  push ax
  push bx

  mov ax, [bp+6]  ; parameter1
  mov value1, ax

  mov bx, [bp+4]  ; parameter2
  mov value2, bx

  pop bx
  pop ax

  pop bp
  ret 4  ; return and then increase SP by 4, because we
         ; pushed 2 parameters onto the stack from the macro
subroutine endp

code ends
end start

Note: This is written in 16-bit MASM DOS assembly.

Macros can accept parameters. Therefore, by defining a macro for a specific subroutine, you can simulate a call to a subroutine with parameters. Inside the macro, you push onto the stack the parameters in the desired order and then make a call to the subroutine.

You can't pass string variables, but you can pass their offset (for more, see: x86 assembly - masm32: Issues with pushing variable to stack).

Upvotes: 0

Peter Cordes
Peter Cordes

Reputation: 363999

This would work, except that from the caller's perspective, your function modifies sp. In 32bit most calling conventions, functions are only allowed to modify eax/ecx/edx, and must save/restore other regs if they want to use them. I assume 16bit is similar. (Although of course in asm you can write functions with whatever custom calling conventions you like.)

Some calling conventions expect the callee to pop the args pushed by the caller, so this would actually work in that case. The ret 4 in Matteo's answer does that. (See the tag wiki for info on calling conventions, and tons of other good links.)


It's super-weird, and not the best way to do things, which is why it isn't normally used. The biggest problem is that it only gives you access to the parameters in order, not random access. You can only access the first 6 or so args, because you run out of registers to pop them into.

It also ties up a register holding the return address. x86 (before x86-64) has very few registers, so this is Really Bad. You could push the return address after popping the other function args into registers, I guess, to free it up for use.

jmp ax would technically work instead of push/ret, but this defeats the return-address predictor, slowing down future ret instructions.


But anyway, making a stack frame with push bp / mov bp, sp is universally used in 16bit code because it's cheap and gives you random-access to the stack. ([sp +/- constant] isn't a valid addressing mode in 16 bit (but it is in 32 and 64bit). ([bp +/- constant] is valid). Then you can re-load from them whenever you need.

In 32 and 64bit code, it's common for compilers to use addressing modes like [esp + 8] or whatever, instead of wasting instructions and tying up ebp. (-fomit-frame-pointer is the default). It means you have to keep track of changes to esp to work out the right offset for the same data in different instructions, so it's not popular in hand-written asm, esp in tutorials / teaching material. In real code you obviously do whatever is most efficient, because if you were willing to sacrifice efficiency you'd just use a C compiler.

Upvotes: 3

Matteo Italia
Matteo Italia

Reputation: 126777

Typically, you use the base pointer (bp on 16 bit, ebp on 32 bit) to refer to parameters and locals.

The basic idea is that every time you enter into a function you save the stack pointer inside the base pointer, to have the stack pointer at when the function was called as a "fixed reference point" throughout execution of the function. In this schema [ebp-something] typically is a local, [ebp+something] is a parameter.

Transposing the typical 32-bit, callee-cleanup calling conventions you can do like this:

caller:

push param1
push param2
call subroutine

subroutine:

push bp       ; save old base pointer
mov bp,sp     ; use the current stack pointer as new base pointer
; now the situation of the stack is
; bp+0 => old base pointer
; bp+2 => return address
; bp+4 => param2
; bp+6 => param1
mov ax,[bp+4] ; that's param2
mov bx,[bp+6] ; that's param1
; ... do your stuff, use the stack all you want,
; just make sure that by when we get here push/pop have balanced out
pop bp        ; restore old base pointer
ret 4         ; return, popping the extra 4 bytes of the arguments in the process

Upvotes: 14

Related Questions