2

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

#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

有什么想法吗?

4

2 回答 2

3

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

于 2013-07-27T19:08:41.190 回答
3

尽管已经为这两种架构定义了明确的 ABI,但编译器并不保证遵守这一点。您可能想知道为什么,原因通常是性能。将变量传递到堆栈在速度方面比使用寄存器更昂贵,因为应用程序需要访问内存来检索它们。这种习惯的另一个例子是编译器如何使用EBP/RBP寄存器。EBP/RBP应该是包含帧指针的寄存器,也就是栈基地址。堆栈基址寄存器允许轻松访问局部变量。但是,帧指针寄存器通常用作提高性能的通用寄存器。这避免了保存、设置和恢复帧指针的指令;它还在许多功能中提供了额外的寄存器,这在 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;
}

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

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

  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 寄存器的类型组成,可以在其最重要的一半中传递和返回。(浮动_128,__m128,__m256)
  • NO_CLASS 此类用作算法中的初始化程序。它将用于填充和空结构和联合。
  • MEMORY 此类包含将通过堆栈在内存中传递和返回的类型(结构类型)

一旦将 a 参数分配给一个类,它就会根据以下规则传递给函数:

  • 内存,在堆栈上传递参数。
  • 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 中颠倒了?

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

于 2013-07-29T09:25:12.323 回答