激活记录-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)
(非零参数尝试将堆栈回溯到父堆栈帧)。这是函数推送的第一件事的地址,即保存的 ebp
或 rbp
如果您使用 -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 x86 标签 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-8
。 j[4]
获取保存的 ebp
。 i
在 j[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 x86 标签 wiki。
如图所示,将 i
从 arg 槽复制到本地。虽然当它调用 printf
时,它再次从 arg 槽复制,而不是它自己的堆栈帧中的副本。
请考虑以下程序:
#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)
(非零参数尝试将堆栈回溯到父堆栈帧)。这是函数推送的第一件事的地址,即保存的 ebp
或 rbp
如果您使用 -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 x86 标签 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-8
。 j[4]
获取保存的 ebp
。 i
在 j[6]
,堆栈上还有 8 个字节。
记住,我们在这里学到的只是 gcc 4.4 碰巧在 -O0
上做的事情。有 no 规则说 j[6]
将引用在任何其他设置或具有不同周围代码的情况下保存 i
副本的位置。
如果您想从编译器输出中学习 asm,请至少查看 -Og
或 -O1
中的 asm。 -O0
在每个语句之后将所有内容存储到内存中,因此它非常嘈杂/臃肿,这使得它更难理解。看你想学什么,-O3
就好了。显然,您必须编写使用输入参数而不是编译时常量执行某些操作的函数,因此它们不会优化掉。请参阅
如图所示,将 i
从 arg 槽复制到本地。虽然当它调用 printf
时,它再次从 arg 槽复制,而不是它自己的堆栈帧中的副本。