x86 assembly: Pass parameter to a function through stack
Asked Answered
H

3

7

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
Hebron answered 29/5, 2016 at 15:9 Comment(0)
V
14

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
Vilipend answered 29/5, 2016 at 15:27 Comment(13)
Isn't BP already used in the subprogram for I don;t know what address? 'The 16-bit BP register mainly helps in referencing the parameter variables passed to a subroutine. The address in SS register is combined with the offset in BP to get the location of the parameter. BP can also be combined with DI and SI as base register for special addressing.' (from tutorialspoint.com/assembly_programming/assembly_registers.htm)Hebron
Why [BP+6]? I know [] are used when referring to an address..and if BP is the address of the subroutine (I guess) then [BP+6] will point to a command from the subroutine right? (I'm kinda new so I can be wrong..). And why 6? (I know that +1 means for example the next address that can point to a var or something else.)Hebron
So ..2 actually means 2 bytes right?...jump over 2 bytes not an entire address(I though +1 means jump over an whole address whatever it's lenght in bytes is..).Hebron
BP is not the address of the subroutine, it's the address of the position of the stack where the old value of BP is stored; all those offset relative to BP are just offsets in the stack, relative to where the stack was when I copied SP in BP. All the "2"s are because each push in 16 bit mode pushes a 2-byte entry on the stack.Vilipend
As I said..I'm kinda new...what does ' address of the position of the stack where the old value of BP is stored' mean?Hebron
See it like this; the current stack position is the location in memory that is pointed to by sp; when you do a push ax, you are actually doing mov [sp], ax (move ax into the memory location pointed by sp - similar to a pointer dereference in C) plus a sub sp,2. Now, with bp you are just taking a snapshot of where sp was when you entered the function - the mov bp, sp does just that. Then, using mov [bp+something] you can refer to positions in the stack relative to where the stack was when you entered the function.Vilipend
For some more detail and nice diagrams, you should look up "x86 stack frame" (although most material is about 32 bit stack frames, it's essentially the same).Vilipend
Thanks...I understand now..with 32 bits it would be +4 no?Hebron
By subtraction could SP get to 0?Hebron
+4: yes, it all gets multiplied by two; SP to 0: in theory yes, but typically it starts overwriting other important stuff before getting there (or, on a modern operating system, the OS kills the process when the stack goes outside its given boundaries - typically by placing some guard page at the end of the stack).Vilipend
@MatteoItalia Your answer has a few errors. You wrote: "In this schema [ebp+something] typically is a local, [ebp-something] is a parameter." The signs needs to be inversed! Locals use negative offsets, parameters use positive offsets.Revkah
@SepRoland: whops, sorry, I thought I fixed it throughout the answer but I missed that point. Fixed now, thank you!Vilipend
Note that the OP's idea does actually barely work, for simple functions with caller-pops. I added an answer to point this out. It's interesting when a newbie identifies a non-standard way to do something that turns out to actually work, even if it's terrible. :PKopple
K
3

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.

Kopple answered 30/5, 2016 at 1:20 Comment(0)
H
0

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).

Harlow answered 8/12, 2018 at 19:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.