尽管已经为这两种架构定义了明确的 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 后,堆栈应如下所示:。
假设编译器正在使用堆栈指针,我们可以通过将 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 中颠倒了?
因为它们没有存储到堆栈中。您以这种方式检索到的地址是调用函数的局部变量的地址。