2010-11-13 150 views
38

我一直是高層次的編碼器,以及架構是相當新的給我,所以我決定在這裏閱讀關於大會教程:對齊堆棧是什麼意思?

http://en.wikibooks.org/wiki/X86_Assembly/Print_Version

遠了教程,如何轉換的說明你好,世界!程序

#include <stdio.h> 

int main(void) { 
    printf("Hello, world!\n"); 
    return 0; 
} 

成等價的彙編代碼被給和產生以下情況:

 .text 
LC0: 
     .ascii "Hello, world!\12\0" 
.globl _main 
_main: 
     pushl %ebp 
     movl %esp, %ebp 
     subl $8, %esp 
     andl $-16, %esp 
     movl $0, %eax 
     movl %eax, -4(%ebp) 
     movl -4(%ebp), %eax 
     call __alloca 
     call ___main 
     movl $LC0, (%esp) 
     call _printf 
     movl $0, %eax 
     leave 
     ret 

對於線中的一條,

andl $-16, %esp 

的解釋是:

該代碼「和」ESP與0xFFFFFFF0, 將堆棧與下一個 最低的16字節邊界對齊。對Mingw的源代碼 的檢查揭示,這可能是針對出現在「_main」 例程中的SIMD 指令,其僅在對齊的 地址上操作。由於我們的例程不包含 包含SIMD指令,此行 是不必要的。

我不明白這一點。有人能給我一個解釋,說明將堆棧與下一個16字節邊界對齊的意義,以及爲什麼它是必需的? andl如何實現這一目標?

+3

http://en.wikipedia.org/wiki/Data_structure_alignment – chrisaycock 2010-11-13 23:33:57

+1

在沒有啓用優化器的情況下查看機器代碼沒什麼意義。 – 2010-11-14 01:23:31

回答

51

假設棧看起來像這樣在進入_main(堆棧指針的地址只是一個例子):

| existing  | 
| stack content | 
+-----------------+ <--- 0xbfff1230 

%ebp,並從%esp減去8保留局部變量的一些空間:

| existing  | 
| stack content | 
+-----------------+ <--- 0xbfff1230 
|  %ebp  | 
+-----------------+ <--- 0xbfff122c 
: reserved  : 
:  space  : 
+-----------------+ <--- 0xbfff1224 

現在,andl指令零的%esp低4位,其可以把它放好;在此特定實例中,它具有保留額外的4個字節的效果:

| existing  | 
| stack content | 
+-----------------+ <--- 0xbfff1230 
|  %ebp  | 
+-----------------+ <--- 0xbfff122c 
: reserved  : 
:  space  : 
+ - - - - - - - - + <--- 0xbfff1224 
: extra space : 
+-----------------+ <--- 0xbfff1220 

這樣做的一點是,有一些「SIMD」(單指令多數據)指令(在x86的土地也稱爲作爲「流式SIMD擴展」的「SSE」),它可以對存儲器中的多個字執行並行操作,但要求這些多個字是從16字節的倍數的地址開始的塊。

一般來說,編譯器不能假設從%esp的特定偏移將導致一個合適的地址(因爲入口函數的狀態%esp取決於調用代碼)。但是,通過以這種方式有意地對齊堆棧指針,編譯器知道將16個字節的任意倍數添加到堆棧指針將導致16字節對齊的地址,這對於這些SIMD指令是安全的。

+0

現在,andl指令清零%esp的低4位,這可能會減少。那麼編譯器怎麼知道有多少字節被減少來平衡堆棧呢? – secmask 2010-11-14 05:49:50

+3

@secmask:在推送原始'%ebp'之後'%esp'的值已經存儲在'%ebp'中,所以它不需要知道,因爲'%ebp'指向保留的頂部空間。 '%esp'通過所示代碼中的'leave'指令得到恢復 - 'leave'等同於'movl%ebp,%esp; popl%ebp'。 – 2010-11-14 13:54:52

3

它應該只在偶數地址,而不是在奇數地址,因爲存在訪問它們的性能不足。

+0

這與性能無關。 CPU根本無法從未對齊的地址獲取數據,因爲這會是一個總線錯誤。 – chrisaycock 2010-11-14 00:08:52

+0

總線錯誤與否,它不會失敗。 – 2010-11-14 04:54:04

+0

@chrisaycock現代處理器可能會有小的性能損失。 – YoYoYonnY 2017-11-21 14:52:34

7

這和byte alignment有關。某些體系結構要求將用於特定操作集的地址與特定的位邊界對齊。例如,如果你想要一個指針的64位對齊,那麼你可以在概念上將整個可尋址的存儲器劃分爲從零開始的64位組塊。如果一個地址與其中一個塊完全匹配,則該地址將「對齊」,並且如果它將一個塊和另一個塊的一部分組合,則該地址將不對齊。

字節對齊的一個重要特徵(假設該數是2的冪)是地址的最低有效位始終爲零。這允許處理器通過簡單地不使用底部的比特來代表具有更少比特的更多地址。

+1

從我身邊也+1!感謝您的解釋。 – Legend 2010-11-14 05:43:05

5

設想這樣在8 「滑動」 地址的多個 「繪圖」

 
addresses 
xxxabcdef... 
    [------][------][------] ... 
registers 

值容易進(64位)寄存器

 
addresses 
     56789abc ... 
    [------][------][------] ... 
registers 

當然在步驟寄存器 「走」 8字節

現在,如果你想把地址xxx5的值放到寄存器中要困難得多:-)


編輯和L -16

-16是二進制

當你 「和」 任何與你得到的值設置爲0的最後4位-16 ...或11111111111111111111111111110000多16個。

3

當處理器將數據從內存載入寄存器時,它需要通過基地址和大小進行訪問。例如,它將從地址10100100獲取4個字節。請注意,該示例末尾有兩個零。這是因爲存儲了四個字節,因此101001的前導位很重要。 (處理器通過獲取101001XX來通過「不關心」來訪問這些內存。)

因此,對齊內存中的內容意味着重新排列數據(通常通過填充)以便所需項目的地址將具有足夠的零字節。繼續上面的例子,我們不能從10100101中獲取4個字節,因爲最後兩位不是0;這會導致總線錯誤。所以我們必須將地址碰撞到10101000(並且在這個過程中浪費了三個地址位置)。

編譯器自動執行此操作,並在彙編代碼中表示。

注意,這是明顯的,如C/C++的優化:

struct first { 
    char letter1; 
    int number; 
    char letter2; 
}; 

struct second { 
    int number; 
    char letter1; 
    char letter2; 
}; 

int main() 
{ 
    cout << "Size of first: " << sizeof(first) << endl; 
    cout << "Size of second: " << sizeof(second) << endl; 
    return 0; 
} 

輸出是

Size of first: 12 
Size of second: 8 

重新排列所述兩個char的指int將被正確地對準,並所以編譯器不必通過填充來衝突基地址。這就是爲什麼第二個規模較小。

13

這聽起來並不是特定的堆棧,而是一般的對齊。也許想到整數倍這個詞。

如果您的內存中的項目大小爲1個字節,單位爲1,則表示它們全部對齊。大小爲兩個字節的東西,那麼整數次數2將對齊,0,2,4,6,8等。非整數倍數1,3,5,7將不會對齊。大小爲4字節,整數倍數爲0,4,8,12等的項目對齊,1,2,3,5,6,7等不等。 8,0,8,16,24和16,16,32,48,64等等也是如此。

這是什麼意思是你可以看看該項目的基地址,並確定它是否對齊。

 
size in bytes, address in the form of 
1, xxxxxxx 
2, xxxxxx0 
4, xxxxx00 
8, xxxx000 
16,xxx0000 
32,xx00000 
64,x000000 
and so on 

在編譯器中的數據與在.text段是相當簡單的根據需要來對齊數據的指令的混合的情況下(當然,依賴於體系結構)。但是堆棧是一個運行時間的東西,編譯器通常無法確定堆棧在運行時的位置。所以在運行時如果你有需要對齊的局部變量,你需要讓代碼以編程方式調整棧。

舉個例子,你在堆棧中有兩個8字節的項目,總共16個字節,你真的希望它們對齊(在8個字節邊界上)。在進入時,函數會像往常一樣從堆棧指針中減去16,爲這兩個項目騰出空間。但要調整它們,需要更多的代碼。如果我們希望這兩個8字節的項目在8個字節的邊界上對齊,減去16後的堆棧指針爲0xFF82,那麼低3位不是0,所以它不會對齊。低三位是0b010。在一般意義上,我們想從0xFF82減去2得到0xFF80。我們如何確定它是2將通過與0b111(0x7)和減去該數量。這意味着一個和一個和一個減法操作。但是,如果我們和0x7(〜0x7 = 0xFFFF ... FFF8)的補碼值使用一個alu操作(只要編譯器和處理器有一個單一的操作碼方式來實現這個操作,如果沒有,它可能比你更多和減去)。

這似乎是你的程序在做什麼。使用-16與和0xFFFF ....和FFF0相同,導致在16字節邊界上對齊的地址。

所以包裝這件事,如果你碰到這樣一個典型的堆棧指針的作品其一路下跌,從高地址內存到低地址,那麼你要

 
sp = sp & (~(n-1)) 

其中n是字節數對齊(必須是權力,但沒關係,大多數對齊通常涉及兩個權力)。如果你說的做了一個malloc(地址從低到高增加),並要對齊的東西地址(記得至少對準大小的malloc比你更需要),然後

 
if(ptr&(~(n-)) { ptr = (ptr+n)&(~(n-1)); } 

或者,如果你想只要拿出如果在那裏,每次執行添加和掩碼。

許多/大部分非x86體系結構都有對齊規則和要求。 x86就指令集而言過於靈活,但就執行情況而言,您可能會爲x86上的未對齊訪問付出代價,因此儘管您可以這樣做,但您應該努力保持對齊狀態其他架構。也許這就是這個代碼所做的。

+1

非常棒的答案,它爲什麼在頁面的底部? – jwbensley 2016-06-08 16:59:43