【问题标题】:why addresses of elements in the stack are reversed in ubuntu64?为什么堆栈中元素的地址在 ubuntu64 中颠倒了?
【发布时间】:2013-07-27 21:04:09
【问题描述】:

我写了一个简单的程序打印出栈中元素的地址

#include <stdio.h>
#include <memory.h>
void f(int i,int j,int k)
{
    int  *pi = (int*)malloc(sizeof(int));
    int a =20;
    printf("%p,%p,%p,%p,%p\n",&i,&j,&k,&a,pi);
}

int main()
{
    f(1,2,3);
    return 0;
}

输出:(在 ubuntu64 中,意外

0x7fff4e3ca5dc,0x7fff4e3ca5d8,0x7fff4e3ca5d4,0x7fff4e3ca5e4,0x2052010

输出:(在 ubuntu32 中,如预期的那样

0xbf9525f0,0xbf9525f4,0xbf9525f8,0xbf9525d8,0x931f008

ubuntu64 环境:

$uname -a
Linux  3.8.0-26-generic #38-Ubuntu SMP Mon Jun 17 21:43:33 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
$gcc -v
Target: x86_64-linux-gnu
gcc version 4.8.1 (Ubuntu 4.8.1-2ubuntu1~13.04)

根据上图,越早的元素被压入堆栈,它会定位到更高的地址, 如果使用调用约定 cdecl ,最右边的参数将首先被压入堆栈。 局部变量压入参数后要入栈

但输出在 ubuntu64 中按预期反转:

the address of k is :0x7fff4e3ca5d4   //<---should have been pushed to the stack first
the address of j is :0x7fff4e3ca5d8   
the address of i is :0x7fff4e3ca5dc   
the address of a is :0x7fff4e3ca5e4   //<---should have been pushed to the stack after i,j,k

有什么想法吗?

【问题讨论】:

  • 调用约定在 32 位和 64 位 x86 上是不同的。在 64 位上,参数在寄存器中传递,所以我认为必须手动将它们推入堆栈,这在您的情况下从左到右发生。在 32 位上,参数在堆栈上传递,从右到左。
  • 来自链接:en.wikipedia.org/wiki/X86_calling_conventions#cite_note-ms-9,看来x86-64没有cdecl调用约定吧?
  • 调用约定是通过寄存器传递参数,然后任何剩余的参数从右到左压入堆栈(cdecl 约定)。不过,这仅适用于 System V ABI。我不确定微软对堆栈做了什么。那里显然有一些“阴影空间”,虽然我不明白为什么这样的事情是必要的。无论如何,与 Ubuntu 相关的调用约定可以在 System V ABI 中找到,它规定参数首先通过寄存器传递,只有在寄存器填充后才使用堆栈。

标签: c assembly x86-64 stack-memory stack-frame


【解决方案1】:

即使已经为这两种架构定义了明确的 ABI,编译器也不保证会遵守这一点。您可能想知道为什么,原因通常是性能。 就速度而言,将变量传递到堆栈中比使用寄存器更昂贵,因为应用程序需要访问内存来检索它们。这种习惯的另一个例子是编译器如何使用EBP/RBP 寄存器。 EBP/RBP应该是包含frame-pointer的寄存器,也就是栈基地址。堆栈基址寄存器允许轻松访问局部变量。然而,frame-pointer 寄存器经常被用作通用寄存器以提高性能。这避免了保存、设置和恢复帧指针的指令;它还在许多功能中提供了额外的寄存器,这在 X86_32 架构中尤其重要,通常程序都渴望寄存器。主要缺点是使得在某些机器上调试不可能。有关更多信息,请查看 gcc 的 -fomit-frame-pointer 选项。

x86_32 和 x86_64 之间的调用函数比较不同。最相关的区别是 x86_64 尝试使用通用寄存器来传递函数参数,并且只有当没有可用的寄存器或参数大于 80 字节时,它才会使用堆栈。

我们从 x86_32 ABI 开始,我稍微改变了你的例子:

#include <stdio.h>
#include <stddef.h> 
#include <stdint.h>

#if defined(__i386__)
  #define STACK_POINTER "ESP"
  #define FRAME_POINTER "EBP" 
#elif defined(__x86_64__)
  #define STACK_POINTER "RSP"
  #define FRAME_POINTER "RBP" 
#else 
  #error Architecture not supported yet!!
#endif

void foo(int i,int j,int k)
{
    int a =20;
    uint64_t stack=0, frame_pointer=0; 

    // Retrieve stack 
asm volatile( 
#if defined (__i386__)
                  "mov %%esp, %0\n"
                  "mov %%ebp, %1\n"
#else 
                  "mov %%rsp, %0\n"
                  "mov %%rbp, %1\n"
#endif
                  : "=m"(stack), "=m"(frame_pointer)
                 : 
                 : "memory");
  // retrieve paramters x86_64 
#if defined (__x86_64__)

    int  i_reg=-1, j_reg=-1, k_reg=-1;

asm volatile  ( "mov %%rdi, %0\n"
                "mov %%rsi, %1\n"
                "mov %%rdx, %2\n"
                 : "=m"(i_reg), "=m"(j_reg), "=m"(k_reg)
                 : 
                 : "memory");
#endif

    printf("%s=%p %s=%p\n", STACK_POINTER, (void*)stack, FRAME_POINTER,  (void*)frame_pointer); 
    printf("%d, %d, %d\n", i, j, k);
    printf("%p\n%p\n%p\n%p\n",&i,&j,&k,&a);


#if defined (__i386__)
      // Calling convention c 
      // EBP --> Saved EBP
      char * EBP=(char*)frame_pointer;   
      printf("Function return address : 0x%x  \n",      *(unsigned int*)(EBP +4)); 
      printf("- i=%d &i=%p \n",*(int*)(EBP+8)  ,  EBP+8 );   
      printf("- j=%d &j=%p \n",*(int*)(EBP+ 12),  EBP+12);   
      printf("- k=%d &k=%p \n",*(int*)(EBP+ 16),  EBP+16);  
#else 
      printf("- i=%d &i=%p \n",i_reg, &i  );   
      printf("- j=%d &j=%p \n",j_reg, &j  );   
      printf("- k=%d &k=%p \n",k_reg ,&k  );  
#endif
}

int main()
{
    foo(1,2,3);
    return 0;
}

foo 使用 ESP 寄存器指向栈顶。 EBP 寄存器充当“基指针”。所有参数都以相反的顺序推入堆栈。 main 传递给 foo 的参数和 foo 中的局部变量都可以作为基指针的偏移量来引用。调用 foo 后,堆栈应如下所示:。

假设编译器正在使用堆栈指针,我们可以通过将 4 字节的偏移量求和到 EBP 寄存器来访问函数参数。请注意,第一个参数位于偏移量 8 处,因为 call 指令将调用函数的返回地址压入堆栈。

  printf("Function return address : 0x%x  \n",      *(unsigned int*)(EBP +4)); 
  printf("- i=%d &i=%p \n",*(int*)(EBP+8)  ,  EBP+8 );   
  printf("- j=%d &j=%p \n",*(int*)(EBP+ 12),  EBP+12);   
  printf("- k=%d &k=%p \n",*(int*)(EBP+ 16),  EBP+16); 

这或多或少是在 x86_32 中将参数传递给函数的方式。

在 x86_64 中有更多可用的寄存器,使用它们来传递函数的参数是有意义的。 x86_64 ABI 可以在这里找到:http://www.uclibc.org/docs/psABI-x86_64.pdf。调用约定从第 14 页开始。

首先将参数分为类。每个参数的类决定了它传递给被调用函数的方式。一些最相关的是:

  • INTEGER 此类由整数类型组成,这些类型适合于 通用寄存器。例如 (int, long, bool)
  • SSE 该类由适合 SSE 寄存器的类型组成。 (浮点数,双精度)
  • SSEUP 该类由适合 SSE 寄存器的类型组成,并且可以 在其中最重要的一半被传递和返回。 ( float_128, __m128,__m256)
  • NO_CLASS 此类在 算法。它将用于填充和空结构和联合。
  • MEMORY 此类包含将在内存中传递和返回的类型 通过堆栈(结构类型)

一旦a参数被分配给一个类,它就会根据 这些规则:

  • MEMORY,在堆栈上传递参数。
  • INTEGER,使用序列 %rdi、%rsi、%rdx、%rcx、%r8 和 %r9 的下一个可用寄存器。
  • SSE,使用下一个可用的 SSE 寄存器,寄存器按从 %xmm0 到 %xmm7 的顺序获取。
  • SSEUP,这八个字节在最后使用的 SSE 寄存器的上半部分传递。

如果没有可用于任何八字节参数的寄存器,则整个 参数在堆栈上传递。如果寄存器已经分配给某些 八字节这样的参数,分配得到恢复。一旦分配了寄存器,在内存中传递的参数就会以相反的顺序被压入堆栈。

由于您传递的是 int 变量,因此参数将被插入到通用寄存器中。

%rdi --> i 
%rsi --> j
%rdx --> k

所以你可以用下面的代码来检索它们:

#if defined (__x86_64__)

    int  i_reg=-1, j_reg=-1, k_reg=-1;

asm volatile  ( "mov %%rdi, %0\n"
                "mov %%rsi, %1\n"
                "mov %%rdx, %2\n"
                 : "=m"(i_reg), "=m"(j_reg), "=m"(k_reg)
                 : 
                 : "memory");
#endif

我希望我已经清楚了。

总之,

为什么在ubuntu64中栈中元素的地址是相反的?

因为它们没有存储到堆栈中。您以这种方式检索到的地址是调用函数的局部变量的地址。

【讨论】:

  • 这里没有以任何方式“尊重”x86-64 SysV ABI; args 在寄存器中传递,因此由被调用者为它们发明地址(例如,通过将它们溢出到堆栈中),并且对此没有顺序要求。此外,在 GP-integer regs 中传递结构或其他聚合对象的大小限制是 16 个字节,而不是 80 个。(只有 FP 成员的结构可以进入 XMM 寄存器...)
【解决方案2】:

对于参数如何传递给函数,以及它们在堆栈中的位置(或在寄存器中,或在共享内存中),绝对没有限制。由编译器决定以调用者和被调用者同意的方式传递变量。除非您强制执行特定的调用约定(用于链接使用不同编译器编译的代码),或者除非有硬件规定的 ABI - 否则无法保证。

【讨论】:

    猜你喜欢
    • 2013-07-25
    • 1970-01-01
    • 2017-11-11
    • 2018-09-30
    • 2012-09-05
    • 1970-01-01
    • 2011-10-27
    • 2020-11-21
    • 1970-01-01
    相关资源
    最近更新 更多