在 C 語言裡,要建立一個字元陣列的字串,常常會使用 sprintf() 這個函數來做格式化的處理。但是實際上,這個函式卻不是那麼「安全」。怎麼說呢?sprintf() 的整個介面長的樣子的是:
int sprintf ( char * str, const char * format, ... )
也就是在使用前,必須要先建立好一個字元陣列的空間,再用這個函式把內容填入,下面就是簡單的例子:
int tmp = 10; char cstr[20]; sprintf( cstr, "%d * %d = %d", tmp, tmp, tmp * tmp );
在這個例子裡,cstr 最後的值,會是「10 * 10 = 100」,看起來好像很好?但是如果把 tmp 的值改成 10000 的話,cstr 則應該要變成「10000 * 10000 = 100000000」,但是由於這時候的字串所需長度為 26,而要寫入的 cstr 的長度只有 20,所以就會造成 buffer overflow 的問題。
像如果是以 Visual C++ 2005 來編譯的話,如果程式裡有用到 sprintf(),他在編譯時就會顯示一個警告訊息:
warning C4996: 'sprintf': This function or variable may be unsafe. Consider using sprintf_s instead.
而要怎麼避免 sprintf 的 buffer overflow 的問題呢?除了微軟建議的 sprintf_s() 外,實際上在 C99 裡, 也多了一個 snprintf() 是用來取代現有的 sprintf() 了~他的介面是:
int snprintf(char *str, size_t size, const char * restrict format, ...)
應該可以明顯看得出來,snprintf() 這個函式比 sprinf() 多了一個參數 size;這個參數的用處,就是用來限制最大的寫入資料量,可以用來避免 buffer overflow。以上面的例子來說,本來寫:
int tmp = 10000; char cstr[20]; sprintf( cstr, "%d * %d = %d", tmp, tmp, tmp * tmp );
的話,會產生 buffer overflow 的問題。而如果改成用 snprintf() 的話,就是變成:
int tmp = 10000; char cstr[20]; snprintf( cstr, sizeof( cstr ), "%d * %d = %d", tmp, tmp, tmp * tmp );
這樣一來,snprintf() 在把資料寫到 cstr 時,最多就只會寫入 20 個字元(cstr 的長度),而不會有 buffer overflow 的問題了~
不過比較討厭的是,MSVC 似乎並沒有直接給這個函式,而是另外給了一個 _snprintf()…雖然功能和參數都大同小異,但是光函式名稱不一樣,就已經增加了些麻煩,更別說在行為模式上也有些不同了。而 _snprintf() 和 snprintf() 主要的差別在於:
_snprintf() (MSVC)
|
snprintf()
|
|
---|---|---|
回傳值
|
|
|
字串內容
|
|
|
|
所以如果要跨平台,大致上應該可以:
-
如果是使用 MSVC 的話,自行定義 snprintf,讓它變成 _snprintf
#ifdef _MSC_VER #define snprintf _snprintf #endif
-
實際使用,則就可以直接用 snprintf
int tmp = 10000; char cstr[20]; int size = sizeof( cstr ); int c = snprintf( cstr, size, "%d * %d = %d", tmp, tmp, tmp*tmp ); if( c > size || c < 0 ) cstr[ size – 1 ] = '';
這樣應該就可以在 g++ 和 MSVC 的環境裡,都可以避免 buffer overflow 和字串結尾沒有 " 的問題了~
但是這樣的做法,實際上是產生一個最大長度為 20 的字串,如果超過的話,雖然不會有 buffer overflow 的問題,但是過長的部分還是要切掉。而如果希望可以針對需要,產生一個夠長的字串的話,其實還可以使用 asprintf() 這個函式。他的用法很簡單,基本上和 sprintf() 很像,只是將第一個參數改成一個 char** 而已;下方就是簡單的範例:
char* cstr; int c = asprintf( &cstr, "%d * %d = %d", tmp, tmp, tmp*tmp );
如此一來,他就會自動替 cstr 產生一塊夠大的記憶體空間,來存放字串了~
不過 asprintf() 這個函式應該不在 C 語言的標準內,而是 GNU 的 extension(可能要加上「#define _GNU_SOURCE」才能使用),所以在 MSVC 裡並沒有提供 asprintf() 可以使用。不過雖然沒有現成的可以用,但是還是可以透過執行兩次 snprintf() 來做到同樣的功能~實作的方法,大致上就是:
char* cstr; int c = snprintf( NULL, 0, "%d * %d = %d", tmp, tmp, tmp*tmp ); cstr = new char[ c + 1 ]; snprintf( cstr, c + 1, "%d * %d = %d", tmp, tmp, tmp*tmp );
做法的主要概念,就是第一次的 snprintf() 實際上並不真正的將字串寫到某個空間,而是單純用來取得字串所需要的長度,然後再根據需要的長度來產生字元陣列,並將資料寫入。
[…] snprintf() […]
讚讚
應該還要做以下修正:
1. char cstr[20] = {‘\0′}; //確保都是’\0’
2. snprintf(cstr, sizeof(cstr)-1, “%d * %d = %d", tmp, tmp, tmp * tmp ); //用sizeof(cstr)-1而不是sizeof(cstr), 因為如果cstr被填滿,雖然沒有overflow, 但是沒有以’\0’結束。
讚讚
sorry, 剛有查到,上述講的已經在snprintf內部做掉了,所以原PO的寫法已是正確的了。
讚讚
沒問題就好
讚讚
解說詳細清楚,謝謝!
讚讚
還請幫忙多多宣傳 :)
讚讚
C++ 的話,建議請使用 stringstream
http://www.cplusplus.com/reference/iostream/stringstream/
或是 Boost::Format
https://kheresy.wordpress.com/2010/10/18/boost_format/
讚讚
[…] Live Space 裡會是怎樣呢?Heresy 有把之前的《用 snprintf / asprintf 取代不安全的 sprintf》一文的程式碼,都改用這個方法來寫了~有杏去的可以看看,Heresy […]
讚讚
[…] sprintf() 一樣,也算是個不安全的函式(參考《用 snprintf / asprintf 取代不安全的 sprintf》),所以個人不是很喜歡使用他。(註:在 gcc 下,應該還不是 thread-safe […]
讚讚
[…] 雖然在標準的 C 或 C++ 裡的確有提供部分的函式、例如 atoi()(參考),可以快速地做到部分的轉換,但是其實都有相當的限制。例如基本上他們只針對內建的部分型別有提供對應的函式(應該是只有 int、long、double),而且也沒有反向的轉換(itoa() 並非標準函式,要用標準的寫法要用 sprintf(),參考);而另外由於不同的型別就是不同的函示、也很難寫出統一個方法,來針對轉換的型別做擴充。 […]
讚讚
[…] 而如果是要用 sprintf 這個函式來產生格式化的字串的話,更有可能產生記憶體使用上的問題,Heresy 之前也有寫過一篇《用 snprintf / asprintf 取代不安全的 sprintf》,就是在講這部分的東西,有興趣的人可以參考看看。 […]
讚讚