Windows+GCC下內存對齊的常見問題

結構/類對齊的聲明方式

gcc和windows對於modifier/attribute的支持實際上是差很少的。好比在gcc的例子中,內存對齊要寫成:windows

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

可是實際上你寫成數組

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

gcc同樣能夠識別。這樣MSVC和gcc就可使用宏完成跨平臺編譯。app

對齊類型的變量在堆與棧上的分配

對齊在如下場合都能提示編譯器爲它的變量分配對齊的地址:函數

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字節對齊的。spa

可是事實上挺奇怪。在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的形式。若是函數體內有對齊的變量,例如:code

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

那麼編譯器在代碼生成時,會在函數的前部插入一段稱爲prolog的代碼,這段代碼會將堆棧修正爲16B對齊,好比blog

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字節或用戶指定大小對齊。

相關文章
相關標籤/搜索