关于x86_64 Linux上的堆栈增长的困惑

时间:2018-10-28 20:24:09

标签: c gcc assembly stack x86-64

我试图完全了解函数调用中的堆栈增长机制,我感到有些困惑。为了更好地理解,我编写了以下简单程序:

#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。

感谢您的澄清。

伙计。

2 个答案:

答案 0 :(得分:7)

-O2内联函数,此时编译器可以随意进行堆栈分配。

在C中,单独对象(例如tmptmp1)之间的地址比较在技术上是未定义的行为,因此地址之间的任何类型的><关系基于函数嵌套的不是是遵循常规规则时优化需要保留的可观察到的副作用。内联函数时,编译器甚至不会尝试这样做。

  

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*