x86 程序集:通过堆栈将参数传递给函数

x86 assembly: Pass parameter to a function through stack

我试图在汇编中编写一个子程序,它将在屏幕上绘制一个正方形。我不认为我可以像在 C++ 中那样将参数传递给子程序,所以我想我可以使用堆栈来存储和访问参数(我不能使用公共数据寄存器,因为变量太多了经过)。

问题是(我记得在某处读过)当我使用调用命令到当前 "program" 的地址时,它被保存在堆栈中,所以当它被使用时 "ret"命令它会知道去哪里 return。但是如果我在堆栈上存储一些东西然后调用函数,我将不得不在某处保存地址(在堆栈顶部)然后安全地弹出参数。然后在代码完成后调用 "ret" 之前,我将不得不推回地址。

我说的对吗?而且,如果是,我可以在哪里存储地址(我不认为地址只有 1 个字节长,所以它适合 AX 或 BX 或任何其他数据寄存器)。我可以使用 IP 来执行此操作吗(虽然我知道这是用于其他用途)?

这是我想象的:

[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

通常,您使用基指针(bp 在 16 位上,ebp 在 32 位上)来引用参数和局部变量。

基本思想是每次你进入一个函数时,你将堆栈指针保存在基指针中,以便在函数的整个执行过程中将堆栈指针作为 "fixed reference point" 调用函数时的指针.在此模式中,[ebp-something] 通常是本地的,[ebp+something] 是参数。

转换典型的 32 位被调用方清理调用约定,您可以这样做:

来电者:

push param1
push param2
call 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

这会起作用,只是从调用者的角度来看,您的函数修改了 sp。在 32 位大多数调用约定中,函数只允许修改 eax/ecx/edx,并且如果他们想使用它们必须 save/restore 其他 regs。我假设 16 位是相似的。 (尽管当然在 asm 中您可以使用您喜欢的任何自定义调用约定来编写函数。)

一些调用约定期望被调用者弹出调用者推送的参数,所以在这种情况下这实际上是可行的。 Matteo 的回答中的 ret 4 就是这样做的。 (请参阅 标签 wiki 了解有关调用约定的信息以及大量其他好的链接。)


它非常奇怪,而且不是做事的最佳方式,这就是它通常不被使用的原因。 最大的问题是它只能让你按顺序访问参数,而不是随机访问。您只能访问前 6 个左右的参数,因为您 运行 无法将它们弹出到寄存器中。

它还绑定了一个保存 return 地址的寄存器。 x86(x86-64 之前)的寄存器很少,所以这真的很糟糕。我想,您可以在将其他函数参数弹出到寄存器后推送 return 地址,以释放它以供使用。

jmp ax 在技术上可以代替 push/ret,但这会破坏 return-地址预测器,从而减慢未来的 ret 指令。


但是无论如何,使用 push bp / mov bp, sp 制作堆栈帧在 16 位代码中普遍使用,因为它很便宜并且可以随机访问堆栈. ([sp +/- constant] 在 16 位中不是有效的寻址模式(但在 32 位和 64 位中是有效的)。([bp +/- constant] 是有效的)。然后你可以在需要时从它们重新加载。

在 32 位和 64 位代码中,编译器通常使用 [esp + 8] 之类的寻址模式,而不是浪费指令和占用 ebp。 (-fomit-frame-pointer 是默认值)。这意味着您必须跟踪对 esp 的更改,以便在不同的指令中计算出相同数据的正确偏移量,因此它在手写 asm 中并不流行,尤其是在教程/教学中 material。在实际代码中,您显然会做最有效的事情,因为如果您愿意牺牲效率,您只需使用 C 编译器即可。

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

要将参数传递给子例程,您可以执行以下技巧,如下例所示:

.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

注意:这是用16位MASM DOS汇编写的。

宏可以接受参数。因此,通过为特定的子程序定义宏,可以模拟调用带参数的子程序。 在宏内部,您按所需顺序将参数压入堆栈,然后调用子例程。

您不能传递字符串变量,但可以传递它们的偏移量(更多信息,请参见:x86 assembly - masm32: Issues with pushing variable to stack)。