Assembly 低级语言中的堆栈和堆栈帧

Assembly 低级语言中的堆栈和堆栈帧,assembly,stack,Assembly,Stack,我正试图将我的头脑集中在函数调用的概念上,因为它们与堆栈相关。这个问题是在低级语言的背景下提出的,而不是在高级语言的背景下提出的 据我目前所知,当调用函数时,局部变量和参数存储在堆栈上的堆栈帧中。每个堆栈帧都与单个函数调用相关联。我不太清楚的部分是谁负责创建框架?我的程序是否应该查看程序中的函数声明并手动将局部变量复制到堆栈上的新框架?通常,处理器供应商或第一家为处理器开发流行语言编译器的公司将定义函数调用方在调用函数之前应该做什么(堆栈上应该是什么,各种寄存器应该包含什么,等等)以及被调用函数

我正试图将我的头脑集中在函数调用的概念上,因为它们与堆栈相关。这个问题是在低级语言的背景下提出的,而不是在高级语言的背景下提出的


据我目前所知,当调用函数时,局部变量和参数存储在堆栈上的堆栈帧中。每个堆栈帧都与单个函数调用相关联。我不太清楚的部分是谁负责创建框架?我的程序是否应该查看程序中的函数声明并手动将局部变量复制到堆栈上的新框架?

通常,处理器供应商或第一家为处理器开发流行语言编译器的公司将定义函数调用方在调用函数之前应该做什么(堆栈上应该是什么,各种寄存器应该包含什么,等等)以及被调用函数在返回之前应该做什么(包括恢复某些寄存器的值,如果它们已经更改,等等)。对于某些处理器,多个约定已变得流行,确保任何给定函数的代码将使用调用代码所期望的约定通常非常重要

在寄存器数量较少的8088/8086上,出现了两个主要约定:C约定,该约定指定调用方在调用函数之前将参数推送到堆栈上,然后将其弹出(这意味着被调用函数应该弹出堆栈的唯一内容是返回地址),以及Pascal约定,该约定指定被调用函数除了弹出返回地址外,还应弹出其所有传递的参数。在8086上,Pascal约定通常允许使用更小的代码(因为堆栈清理只需要为每个可调用函数执行一次,而不是为每个函数调用执行一次,并且因为8086包含一个RET版本,该版本在弹出返回地址后向堆栈指针添加一个指定值。Pascal约定的一个缺点是它要求被调用函数知道如何操作。)将要传递y字节的参数值。如果调用的函数没有弹出正确的字节数,则几乎肯定会发生堆栈损坏


在许多较新的处理器上,具有少量固定参数的例程通常不会将其参数推送到堆栈上。相反,编译器供应商将指定在调用函数之前将前几个参数放入寄存器。这通常比使用堆栈基实现的性能更好d参数。但是,具有许多参数或变量参数列表的例程必须仍然使用堆栈。

要对supercat的答案进行一点扩展,设置堆栈帧是调用和被调用函数的共同责任。堆栈帧通常指的是例程特定调用的所有本地数据。然后,调用例程通过首先将任何基于堆栈的参数推送到堆栈上,然后通过调用例程来生成返回地址,从而构建外部堆栈帧。然后,被调用例程通过(通常)推(保存)来构建剩余的堆栈帧(内部堆栈帧)堆栈上的当前帧指针,并设置一个指向下一个空闲堆栈插槽的新指针。然后,它为堆栈上的局部变量保留堆栈,并且根据使用的语言,也可能在此时初始化它们。然后,帧指针可用于访问基于堆栈的参数和局部变量,其中一个具有负,另一个偏移量为正。退出例程时,旧堆栈帧将恢复,本地数据和参数将弹出,如supercat所述。

int myfun ( int a, int b, int c)
{
    a = a + b;
    b+=more_fun(a,c)
    return(a+b+c);
}
int onefun ( int a, int b )
{
    return(a+b)
}

onefun:
;because of the hardware
;sp+0 return address
;sp+1 a
;sp+2 b
load r0,[sp+1] (get a)
load r1,[sp+2] (get b)
add r1,r2
;skipping over the hardware use of the stack we return on what will be the
;top of stack after the hardware pops the return address
store [sp+1],r1 (store a+b as return value)
return (pops return address off of stack, calling function pops the other two 
   ;to clean up)
假设你有一种像C这样的语言,它允许递归。要做到这一点,一个函数的每个实例都必须与该函数的其他实例相独立。堆栈是代码可以“分配”的最佳位置在不知道物理地址的情况下,分配中的引用项都是通过引用访问的。您所关心的是在函数上下文中跟踪该引用,并将堆栈指针还原到输入函数时的位置

现在,您必须有一个调用约定,一个适合递归等。两个流行的选择(使用简化模型)是寄存器传递和堆栈传递。实际上,您可以有并且实际上将有混合(基于寄存器的,您将耗尽寄存器,并且必须为剩余参数返回堆栈)

假设我所说的虚拟硬件能够神奇地处理返回地址,而不会弄乱寄存器或堆栈

int myfun ( int a, int b, int c)
{
    a = a + b;
    b+=more_fun(a,c)
    return(a+b+c);
}
int onefun ( int a, int b )
{
    return(a+b)
}

onefun:
;because of the hardware
;sp+0 return address
;sp+1 a
;sp+2 b
load r0,[sp+1] (get a)
load r1,[sp+2] (get b)
add r1,r2
;skipping over the hardware use of the stack we return on what will be the
;top of stack after the hardware pops the return address
store [sp+1],r1 (store a+b as return value)
return (pops return address off of stack, calling function pops the other two 
   ;to clean up)
寄存器传递。定义一组特定的硬件/处理器寄存器来保存参数,假设r0总是第一个参数,r1是第二个,r2是第三个。并且假设返回值是r0(这是简化的)

堆栈传递。让我们定义在堆栈上推送的第一件事是最后一个参数,然后是最后一个参数。当返回时,让我们说返回值是堆栈上的第一件事

为什么要声明调用约定?这样调用者和被调用者都能准确地知道规则是什么以及在哪里找到参数。寄存器传递表面上看起来很好,但当寄存器用完时,你必须在堆栈上保存内容。当你想从一个被调用者变成另一个函数的调用者时,你可能需要在调用寄存器中保留项,这样你就不会丢失这些值。你就在堆栈上了

int myfun ( int a, int b, int c)
{
    a = a + b;
    b+=more_fun(a,c)
    return(a+b+c);
}
int onefun ( int a, int b )
{
    return(a+b)
}

onefun:
;because of the hardware
;sp+0 return address
;sp+1 a
;sp+2 b
load r0,[sp+1] (get a)
load r1,[sp+2] (get b)
add r1,r2
;skipping over the hardware use of the stack we return on what will be the
;top of stack after the hardware pops the return address
store [sp+1],r1 (store a+b as return value)
return (pops return address off of stack, calling function pops the other two 
   ;to clean up)
a、 b和c在调用more_fun之后使用,more_fun至少需要r0和r1来传递参数a和c,因此您需要将r0和r1保存在某个地方,以便1)使用它们调用more_fun()和2)这样做
push b
push a
call myfun
pop result
pop and discard
;at this point sp+0 = a, sp+1 = b, but we need room for c, so
sp=sp-1 (provide space on stack for local variable c)
;sp+0 = c
;sp+1 = a
;sp+2 = b
load r0,[sp+1] (get a)
load r1,[sp+2] (get b)
add r0,r1
store [sp+0],r0 (store c)
load r0,[sp+1] (get a)
;r1 already has b in it
push r1 (b)
push r0 (a)
call more_fun
pop r0 (return value)
pop r1 (discarded to clean up stack)
;stack pointer has been cleaned, as was before the call
load r1,[sp+0] (get c)
add r1,r0 (c = c+return value)
store [sp+0],r1 (store c)(dead code)
sp = sp + 1 (we have to put the stack pointer back to where 
   ;it was when we were called
;r1 still holds c, the return value
store [sp+0],r1 (place the return value in proper place 
   ;relative to callers stack)
return
int onefun ( int a, int b )
{
    return(a+b)
}

onefun:
;because of the hardware
;sp+0 return address
;sp+1 a
;sp+2 b
load r0,[sp+1] (get a)
load r1,[sp+2] (get b)
add r1,r2
;skipping over the hardware use of the stack we return on what will be the
;top of stack after the hardware pops the return address
store [sp+1],r1 (store a+b as return value)
return (pops return address off of stack, calling function pops the other two 
   ;to clean up)
int myfun ( int a, int b)
{ 
   return(some_fun(a+b));
}

myfun:
;rx = return address
;r0 = a, first parameter
;r1 = b, second parameter
push rx ; we are going to make another call we have to save the return
        ; from myfun
;since we dont need a or b after the call to some_fun we can destroy them.
add r0,r1 (r0 = a+b) 
;we are all ready to call some_fun first parameter is set, rx is saved
;so the call can destroy it
call some_fun
;r0 is the return from some_fun and is going to be the return from myfun, 
;so we dont have to do anything it is ready
pop rx ; get our return address back, stack is now where we found it 
       ; one push, one pop
mov pc,rx ; return