在VC中使用安全版字符串操作函数

1.前言

对于直接使用C-Style字符串的C/C++ Coder来说,一个潜在的危险是如果目标缓冲区的大小不足以容纳要拷贝的文本,则会发生内存破坏

strcpy/wcscpy以及其他大多数旧式的字符串处理函数来说,最大的问题是,它们只根据字符串末尾的\0来判断文本是否结束,而指定缓冲区的大小对它们来说是透明的,这就使得它们自己也不知道自己是否做了不该做的事情。

考虑下面的例子:

1 TCHAR szText[20];
2 _tcscpy(szText, _T("KC is a handsome boy"));
3 wcout<

上面的代码会发生什么事情呢?

缓冲区的大小是20,文本的字符数刚好是20,但是我们遗漏了结束符\0,所以文本实际上比缓冲区大了1,而这个多出来的1,很黄很暴力的占据了别人的内存空间。

于是,经典的内存破坏出现了

在之前,这种问题一直是恶意代码滋生的温床

不过好在很多人都意识到了这个问题,并且提供了一系列新的更安全的函数来取代旧式的操作函数。

这次,我们针对的是VC

2.早期不是方案的方案

在CRT中,存在一些名为strncpy/wcsncpy等的函数,它们可以控制复制的字符数

以wcsncpy为例,函数原型如下

1 wchar_t *wcsncpy(
2    wchar_t *strDest,
3    const wchar_t *strSource,
4    size_t count
5 );

和wcscpy相比,多了一个count参数,而这个参数指定了要拷贝的字符数(Number of characters to be copied)

但是很可惜,这个函数实际上并没有我们认为的那么安全,如果我们指定的count数大于缓冲区的大小,那么结果会是灾难性的,因为wcsncpy会完整的写满count个字符(不够的用\0填充)

而事实上,很多使用这个函数的人通常会这么调用函数:

1 TCHAR szText[20];
2 _tcsncpy(szText, _T("KC is smart"), _countof(szText));
3 wcout<

这个做法看上去很好,实际上却只是扬汤止沸之法,问题来源于wcsncpy的“古怪”行为

如果要复制的文本长度大于缓冲区的长度,而我们传递的count数是缓冲区的长度(事实上只能这么做……),那么wcsncpy会复制count个字符,然后停止,这看起来就像字符串截断。

但是麻烦在于wcsncpy不会往后面添上\0!考虑下面的代码

1 TCHAR szText[10];
2 _tcsncpy(szText, _T("KC is a handsome boy"), _countof(szText));
3 wcout<// will cause a unexpected problem

调用函数后,szText的实际内容是{KC is a ha}

如同一位先哲所说:没有经历过XXOO的人生是不完整的。没有\0的字符串就像传说中的太监,下面呢,没有了

所以,wcsncpy并不能真正的解决问题,

3.最新的解决方案

M$自VS2005之后,增加了一组新的且更安全的字符串操作函数。在大多数情况下,这组新函数能够防止因缓冲区过小而造成的内存破坏。

新函数的函数名在原有函数名的后面加上了_s,表示security。比如wcscpy对应的函数是wcscpy_s

wcscpy_s的函数原型如下:

1 errno_t wcscpy_s(
2    wchar_t *strDestination,
3    size_t numberOfElements,
4    const wchar_t *strSource
5 );

和wcsncpy不同,这里的numberOfElements代表缓冲区的大小而不是要拷贝的字符数

考虑如下代码:

1 TCHAR szText[20];
2 _tcscpy_s(szText, _countof(szText), _T("KC is a handsome boy"));
3 wcout<

由于缓冲区大小小于文本大小,wcscpy_s会报错。如果是DEBUG版本,则会出现一个Assert Failed的对话框,而如果是RELEASE版本,则会产生一个运行时错误。

在VC中使用安全版字符串操作函数_第1张图片

(SPX对Win7支持很不好…………………………)

不过,我们可以提供自己的处理函数,在wcscpy_s出错时进行更友好的处理。

处理函数的原型如下:

1 void InvalidParameterHandler(const wchar_t* expression,
2                                const wchar_t* function,
3                                const wchar_t* file,
4                                unsigned int line,
5                                uintptr_t pReserved);

实例代码如下

01 int _tmain(int argc, _TCHAR* argv[])
02 {
03     _set_invalid_parameter_handler(MyInvalidParameterHandler);
04     _CrtSetReportMode(_CRT_ASSERT, 0);  // Disable assert box
05  
06     TCHAR szText[20];
07     errno_t err = _tcscpy_s(szText, _countof(szText), _T("KC is a handsome boy"));
08  
09     if (err)
10     {
11         wcout<
12     }
13  
14     _getch();
15     return 0;
16 }
17  
18 void MyInvalidParameterHandler(const wchar_t* expression,
19                                const wchar_t* function,
20                                const wchar_t* file,
21                                unsigned int line,
22                                uintptr_t pReserved)
23 {
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);
27 }

上面的代码中,我们仍需要用_CrtSetReportMode来防止CRT代码触发assert对话框的出现,不过这个并不会影响我们自己的断言代码

设置好出错处理函数后,一旦捕获错误,就会输出入相应内容

在VC中使用安全版字符串操作函数_第2张图片

这里还需要注意的是,如果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,
2     size_t cchDest,
3     LPCTSTR pszSrc
4 );

这里的cch是M$匈牙利命名的一个典型规则,代表count of characters。而这里的cchDest也显然表示缓冲区大小

和wcscpy_s不同,如果缓冲区太小不足以容纳文本,那么StringCchCopy会自动截断文本,大小为cchDest-1,然后在最后补上\0

考虑下面的例子:

1 TCHAR szText[20];
2 StringCchCopy(szText, _countof(szText), _T("KC is a handsome boy"));
3 wcout<

结果会输出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.如果需要使用非安全的旧式函数,最好在操作前对缓冲区大小和文本大小进行安全测试


你可能感兴趣的:(c/c++)