【问题标题】:The stack and stack frames in a low level language低级语言中的栈和栈帧
【发布时间】:2012-05-30 21:14:09
【问题描述】:

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

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

【问题讨论】:

    标签: assembly stack


    【解决方案1】:

    是的……

    假设你有一种像 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);
    }
    

    a、b 和 c 在调用 more_fun 之后使用,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
    

    调用函数(caller)知道在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 
    

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

    堆栈帧呢?

    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
    

    【讨论】:

    • 感谢您的详细解释。
    【解决方案2】:

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

    在具有少量寄存器的 8088/8086 上,出现了两个主要约定:C 约定,它规定调用者应在调用函数之前将参数压入堆栈,然后将其弹出(意味着被调用函数唯一应该从堆栈中弹出的是返回地址)和 Pascal 约定,它指定被调用函数除了弹出返回地址外,还应该弹出所有传递的参数。在 8086 上,Pascal 约定通常允许代码稍小一些(因为堆栈清理只需要对每个可调用函数进行一次,而不是对每个函数调用一次,并且因为 8086 包含一个版本的 RET,它添加了一个指定的值到堆栈指针在弹出返回地址之后。Pascal 约定的一个缺点是它要求被调用函数知道将要传递多少字节的参数。如果被调用函数没有' 弹出的字节数不正确,堆栈损坏几乎肯定会发生。

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        猜你喜欢
        • 2013-05-17
        • 2017-02-20
        • 2016-02-29
        • 1970-01-01
        • 2012-01-19
        • 1970-01-01
        • 2015-11-07
        • 2019-12-12
        • 2013-11-05
        相关资源
        最近更新 更多