最近寫了一段程式,發現在大壓力下會隨機 crash,訊號為 SEGV,gdb 檢視從出錯程式碼是 __int128 的一次訪問,彙編顯示出錯指令為

vmovdqa

。程式使用 gcc 7 編譯,重要的編譯引數是:

-O2 -march=sandybridge

。第一眼看到這條彙編,就可以猜到是非對齊記憶體訪問問題。下面從最小的 POC 程式碼來分析此問題出現的原因。

POC 分析

POC 程式碼如下:

struct B {

__int128 c;

uint64_t d;

};

struct A {

char a;

// struct B b;

char b[0];

};

int main() {

A* a = (A*)malloc(sizeof(struct A) + sizeof(B));

struct B* b = (struct B*)a->b;

__int128 bb;

b->c = bb;

asm volatile(“” : “+m,r”(b->c) : : “memory”);

return 0;

}

這裡選擇 3 種不同的編譯選項,我們來看一下生成的彙編指令。

編譯選項選擇空,也就是預設

-O0

,程式不會 crash,

b->c = bb;

對應的彙編為: mov QWORD PTR [rcx], rax

mov QWORD PTR [rcx+8], rdx

編譯選項選擇

-O2

,程式會 crash,

b->c = bb;

對應的彙編為

movaps XMMWORD PTR [rax+1], xmm0

編譯選項選擇

-O2 -march=sandybridge

,程式會 crash,

b->c = bb;

對應的彙編為

vmovdqa XMMWORD PTR [rax+1], xmm0

MOVAPS & VMOVDQA

這兩條指令,支援 128 bit 的操作,同時要求 16B 地址對齊,如果違反對齊規則,就會觸發 SEGV。

MOVAPS — Move Aligned Packed Single-Precision Floating-Point Values

Opcode/Instruction Op/En 64/32 bit Mode Support CPUID Feature Flag Description

NP 0F 28 /r MOVAPS xmm1, xmm2/m128 A V/V SSE Move aligned packed single-precision floating-point values from xmm2/mem to xmm1。

NP 0F 29 /r MOVAPS xmm2/m128, xmm1 B V/V SSE Move aligned packed single-precision floating-point values from xmm1 to xmm2/mem。

VEX。128。0F。WIG 28 /r VMOVAPS xmm1, xmm2/m128 A V/V AVX Move aligned packed single-precision floating-point values from xmm2/mem to xmm1。

VEX。128。0F。WIG 29 /r VMOVAPS xmm2/m128, xmm1 B V/V AVX Move aligned packed single-precision floating-point values from xmm1 to xmm2/mem。

VEX。256。0F。WIG 28 /r VMOVAPS ymm1, ymm2/m256 A V/V AVX Move aligned packed single-precision floating-point values from ymm2/mem to ymm1。

MOVDQA,VMOVDQA32/64—Move Aligned Packed Integer Values

Opcode/Instruction Op/En 64/32 bit Mode Support CPUID Feature Flag Description

NP 0F 28 /r MOVAPS xmm1, xmm2/m128 A V/V SSE Move aligned packed single-precision floating-point values from xmm2/mem to xmm1。

NP 0F 29 /r MOVAPS xmm2/m128, xmm1 B V/V SSE Move aligned packed single-precision floating-point values from xmm1 to xmm2/mem。

VEX。128。0F。WIG 28 /r VMOVAPS xmm1, xmm2/m128 A V/V AVX Move aligned packed single-precision floating-point values from xmm2/mem to xmm1。

VEX。128。0F。WIG 29 /r VMOVAPS xmm2/m128, xmm1 B V/V AVX Move aligned packed single-precision floating-point values from xmm1 to xmm2/mem。

VEX。256。0F。WIG 28 /r VMOVAPS ymm1, ymm2/m256 A V/V AVX Move aligned packed single-precision floating-point values from ymm2/mem to ymm1。

可以看到,

movaps

是 SSE 指令,

vmovdqa

至少是 AVX 指令。

對於 GCC 來說,在

x86-64

下,預設 march 是

x86-64

,具有最好的相容性,其支援 SSE 指令。可以透過

gcc -Q ——help=target | grep —— ‘-march=’

來檢視 GCC 預設 march 屬性。

對於

Sandy Bridge

架構,至少支援

AVX

XSAVE

等指令。

所以,我們可以看到,編譯選項開了

-march=sandybridge

之後,使用上了 AVX 指令。

同時,如果不開編譯器最佳化,那麼使用 mov 指令來完成 4 位元組的搬遷,開啟 O2 最佳化,會使用對應的 SSE 或者 AVX 指令來最佳化 mov 操作。

結構體記憶體佈局

在 POC 程式碼中,結構體的記憶體佈局如下:

__int128 的安全使用

image。png

alignof(A) = 1

, 那麼

struct B

的起始地址一定不是 16B 對齊的,接下來使用 SSE 或者 AVX 指令訪問 B 的第一個成員一定會 crash。

如果程式碼如下:

struct B {

__int128 c;

uint64_t d;

};

struct A {

char a;

struct B b;

};

此時就不會 crash,此時的記憶體佈局如下:

__int128 的安全使用

image。png

在 x86-64 體系結構下,malloc/new 預設返回的地址是 16B 對齊的,因為

alignof(max_align_t) == 16

資料結構本身也有對齊的要求,這裡

alignof(A) = alignof(B) = 16

,因為型別的最低對齊長度是由最長的基本資料單元的長度決定的。

總結

我們這裡,

struct B

並不是直接 malloc 出來的,而是嵌在另外一個數據結構後面,如果前面的資料結構沒有感知到 16B 的對齊,那麼 struct B 的起始地址就會造成非 16B 對齊。這是導致此次 crash 的關鍵所在。

__int128 在搭配記憶體池使用,像 POC 程式碼一樣,稍有不慎就會違反訪問規則,觸發 SEGV。

感興趣的可以在 godbolt 上試試:

https://

godbolt。org/z/Pnnv3Yfvd

參考

https://

hjlebbink。github。io/x86

doc/html/MOVDQA,VMOVDQA32_64。html

https://www。

felixcloutier。com/x86/m

ovaps