Macos 激活记录-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]); }
我希望j[-2]打印15,但它打印0。有人能解释一下我遗漏了什么吗?我在OS X 10.5.8上使用GCC 4.0.1(是的,我住在岩石下,但这不是重点)。理论上你是对的,但实际上这取决于很多问题。例如,调用约定、操作系统类型和版本,以及编译器类型和版本。Macos 激活记录-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]); },macos,gcc,assembly,x86,abi,Macos,Gcc,Assembly,X86,Abi,我希望j[-2]打印15,但它打印0。有人能解释一下我遗漏了什么吗?我在OS X 10.5.8上使用GCC 4.0.1(是的,我住在岩石下,但这不是重点)。理论上你是对的,但实际上这取决于很多问题。例如,调用约定、操作系统类型和版本,以及编译器类型和版本。 您只能通过查看代码的最终反汇编来明确说明这一点。如果您确实希望在中找到堆栈帧的地址(非零参数尝试将堆栈回溯到父堆栈帧)。这是该函数推送的第一件事的地址,即如果使用-fn编译,则保存的ebp或rbp。如果要修改堆栈上的返回地址,可以使用\uuu
您只能通过查看代码的最终反汇编来明确说明这一点。如果您确实希望在中找到堆栈帧的地址(非零参数尝试将堆栈回溯到父堆栈帧)。这是该函数推送的第一件事的地址,即如果使用
-fn编译,则保存的ebp
或rbp
。如果要修改堆栈上的返回地址,可以使用\uuuu内置\u帧\u地址(0)
的偏移量来修改,但要可靠地读取它,可以使用\uuuuuu内置\u返回\u地址(0)
GCC在通常的x86 ABI中保持堆栈16字节对齐。在返回地址和j[1]
之间很容易出现间隙。从理论上讲,它可以将j[]
放得尽可能低,或者将其优化掉(或者设置为只读静态常量,因为没有任何东西写入它)
如果使用优化编译,i
可能不会存储在任何位置,并且
my\u f(int i)
内联到main
另外,正如@EOF所说,j[-2]
是图表底部下方的两个点。(低地址位于底部,因为堆栈向下生长)。另请注意,上的图表在顶部的地址较低。我的答案中的ASCII图底部的地址较低
如果您使用-O0
编译,那么还有一些希望。在64位代码(gcc和clang的64位构建的默认目标)中,调用约定传递寄存器中的前6个参数,因此内存中唯一的i
将位于main
的堆栈帧中
此外,在AMD64代码中,j[3]
可能是返回地址的上半部分(或已保存的%rbp),如果j[]
放在其中一个没有间隙的下半部分。(指针是64位,int
仍然是32位。)j[2]
,第一个越界元素,将别名到低32位(在英特尔术语中称为低dword,其中“字”是16位。)
这项工作的最大希望是在未优化的32位代码中,
使用没有寄存器参数的调用约定。(例如。另请参见标记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).
因此,gcc将j
放在ebp-16
,而不是我猜的ebp-8
j[4]
获取保存的ebp
i
位于j[6]
,堆栈上还有8个字节
记住,我们在这里学到的只是GCC4.4在-O0
上的作用。没有任何规则规定,j[6]
指的是在任何其他设置上或使用不同的周围代码保存i
副本的位置
如果您想从编译器输出中学习asm,请至少查看-Og
或-O1
中的asm-O0
在每条语句之后都将所有内容存储到内存中,因此它非常嘈杂/臃肿,这使得它更难理解。取决于你想学什么,-O3
很好。显然,您必须编写使用输入参数而不是编译时常量的函数,这样它们就不会进行优化。请参阅(特别是Matt Godbolt的CppCon2017演讲的链接)和tag wiki中的其他链接
如图所示,将i
从arg插槽复制到本地插槽。虽然当它调用printf
时,它会再次从arg插槽复制,而不是从它自己的堆栈帧中复制。如果是64位,地址是8字节,如果int是4字节,则'i
位于-3
。这是理论上的,实际上可能会推迟。更好的方法是使用gdb并打印堆栈内存,以确保更可靠。访问数组的边界外是非常困难的。你不应该对这个项目的产出抱有任何期望program@M.M这是正确的。C标准没有说明任何关于激活记录结构的内容,程序也无法在没有未定义行为的情况下检查这些记录。堆栈上j[0]
和j[1]
的顺序可能不像您所期望的那样。C保证&j[0]<&j[1]
,堆栈向下增长。如果有的话,您“想要”的越界访问位于j[4]
,但正如其他人已经指出的,这是未定义的。您是否试图将此代码用于特定编程目的,或者这只是“我想了解32位OSX代码中的堆栈帧可能是什么样子?”。理论上,IA-32函数调用约定有一个很好的描述,他是不对的:“理论上,他的程序是未定义的,理论上C编程语言标准是这样说的。实际上,对于某些编译器的某些版本,他可能是对的。他最好只打印my_f中各种变量的地址;这将帮助他理解他的特定编译器是如何将变量放入内存的(如果确实是这样的话:标准并不要求它对任何特定变量都这样做)。在课堂教学中,在白板上讲授C和函数调用时,你将解释与此完全相同的堆栈图,这就是我所说的“理论上”的意思。但正如我所说,你不会在现实世界中观察到这一点。对激活记录的理论描述没有错。他的程序访问j[-2];Apple IA 32位约定(OP使用32位OSX代码)在上有文档记录。如果你想看到他的编译生成的代码,我已经证明了
# 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).
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