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



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

问题是(我记得在某处读到)当我对当前"程序"的地址使用 call 命令时,它会保存在堆栈上,因此当它使用"ret"命令时,它会知道在哪里返回。但是,如果我在堆栈上存储一些东西然后调用该函数,我将不得不将地址保存在某个地方(即堆栈顶部),然后安全地弹出参数。然后在代码完成后,在调用"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 位上)来引用参数和局部变量。

基本思想是,每次输入函数时,都将堆栈指针保存在基指针内,以便在函数的整个执行过程中将堆栈指针作为"固定参考点"调用函数时。在此架构中,[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,并且如果它们想使用它们,必须保存/恢复其他注册。 我假设 16 位是类似的。 (当然,在 asm 中,您可以使用您喜欢的任何自定义调用约定编写函数。

一些调用约定期望被调用方弹出调用方推送的参数,因此在这种情况下这实际上有效。 马泰奥回答中的ret 4就是这样做的。 (有关调用约定的信息以及大量其他良好的链接,请参阅 x86 标签 wiki。


非常奇怪,不是最好的做事方式,这就是为什么它通常不被使用的原因。最大的问题是它只允许您按顺序访问参数,而不是随机访问。 您只能访问前 6 个左右的参数,因为您用完了寄存器来弹出它们。

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

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


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

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

我认为

我不能像在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 程序集 - masm32:将变量推送到堆栈的问题)。

最新更新