原标题:排查了一周的灵异Bug,真相是一个不起眼的类型转换
大家好,我是仲一。最近在排查一个嵌入式 Linux 项目,有个模块跑着跑着,局部变量会莫名其妙被改掉。反复定位下来,根因很小——一个 unsigned int 的地址,被当成 unsigned long long * 传进了函数。
用几段代码还原一下现场。
第一段代码
#include <stdio.h>
#define CONFIG_SIMPLE_SYS_BOOTARGS "mem=%luM console=ttyS0,115200 root=/dev/mtdblock%d rootfstype=squashfs"
void test(unsigned long long *size)
{
unsignedlonglong memsize = 456789;
*size = memsize;
}
int main()
{
unsignedint var = 272;
int var2 = 14;
char var3[256]={0};
printf(" var2:%dn",var2);
test((&var));
printf(" var2:%dn",var2);
sprintf(var3, CONFIG_SIMPLE_SYS_BOOTARGS, var, var2);
printf(" var2:%dn",var2);
return0;
}
问题出在 test((&var))。var 只占了 4 字节,test 期望的是一个 unsigned long long *——它要写 8 字节。编译器会报个 warning,但程序照常运行,test 往 var 的地址一口气写 8 字节,多出来的 4 字节正好踩到相邻的 var2。
实际项目里的写法更隐蔽一点:
#include <stdio.h>
#define CONFIG_SIMPLE_SYS_BOOTARGS "mem=%luM console=ttyS0,115200 root=/dev/mtdblock%d rootfstype=squashfs"
void boot_getMemSize(unsigned long long *size)
{
unsignedlonglong memsize = 419430400;
*size = memsize;
}
int main()
{
unsignedint memSize = 272;
int partIndex = 14;
char tempBuf[256]={0};
printf(" partIndex:%dn",partIndex);
boot_getMemSize((&memSize));
printf(" partIndex:%dn",partIndex);
sprintf(tempBuf, CONFIG_SIMPLE_SYS_BOOTARGS, memSize, partIndex);
printf(" tempBuf:%sn",tempBuf);
printf(" partIndex:%dn",partIndex);
return0;
}
这种问题常见的原因就两个:要么一开始就把 unsigned int 当 unsigned long long 传了,要么函数接口升级了类型,调用方没跟上。
那能不能不动变量类型,只改函数签名来绕过?有人试过这么写:
#include <stdio.h>
#define CONFIG_SIMPLE_SYS_BOOTARGS "mem=%luM console=ttyS0,115200 root=/dev/mtdblock%d rootfstype=squashfs"
void boot_getMemSize(unsigned long long **size)
{
staticunsignedlonglong memsize = 419430400;
*size = &memsize;
}
int main()
{
unsignedint *memSize;
int partIndex = 14;
char tempBuf[256]={0};
printf("[] partIndex1:%dn",partIndex);
boot_getMemSize(&memSize);
printf("[] partIndex2:%d,memSize=%lun",partIndex, *memSize);
sprintf(tempBuf, CONFIG_SIMPLE_SYS_BOOTARGS, *memSize, partIndex);
printf("[] tempBuf:%sn",tempBuf);
printf("[] partIndex3:%dn",partIndex);
return0;
}
绕了个弯,但引入了 static 变量,多线程或函数重入就炸了,不推荐。
反汇编看一下
回到第一种情况,看反汇编就一清二楚了。
000000000040063c <main>:
int main()
{
40063c: a9ae7bfd stp x29, x30, [sp,#-288]!
400640: 910003fd mov x29, sp
unsigned int var = 10;
400644: 52800140 mov w0, #0x0a
400648: b9011ba0 str w0, [x29,#280]
int var2 = 5;
40064c: 528000a0 mov w0, #0x5
400650: b9011fa0 str w0, [x29,#284]
...
test((&var));
400678: 910463a0 add x0, x29, #0x118
40067c: 97ffffe5 bl 400610 <test>
...
var 在 x29+280,var2 在 x29+284,紧挨着。test 的 x0 指向了 var。
再看 test 的函数体:
void test(unsigned long long *var)
{
...
*var = tempVar;
400624: f94007e0 ldr x0, [sp,#8]
400628: f9400fe1 ldr x1, [sp,#24]
40062c: f9000001 str x1, [x0]
}
str x1, [x0],一条指令写 8 字节。x0 指向的是 var(4 字节),后 4 字节直接盖到 var2 上去了。
总结
根因一句话:函数参数类型和实参类型不匹配。8 字节写入 4 字节的目标,多出来的字节肯定踩到旁边的变量。
平时写代码,编译器的 -Wpointer-sign 和 -Wformat 开关建议打开。如果有强制类型转换,特别留心两边的内存大小。
常见踩坑场景:接口升级改了参数类型,调用方漏了没改用了 void* 做通用参数,传参时类型搞错头文件更新了,其他模块没重新编译
遇到局部变量鬼畜改值的,先看看是不是指针类型大小不匹配。
160