儘管已經爲兩種體系結構定義了明確的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中找到,它指出首先通過寄存器傳遞參數,並且只有在寄存器填充後才使用堆棧。 –