專案中發現一個函數使用變長數組(VLA),對超出範圍的索引賦值後,數組首地址變為 NULL。簡化後的複現代碼如下:
#include <stdio.h>
int test(int n) {
char *arg[n + 4];
printf("before:arg = %p\n", &arg[0]);
arg[16] = NULL; // 越界寫入
printf("after:arg = %p\n", &arg[0]);
return 0;
}
int main() {
test(2);
return 0;
}
在 i386 平台執行,arg[16] = NULL 後再印出 &arg[0] 顯示為 NULL。
使用 GDB 設中斷並單步執行(si),觀察 $pc 對應的組合語言:
(gdb) b test
(gdb) display /i $pc
(gdb) run
(gdb) si // 重複單步
關鍵組合語言片段(objdump -d 反組譯對照):
804835b: 89 e0 mov %esp,%eax
804835d: 89 c3 mov %eax,%ebx
804835f: 8b 45 08 mov 0x8(%ebp),%eax
8048362: 83 c0 04 add $0x4,%eax
8048365: c1 e0 02 shl $0x2,%eax
8048368: 83 c0 0f add $0xf,%eax
804836b: 83 c0 0f add $0xf,%eax
804836e: c1 e8 04 shr $0x4,%eax
8048371: c1 e0 04 shl $0x4,%eax
8048374: 29 c4 sub %eax,%esp
8048376: 8d 44 24 08 lea 0x8(%esp),%eax
804837a: 89 45 e8 mov %eax,0xffffffe8(%ebp)
...
mov %esp,%eax; mov %eax,%ebx — 將當前 esp 存入 ebx,函數結束時恢復用。mov 0x8(%ebp),%eax; add $0x4,%eax — 取參數 n,加 4 得到 n+4。shl $0x2,%eax — 左移 2 位(×4,sizeof(char*))add $0xf + shr $0x4 + shl $0x4 — 標準 16-byte 對齊演算法:(size + 15) & ~15sub %eax,%esp — 向下擴展棧,預留 VLA 空間。lea 0x8(%esp),%eax 等 — 將對齊後的基址存入 ebp-0x18、ebp-0x8 等槽位,後續 &arg[0] 實際讀取的是這些槽位的值。VLA 在棧上動態分配,基址計算過程中會將對齊後的地址暫存至棧上臨時槽位。arg[16] = NULL 越界寫入剛好覆蓋了存放基址的臨時槽位(ebp-0x8),導致後續讀取 &arg[0] 時取到被覆蓋的 NULL。這說明 VLA 的元数据(基址指標)緊鄰數組本體,極易被越界寫入破壞。