2013-07-27 15 views
3

我编写一个简单的程序来打印出的元素的地址堆输出为什么堆栈中的元素的地址在ubuntu64中相反?

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

enter image description here

根据上述图,即越早元件一直推到堆栈中,较高地址它将定位, 并且如果使用调用约定的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 

任何关于它的想法?

+7

调用约定在32位和64位x86上不同。在64位上,参数被传递到寄存器中,所以我认为它们必须手动推入堆栈,在您的情况下会从左到右发生。在32位上,参数在堆栈上从右向左传递。 –

+0

从链接:http://en.wikipedia.org/wiki/X86_calling_conventions#cite_note-ms-9,似乎x86-64没有cdecl调用约定,对吧? – camino

+0

调用约定是通过寄存器传递参数,然后将任何剩余的参数从右到左推入堆栈(cdecl约定)。不过,这仅适用于System V ABI。我不确定微软如何处理堆栈。那里显然有一些“影子空间”,但我不明白为什么这样的事情是必要的。在任何情况下,与Ubuntu相关的调用约定都可以在System V ABI中找到,它指出首先通过寄存器传递参数,并且只有在寄存器填充后才使用堆栈。 –

回答

4

尽管已经为两种体系结构定义了明确的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寄存器正在由富指向堆栈的顶部。 EBP寄存器充当“基址指针”。所有参数都以相反顺序推入堆栈。 main传递给foo的参数和foo中的局部变量都可以被引用为基指针的偏移量。调用foo后,堆栈应该如下所示:stack fram x86 32bit

假设编译器正在使用堆栈指针,我们可以通过将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寄存器中,并且可以在最重要的一半中传递并返回 。 (float_128,__m128,__ m256)
  • NO_CLASS该类用作 算法中的初始值设定项。它将用于填充和空的结构和联合。
  • MEMORY该类包括将被传递,并经由栈存储器 返回类型(结构类型)

一旦参数被分配给一个类,它是按照 传递给函数这些规则:

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

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

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

​​

所以,你可以找回他们,我们下面的代码:

#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反转?

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

2

对于如何将参数传递给函数,以及它们在堆栈中(或寄存器或共享内存中)的位置绝对没有限制。编译器需要以调用者和被调用者达成一致的方式传递变量。除非您强制使用特定的调用约定(用于链接使用不同编译器编译的代码),否则,除非硬件支持ABI - 否则不能保证。