我试图完全了解函数调用中的堆栈增长机制,我感到有些困惑。为了更好地理解,我编写了以下简单程序:
#include <stdio.h>
#include <stdint.h>
void callee(uint32_t* p)
{
uint32_t tmp = 9;
printf("callee - tmp is located at address location:%p and p is:%p \n", &tmp, p);
}
void caller()
{
uint32_t tmp1 = 12;
printf("caller - address of tmp1:%p \n", &tmp1);
calle(&tmp1);
}
int main(int argc, char** argv)
{
caller();
return 0;
}
并使用在线汇编器转换器,得到以下汇编输出(我只留下了callee
函数的代码):
.LC0:
.string "callee - tmp is located at address location:%p and p is:%p \n"
calle:
push rbp
mov rbp, rsp
sub rsp, 32 // command 1
mov QWORD PTR [rbp-24], rdi
mov DWORD PTR [rbp-4], 9 // command 2
mov rdx, QWORD PTR [rbp-24]
lea rax, [rbp-4]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
据我了解,考虑到命令1和2 (如上所述),当我进行编译时,堆栈的确向着低位地址和已编译代码的(样本)输出向下扩展使用命令gcc myProg.c -o prog
,如下所示:
呼叫者-tmp1:0x7ffe423e8ed4的地址
callee-tmp位于地址位置:0x7ffe423e8eb4,p为:0x7ffe423e8ed4
可以看出,实际上callee
函数内分配的局部变量位于比caller
函数内局部变量低的内存地址中。到目前为止,很好。 / p>
还,当我使用-O2
选项(即:gcc -O2 myProg.c -o prog
)编译程序时,编译后的代码的(样本)输出如下:< / p>
呼叫者-tmp1的地址: 0x7fff0d5bfa90
被叫方-tmp位于地址位置: 0x7fff0d5bfa94 ,p是: 0x7fff0d5bfa90
这一次描绘的是,callee
堆栈帧中分配的局部变量位于比caller
函数中的局部变量更高的内存地址中。
所以我的问题是--O2
优化选项可以“优化”堆栈增长机制实际上发生变化或者我在这里缺少某些东西的情况??
gcc
版本:7.3
体系结构:x86_64
操作系统:Ubuntu 18.04。
感谢您的澄清。
伙计。
答案 0 :(得分:7)
-O2
内联函数,此时编译器可以随意进行堆栈分配。
在C中,单独对象(例如tmp
和tmp1
)之间的地址比较在技术上是未定义的行为,因此地址之间的任何类型的>
或<
关系基于函数嵌套的不是是遵循常规规则时优化需要保留的可观察到的副作用。内联函数时,编译器甚至不会尝试这样做。
ISO C11 draft n1548,第6.5.8节关系运算符
5)比较两个指针时,结果取决于指针中的相对位置 指向对象的地址空间。如果两个指向对象类型的指针都指向 同一对象,或者两者都指向同一数组对象的最后一个元素, 比较相等。 如果指向的对象是同一聚合对象的成员, 指向以后声明的结构成员的指针比指向成员的指针大 在结构中更早声明,并指向具有较大下标的数组元素的指针 值比较大于指向具有较低下标的相同数组元素的指针 价值观。指向同一联合对象的成员的所有指针比较相等。如果 表达式P指向数组对象的元素,表达式Q指向数组对象 同一数组对象的最后一个元素,指针表达式Q + 1比较大于 P. 在所有其他情况下,行为均未定义
将地址转换为uintptr_t
之类的整数,或者将其打印出来并在您的脑海中进行比较,虽然不是UB,但是仍然不能保证结果基于任何内容。
答案 1 :(得分:4)
由于对printf
的{{1}}调用已优化到calle
函数中,请参见godbolt。
gcc 7.3 caller
的汇编输出:
-O2
您可以看到.LC0:
.string "calle - tmp is located at address location:%p and p is:%p \n"
calle:
sub rsp, 24
mov rdx, rdi
xor eax, eax
lea rsi, [rsp+12]
mov edi, OFFSET FLAT:.LC0
mov DWORD PTR [rsp+12], 9
call printf
add rsp, 24
ret
.LC1:
.string "caller - address of tmp1:%p \n"
caller:
sub rsp, 24
mov edi, OFFSET FLAT:.LC1
xor eax, eax
lea rsi, [rsp+8]
mov DWORD PTR [rsp+8], 12
call printf
lea rdx, [rsp+8]
lea rsi, [rsp+12]
mov edi, OFFSET FLAT:.LC0
xor eax, eax
mov DWORD PTR [rsp+12], 9
call printf
add rsp, 24
ret
main:
sub rsp, 8
xor eax, eax
call caller
xor eax, eax
add rsp, 8
ret
函数被内联到calle
中,因此caller
函数调用caller
两次,首先使用LC1字符串,然后使用LC0字符串。第一次打印printf
的地址为rsp+8
,第二次打印tmp1
的地址为rsp+12
。 gcc可以自由选择其选择的变量顺序。
您可以将tmp2
属性设置为__attribute__((__noinline__))
来“修复”,但是...您不应该期望变量地址有任何顺序(除非可以,例如数组和结构)。
P.S。从技术上讲,未使用calle
指针调用"%p"
printf修饰符是未定义的行为,因此在打印之前,应将printf arg强制转换为void*
。 void*