Windows+GCC下記憶體對齊的常見問題

空明流轉發表於2013-11-27

結構/類對齊的宣告方式

gcc和windows對於modifier/attribute的支援其實是差不多的。比如在gcc的例子中,記憶體對齊要寫成:

class X
{
  //...
} __attribute__((aligned(16)));

但是實際上你寫成

class __attribute__((aligned(16))) X 
{
    /*...*/
};

gcc一樣可以識別。這樣MSVC和gcc就可以使用巨集完成跨平臺編譯。

對齊型別的變數在堆與棧上的分配

對齊在以下場合都能提示編譯器為它的變數分配對齊的地址:

void foo()
{
    X v; // v是個棧上的16位元組對齊的變數
    X* p = new X; // p是堆上的16位元組對齊的指標
    X* a = new X[ARRAY_SIZE]; // 那麼這個呢?
}

棧上的變數堆上分配出的變數,因為align這個hint的存在,都能滿足16位元組對齊的要求。但是陣列呢?按照一般規律來分析,對齊後的sizeof(X),一定是對齊的整數倍。比如16位元組對齊的話,那麼X的大小隻能是16的倍數。所以對於本例的陣列而言,編譯器應該也能知道a應該是16位元組對齊的。

但是事實上挺奇怪。在MSVC上,p和a都很好的遵守了對齊的要求;在gcc上,p是對齊的,但是a卻不是。其實這個問題在2004年便有人提出來,只是到目前為止一直都沒有人動手過。當然,標準也沒有規定X的陣列就一定是要對齊的。要解決這個問題,要麼過載class的operator new/delete,要麼用memalign/aligned_malloc分配出對齊的記憶體,再placement new。出於易用性,我選擇的是操作符過載。

clang對於對齊的支援更乾脆:16B的對齊已經夠用了。所以align完全被編譯器忽視了。結果Intel出來了AVX,Clang就傻逼了。不知道這個問題3.4會不會修正。

編譯器如何實現記憶體對齊

MSVC在x86下預設是支援的4B的記憶體對齊。也就是說在函式入口處,ESP和EBP只保證是4位元組對齊的。這時,當前函式域棧上變數的地址都是ESP + 4 * x的形式。如果函式體內有對齊的變數,例如:

void foo()
{
    int __declspec(align(16)) x;
    // ...
}

那麼編譯器在程式碼生成時,會在函式的前部插入一段稱為prolog的程式碼,這段程式碼會將堆疊修正為16B對齊,比如

PUSH EBP
MOV  EBP, ESP
SUB  ESP, XXX
AND  ESP, 0xFFFFFFF0h

這樣ESP就一定是16位元組對齊的。這個時候給x分配的地址,就可以是ESP + 0x10 * n的形式,這樣就滿足了對齊的需要。

在GCC上,gcc認為所有的函式都有義務在呼叫其它函式的時候,ESP是16位元組對齊的(當然,可以通過編譯選項修改這一要求)。不光是呼叫方會這樣保證,被呼叫方也是這樣預設的。所以GCC為了呼叫效率更高一點,便根據呼叫方的假設,去掉了“堆疊修正”這個步驟。

原來的程式碼可能就變成了

PUSH EBP             ; 假設這裡的ESP是16B對齊的,Push了EBP,ESP就是16x-4了。
MOV  EBP, ESP
SUB  ESP, 0x0000023Ch ; 減完以後這裡又是16位元組對齊了

那麼當被呼叫方遵守這個約定的時候,ESP當然就是16位元組對齊的。但是有一種情況例外。在MinGW下,執行緒的入口函式是被API回撥的。這個函式很可能是按照Windows的標準4個位元組對齊的。這樣,在沒有堆疊修正的情況下,整個執行緒呼叫鏈16B對齊的默契就被打破了。如果這個時候出現了SSE程式碼試圖存取“16位元組對齊”的變數,那可能就會發生segment fault的異常,因為這些變數的地址並不是對齊的。

解決這個問題,有兩種常見的辦法:第一,寫一個Wrapper函式,對齊ESP後轉發呼叫;第二,使用編譯選項-mstackrealign。這個選項會為所有函式增加堆疊修正的PROLOG程式碼,以保證函式棧幀一定是按照16位元組或使用者指定大小對齊。

相關文章