C語言並不是一種很方便的語言,它的字串就是一例。按照C語言的定義,“字串就是一段記憶體空間,裡麵包含ASCII字元,並且,以”“結尾,總共能存放n-1個字元。”按照這個描述,字串處理確實很麻煩,還很容易出錯。
為了方便使用者,C語言標準庫向使用者提供了一些字串函數,如字串拷貝、構造、清空等函數,在一定程度上方便了使用者的使用。但是,我無意中發現,這些函數還是有些隱患的。
事情很簡單,我注意到我寫的一些程式,老是有記憶體讀寫錯誤,但是,經過仔細檢查我所有的資料Buffer,以及相關的處理函數,又沒有找到什麼錯誤。於是我把懷疑的目光投向我常用的一些字串處理函數上,如strcpy、sprintf等。在經過幾次仔細地跟蹤之後,我發現記憶體錯誤出自於此。於是,我開始研究如何安全地使用字串這個話題。
1.字串拷貝函數
1.1 不安全的strcpy
首先,我寫了這樣一個測試函數:
void strcpyTest0()
{
int i;
char szBuf[128];
for(i=0;i<128;i++) szBuf[i]='*';
szBuf[127]=''; //構造一個全部是*的字串
char szBuf2[256];
for(i=0;i<256;i++) szBuf2[i]='#';
szBuf2[255]=''; //構造一個全部是#的字串
strcpy(szBuf,szBuf2);
printf("%sn",szBuf);
}
很簡單,把一個字串拷貝到另外一個空間,但是,很不幸,源字串比目標地址要長,因此,程式很悲慘地死去了。
1.2 還是不安全的strncpy
通過上例,我發現我需要在拷貝時多輸入一個參數,來標明目的地址有多長,檢查C語言的庫函數說明,有一個strncpy可以達到這個目的,這個函數的原型如下:
char *strncpy( char *strDest, const char *strSource, size_t count );
好了,這下我們的問題解決了,我寫出了如下代碼:
void strcpyTest1()
{
int i;
char szBuf[128];
for(i=0;i<128;i++) szBuf[i]='*';
szBuf[127]='';
char szBuf2[256];
for(i=0;i<256;i++) szBuf2[i]='#';
szBuf2[255]='';
strncpy(szBuf,szBuf2,128);
printf("%sn",szBuf);
}
一切都顯得很好,但是,當我輸出結果的時候,發現了問題,字串後面有時會跟幾個奇怪的字元,好像沒有用“”結束,於是我把上面的拷貝語句改成“strncpy(szBuf,szBuf2,8);”,只拷貝8個字元,問題出現了,程式輸出如下:
########***********************************************************************************************************************
果然,當請求的目標地址空間比源字串空間要小的時候,strncpy將不再用“”來結束字串。巨大的隱患。
1.3 安全地字串拷貝函數
我仔細想了想,我認為我需要如下一個字串拷貝函數:
1、允許用一個整數界定目標地址空間尺寸。
2、當目標地址空間nD小於源字串長度nS時,應該只拷貝nD個位元組。
3、任何情況下,目標地址空間均應該以“”結束,保持一個合法的字串身份。因此,得到的字串最大長度為nD-1.
於是,我寫了這麼一個字串拷貝函數:
void xg_strncpy1(char *pD, char *pS,int nDestSize)
{
memcpy(pD,pS,nDestSize);
*(pD+nDestSize-1)='';
}
很EASY是不,將這個拷貝函數代入上面的例子,只輸出7個“#”, 結果正確。
1.4 記憶體讀錯誤的思考
本來以為可以就此打住了,不過,沒多久,我就發現一個奇怪的現象,這個函數在VC的Debug模式下有錯誤,但是Release模式下卻一切正常。
我奇怪了很久,終於有一天我忍不住了,決定解決這個問題,我把上面的memcpy用自己的一個複製迴圈代替,單步跟蹤,想看看究竟怎麼回事?
原因找到了,我希望拷貝一個256位元組長的字串,但是,拷貝到第33位元組時出錯,檢查程式,發現我的源字串空間只有32 Bytes,原來,我上面的代碼只是防止了記憶體寫出界,但沒有針對讀出界進行檢查,在VC的Debug模式下,記憶體讀出界也是一種非法錯誤,因此被報錯。
知道了原因,解決就很簡單了,我把上面的拷貝函數改成如下形狀:
void xg_strncpy2(char *pD, char *pS,int nDestSize)
{
int nLen=strlen(pS)+1;
if(nLen>nDestSize) nLen=nDestSize;
memcpy(pD,pS,nLen);
*(pD+nLen-1)='';
}
一切OK.
2.字串建構函式
2.1 不安全的sprintf
如同上例,我在修改拷貝函數的同時,我也想到了另外一個我常用的字串建構函式sprintf,顯然,這個函數沒有界定目標地址空間的尺寸,也是不安全的,下面的代碼將會造成崩潰:
void sprintfTest0()
{
int i;
char szBuf[128];
for(i=0;i<128;i++) szBuf[i]='*';
szBuf[127]='';
char szBuf2[256];
for(i=0;i<256;i++) szBuf2[i]='#';
szBuf2[255]='';
sprintf(szBuf,szBuf2);
printf("%sn",szBuf);
}
2.2 還是不安全的_snprintf
查閱庫函數手冊,找到這麼一個函數_snprintf,其函數原型如下:
int _snprintf( char *buffer, size_t count, const char *format [, argument] …… );
這個函數允許界定目標地址尺寸,但是,由於研究拷貝函數的經驗,我懷疑它也有strncpy相同的問題,因此,我寫了這麼一段代碼測試:
void sprintfTest1()
{
int i;
char szBuf[128];
for(i=0;i<128;i++) szBuf[i]='*';
szBuf[127]='';
char szBuf2[256];
for(i=0;i<256;i++) szBuf2[i]='#';
szBuf2[255]='';
_snprintf(szBuf,8,szBuf2);
printf("%sn",szBuf);
}
果然,程式輸出如下:
########***********************************************************************************************************************
同樣的錯誤,沒有用“”結束,我必須另外想方法。
另外,還發現了另外一個不足,就是這個時候,_snprintf函數返回-1,不再返回列印的字元數,那麼,我們如果使用如下代碼將會造成邏輯錯誤,甚至可能崩潰:
char szBuf[256];
int nCount=0;
while(1) //這裡表示迴圈構造
{
nCount+=_snprintf(szBuf+nCount,256-nCount,”... ...”); //多個字串構造成一個字串
}
注意,代碼利用_snprintf返回的值,來確定下一個起始點,這很常用,但是,當_snprintf返回-1的時候,有可能會寫到*(szBuf-1)的位置上,典型的記憶體寫出界。
2.3 安全地字串建構函式
經過仔細思考,我構造了如下一個函數:
int xg_printf(char* szBuf,int nDestSize,char *szFormat, ...)
{
int nListCount=0;
va_list pArgList;
va_start (pArgList,szFormat);
nListCount+=_vsnprintf(szBuf+nListCount,
nDestSize-nListCount,szFormat,pArgList);
va_end(pArgList);
*(szBuf+nDestSize-1)='';
return strlen(szBuf);
}
注意,這裡我採用了變參函數設計,為的是和sprintf一樣方便,另外,最後一個return也非常重要,因為很多場合,我們需要知道究竟列印了多少字元。將這段函數代入上面的例子後一切正常。
總結:C語言字串庫函數可能是出於提高效能目的,在一旦條件不夠的時候,往往直接返回,忘了採用“”結束字串。這會造成下一次讀取字串時,資料邊界不可控。格式化列印函數,傳回值設計不合理,不永遠是一個正整數,會造成邏輯隱患。因此,建議大家有興趣可以參考一下我提供的兩個函數。