随机栈数据引发的bug

最近处理一起客户问题,客户反馈使用我司 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 数据,程序运行,函数栈数据是随机的。

代码分析

  1. 函数栈数据一般也会使用字节对齐机制,局部变量 temp 分配两个字节内存,根据字节对齐的规则,程序运行的内存中后续至少有2个字节的随机数据。
  2. sscanf_s 会根据格式 %xtemp[0] 指针位置直到字符串结尾,最长转换8个字符(4字节),因为 temp 数组并未设定截止符 “\0” ,当随机数据满足 16 进制的规则(09,af, A~F)时,实际转换的字符长度大于2。
  3. *cc = j 时,变量 ccunsigned 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 和无效随机值,造成转换结果与预期不一致,并且会对整个字符串的转换过程产生持续性的影响。

解决方案

  1. temp 指定结束符,例如:temp[3] = {0}
  2. sscanf_s 转换时指定长度(2),例如:sscanf_s(temp, “%02x”, &j);

问题反思

  1. 随机栈数据时非常难以发现的一种bug,对程序的健壮性产生极大的影响,随机偶现的特性决定问题一旦复现,会造成程序一直无法正常运行的情况,直到重启,产生的随机数据不会对程序功能产生影响。
  2. C 语言代码简单,但函数使用过程中容易忽略细节,尤其是内存操作:淹栈、溢出、内存泄露都是非常常见的问题,写代码时需要重点留意每一行代码的执行逻辑和预期结果的判断,否则会导致代码失控。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章