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 激活记录-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

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

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

如果您确实希望在中找到堆栈帧的地址(非零参数尝试将堆栈回溯到父堆栈帧)。这是该函数推送的第一件事的地址,即如果使用
-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