史萊姆最近被一些問題困擾著 :
到底要怎樣才能確定 sprintf 不會 buffer overflow 以及有正確的 zero end 呢 ?
sprintf 與 sprintf_s 到底有何不同 ?
看見很多人的 char buffer 都用 [BUF_MAX+1] 來宣告 ... 為何要 +1 ?
這小小的問題一點一點地在心中擴大,為了避免連夢中都會出現
本著實驗的精神,我打開 VS2010 做了以下的測試 ...
(註:以下程式碼會採用 TCHAR 等 unicode 轉換相容語法)
首先,我先定義了一些常數
// // 暫存區大小 const unsigned int c_BUFFER_MAX = 8; // 字串大小 const unsigned int c_STRING_MAX = 10; // 超過暫存區的字串 const TCHAR c_Copy_String[c_STRING_MAX+1] = _T("1234567890"); // 小於暫存區的字串 const TCHAR c_Safe_String[c_BUFFER_MAX+1] = _T("1234567"); //
字串的記憶體狀態如圖所示
然後使用結構來包裝三個 char array ,這是為了要確保記憶體會是連續的
並且在預設建構式將暫存區做 zero initialize
// struct Buf3 { TCHAR szBuf1[c_BUFFER_MAX]; TCHAR szBuf2[c_BUFFER_MAX]; TCHAR szBuf3[c_BUFFER_MAX]; Buf3() { memset(szBuf1, 0, sizeof(szBuf1)); memset(szBuf2, 0, sizeof(szBuf2)); memset(szBuf3, 0, sizeof(szBuf3)); } }; //
另外建置一個對照組,是尾端有 +1 的版本
同樣的初始化的時候,全部設定成為 0 ,包含額外的那一個
// (暫存區尾端+1) struct Buf3_s { TCHAR szBuf1[c_BUFFER_MAX+1]; TCHAR szBuf2[c_BUFFER_MAX+1]; TCHAR szBuf3[c_BUFFER_MAX+1]; Buf3_s() { memset(szBuf1, 0, sizeof(szBuf1)); memset(szBuf2, 0, sizeof(szBuf2)); memset(szBuf3, 0, sizeof(szBuf3)); } }; //
來看看字串大小是否如預期一樣
// 取得字串大小 size_t sizeCopyString = 0; size_t sizeSafeString = 0; sizeCopyString = _tcslen(c_CopyString); // 10 sizeSafeString = _tcslen(c_SafeString); // 7 // 這只是字串大小,實際儲存大小會多出一個 \0 結尾
果然,sizeCopyString 大小為 10
接著,產生一個暫存區結構,並且看看第二個暫存區大小
// 宣告暫存區 Buf3 stBuf; size_t sizeBuffer = 0; size_t sizeCount = 0; sizeBuffer = sizeof(stBuf.szBuf2); // 16 sizeCount = sizeof(stBuf.szBuf2) / sizeof(TCHAR); // 8 // 注意:記憶體大小與元素數量不一定相同
果然,sizeBuffer 大小為 16 bytes (因為是 unicode)
而內含元素數量為 8 個
========== 喘口氣分隔線,休息一下 ==========
實驗開始 :
首先,使用最常用的 sprintf 系列來複製字串
// sprintf _stprintf(stBuf.szBuf2, _T("%s"), c_CopyString); // wrong usage -> buffer overflow.結果:錯誤的用法, 暫存區溢位
如圖所示
你看,硬生生的把後面的暫存區蓋掉了三個,真是慘劇啊!!
(除了數字 9 與 0 之外,還外加一個 \0 結尾在 [2] 的位置,因為原本就是 \0 所以看不出來)
後方的受害者永遠找不出到底是誰蓋掉了自己的記憶體區塊
(因為有可能是從很遠的地方一路蓋過來)
我認為這是最難追蹤的 bug ! 千萬要避免
於是,微軟的編譯器在編譯時,會很熱心地警告使用者,要用尾端有加 _s 的呼叫
===
註:編譯期警告為
warning C4996: 'XXX': This function or variable may be unsafe. Consider using XXX_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS.
===
那麼,讓我們來試試看
// sprintf_s _stprintf_s(stBuf.szBuf2, _T("%s"), c_CopyString); // assert -> buffer too small.結果:跳出暫存區太小的警告, 程式中斷
應用程式直接死掉了 ... 而且是 ASSERT 等級, 連 try ... catch 都抓不到
在資料面來說,確實是"安全"了,但是對於應用程式來說卻問題大了
尤其是在背景運行的伺服器,發生了這種狀況,可能連 dump 都來不及就消失了 T_T
於是,你可能會常常被叫去老闆的辦公室喝茶XD
那 ... 有沒有其他的解法呢 ?
啊,我知道了,給他目標緩衝區的大小,就可以預防了吧?
// sprintf_s _stprintf_s(stBuf.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // wrong usage -> buffer overflow.結果:錯誤的用法, 暫存區溢位
咦?雖然程式沒有中斷但是卻還是溢位了,你不是說這是安全的呼叫嗎?!
其實,第二的參數是指元素數量,如果給了過大的值,還是一樣會有溢位的情況
會通過安全檢查是因為 sizeBuffer > _tcslen(c_CopyString) 的關係
簡單說,就是用錯方法(給錯參數)啦XD
那換其他幾種呼叫呢?
// sprintf_s _stprintf_s(stBuf.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // assert -> buffer too small. _stprintf_s(stBuf.szBuf2, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _stprintf_s(stBuf.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // assert -> buffer too small.結果:跳出暫存區太小的警告, 程式中斷
使用 _s 的安全呼叫,大部分的情況只要
目標緩衝區 < 來源大小 都會用 ASSERT 報錯,以確保資料是正確的 ...
但是程式就直接掛點了 orz
不然這樣,我們用 snprintf 系列的,規定他複製的大小總可以吧 ...?
// snprintf _sntprintf(stBuf.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // wrong usage -> buffer overflow _sntprintf(stBuf.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // wrong usage -> buffer overflow.結果:錯誤的用法, 暫存區溢位, 編譯時期警告
同上,第二個參數是指複製的元素數量,如果多給了,一樣會造成緩衝區溢位
那麼,如果給他元素數量總可以吧?
// snprintf _sntprintf(stBuf.szBuf2, sizeCount, _T("%s"), c_CopyString); // danger -> no zero end.結果:危險的用法, 沒有 0 結尾
至少沒有發生緩衝區溢位的情形了
不過原本應該有的 \0 結尾卻沒地方可以放而憑空消失了!!
為了預防這種狀況,我們必須要把元素減去 1 來給結尾額外的空間
// snprintf _sntprintf(stBuf.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end.結果:正確的 0 結尾
總算找到能夠安全複製字串的方法了!!
雖然這有個小缺點,就是多出來的字串會被 截斷
不過總比 緩衝區溢位 或是 ASSERT 中斷程式要來得好,我如此認為
那試試看有安全的 _s 看看?
// snprintf_s _sntprintf_s(stBuf.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf.szBuf2, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end.結果還是只有最後的方法能平安複製,其他的方法都會讓程式中斷
再來看看多載的另一個版本
// snprintf_s _sntprintf_s(stBuf.szBuf2, sizeBuffer, sizeCopyString, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _sntprintf_s(stBuf.szBuf2, sizeCount, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf.szBuf2, sizeCount, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end.也是只有最後的方法才能平安複製,給大家當個參考
既然要給元素數量減一的數量,那我們一開始就已經知道元素數量了
因為緩衝區的陣列,我們是使用常數 c_BUFFER_MAX 去定義的
所以,可以將語法換成如下所示:
// 參考範例 _stprintf_s(stBuf.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // assert -> buffer too small _stprintf_s(stBuf.szBuf2, c_BUFFER_MAX-1, _T("%s"), c_CopyString); // assert -> buffer too small _sntprintf(stBuf.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // danger -> no zero end. _sntprintf(stBuf.szBuf2, c_BUFFER_MAX-1, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // assert -> buffer too small _sntprintf_s(stBuf.szBuf2, c_BUFFER_MAX-1, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf.szBuf2, c_BUFFER_MAX, c_BUFFER_MAX-1, _T("%s"), c_CopyString); // ok -> has zero end.
大家可以比較看看差異在哪 ^_^
========== 喘口氣分隔線,休息一下 ==========
接下來試試看有多 +1 的暫存區會有怎樣不同的結果
Buf3_s stBuf_s; sizeBuffer = sizeof(stBuf_s.szBuf2); // 18 sizeCount = sizeof(stBuf_s.szBuf2) / sizeof(TCHAR); // 9確認,sizeBuffer 大小為 18 bytes (因為是 unicode)
而內含元素數量為 9 個
實驗趴特兔開始 :
以下就不贅述了,基本上都跟上面一樣,但是要注意有些地方的結果有微妙的差異
// _stprintf(stBuf_s.szBuf2, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _stprintf_s(stBuf_s.szBuf2, _T("%s"), c_CopyString); // assert -> buffer too small. _stprintf_s(stBuf_s.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _stprintf_s(stBuf_s.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // assert -> buffer too small. _stprintf_s(stBuf_s.szBuf2, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _stprintf_s(stBuf_s.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf(stBuf_s.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _sntprintf(stBuf_s.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _sntprintf(stBuf_s.szBuf2, sizeCount, _T("%s"), c_CopyString); // danger -> no zero end. _sntprintf(stBuf_s.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf_s.szBuf2, sizeBuffer, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf_s.szBuf2, sizeCopyString, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf_s.szBuf2, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf_s.szBuf2, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf_s.szBuf2, sizeBuffer, sizeCopyString, _T("%s"), c_CopyString); // wrong usage -> buffer overflow. _sntprintf_s(stBuf_s.szBuf2, sizeCount, sizeCount, _T("%s"), c_CopyString); // assert -> buffer too small. _sntprintf_s(stBuf_s.szBuf2, sizeCount, sizeCount-1, _T("%s"), c_CopyString); // ok -> has zero end.
同樣的,既然需要的是元素數量,那麼我們也可以直接給常數
不過在這邊不太一樣的是,因為我們宣告時已經事先 +1 了
所以這邊只要給常數值就可以了,不用額外計算,很方便吧XD
// _stprintf_s(stBuf_s.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // assert -> buffer too small _stprintf_s(stBuf_s.szBuf2, c_BUFFER_MAX-1, _T("%s"), c_CopyString); // assert -> buffer too small _sntprintf(stBuf_s.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf_s.szBuf2, c_BUFFER_MAX, _T("%s"), c_CopyString); // ok -> has zero end. _sntprintf_s(stBuf_s.szBuf2, c_BUFFER_MAX+1, c_BUFFER_MAX, _T("%s"), c_CopyString); // ok -> has zero end.
好棒,因為多了一格可以寫入結尾,所以也不會發生 ASSERT 讓程式中斷了!! ^o^
===
總結:
只有上述會發生"跳出暫存區太小的警告, 程式中斷"
或者是成功的複製並且有 0 結尾的案例,才是安全可用的
目前我主要會使用 _snprintf 來複製字串到暫存區
並且在暫存區預設大小宣告後面 +1 用來存放結尾的 \0
===
既然找到了方法,或許也會有人疑惑,這幾種呼叫到底速度差多少?
效能評比 :
每種指令共執行 10 次, 其中會跑 10,000 次迴圈之後統計經過時間
_stprintf()
Process Time : 0.00640352
Process Time : 0.00498874
Process Time : 0.00507428
Process Time : 0.0054189
Process Time : 0.00503517
Process Time : 0.00555856
Process Time : 0.00544438
Process Time : 0.00511618
Process Time : 0.00560814
Process Time : 0.00508056
_stprintf_s()
Process Time : 0.00655505
Process Time : 0.006576
Process Time : 0.0063068
Process Time : 0.00592866
Process Time : 0.00618704
Process Time : 0.00634346
Process Time : 0.00621532
Process Time : 0.00603376
Process Time : 0.00595206
Process Time : 0.00645938
_sntprintf()
Process Time : 0.00656133
Process Time : 0.0067181
Process Time : 0.00601665
Process Time : 0.00699813
Process Time : 0.00649464
Process Time : 0.00588921
Process Time : 0.00676
Process Time : 0.00663431
Process Time : 0.00601595
Process Time : 0.0064737
_sntprintf_s()
Process Time : 0.00808889
Process Time : 0.00786718
Process Time : 0.00768282
Process Time : 0.00837694
Process Time : 0.00739302
Process Time : 0.00789266
Process Time : 0.00790768
Process Time : 0.00796843
Process Time : 0.00748799
Process Time : 0.00742235
可以看見,要使用安全的作法,確實會造成效能損失
但是那也只是多了一次判斷,一般的人根本感覺不到
以上,希望可以提供給大家做為參考
沒有留言:
張貼留言