激活记录 - C.

时间:2016-05-15 06:06:31

标签: macos gcc assembly x86 abi

请考虑以下计划:

#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()的{​​{3}}应如下所示:

    ------------
    |     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(是的,我生活在摇滚之下,但除了这一点之外)。

2 个答案:

答案 0 :(得分:6)

如果你真的想要在GNU C, use
__builtin_frame_address(0)
中使用地址 (非零args尝试将堆栈回溯到父堆栈帧) 。这是函数推送的第一件事的地址,即如果您使用ebp编译,则保存为rbp-fno-omit-frame-pointer。如果您希望修改堆栈上的返回地址,您可以使用__builtin_frame_address(0)的偏移量来执行此操作,但只需使用__builtin_return_address(0)可靠地读取它。 / p>

GCC使堆栈在通常的x86 ABI中保持16字节对齐。返回地址与j[1]之间可能存在差距。从理论上讲,它可以将j[]放在尽可能远的地方,或者将其优化(或者只读取一个只读的静态常量,因为没有任何内容写入)。

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

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

如果你使用-O0编译,那么就有一些希望。在64位代码(64位构建的gcc和clang的默认目标)中,调用约定传递了寄存器中的前6个args,因此内存中唯一的i将位于main&#39; s堆栈框架。

此外,在AMD64代码中,j[3]可能是返回地址的上半部分(或保存的%rbp),如果j[]位于其中一个没有间隙的地方之下。 (指针是64位,int仍然是32位。)j[2],第一个越界元素,将别名到低32位(在英特尔术语中称为低dword,其中a&#34 ; word&#34;是16位。)

最好的希望是使用未优化的32位代码

使用没有register-args的调用约定。 (例如x86 32bit SysV ABI。另请参阅代码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对齐,尽管没有要求或保证本地数组具有任何特定的对齐。 (在AMD64 SysV ABI中,C99可变长度数组是16B对齐的除外。我不记得有非可变长度数组的保证,但我没有检查。)

如果函数保存了任何其他调用保留的寄存器(如ebx)以便它可以使用它们,那些保存的寄存器将在保存的ebp之前或之后,用于本地的空间。< / p>

j[4] 可能在32位代码中工作,例如@EOF建议。我假设他按照我的相同推理到达了4,但忘了提到它只适用于32位代码。

看着asm:

当然,真正发生的事情要好于所有猜测和挥手。

我使用-xc -O0 -Wall -fverbose-asm -m32将您的函数放在Godbolt compiler explorer上,使用最早的gcc版本(4.4.7)。 -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]获取已保存的ebpi位于j[6],堆栈上方多8个字节。

请记住,我们在这里学到的只是gcc 4.4在-O0发生的事情。 没有规则表示j[6]将引用在任何其他设置上保留i副本的位置,或者包含不同周围代码的位置。

如果您想从编译器输出中学习asm,请至少从-Og-O1查看asm。 -O0在每个语句之后将所有内容存储到内存中,因此它非常嘈杂/臃肿,这使得它更难以遵循。根据您想要学习的内容,-O3是好的。显然,您必须编写使用输入参数而不是编译时常量执行某些操作的函数,因此它们不会进行优化。请参阅How to remove "noise" from GCC/clang assembly output?(特别是指向Matt Godbolt的CppCon2017演讲的链接)以及标签wiki中的其他链接。

clang 3.5

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

答案 1 :(得分:0)

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