Linux錯誤(6)X64向量指令訪問地址未對齊引起SIGSEGV
Author: Once Day Date: 2025年4月4日
一位熱衷于Linux學習和開發的菜鳥,試圖譜寫一場冒險之旅,也許終點只是一場白日夢…
漫漫長路,有人對你微笑過嘛…
全系列文章可參考專欄: Linux實踐記錄_Once_day的博客-CSDN博客
文章目錄
- Linux錯誤(6)X64向量指令訪問地址未對齊引起SIGSEGV
- 1. 問題分析
- 1.1 現象介紹
- 1.2 分析原因
- 1.3 解決思路
- 1.4 內存申請與對齊
- 2. 實例驗證
- 2.1 使用posix_memalign對齊內存
- 2.2 使用aligned_alloc 對齊內存
- 3. 總結
1. 問題分析
1.1 現象介紹
X64設備上,如果定義一個結構體,其包含連續的整數字段,并且存在類似的算術操作,編譯會自動優化代碼,生成向量指令SSE/AVX(xmm0),但是由于這些結構體的地址沒有對齊到16字節,讀取數據時會觸發SIGSEGV錯誤,造成coredump。
如下面這段代碼在O3優化等級下,會生成向量指令,從而觸發SIGSEGV問題:
struct result128 {uint64_t low;uint64_t high;
} __attribute__((aligned(16)));/* 禁止內聯 */
__attribute__((noinline)) void data128_add(struct result128 *result128, uint64_t low, uint64_t high)
{result128->low += low;result128->high += high;
}// data128_add函數二進制反匯編如下
// 1374: 66 48 0f 6e c6 movq %rsi,%xmm0
// 1379: 66 48 0f 6e ca movq %rdx,%xmm1
// 137e: 66 0f 6c c1 punpcklqdq %xmm1,%xmm0
// 1382: 66 0f d4 07 paddq (%rdi),%xmm0
// 1386: 0f 29 07 movaps %xmm0,(%rdi)
GDB調試運行結果如下,可以清晰看到執行的指令、代碼行和數據地址信息:
整個源碼文件如下:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>struct result128 {uint64_t low;uint64_t high;
} __attribute__((aligned(16)));/* 禁止內聯 */
__attribute__((noinline)) void data128_add(struct result128 *result128, uint64_t low, uint64_t high)
{result128->low += low;result128->high += high;// -O3 編譯會生成如下代碼
// 1374: 66 48 0f 6e c6 movq %rsi,%xmm0
// 1379: 66 48 0f 6e ca movq %rdx,%xmm1
// 137e: 66 0f 6c c1 punpcklqdq %xmm1,%xmm0
// 1382: 66 0f d4 07 paddq (%rdi),%xmm0
// 1386: 0f 29 07 movaps %xmm0,(%rdi)
}int main(void)
{// 申請一段內存, 64字節uint8_t *data = malloc(64);memset(data, 0xb, 64);// 手動對齊到 1/2/4/8/16 字節uint8_t *data1 = (uint8_t *)(((uintptr_t)data + 0) & ~0) + 1;uint16_t *data16 = (uint16_t *)(((uintptr_t)data + 1) & ~1);uint32_t *data32 = (uint32_t *)(((uintptr_t)data + 3) & ~3);uint64_t *data64 = (uint64_t *)(((uintptr_t)data + 7) & ~7);uint64_t *data128 = (uint64_t *)(((uintptr_t)data + 15) & ~15);printf("data addr : %p \n", data);printf(" data1 addr : %p \n", data1);printf(" data16 addr : %p \n", data16);printf(" data32 addr : %p \n", data32);printf(" data64 addr : %p \n", data64);printf(" data128 addr : %p \n", data128);printf("Data load test:\n");printf(" 2 bytes, unalign: 0x%x(%p).\n", *(uint16_t *)data1, data1);printf(" 4 bytes, unalign: 0x%x(%p).\n", *(uint32_t *)data1, data1);printf(" 8 bytes, unalign: 0x%lx(%p).\n", *(uint64_t *)data1, data1);// 使用ASM內聯匯編讀取到xmm0寄存器struct result128 result128;// 讀取 128位數據到xmm0寄存器__asm__ volatile("movdqa %0, %%xmm0" : : "m"(*data128) : "%xmm0");// 將xmm0寄存器的值存儲到result128中__asm__ volatile("movdqa %%xmm0, %0" : "=m"(result128) : : "%xmm0");printf(" 16 bytes align, result: 0x%lx, 0x%lx.\n", result128.low, result128.high);struct result128 *result128_addr = (struct result128 *)data1;data128_add(result128_addr, 0x1234567890abcdef, 0xfedcba0987654321);printf(" 16 bytes unalign, result: 0x%lx, 0x%lx.\n", result128.low, result128.high);return 0;
}
1.2 分析原因
在X64架構下,未對齊的內存訪問(如2/4/8字節非對齊訪問)可能會導致性能下降,但通常不會引發SIGSEGV錯誤。當使用SSE/AVX向量指令(如paddq)訪問未對齊的內存時,會觸發SIGSEGV錯誤,因為這些指令要求內存地址必須對齊到16字節邊界。
編譯器在O3優化等級下,識別出result128結構體的連續整數字段,并自動生成了向量指令來優化代碼。雖然result128結構體本身聲明了16字節對齊,但實際傳入的結構體指針可能沒有對齊到16字節邊界,導致向量指令訪問未對齊內存而觸發SIGSEGV。
1.3 解決思路
確保傳入的result128結構體指針已對齊到16字節邊界。可以使用posix_memalign
、aligned_alloc
等函數分配對齊的內存。如果無法保證傳入指針的對齊性,可以在函數內部使用memcpy將未對齊的數據復制到局部的、已對齊的result128結構體中,再進行計算和寫回。
可以使用#pragma pack(16)
聲明結構體,強制編譯器始終按16字節對齊結構體。但這可能會浪費內存空間。也可以使用__attribute__((aligned(16)))
修飾函數參數,確保傳入的指針已對齊。但這需要調用方遵循對齊要求。
在編譯選項中使用-mno-sse
或-mno-avx
禁用向量指令,避免自動向量化。但這會影響性能。
如果以上方法都無法實現,可以嘗試修改算法,避免在結構體上使用連續的算術操作,從而避免觸發向量化。
1.4 內存申請與對齊
常見的動態內存分配函數如malloc、calloc、realloc等,默認情況下返回的內存地址已經滿足了基本的對齊要求,一般是按照系統的最大基本數據類型對齊(如long double、指針等)。但這些函數無法直接指定更大的對齊字節數。
以下是一些支持指定內存對齊字節數的函數:
(1)posix_memalign (POSIX標準)
int posix_memalign(void **memptr, size_t alignment, size_t size);
posix_memalign可以指定alignment參數,要求必須是2的冪次且至少為sizeof(void*)
。函數將分配size字節的內存,并確保內存地址按alignment字節對齊。
(2)aligned_alloc (C11標準)
void *aligned_alloc(size_t alignment, size_t size);
aligned_alloc類似于posix_memalign,但將對齊的內存地址直接返回,而不是通過指針參數傳遞。
(3)memalign (GNU擴展)
void *memalign(size_t alignment, size_t size);
memalign是GNU的擴展函數,功能與aligned_alloc相似,但可移植性較差。
(4)_aligned_malloc (Windows)
void *_aligned_malloc(size_t size, size_t alignment);
_aligned_malloc是Windows平臺下的對齊內存分配函數,類似于aligned_alloc。
(5)valloc (已廢棄)
void *valloc(size_t size);
valloc分配的內存按虛擬內存頁大小(通常為4KB)對齊,但已被廢棄,不建議使用。
使用這些對齊內存分配函數獲取的內存,必須使用對應的內存釋放函數(如aligned_free、free等)來釋放,以避免內存泄漏。
2. 實例驗證
2.1 使用posix_memalign對齊內存
使用posix_memalign申請16字節對齊內存,執行函數,然后釋放:
struct result128 *result128_addr;
int32_t ret = posix_memalign((void **)&result128_addr, 16, sizeof(struct result128));
if (ret != 0) {printf("posix_memalign failed, ret: %d.\n", ret);return -1;
}
data128_add(result128_addr, 0x1234567890abcdef, 0xfedcba0987654321);
printf(" 16 bytes unalign, result: 0x%lx, 0x%lx.\n", result128_addr->low,result128_addr->high);// 釋放內存
free(data);
free(result128_addr);
在類Unix系統(如Linux、macOS等)中,只需包含<stdlib.h>
即可使用posix_memalign函數。
posix_memalign是POSIX標準定義的函數,在某些嵌入式系統或者非POSIX兼容的操作系統上可能無法使用。在這種情況下,可以考慮使用其他平臺特定的對齊內存分配函數,或者自行實現對齊內存分配的邏輯。
2.2 使用aligned_alloc 對齊內存
使用與posix_memalign類似,如下:
struct result128 *result128_addr = aligned_alloc(16, sizeof(struct result128));
data128_add(result128_addr, 0x1234567890abcdef, 0xfedcba0987654321);
printf(" 16 bytes unalign, result: 0x%lx, 0x%lx.\n", result128_addr->low,result128_addr->high);// 釋放內存
free(data);
free(result128_addr);
aligned_alloc函數是C11標準引入的,用于分配指定對齊字節數的內存。
aligned_alloc函數返回一個指向對齊內存的指針,該內存塊的大小為size字節,并按alignment字節對齊。如果分配成功,返回的指針可以直接傳遞給free函數釋放。
aligned_alloc函數與普通的malloc函數都遵循了相同的內存管理約定,即使用free函數釋放內存。
3. 總結
在X64架構下,使用未對齊的內存地址進行SSE/AVX向量指令訪問時,可能會觸發SIGSEGV錯誤。這通常發生在編譯器對包含連續整數字段的結構體進行自動向量化優化時。
為了避免這類問題,可以采取以下措施:
- 確保傳入的結構體指針已對齊到16字節邊界,可使用posix_memalign、aligned_alloc等函數分配對齊內存。
- 在函數內部使用memcpy處理未對齊數據,復制到局部的對齊結構體中進行計算和寫回。
- 在編譯選項中禁用向量指令,或修改算法避免觸發自動向量化。
常見的動態內存分配函數如malloc、calloc等默認返回的內存已經滿足基本的對齊要求(8字節),但無法直接指定更大的對齊字節數。為此,可以使用posix_memalign、aligned_alloc、memalign、_aligned_malloc等支持指定對齊字節數的函數。
在實際應用中,應優先使用標準的對齊內存分配函數,遵循最小適配原則,并使用對應的內存釋放函數,以提高代碼的可移植性、兼容性和內存使用效率。