尽管已经为两种体系结构定义了明确的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后,堆栈应该如下所示:。
假设编译器正在使用堆栈指针,我们可以通过将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反转?
因为它们没有存储到堆栈中。您以这种方式检索到的地址是调用者函数的局部变量的地址。
调用约定在32位和64位x86上不同。在64位上,参数被传递到寄存器中,所以我认为它们必须手动推入堆栈,在您的情况下会从左到右发生。在32位上,参数在堆栈上从右向左传递。 –
从链接:http://en.wikipedia.org/wiki/X86_calling_conventions#cite_note-ms-9,似乎x86-64没有cdecl调用约定,对吧? – camino
调用约定是通过寄存器传递参数,然后将任何剩余的参数从右到左推入堆栈(cdecl约定)。不过,这仅适用于System V ABI。我不确定微软如何处理堆栈。那里显然有一些“影子空间”,但我不明白为什么这样的事情是必要的。在任何情况下,与Ubuntu相关的调用约定都可以在System V ABI中找到,它指出首先通过寄存器传递参数,并且只有在寄存器填充后才使用堆栈。 –