请考虑以下计划:
#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(是的,我生活在摇滚之下,但除了这一点之外)。
答案 0 :(得分:6)
如果你真的想要在GNU C, use GCC使堆栈在通常的x86 ABI中保持16字节对齐。返回地址与 如果使用优化进行编译, 另外,就像@EOF所说, 如果你使用 此外,在AMD64代码中, 使用没有register-args的调用约定。 (例如x86 32bit SysV ABI。另请参阅x86代码wiki)。 在这种情况下,您的堆栈将如下所示: 8字节 如果函数保存了任何其他调用保留的寄存器(如 当然,真正发生的事情要好于所有猜测和挥手。 我使用 所以gcc将 请记住,我们在这里学到的只是gcc 4.4在 如果您想从编译器输出中学习asm,请至少从 如图所示,将
__builtin_frame_address(0)
中使用地址 (非零args尝试将堆栈回溯到父堆栈帧) 。这是函数推送的第一件事的地址,即如果您使用ebp
编译,则保存为rbp
或-fno-omit-frame-pointer
。如果您希望修改堆栈上的返回地址,您可以使用__builtin_frame_address(0)
的偏移量来执行此操作,但只需使用__builtin_return_address(0)
可靠地读取它。 / p>
j[1]
之间可能存在差距。从理论上讲,它可以将j[]
放在尽可能远的地方,或者将其优化(或者只读取一个只读的静态常量,因为没有任何内容写入)。i
可能不会存储在任何地方,并且
my_f(int i)
被内联到main
。j[-2]
是图表底部下方的两个点。 (低地址位于底部,因为堆栈增长了)。另请注意, wikipedia (from the link I edited into the question)上的图表使用顶部的低地址绘制。我的答案中的ASCII图在底部有低地址。-O0
编译,那么就有一些希望。在64位代码(64位构建的gcc和clang的默认目标)中,调用约定传递了寄存器中的前6个args,因此内存中唯一的i
将位于main
&#39; s堆栈框架。j[3]
可能是返回地址的上半部分(或保存的%rbp),如果j[]
位于其中一个没有间隙的地方之下。 (指针是64位,int
仍然是32位。)j[2]
,第一个越界元素,将别名到低32位(在英特尔术语中称为低dword,其中a&#34 ; word&#34;是16位。)最好的希望是使用未优化的32位代码
# 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).
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
j
放在ebp-16
,而不是我猜到的ebp-8
。 j[4]
获取已保存的ebp
。 i
位于j[6]
,堆栈上方多8个字节。-O0
发生的事情。 没有规则表示j[6]
将引用在任何其他设置上保留i
副本的位置,或者包含不同周围代码的位置。-Og
或-O1
查看asm。 -O0
在每个语句之后将所有内容存储到内存中,因此它非常嘈杂/臃肿,这使得它更难以遵循。根据您想要学习的内容,-O3
是好的。显然,您必须编写使用输入参数而不是编译时常量执行某些操作的函数,因此它们不会进行优化。请参阅How to remove "noise" from GCC/clang assembly output?(特别是指向Matt Godbolt的CppCon2017演讲的链接)以及x86标签wiki中的其他链接。i
从arg插槽复制到本地。虽然它调用printf
时,它会再次从arg槽复制,而不是复制到自己的堆栈帧中。
答案 1 :(得分:0)
理论上你是对的,但实际上它取决于很多问题。这些是例如调用约定,操作系统类型和版本,以及编译器类型和版本。 您只能通过查看代码的最终反汇编来具体说明。