激活记录-C

Activation records - C

请考虑以下程序:

#include <stdio.h>

void my_f(int);

int main()
{
    int i = 15;
    my_f(i);
}

void my_f(int i)
{
    int j[2] = {99, 100};
    printf("%d\n", j[-2]);
}

我的理解是 my_f()activation record (aka stack frame) 应该是这样的:

    ------------
    |     i    |    15
    ------------
    | Saved PC |    Address of next instruction in caller function
    ------------
    |   j[0]   |    99
    ------------
    |   j[1]   |    100
    ------------

我希望 j[-2] 打印 15,但它打印了 0。有人可以解释一下我在这里缺少什么吗?我在 OS X 10.5.8 上使用 GCC 4.0.1(是的,我生活在岩石下,但这不是重点)。

理论上你是对的,但实际上这取决于很多问题。这些是例如调用约定、操作系统类型和版本,以及编译器类型和版本。 您只能通过查看代码的最终反汇编来具体说明。

如果你真的想要地址GNU C中,使用
__builtin_frame_address(0)
(非零参数尝试将堆栈回溯到父堆栈帧)。这是函数推送的第一件事的地址,即保存的 ebprbp 如果您使用 -fno-omit-frame-pointer 编译。如果你想 修改 堆栈上的 return 地址,你可以用 __builtin_frame_address(0) 的偏移量来做到这一点,但要可靠地读取它,请使用__builtin_return_address(0).


GCC 在通常的 x86 ABI 中保持堆栈 16 字节对齐。 return 地址和 j[1] 地址之间很容易有差距。从理论上讲,它可以将 j[] 放得尽可能低,或者将其优化掉(或将其优化为只读静态常量,因为没有任何内容写入它)。

如果您使用优化进行编译,i 可能不会存储在任何地方,并且 my_f(int i) 内联到 main.

另外,正如@EOF 所说,j[-2] 是图表底部下方的两个点。 (低地址在底部,因为堆栈向下增长)。另请注意,wikipedia (from the link I edited into the question) 上的图表是在顶部绘制的低地址。我回答中的ASCII图在底部有低地址。

如果您使用 -O0 进行编译,那么还有一些希望。在 64 位代码(gcc 和 clang 的 64 位构建的默认目标)中,调用约定传递寄存器中的前 6 个参数,因此内存中唯一的 i 将在 main 的堆栈帧中。

此外,在 AMD64 代码中,j[3] 可能是 return 地址(或保存的 %rbp)的上半部分,如果 j[] 被放置在具有没有差距。 (指针是 64 位,int 仍然是 32 位。)j[2],第一个越界元素,将别名到低 32 位(在 Intel 术语中也称为低双字,其中 "word" 是 16 位。)


最好的希望是在未优化的 32 位代码中,

使用没有寄存器参数的调用约定。 (例如 x86 32bit SysV ABI. See also the 标签 wiki)。

在这种情况下,您的堆栈将如下所示:

# 32bit stack-args calling convention, unoptimized code

  higher addresses
^^^^^^^^^^^^
| argv     |
------------
| argc     |
-------------------
| main's ret addr |
-------------------
|   ...    |
|  main()'s local variables and stuff, layout decided by the compiler
|   ...    |
------------
|     i    |    # function arg
------------ <--   16B-aligned boundary for the first arg, as required in the ABI
| ret addr |
------------ <--- esp pointer on entry to the function
|saved ebp |  # because gcc -m32 -O0 uses -fno-omit-frame-pointer
------------ <--- ebp after  mov ebp, esp  (part of no-omit-frame-pointer)
  unpredictable amount of padding, up to the compiler.  (likely 0 bytes in this case)
  but actually not: clang 3.5 for example makes a copy of it's arg (`i`) here, and puts j[] right below that, so j[2] or j[5] will work
------------
|  j[1]    |
------------
|  j[0]    |
------------
|          |
vvvvvvvvvvvv   Lower addresses.  (The wikipedia diagram is upside-down, IMO: it has low addresses at the top).

8 字节的 j 数组很可能会被放置在 push ebp 写入的值的正下方,没有间隙。这将使 j[0] 16B 对齐,尽管不要求或保证本地数组具有任何特定的对齐方式。 (除了 C99 可变长度数组是 16B 对齐的,在 AMD64 SysV ABI 中。我不记得有非可变长度数组的保证,但我没有检查。)

如果该函数保存任何其他调用保留寄存器(如 ebx)以便它可以使用它们,那些保存的寄存器将在保存的 ebp 之前或之后,在 [=123= 以上] 用于本地人。

j[4] 可能 在 32 位代码中工作,就像@EOF 建议的那样。我假设他按照我所做的相同推理得出 4,但忘了提及它仅适用于 32 位代码。


查看汇编:

当然,真正发生的事情比所有这些猜测和挥手要好得多。

我把你的函数放在 Godbolt compiler explorer 上,它有最旧的 gcc 版本 (4.4.7),使用 -xc -O0 -Wall -fverbose-asm -m32-xc 是编译为 C,而不是 C++。

my_f:
    push    ebp     #
    mov     ebp, esp  #,
    sub     esp, 40   #,              # no idea why it reserves 40 bytes.  clang 3.5 only reserves 24
    mov     DWORD PTR [ebp-16], 99    # j[0]
    mov     DWORD PTR [ebp-12], 100   # j[1]
    mov     edx, DWORD PTR [ebp+0]    ######   This is the j[4] load
    mov     eax, OFFSET FLAT:.LC0     # put the format string address into eax
    mov     DWORD PTR [esp+4], edx    # store j[4] on the stack, to become an arg for printf
    mov     DWORD PTR [esp], eax      # store the format string
    call    printf  #
    leave
    ret

所以gcc把j放在了ebp-16,而不是我猜的ebp-8j[4] 获取保存的 ebpij[6],堆栈上还有 8 个字节。

记住,我们在这里学到的只是 gcc 4.4 碰巧在 -O0 上做的事情。有 no 规则说 j[6] 将引用在任何其他设置或具有不同周围代码的情况下保存 i 副本的位置。

如果您想从编译器输出中学习 asm,请至少查看 -Og-O1 中的 asm。 -O0 在每个语句之后将所有内容存储到内存中,因此它非常嘈杂/臃肿,这使得它更难理解。看你想学什么,-O3 就好了。显然,您必须编写使用输入参数而不是编译时常量执行某些操作的函数,因此它们不会优化掉。请参阅 (especially the link to Matt Godbolt's CppCon2017 talk), and other links in the 标签 wiki。


clang 3.5.

如图所示,将 i 从 arg 槽复制到本地。虽然当它调用 printf 时,它再次从 arg 槽复制,而不是它自己的堆栈帧中的副本。