最近处理一起客户问题,客户反馈使用我司 SDK 后偶现异常,只有重启计算机才能正常。客户的软件产品质量受到最终用户质疑,不得已只能提供排不使用我司 SDK 的“稳定版”给用户使用。
起初排查我司的是否存在 bug ,经过一番排查最后定位到客户代码存在 问题,造成偶现 bug 的代码如下:
// 代码来自网上
// 将十六进制的字符串转换成二进制数据
void HexToUnsignedChar(char *intput, unsigned int inputLongth, unsigned char *output)
{
char * inputTemp = intput;
unsigned char *cc = output;
int n;
char temp[2];
unsigned int j = 0;
for (int i = 0; i < inputLongth; i++)
{
temp[0] = *inputTemp;
temp[1] = *(inputTemp + 1);
inputTemp = inputTemp + sizeof(char) * 2;
sscanf_s(temp, "%x", &j);
*cc = j;
cc++;
}
}
惊现 BUG
Debug 一切正常
默认内存填充 CC,隐藏 BUG,无法发觉问题。
Release 偶现 BUG
随机栈数据引发的 BUG,Release 没有编译器默认填充的 CC 数据,程序运行,函数栈数据是随机的。
代码分析
- 函数栈数据一般也会使用字节对齐机制,局部变量
temp
分配两个字节内存,根据字节对齐的规则,程序运行的内存中后续至少有2个字节的随机数据。 sscanf_s
会根据格式%x
从temp[0]
指针位置直到字符串结尾,最长转换8个字符(4字节),因为temp
数组并未设定截止符 “\0” ,当随机数据满足 16 进制的规则(09,af, A~F)时,实际转换的字符长度大于2。- 当
*cc = j
时,变量cc
是unsigned char
类型(1字节),将 j(int) 的数据赋值给 cc(unsigned char) 时会强制类型转换,截取 j(int) 的最低位字节数据赋值给 cc。
假设一种场景:
输入转换字符串数据:FD,对应的二进制数据 0x46, 0x44
期望转换后的结果:0xFD
随机栈数据:0x33, 0x11
,此时 temp 内存指向数据为 0x46, 0x44, 0x33, 0x11 ,手动构造复现问题情境,修改 temp 变量后内存如下图所示:
[外链图片转存失败(img-Gz2FLygq-1564929965656)(index_files/109d8905-dff0-4a2c-8211-28b2ec241ab3.png)]
实际转换结果:0xD3
代码执行逻辑如下:
1.temp[2] = {0x46, 0x44} // 随机栈数据 0x33, 0x11
2.sscanf_s(temp, "%x", &j); 执行后 j = 0x0FD3 // 因为 0x11 的 ASCII 不符合转二进制的规则([0,9],[a,z],[A,Z]),会被当做终止符。
3.*cc = 0xD3
场景复现
复现方法,编写示例程序,编译成 Release 版本,在代码中增加调试调试信息,检查 temp 变量后是否存在随机栈数据,如果存在,则输出调试信息,反复执行多次。
测试代码:
#include <stdio.h>
#include <string.h>
//{x012,0x34} -> "1234"
int bytes_to_hexstr( const void* data, int count, char* hex)
{
int i = 0;
for( i; i < count; i++)
sprintf(hex + 2*i, "%02X", ((unsigned char*)data)[i]);
return 0;
}
void HexToUnsignedChar(char *intput, unsigned int inputLongth, unsigned char *output)
{
char * inputTemp = intput;
unsigned char *cc = output;
int n;
char temp[2];
unsigned int j = 0;
for (int i = 0; i < inputLongth; i++)
{
temp[0] = *inputTemp;
temp[1] = *(inputTemp + 1);
// 测试代码:检查内存中 temp 变量后的随机栈数据
if ((temp[2] > '0' && temp[2] < '9') || (temp[2] > 'a' && temp[2] < 'f') || (temp[2] > 'A' && temp[2] < 'F'))
{
printf("%d temp = %s\n", i + 1, temp);
//getchar();
}
inputTemp = inputTemp + sizeof(char) * 2;
sscanf_s(temp, "%x", &j);
*cc = j;
cc++;
}
}
int main()
{
// 测试
unsigned char outbuf[256] = {0};
char code[] = "FD66C759F7F578985DA3AF8057C56E32";
HexToUnsignedChar(code, strlen(code) / 2, outbuf);
// 输出转换后的结果
char new_buf[256] = {0};
bytes_to_hexstr(outbuf, strlen(code) / 2, new_buf);
printf("new_buf = %s\n", new_buf);
getchar();
return 0;
}
执行多次,得到一次异常结果:
1 temp = FD3
2 temp = 663
3 temp = C73
4 temp = 593
5 temp = F73
6 temp = F53
7 temp = 783
8 temp = 983
9 temp = 5D3
10 temp = A33
11 temp = AF3
12 temp = 803
13 temp = 573
14 temp = C53
15 temp = 6E3
16 temp = 323
new_buf = D363739373538383D333F3037353E323
从执行结果可以看出,问题复现时,内存中 temp 变量后存在随机数据 0x33 和无效随机值,造成转换结果与预期不一致,并且会对整个字符串的转换过程产生持续性的影响。
解决方案
- temp 指定结束符,例如:temp[3] = {0}
- sscanf_s 转换时指定长度(2),例如:sscanf_s(temp, “%02x”, &j);
问题反思
- 随机栈数据时非常难以发现的一种bug,对程序的健壮性产生极大的影响,随机偶现的特性决定问题一旦复现,会造成程序一直无法正常运行的情况,直到重启,产生的随机数据不会对程序功能产生影响。
- C 语言代码简单,但函数使用过程中容易忽略细节,尤其是内存操作:淹栈、溢出、内存泄露都是非常常见的问题,写代码时需要重点留意每一行代码的执行逻辑和预期结果的判断,否则会导致代码失控。