我正在学习assembly
,让我有点困惑的主题是function
调用约定。本站上有一个简单函数定义和调用的例子,代码如下:
section .text
global _start ;must be declared for using gcc
_start: ;tell linker entry point
mov ecx,'4'
sub ecx, '0'
mov edx, '5'
sub edx, '0'
call sum ;call sum procedure
mov [res], eax
mov ecx, msg
mov edx, len
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov ecx, res
mov edx, 1
mov ebx, 1 ;file descriptor (stdout)
mov eax, 4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
sum:
mov eax, ecx
add eax, edx
add eax, '0'
ret
section .data
msg db "The sum is:", 0xA,0xD
len equ $- msg
segment .bss
res resb 1
字符串
我想知道为什么没有标准的函数prolouge:
push ebp ; save previous stackbase-pointer register
mov ebp, esp ; ebp = esp
型
和函数尾声?
如果没有它怎么工作?上面的代码是什么样的程序集风格?NASM
,MASM
还是别的什么?
抱歉,如果这个问题有点蹩脚,但我是一个新手。Thx用于解释。
1条答案
按热度按时间tyu7yeag1#
让我们假设你有这个堆栈(图片来自维基百科):
x1c 0d1x的数据
要调用DrawSquare函数,首先必须将其参数压入堆栈。那你呢
字符串
它的作用是
型
所以你现在在堆栈上也有了返回地址。
然后,您刚才调用的函数会建立自己的堆栈帧。
型
保存前一个函数的基指针(稍后会更清楚为什么);然后,
型
将基指针移动到堆栈指针当前所在的位置(本例中为推送的基指针),使基指针指向前一个基指针。
这样,我们就可以自由地移动堆栈指针,并在完成后恢复它。我为什么这么说因为函数中非常重要的一点是局部变量,你将把它们存储在堆栈中。
所以你为你的局部变量腾出空间(你也可以压入它们,但通常你是这样做的),由于堆栈向下增长,你通过减去堆栈指针来向下移动堆栈指针。
型
然后你可以通过从基址指针中减去来访问这些局部变量(这就是为什么我们需要它在函数帧的开头保持静止)
型
也可以通过基址指针访问参数
型
然后我们决定调用另一个函数DrawLine。
所以我们把它的参数压入栈中,就像我们之前做的一样;宣布死亡
型
函数初始化它的栈帧就像以前一样
型
完成后,只需重置栈指针和基指针,就可以关闭其调用帧
型
那么函数也将使用(if instdcall)(使用cdecl(这是另一种调用约定)调用者会清除参数)
型
“清除”其参数,并将指令指针恢复到进行调用的位置。实际上,eip在清除参数之前就恢复了,因为参数在堆栈上的eip之下;但是我们自己不能这样做,因为在恢复eip(返回)之后,我们不能在函数中做更多的事情,所以对于
ret 8
,我们告诉处理器做型
和/或
型
与此同时
然后DrawSquare函数在完成时做同样的事情。
^这是常规(stdcall)调用堆栈帧的工作方式^
在你的例子中,_start甚至不是一个函数,它是程序的入口点。实际上,它没有返回地址推送到堆栈上,因此esp实际上指向argc。
sum函数没有任何局部变量,因此函数框架只会减慢程序速度并占用更多空间。
由于栈帧只在访问栈上的参数和/或局部变量时有用(这两件事也可以通过只使用esp而不使用帧来完成,但是当添加局部变量时,通常使用ebp,因为它不像esp那样移动),所以这里不需要它。
sum函数所做的是通过寄存器传递参数,如果参数很少,这很好。