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
就是这样做的。 (请参阅 x86 标签 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)。
我试图在汇编中编写一个子程序,它将在屏幕上绘制一个正方形。我不认为我可以像在 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
就是这样做的。 (请参阅 x86 标签 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)。