如何在Assembly中正确编写函数?

ifmq2ha2  于 2023-08-06  发布在  其他
关注(0)|答案(1)|浏览(108)

我正在学习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


和函数尾声?
如果没有它怎么工作?上面的代码是什么样的程序集风格?NASMMASM还是别的什么?
抱歉,如果这个问题有点蹩脚,但我是一个新手。Thx用于解释。

tyu7yeag

tyu7yeag1#

让我们假设你有这个堆栈(图片来自维基百科):
x1c 0d1x的数据
要调用DrawSquare函数,首先必须将其参数压入堆栈。那你呢

call DrawSquare

字符串
它的作用是

push eip ; push the current instruction pointer on the stack
jmp DrawSquare


所以你现在在堆栈上也有了返回地址
然后,您刚才调用的函数会建立自己的堆栈帧

push ebp


保存前一个函数的基指针(稍后会更清楚为什么);然后,

mov ebp, esp


基指针移动到堆栈指针当前所在的位置(本例中为推送的基指针),使基指针指向前一个基指针
这样,我们就可以自由地移动堆栈指针,并在完成后恢复它。我为什么这么说因为函数中非常重要的一点是局部变量,你将把它们存储在堆栈中。
所以你为你的局部变量腾出空间(你也可以压入它们,但通常你是这样做的),由于堆栈向下增长,你通过减去堆栈指针来向下移动堆栈指针。

sub esp, 4 ; make room for 4 bytes
           ; or one double word variable (32 bit)
           ; or two one-word variables (2 x 16 bit)
           ; or 4 byte variables (4 x 8 bit)


然后你可以通过从基址指针中减去来访问这些局部变量(这就是为什么我们需要它在函数帧的开头保持静止)

mov [ebp - 4], 0xAABBCCDD ; move some random value (32 bit) into the new variable
                          ; subtract 4 from ebp because it's a 4-byte variable


也可以通过基址指针访问参数

mov eax, [ebp + 8] ; first 32 bit parameter
                   ; add 8 to ebp because
                   ; at [ebp] is the previous base pointer (32 bits)
                   ; and at [ebp + 4] is the return address (32 bits)
                   ; so 32 + 32 is 64, 8 bytes of offset to access the first parameter


然后我们决定调用另一个函数DrawLine。
所以我们把它的参数压入栈中,就像我们之前做的一样;宣布死亡

push ebx
push eax
call DrawLine


函数初始化它的栈帧就像以前一样

push ebp ; DrawSquare's base pointer
mov ebp, esp


完成后,只需重置栈指针基指针,就可以关闭其调用帧

mov esp, ebp ; moves the stack pointer to where the base pointer is.
             ; ebp still points to the old ebp
pop ebp ; pops the previous ebp value into ebp
        ; effectively restoring the previous functions' base pointer


那么函数也将使用(if instdcall)(使用cdecl(这是另一种调用约定)调用者会清除参数)

ret 8


“清除”其参数,并将指令指针恢复到进行调用的位置。实际上,eip在清除参数之前就恢复了,因为参数在堆栈上的eip之下;但是我们自己不能这样做,因为在恢复eip(返回)之后,我们不能在函数中做更多的事情,所以对于ret 8,我们告诉处理器做

pop eip ; restore esp from the stack, where the return address lies


和/或

add esp, 8 ; move the stack pointer before the parameters, effectively "clearing" them


与此同时
然后DrawSquare函数在完成时做同样的事情。

^这是常规(stdcall)调用堆栈帧的工作方式^

在你的例子中,_start甚至不是一个函数,它是程序的入口点。实际上,它没有返回地址推送到堆栈上,因此esp实际上指向argc。

sum函数没有任何局部变量,因此函数框架只会减慢程序速度并占用更多空间。

由于栈帧只在访问栈上的参数和/或局部变量时有用(这两件事也可以通过只使用esp而不使用帧来完成,但是当添加局部变量时,通常使用ebp,因为它不像esp那样移动),所以这里不需要它。

sum函数所做的是通过寄存器传递参数,如果参数很少,这很好。

相关问题