代码之家  ›  专栏  ›  技术社区  ›  Nishant Florentin

我们什么时候在函数中创建基指针?在局部变量之前还是之后?

  •  1
  • Nishant Florentin  · 技术社区  · 7 年前

    我正在读 Programming From Ground Up 书我看到了两个不同的例子 base pointer %ebp 从当前堆栈位置创建 %esp .

    在一种情况下,它是在局部变量之前完成的。

    _start:
            # INITIALIZE PROGRAM
            subl  $ST_SIZE_RESERVE, %esp       # Allocate space for pointers on the
                                               # stack (file descriptors in this
                                               # case)
            movl  %esp, %ebp
    

    这个 _start 然而,与其他函数不同,它是程序的入口点。

    在另一种情况下,它是在之后完成的。

    power:
            pushl %ebp           # Save old base pointer
            movl  %esp, %ebp     # Make stack pointer the base pointer
            subl  $4, %esp       # Get room for our local storage
    

    所以我的问题是,我们是否先为 local variables 在堆栈中创建 基本指针 或者首先创建 基本指针 然后为 ?

    即使我 把它们混在一起 在程序的不同功能中?一个函数在之前执行,另一个在之后执行,等等 C 创建机器代码时是否有特定的约定?

    我的推理是,函数中的所有代码都与 基本指针 ,那么,只要该函数遵循创建堆栈引用所依据的约定,它就可以正常工作?

    感兴趣的人很少有相关链接:

    Function Prologue

    2 回复  |  直到 7 年前
        1
  •  3
  •   Nishant Florentin    7 年前

    在第一种情况下,你不在乎保存——这是切入点。你在捣乱 %ebp 当你退出程序时,谁会关心寄存器的状态?这不再重要,因为您的应用程序已经结束。但在函数中,当您从该函数返回时,调用方肯定不希望 %ebp 垃圾。现在可以修改了吗 %esp 首先,然后保存 %ebp 然后使用 %ebp ? 当然,只要在函数的另一端以相同的方式展开,就可能根本不需要帧指针,通常这只是个人的选择。

    你只需要对世界有一个相对的了解。帧指针通常只是为了使编译器作者的工作更容易,实际上它通常只是为了浪费许多指令集的寄存器。也许是因为一些老师或教科书是这样教的,没有人问为什么。

    对于编码健全性、编译器作者的健全性等,如果您需要在函数期间使用堆栈来拥有一个基址,从该基址偏移到堆栈的某个部分,那么这是可取的。或者至少在设置之后和清理之前。这可以是堆栈指针(sp)本身,也可以是帧指针,有时从指令集可以明显看出。有些堆栈向下扩展(在地址空间中接近零),堆栈指针在中只能有正偏移量 sp 基于地址(理智)或一些消极的(疯狂)(不太可能,但让我们说它在那里)。因此,您可能需要一个通用寄存器。也许有一些你不能使用 服务提供商 在寻址时,您必须使用通用寄存器。

    总之,为了保持理智,您需要一个参考点来抵消堆栈中的项目,更痛苦但使用更少内存的方法是在进行时添加和删除内容:

    x is at sp+4
    push a
    push b
    do stuff
    x is at sp+12
    pop b
    x is at sp+8
    call something
    pop a
    x is at sp+4
    do stuff
    

    做更多的工作,但可以使程序(编译器)保持跟踪,并且比人工更不容易出错,但在调试编译器输出(人工)时,更难跟踪。因此,通常我们燃烧堆栈空间,并有一个参考点。帧指针可用于使用基指针(bp)分离传入参数和局部变量,例如,将其作为函数中的静态基址,以及 作为局部变量的基址(通过 服务提供商 如果指令集提供如此大的偏移量,则可以用于所有内容)。所以通过推动 bp 然后修改 服务提供商 服务提供商 可以四处走动,也许是为了当地的东西(虽然通常不理智),并且 英国石油公司 如果这是一种调用约定,规定所有参数都在堆栈上(通常在没有很多通用寄存器的情况下),则可以用作获取参数的静态位置。有时,您会看到参数被复制到堆栈上的本地分配,以供以后使用,但是,如果您有足够的寄存器,您可能会看到一个寄存器被保存在堆栈上并在函数中使用,而不需要使用基址和偏移量访问堆栈。

    unsigned int more_fun ( unsigned int x );
    unsigned int fun ( unsigned int x )
    {
        unsigned int y;
        y = x;
        return(more_fun(x+1)+y);
    }
    
    00000000 <fun>:
       0:   e92d4010    push    {r4, lr}
       4:   e1a04000    mov r4, r0
       8:   e2800001    add r0, r0, #1
       c:   ebfffffe    bl  0 <more_fun>
      10:   e0800004    add r0, r0, r4
      14:   e8bd4010    pop {r4, lr}
      18:   e12fff1e    bx  lr
    

    不要把你们在课本、白板(或StackOverflow中的答案)上看到的东西当成福音。仔细思考问题,并考虑其他选择。

    • 备选方案是否在功能上被破坏?
    • 它们在功能上是否正确?
    • 是否存在可读性等缺点?
    • 表演
    • 性能命中率是普遍的还是取决于如何 内存是慢还是快?
    • 备选方案是否会生成更多的代码,这会影响性能,但 也许代码是流水线的,而不是随机的内存访问?
    • 如果我不使用帧指针,架构是否让我重新获得 通用登记簿?

    在第一个示例中 正在被丢弃,这通常是不好的,但这是程序的入口点,没有必要保留 英国石油公司 (除非操作系统指示)。

    然而,在函数中,基于调用约定,我们假设 英国石油公司 由调用者使用并且必须保留,因此必须将其保存在堆栈上才能使用。在这种情况下,它似乎想用来访问调用程序在堆栈上传入的参数,然后 服务提供商 移动以腾出空间(以及可能的访问权限,但如果需要,则不一定需要 英国石油公司 可以使用)局部变量。

    服务提供商 先推后推 英国石油公司 ,你基本上会有两个指针,一个推离对方的宽度,这有什么意义吗?有两个帧指针有意义吗?如果有,让它们几乎相同的地址有意义吗?

    通过推动 英国石油公司 首先,如果调用约定最后推送第一个参数,那么作为编译器作者,您可以 bp+N 对于固定值N,始终或理想情况下始终指向第一个参数 bp+M 总是指向第二个。对我来说有点懒,但如果登记簿要被烧掉,那就烧掉它。。。

        2
  •  1
  •   Peter Cordes    7 年前

    _start 不是函数。这是你的切入点。没有返回地址,也没有呼叫者的值 %ebp 保存。

    这个 i386 System V ABI doc 建议(在第节中 2.3.1初始堆栈和寄存器状态 )您可能希望将%ebp设为零以标记最深的堆栈帧。(即在第一次 call 指令,因此保存 ebp 当第一个函数将 ebp公司 . 见下文)。

    C在创建机器代码时是否有特定的约定?

    不,与其他一些x86系统不同的是 i386 System V ABI 不需要太多关于堆栈帧布局的信息。(Linux使用System V ABI/调用约定,您正在使用的书(PGU)是针对Linux的。)

    在某些调用约定中,设置 ebp公司 不是可选的,必须按下函数输入序列 ebp公司 链接列表 允许异常处理程序(或调试器)回溯堆栈的堆栈帧。( How to generate the backtrace by looking at the stack values? ). 我认为这在32位Windows代码中是SEH(结构化异常处理)所必需的,至少在某些情况下是这样,但我不知道细节。

    i386 SysV ABI定义了一种用于堆栈展开的替代机制,该机制使用另一节中的元数据使帧指针成为可选的( .eh_frame and .eh_frame_hdr 其中包含由创建的元数据 .cfi_... 汇编程序指令,理论上,如果希望通过函数展开堆栈来工作,您可以自己编写这些指令。i、 e.如果您调用任何C++代码 throw 工作。)

    如果您想在当前gdb中使用传统的帧漫游,您必须自己定义一个gdb函数,如 gdb backtrace by walking frame pointers Force GDB to use frame-pointer based unwinding . 或者如果你的可执行文件没有 .eh\U框架 第节, gdb will use the EBP-based stack-walking method .

    如果使用 gcc -fno-omit-frame-pointer ,您的调用堆栈将具有此链表属性,因为当C编译器 制作合适的堆叠框架,他们推动 ebp公司 第一

    IIRC, perf 具有在分析时使用帧指针链获取回溯的模式,显然这比默认模式更可靠 .eh\U框架 正确计算哪些函数使用了最多的CPU时间。(或导致最多的缓存未命中、分支预测失误或其他性能计数器计数的情况。)


    即使我在一个程序的不同功能中把它们混合在一起,这两种方法难道都不管用吗?一个函数在之前执行,另一个在之后执行,等等。

    是的,它会工作得很好。事实上 setting up ebp at all is optional ,但当手写时,更容易有一个固定的基础(不像 esp 当你按下/弹出时,它会四处移动)。

    出于同样的原因,遵守 mov %esp, %ebp 一推(旧的)后 %ebp ),因此第一个函数arg始终位于 ebp+8 . 看见 What is stack frame in assembly? 按照惯例。

    但是您可以通过使用 ebp公司 点位于您保留的一些空间的中间,因此所有内存都可以通过 ebp + disp8 寻址模式可用。( disp8 是有符号8位位移:如果我们限制为4字节对齐位置,则为128到+124)。这节省了代码字节,而不需要disp32来达到更远的距离。所以你可以这么做

    bigfunc:
        push   %ebp
        lea    -112(%esp), %ebp   # first arg at ebp+8+112 = 120(%ebp)
        sub    $236, %esp         # locals from -124(%ebp) ... 108(%ebp)
                                  # saved EBP at 112(%ebp), ret addr at 116(%ebp)
                                  # 236 was chosen to leave %esp 16-byte aligned.
    

    或者延迟保存任何寄存器,直到为局部变量保留空间之后,这样我们就不会用掉任何位置(除了ret addr)和我们不想处理的保存值。

    bigfunc2:                     # first arg at 4(%esp)
        sub    $252, %esp         # first arg at 252+4(%esp)
        push   %ebp               # first arg at 252+4+4(%esp)
        lea    140(%esp), %ebp    # first arg at 260-140 = 120(%ebp)
    
        push   %edi              # save the other call-preserved regs
        push   %esi
        push   %ebx
                 # %esp is 16-byte aligned after these pushes, in case that matters
    

    leave 因为 esp = ebp 这是不对的。使用“正常”堆栈帧序列,您可以使用 mov ,然后使用 离开 . 或还原 esp 指向最后一次推送(带 add ),并使用 pop 说明。)

    但是如果你要这么做,使用 ebp公司 而不是 ebx 或者别的什么。事实上,使用 ebp公司 :the 0(%ebp) 寻址模式要求disp8为0,而不是无位移,但 %ebx 不会。所以使用 %ebp 对于非指针暂存寄存器。或者至少有一个在没有位移的情况下不会取消引用的。(这种怪癖与实际帧指针无关: (%ebp) 是保存的EBP值。顺便说一句,编码意味着 (%ebp) (12345) my_label )

    这些例子都是人为的;除非是数组,否则通常不需要太多的局部空间,然后使用索引寻址模式或指针,而不仅仅是相对于 ebp公司 . 但也许你需要一些32字节的AVX向量空间。在只有8个向量寄存器的32位代码中,这是合理的。

    不过,AVX512 compressed disp8在很大程度上击败了64字节AVX512向量的这一论点。(但32位模式下的AVX512仍然只能使用8个向量寄存器,即zmm0-zmm7,因此很容易溢出一些。在64位模式下,只能使用x/ymm8-15和zmm8-31。)