繁体   English   中英

堆栈和堆栈框架采用低级语言

[英]The stack and stack frames in a low level language

我试图围绕函数调用的概念,因为它们与堆栈有关。 这个问题是在低级语言的背景下提出的,而不是高级语言。

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

是...

假设您有一种允许递归的C语言。 为了使其工作,函数的每个实例必须自包含该函数的其他实例。 堆栈是完美的地方,因为代码可以在分配中“分配”和引用项而不需要知道物理地址,所有这些都是通过引用访问的。 您所关心的只是在函数的上下文中跟踪该引用,并将堆栈指针恢复到您输入函数时的位置。

现在你必须有一个调用约定,一个适合递归的等等。两个流行的选择(使用简化模型)是寄存器传递和堆栈传递。 你可以拥有并且实际上将实际存在混合(基于寄存器,你将用完寄存器并且必须恢复到其余参数的堆栈)。

假设我正在谈论的虚构硬件神奇地处理返回地址而不会弄乱寄存器或堆栈。

注册传递。 定义一组特定的硬件/处理器寄存器来保存参数,假设r0始终是第一个参数,r1是第二个参数,r2是第三个参数。 并且假设返回值是r0(这是简化的)。

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

为什么要声明一个调用约定? 这样调用者和被调用者都知道规则是什么以及在何处查找参数。 寄存器传递在表面看起来很棒,但是当你用完寄存器时,你必须将东西保存在堆栈中。 当你想从被调用者变成另一个函数的调用者时,你可能必须保留调用寄存器中的项目,这样你就不会丢失这些值。 你在堆栈上。

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

在调用more_fun之后使用a,b和c,more_fun至少需要r0和r1来传递参数a和c所以你需要将r0和r1保存在某处,以便你可以1)使用它们来调用more_fun ()和2)这样你就不会丢失从more_fun()返回后需要的值a和b。 您可以将它们保存在其他寄存器中,但是如何保护这些寄存器不被被调用的函数修改。 最终,东西被保存在堆栈中,这是动态的,并且通过引用而不是物理地址来访问。 所以

有人想打电话给myfun我们正在使用注册表。

r0 = a
r1 = b
r2 = c
call myfun
;return value in r0

myfun:
r0 = r0 + r1 (a = a + b)
;save a and b so we dont lose them
push r0 (a)
push r1 (b)
r0 = r0 (a) (dead code, can be optimized out)
r1 = r2 (c)
call more_fun
;morefun returns something in r0
pop r1 (recover b)
r1 = r1 + r0 (b = b+return value)
pop r0 (recover a)
;r0 is used for returning a value from a function
r0 = r0 + r1 (= a+b)
r0 = r0 + r2 (=(a+b)+c)
return

调用函数(调用者)知道在r0,r1,r2中准备三个参数并在r0中取一个返回值。 被调用者知道接受r0,r1,r2作为传入参数并返回r0并且它知道当它成为某个其他函数的调用者时它必须保留一些东西。

如果我们使用堆栈来使用我们的调用约定传递参数

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

现在我们必须制定一些注册规则,我们是否定义了调用规则来说明1)你可以销毁任何寄存器(但sp和pc和psr),2)你必须保留每个寄存器,这样当你返回时调用函数永远不会看到它的寄存器发生了变化,或者你是否定义了3)某些寄存器是临时的,可以随意修改,如果使用则必须保留一些寄存器。 我要说你可以销毁除sp,pc和spr之外的寄存器以简化。

我们还有一个问题需要解决。 谁清理堆栈? 当我调用morefun时,我在堆栈中有两个项目进入,只有出路的返回值,清理堆栈。 有两个选择,来电清理,被叫清洁,我去打电话清理。 这意味着被调用者必须以堆栈的方式从函数中返回,它会在堆栈上留下任何东西,并且它不会从堆栈中取出太多东西。

呼叫者:

push c
push b
push a
call myfun
pop result
pop and discard
pop and discard

假设使用此硬件,堆栈指针sp指向堆栈上的当前项

myfun:
;sp points at a
load r0,[sp+0] (get a)
load r1,[sp+1] (get b)
add r0,r1 (a = a+b)
store [sp+0],r0 (the new a is saved)
;prepare call to more_fun
load r0,[sp+2] (get c)
load r1,[sp+0] (get a)
push r0 (c)
push r1 (a)
call more_fun
;two items on stack have to be cleaned, top is return value
pop r0 (return value)
pop r1 (discarded)
;we have cleaned the stack after calling more_fun, our offsets are as
   ;they were when we were called
load r1,[sp+1] (get b)
add r1,r0 (b = b + return value)
store [sp+1],r1
load r0,[sp+0] (get a)
load r1,[sp+1] (get b)
load r2,[sp+2] (get c)
add r0,r1 (=a+b)
add r0,r2 (=(a+b)+c)
store [sp+0],r0 (return value)
return 

所以我在飞行中写了所有这些可能有一个bug。 所有这一切的关键是你必须定义一个调用约定,如果每个人(调用者和被调用者)遵循调用约定,它使编译变得容易。 诀窍在于制定一个有效的调用约定,正如您在上面所看到的,我们必须修改约定并添加规则以使其即使对于这样一个简单的程序也能工作。

堆栈框架怎么样?

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

使用基于堆栈

呼叫者

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

被调用者,如果它使用堆栈并移动堆栈指针,它必须将其放回调用它的位置。 您可以通过在堆栈上添加适当数量的东西来创建堆栈帧以进行本地存储。 您可能有局部变量,并且通过编译过程,您可能提前知道您还必须保留一定数量的寄存器。 最简单的方法是只添加所有内容并将堆栈指针移动一次以用于整个函数,并在返回之前将其重新放回一次。 您可以更聪明地继续移动堆栈指针,随着时间的推移调整偏移量,编码更加困难,更容易出错。 像gcc这样的编译器倾向于将堆栈指针移动到函数中并在离开之前返回它。

一些指令集在通话时向堆栈添加内容并在返回时将其删除,您必须相应地调整偏移量。 同样,围绕调用另一个函数的创建和清理可能需要处理与堆栈的硬件使用相关的处理(如果有的话)。

让我们说当你打电话时硬件推送堆栈顶部的返回值。

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)

某些处理器在调用函数时使用寄存器来保存返回值,有时硬件指示哪个寄存器,有时编译器选择一个并将其用作约定。 如果你的函数没有调用任何其他函数,你可以不使用返回地址寄存器并将它用于返回,或者你可以在某个时候将它推到堆栈上,然后在返回之前弹出它然后用它来返回。 如果你的函数确实调用了另一个函数,你必须保留该返回地址,这样对下一个函数的调用不会破坏它,你也找不到回家的路。 因此,如果可以,请将其保存在另一个寄存器中,或将其保存在堆栈中

使用我们定义的上述寄存器调用约定,加上一个名为rx的寄存器,当进行调用时,硬件会将返回地址放在rx中。

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

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

在具有少量寄存器的8088/8086上,出现了两个主要约定:C约定,它指定调用者应该在调用函数之前将参数压入堆栈并在之后将其弹出(意味着唯一的事情是调用函数应该弹出堆栈是返回地址)和Pascal约定,它指定被调用的函数除弹出返回地址外还应弹出其所有传递的参数。 popping the return address. The one disadvantage of the Pascal convention was that it required the called function to know how many bytes' worth of parameters were going to be passed. If the called function didn't pop exactly the right number of bytes, stack corruption was almost certain to occur. 在8086上,Pascal约定通常允许稍微更小的代码(因为堆栈清理只需要为每个可调用函数发生一次,而不是每次调用函数一次,并且因为8086包含一个版本的RET,它将指定的值添加到弹出返回地址的堆栈指针.Pascal约定的一个缺点是它需要被调用的函数知道将传递多少字节的参数。如果被调用的函数没有正确地弹出正确的数字字节数,堆栈损坏几乎肯定会发生。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM