2013-07-27 18 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 - 否則不能保證。