隨機棧數據引發的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 語言代碼簡單,但函數使用過程中容易忽略細節,尤其是內存操作:淹棧、溢出、內存泄露都是非常常見的問題,寫代碼時需要重點留意每一行代碼的執行邏輯和預期結果的判斷,否則會導致代碼失控。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章