1、问题背景
最近在开发过程中遇到一个非常诡异的问题:
- 某些变量被篡改成异常值
- 加一行 printf 后,问题消失或表现改变
- Debug 版本容易复现
- Release 版本基本正常
这类问题非常典型,但也非常容易误导人,因为它看起来像:
随机问题 / 编译器问题 / 栈问题 / 多线程问题
但最终定位下来,其实是一个经典错误:
👉 非法类型转换 + 栈内存越界写(Undefined Behavior)
2、最小复现代码
测试平台:Aarch64
c
#include <stdio.h>
#include <stdint.h>
void overflow(uint64_t *p) {
uint64_t a = 0XFF;
*p = a;
}
void test() {
uint8_t a;
uint32_t x = 0x11111111;
uint32_t y = 0x22222222;
printf("Before:\n");
printf("x = 0x%x, y = 0x%x\n", x, y);
overflow((uint64_t *)&a);
printf("\nAfter:\n");
printf("x = 0x%x, y = 0x%x\n", x, y);
}
int main() {
test();
return 0;
}
3、运行现象
Debug 版本
bash
[root@sylixos:/apps/app_test]# ./app_test
Before:
x = 0x11111111, y = 0x22222222
After:
x = 0x11000000, y = 0x0
👉 x 和 y 都被破坏了
Release 版本
bash
[root@sylixos:/apps/app_test]# ./app_test
Before:
x = 0x11111111, y = 0x22222222
After:
x = 0x11111111, y = 0x22222222
👉 看起来完全正常
4、问题本质
核心问题在这一行:
c
overflow((uint64_t *)&a);
把 1 字节变量当成 8 字节变量,然后越界写。
c
*p = a; // 写 8 字节
我们观察反汇编:
Debug 版本:
c
void test() {
744: a9be7bfd stp x29, x30, [sp, #-32]!
748: 910003fd mov x29, sp
74c: 39005fff strb wzr, [sp, #23] // 将 0 存储到 [sp + 23] 这个栈内存地址中
750: 3200e3e0 mov w0, #0x11111111 // 将立即数 0x11111111 加载到 w0 寄存器
754: b9001fe0 str w0, [sp, #28] // 将 w0 寄存器的值存储到栈上
758: 3203e3e0 mov w0, #0x22222222 // 将立即数 0x22222222 加载到 w0 寄存器
75c: b9001be0 str w0, [sp, #24] // 将 w0 寄存器的值存储到栈上
......
......
76c: b9401be2 ldr w2, [sp, #24] // 从栈中读取 y
770: b9401fe1 ldr w1, [sp, #28] // 从栈中读取 x
774: 90000000 adrp x0, 0 <.plt-0x480>
778: 911fe000 add x0, x0, #0x7f8
77c: 97ffff49 bl 4a0 <printf@plt>
780: 91005fe0 add x0, sp, #0x17 // x0 保存的是变量 a 的地址,0x17 换算成十进制就是 23
784: 97ffff4f bl 4c0 <overflow@plt>
788: 90000000 adrp x0, 0 <.plt-0x480>
78c: 91204000 add x0, x0, #0x810
790: 97ffff48 bl 4b0 <puts@plt>
794: b9401be2 ldr w2, [sp, #24] // 从栈中读取 y
798: b9401fe1 ldr w1, [sp, #28] // 从栈中读取 x
79c: 90000000 adrp x0, 0 <.plt-0x480>
7a0: 911fe000 add x0, x0, #0x7f8
7a4: 97ffff3f bl 4a0 <printf@plt>
}
👉 栈布局大致是:
bash
sp+23 : a (1 byte)
sp+24 : y (4 bytes)
sp+28 : x (4 bytes)
写入发生什么?
c
*p = 0xFF;
👉 实际写:
sp+23 ~ sp+30 (连续 8 字节)
覆盖范围:
a + y + x 的一部分
4.1 为什么 Debug 会显现问题,Release 却不会?
查看 Release 的反汇编:
c
void test() {
750: a9bd7bfd stp x29, x30, [sp, #-48]!
754: 90000000 adrp x0, 0 <.plt-0x480>
758: 911f8000 add x0, x0, #0x7e0
75c: 910003fd mov x29, sp
760: f9000bf3 str x19, [sp, #16]
764: 90000013 adrp x19, 0 <.plt-0x480>
768: 3900bfff strb wzr, [sp, #47]
76c: 97ffff51 bl 4b0 <puts@plt>
770: 3203e3e2 mov w2, #0x22222222 // #572662306
774: 3200e3e1 mov w1, #0x11111111 // #286331153
778: 911fa273 add x19, x19, #0x7e8
77c: aa1303e0 mov x0, x19
780: 97ffff48 bl 4a0 <printf@plt>
784: 9100bfe0 add x0, sp, #0x2f
788: 97ffff4e bl 4c0 <overflow@plt>
78c: 90000000 adrp x0, 0 <.plt-0x480>
790: 91200000 add x0, x0, #0x800
794: 97ffff47 bl 4b0 <puts@plt>
798: aa1303e0 mov x0, x19
79c: 3203e3e2 mov w2, #0x22222222 // #572662306
7a0: 3200e3e1 mov w1, #0x11111111 // #286331153
7a4: 97ffff3f bl 4a0 <printf@plt>
}
7a8: f9400bf3 ldr x19, [sp, #16]
7ac: a8c37bfd ldp x29, x30, [sp], #48
7b0: d65f03c0 ret
7b4: d503201f nop
7b8: d503201f nop
7bc: d503201f nop
👉 x 和 y:
- 根本没从内存读
- 直接用立即数
c
mov w2, #0x22222222
mov w1, #0x11111111
Release 版本改变了栈布局、重排变量、使用寄存器缓存数据
4.2 为什么加 printf 会改变程序执行结果?
原因: printf 会影响栈布局
- 压栈更多数据
- 改变变量偏移