__int128 的安全使用
最近寫了一段程式,發現在大壓力下會隨機 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 程式碼中,結構體的記憶體佈局如下:
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,此時的記憶體佈局如下:
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