最近處理一起客戶問題,客戶反饋使用我司 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 語言代碼簡單,但函數使用過程中容易忽略細節,尤其是內存操作:淹棧、溢出、內存泄露都是非常常見的問題,寫代碼時需要重點留意每一行代碼的執行邏輯和預期結果的判斷,否則會導致代碼失控。