1.前言
对于直接使用C-Style字符串的C/C++ Coder来说,一个潜在的危险是如果目标缓冲区的大小不足以容纳要拷贝的文本,则会发生内存破坏
strcpy/wcscpy以及其他大多数旧式的字符串处理函数来说,最大的问题是,它们只根据字符串末尾的\0来判断文本是否结束,而指定缓冲区的大小对它们来说是透明的,这就使得它们自己也不知道自己是否做了不该做的事情。
考虑下面的例子:
2 |
_tcscpy(szText, _T( "KC is a handsome boy" )); |
上面的代码会发生什么事情呢?
缓冲区的大小是20,文本的字符数刚好是20,但是我们遗漏了结束符\0,所以文本实际上比缓冲区大了1,而这个多出来的1,很黄很暴力的占据了别人的内存空间。
于是,经典的内存破坏出现了
在之前,这种问题一直是恶意代码滋生的温床
不过好在很多人都意识到了这个问题,并且提供了一系列新的更安全的函数来取代旧式的操作函数。
这次,我们针对的是VC
2.早期不是方案的方案
在CRT中,存在一些名为strncpy/wcsncpy等的函数,它们可以控制复制的字符数
以wcsncpy为例,函数原型如下
3 |
const wchar_t *strSource, |
和wcscpy相比,多了一个count参数,而这个参数指定了要拷贝的字符数(Number of characters to be copied)
但是很可惜,这个函数实际上并没有我们认为的那么安全,如果我们指定的count数大于缓冲区的大小,那么结果会是灾难性的,因为wcsncpy会完整的写满count个字符(不够的用\0填充)
而事实上,很多使用这个函数的人通常会这么调用函数:
2 |
_tcsncpy(szText, _T( "KC is smart" ), _countof(szText)); |
这个做法看上去很好,实际上却只是扬汤止沸之法,问题来源于wcsncpy的“古怪”行为
如果要复制的文本长度大于缓冲区的长度,而我们传递的count数是缓冲区的长度(事实上只能这么做……),那么wcsncpy会复制count个字符,然后停止,这看起来就像字符串截断。
但是麻烦在于wcsncpy不会往后面添上\0!考虑下面的代码
2 |
_tcsncpy(szText, _T( "KC is a handsome boy" ), _countof(szText)); |
调用函数后,szText的实际内容是{KC is a ha}
如同一位先哲所说:没有经历过XXOO的人生是不完整的。没有\0的字符串就像传说中的太监,下面呢,没有了
所以,wcsncpy并不能真正的解决问题,
3.最新的解决方案
M$自VS2005之后,增加了一组新的且更安全的字符串操作函数。在大多数情况下,这组新函数能够防止因缓冲区过小而造成的内存破坏。
新函数的函数名在原有函数名的后面加上了_s,表示security。比如wcscpy对应的函数是wcscpy_s
wcscpy_s的函数原型如下:
2 |
wchar_t *strDestination, |
3 |
size_t numberOfElements, |
4 |
const wchar_t *strSource |
和wcsncpy不同,这里的numberOfElements代表缓冲区的大小而不是要拷贝的字符数
考虑如下代码:
2 |
_tcscpy_s(szText, _countof(szText), _T( "KC is a handsome boy" )); |
由于缓冲区大小小于文本大小,wcscpy_s会报错。如果是DEBUG版本,则会出现一个Assert Failed的对话框,而如果是RELEASE版本,则会产生一个运行时错误。
(SPX对Win7支持很不好…………………………)
不过,我们可以提供自己的处理函数,在wcscpy_s出错时进行更友好的处理。
处理函数的原型如下:
1 |
void InvalidParameterHandler( const wchar_t * expression, |
2 |
const wchar_t * function, |
实例代码如下
01 |
int _tmain( int argc, _TCHAR* argv[]) |
03 |
_set_invalid_parameter_handler(MyInvalidParameterHandler); |
04 |
_CrtSetReportMode(_CRT_ASSERT, 0); |
07 |
errno_t err = _tcscpy_s(szText, _countof(szText), _T( "KC is a handsome boy" )); |
18 |
void MyInvalidParameterHandler( const wchar_t * expression, |
19 |
const wchar_t * function, |
24 |
wprintf(L "Invalid parameter detected in function %s." |
25 |
L " File: %s Line: %d\n" , function, file, line); |
26 |
wprintf(L "Expression: %s\n" , expression); |
上面的代码中,我们仍需要用_CrtSetReportMode来防止CRT代码触发assert对话框的出现,不过这个并不会影响我们自己的断言代码
设置好出错处理函数后,一旦捕获错误,就会输出入相应内容
这里还需要注意的是,如果wcscpy_s函数失败,那么缓冲区第一个字符会被设置成\0,其他剩下的字符则会自动用填充符0xfd填充。这个和编译器在执行运行时检查有关
M$为大多数可能引发内存破坏的字符串操作函数都配备了安全版,包括前面提到的wcsncpy系列
ps:据说Linux下提供了类似的函数,比如wcslcpy,具体KC表示不清楚
4.WinAPI的安全版字符串操作函数
既然CRT的旧式字符串操作函数会导致内存破坏,那么旧式的WinAPI字符串操作函数(比如lstrcpy)也自然无法幸免。
不过M$很迅速的加入了新版的更安全的字符串操作函数API替代原有的缺陷API,比如StringCchCopy、StringCchCat等等
这些新版的API位于strsafe.h,而且从VC2003开始就提供了这个系列的API
以StringCchCopy为例,这个函数的原型是
1 |
HRESULT StringCchCopy( LPTSTR pszDest, |
这里的cch是M$匈牙利命名的一个典型规则,代表count of characters。而这里的cchDest也显然表示缓冲区大小
和wcscpy_s不同,如果缓冲区太小不足以容纳文本,那么StringCchCopy会自动截断文本,大小为cchDest-1,然后在最后补上\0
考虑下面的例子:
2 |
StringCchCopy(szText, _countof(szText), _T( "KC is a handsome boy" )); |
结果会输出KC is a handsome bo,并且返回STRSAFE_E_INSUFFICIENT_BUFFER指示字符串截断
如果不希望字符串被截断(有时候截断字符串并不是我们需要的结果,并且可能会带来不可预期的结果),那么可以求助带Ex的扩展版API
有关StringCchCopy等函数的详情,请参考相应文档
5.一些建议
1.在C++中尽可能使用库提供的字符串类,比如STL的string/wstring,MFC提供的CString,以减少自己操作C-Style字符串的可能
2.如果真的需要操作C-Style字符串,使用安全版的操作函数。比如wcscpy_s(VC2005以上支持),StringCchCopy(VC2003以上支持)
3.如果需要使用非安全的旧式函数,最好在操作前对缓冲区大小和文本大小进行安全测试