gcc 为什么堆栈指针和帧指针具有相同的地址?

euoag5mw  于 2022-11-30  发布在  其他
关注(0)|答案(1)|浏览(187)

根据Computer Organization and Design MIPS 5th ed第103页,我的印象是帧指针$fp被设置为堆栈的第一个字。

int func(int g) {
    int f = 9;
    return g+f;
}

mips gcc 12.2.0生成的汇编代码。

func:
        addiu   $sp,$sp,-24
        sw      $fp,20($sp)
        move    $fp,$sp
        sw      $4,24($fp)
        li      $2,9                        # 0x9
        sw      $2,8($fp)
        lw      $3,24($fp)
        lw      $2,8($fp)
        nop
        addu    $2,$3,$2
        move    $sp,$fp
        lw      $fp,20($sp)
        addiu   $sp,$sp,24
        jr      $31
        nop

如果$fp(帧指针)和$sp(堆栈指针)包含相同的地址,这是否有效?我认为$fp应该指向堆栈上的第一个字,即

func:
        addiu   $sp,$sp,-24
        sw      $fp,20($sp)
        addiu   $fp,$sp,20          #let $fp point to the first word on the stack

对于$fp应该指向堆栈的哪一部分,是否有任何规则,或者这完全取决于软件开发人员/编译器的决定?

xdnvmnnf

xdnvmnnf1#

愚者似乎遵循ABI链接的in this answer
ABI要求:
在堆栈指针寄存器的任何其它使用之前,必须调整堆栈指针以分配堆栈帧。
一个函数通过从进入该函数的$sp中减去堆栈帧的大小来分配堆栈帧。这个$sp调整必须在$sp在函数中使用之前以及在任何跳转或分支指令之前发生。
因此,不可能实现书中描述的帧指针,这需要一个move $fp, $sp,然后一个addiu $sp, $sp, XX
因此,愚者(未优化)根据此ABI生成的代码在帧下方有一个fp
ABI还规定了归航/阴影区:即使前四个args没有在堆栈上传递,调用程序也必须始终保留堆栈上的相应空间,以便被调用程序可以将args寄存器保存在该空间上。
通过查看指令sw $4,24($fp)并注意$fp + 24 = original $sp = just above the allocated frame,您可以看到此行为。
这意味着即使是非叶函数(调用其他函数的函数)通常也会有$sp = $fp,因为编译器知道它需要多少空间。
但是,您可以创建不满足此条件的情况,例如by using the infamous alloca

#include <alloca.h>

int bar(int, int, int, int, int);

int func(int g) {
    int f = 9;
    void* h = alloca(g);
    return bar(f, f, f, f, f);
}

此代码经过编译(未优化),可实现以下功能:

func:
        addiu   $sp,$sp,-48 #Reserve space for the frame
        sw      $31,44($sp) #Save ra in the highest slot
        sw      $fp,40($sp) #Save fp in the slow below
        move    $fp,$sp     #Set the frame pointer
        
        sw      $4,48($fp)  #Spill the first arg (g) in the homing space
        li      $2,9               
        sw      $2,32($fp)  #f = 9
        
        lw      $2,48($fp)  #g
        nop
        addiu   $2,$2,7
        srl     $2,$2,3
        sll     $2,$2,3     #(g + 7) / 8 * 8 = g aligned on a multiple of 8 (required by the ABI)
        
        subu    $sp,$sp,$2  #alloc g (aligned) bytes on the stack
        
        addiu   $2,$sp,24   #Make a pointer 24 bytes ABOVE the new stack pointer
                    #So there still are 16 byte (homing space) + 4 byte (5th arg to bar) + 4 bytes (alignment)
                    #free just above the stack pointer
                    #Note: we can steal this space from the g bytes allocated because there was a corresponding
                    #space in the frame initially allocated
        addiu   $2,$2,7
        srl     $2,$2,3
        sll     $2,$2,3     #Align this pointer to 8 bytes
        sw      $2,36($fp)  #h = that pointer
        
        lw      $2,32($fp)  #f
        nop
        sw      $2,16($sp)  #Note f is stored relative to SP
        lw      $7,32($fp)
        lw      $6,32($fp)
        lw      $5,32($fp)
        lw      $4,32($fp)  #args
        jal     bar     #call bar
        nop

        move    $sp,$fp     #Restore the stack pointer just below the frame
        lw      $31,44($sp)
        lw      $fp,40($sp) #Restore the regs
        addiu   $sp,$sp,48  #Restore the stack pointer 
        jr      $31
        nop

为了更好地理解正在发生的事情,绘制堆栈的状态可能是值得的。
一般来说,编译器使用的帧处理策略会随着时间的推移而变化,您必须对书籍中的内容持保留态度,因为没有人有时间或可能在每次发布新版本的编译器时更新和重新发布书籍。
只要一定要理解书中的例子是如何工作的,你就能很容易地适应新的约定。

相关问题