您的位置:首页 > 其它

安全使用CString,今日害死我了

2012-12-15 10:38 1301 查看
文章出处:http://www.diybl.com/course/3_program/c++/cppjs/2008828/138377.html

1. 安全使用CString

 

今天我花了差不多一下午的功夫,解决了一个很隐蔽的bug,包括修改和排除相关的可能存在隐患代码。

就是一个关于CString的使用问题,重点体现在Format上。

目前我们的代码里,对于Format的应用可以分为下面的几种方式:

 

① 格式字符串(format)和可变参数(args)都为非目标字符串对象(str)

CString str;

str.Format( format, args );

 

② 将目标字符串对象(str)初始化为格式字符串(format),并作为格式化参数使用

CString str = format;

str.Format( str, args );

 

③ 将目标字符串对象(str)初始化为某文本参数(args),并作为某一可变参数使用

CString str = text;

str.Format( format, str, args );

 

各位仔细分析一下,这几种方式会不会出现什么问题或安全隐患?

 

回答这个问题,需要了解WTL::CString::Format()的实现原理。查看一下WTL::CString的源代码可以看出,

CString::Format并没有真正实现格式化字符串的操作,而只是对相关API的封装而已,即_vstprintf_s/_vstprintf

 

Format的实现伪代码如下(完整流程):

 

BOOL CString::Format( LPCTSTR lpszFormat, va_list argList )

{

// 预估算格式化后的结果字符串长度nMaxLen

int nMaxLen = PreCalcLength( lpszFormat, argList );

 

// 为目标串分配足够的内存

if( GetData()->nRefs > 1 ||                 // 有其他CString对象引用了本字符串,这时必须重新分配内存

    nMaxLen > GetData()->nAllocLength )    // 原分配的内存不足,重新分配

{

// 缓存原有字串。当重新分配内存之后,需要将原有字符串拷贝到新内存中

CStringData* pOldData = GetData();

int nOldLen = GetData()->nDataLength;

 

// 由于存在引用计数,所有可能存在原始内存足够但却必须重新分配的情况(不能修改被其他CString对象引用的字符串内容)

if( nMaxLen < nOldLen )

nMaxLen = nOldLen;

if( !AllocBuffer( nMaxLen ) )

return FALSE;

 

// 将原字符串内容拷贝到新内存中

memcpy(m_pchData, (nMaxLen + 1) * sizeof(TCHAR), pOldData->data(), (nOldLen + 1) * sizeof(TCHAR));

GetData()->nDataLength = nOldLen;

 

// 释放对原有字符串对象的引用,这一步执行之后,原有字符串对象将被销毁,或其他应用者接管

CString::Release(pOldData);

}

 

// 调用库函数/API进行真正的格式化字符串操作

int nRet = _vstprintf( lpszFormat, argList );

ASSERT( nRet <= GetAllocLength() );

 

// 扫描结果字符串,设置正确的字符串结尾(添加'\0')

ReleaseBuffer();

 

return TRUE;

}

 

从伪代码看出,Format的基本功能分为三步:1. 估算结果字符串长度,2. 分配足够的内存,3. 调用API格式化字符串。

 

那么,现在我们再回过头来分析文章开头给出的Format的三种使用方式,就不难发现问题了。

问题就在于,在执行vsprintf时可能存在字符串拷贝动作的源字符串和目标字串内存区域相重叠的情况!想想下面这段代码会有什么结果?

示例1:

char str[] = {"abcdefg1234567890"};

char* pDest = str, *pSrc = &str[1];

strcpy( pDest, pSrc );

 

天知道strcpy内部实现的是逐个字符拷贝的还是几个几个一起拷贝的?

那如果将参数pDest和pSrc交换位置呢?

示例2:

char str[] = {"abcdefg1234567890"};

char* pDest = &str[1], *pSrc = str;

strcpy( pDest, pSrc );

 

哈哈,这回如果使用strncpy,可以防止内存越界,却无法得到我们想要的结果了!

 

vsprintf的实现也是一样,因为在其可变参数列表的规则里,无法指定"%s"类型源字符串参数的长度,这就只能靠'\0'结束符来判断字符串拷贝的边界,但这个边界却可能随时被我们冲掉!

 

在CString::Format里,如果经过估算,该对象本身已分配的内存足以保存格式化结果串而没有重新分配内存,这时候Format的使用方法②和方法③就出现了类似的问题。

方法②可能出现死循环和内存越界的危险,而方法③中,则可能将一个已经被修改了的源字符串(也是目标串)作为拷贝对象,同样可能出现死循环和内存访问越界的危险!

 

另外,我们看到在分配内存这一步,原有字符串就已经被释放了,而_vstprintf却是在这之后被调用。就是说_vstprintf可能会将一个已经被销毁的字符串作为拷贝源,这显然是极其危险的,应当极力避免!

 

有兴趣的可以对简单测试一下下面的代码段,它们都是根据Format的原理精心构建出来的,非常可爱!

 

if(true)

{

// 执行结果:崩溃!

CString str = _T("%s test %s");

str.Format( str, _T("123"), _T("0") );

}

if(true)

{

// 执行结果:str == "8888"!错误!

CString str = _T("text2");

str = _T("abc");

str.Format( _T("%c%s"), _T('8'), str );

}

if(true)

{

// 执行结果:崩溃!

// 这是从现有工程中摘出来的一段代码,就是她为我这篇文章提供了最有力的证据,感谢她!

CString str = TEXT("<?xml version=\"1.0\" encoding=\"utf-8\" ?>")

  TEXT("<args>")

  TEXT("<personal nickname =\"%s\" gender =\"%s\" />")

  TEXT("</args>");

str.Format( str, _T("123"), _T("0") );

}

 

修改建议:现有代码中这样的用法有很多,在一般情况下问题不容易暴露出来,但一旦出现,后果非常严重!

所以,建议尽量避免这种使用方式。如果已经用了,可以巧妙利用CString的引用计数功能来解决,即通过构造一个新CString对象来防止源字符串对象被修改或被释放。

如下面的形式:

 

1.   CString str = format;

str.Format( str, args );

str.Format( CString(str), args );    // 安全

 

2.   CString str = text;

str.Format( format, str, args );

str.Format( format, CString(str), args );    // 安全

 

2. 提升CString的执行效率

CString封装的非常强大,但是从Format伪代码可以看出,其内部操作可能涉及到频繁的内存分配和字符串拷贝,而内存分配过程确也是非常耗时的,在Mobile上面速度就更慢。

所以建议做大量操作时根据实际情况预先估算大小并一次性分配足够的内存,程序效率定会大幅提升!像下面的代码,至少应该做这样的优化:

 

CString str = _T("http://");

str += strHost;

str += _T("/");

str += strPath;

优化为:

CString str;

str.Format( _T("http://%s/%s"), strHost, strPath );

 

Format的由于存在预处理与格式分析,执行效率比单纯的字符串拷贝要慢很多,现在代码中存在很多类似下面这样的代码,个人认为也是应该避免的。

a).

CString strFormat = _T("%d");

CString strResult;

strResult.Format( strFormat, nID );

// 改为

strResult.Append( nID );    // CString& CString::Append(int n);

b).

CString strTemp( _T("text") );

CString strResult;

strResult.Format( _T("%s"), strTemp );

// 改为

strResult = _T("text");

c).

CString strHost;

strHost.Format( _T("http://%s"), szHost );

CString strPath;

strPath.Format( _T("%s/%s"), szDir, szFile );

CString strURL;

strURL.Foramt( _T("%s/%s"), strHost, strPath );

// 改为:

strURL.Format( _T("http://%s/%s/%s"), szHost, szDir, szFile );

 

 

3. 关于CString的模板膨胀

 

WTL::CString采用模板编写,所有函数都是内联函数。在程序代码中用到一次CString将会生成大量相同的代码段!当CString被作为参数以对象的形式传递或返回时,代码膨胀非常明显(我们在以前的项目中做过测试,采用对象方式传递CString类型参数比使用引用方式传递编译后的目标程序大约10~30%)。所以建议:

    a. 在作为参数时将CString弱化为LPCTSTR或采用引用方式传递,前者更方便于与不适用CString的部分代码相兼容,这应该也是WTL的大多数类都不依赖于其他类的原因。

    b. 尽量避免函数返回CString类型的对象(其他对象也一样),避免频繁构造和析构对象,也避免代码冗余膨胀。而是将CString对象以引用参数的形式传入函数,处理之后可以以引用返回,或返回函数执行结果(一般为BOOL类型),如下形式:

 

CString foo( CString strSrc );

改为:

CString& foo( IN const CString& strSrc, INOUT CString& strDest );    (不推荐,不便检查函数执行成功与否,可以通过设置strDest为空返回,但需要多一步操作)

或:

BOOL foo( IN const CString& strSrc, INOUT CString& strDest );        (推荐)

或:

BOOL foo( IN LPCTSTR lpszSrc, INOUT CString& strDest );               (推荐,lpszSrc参数可以直接使用任一字符串而不是限定在CString对象上)

或:

BOOL foo( IN LPCTSTR lpszSrc, INOUT LPTSTR lpszDest,  IN int nLength );    (API形式)

 

除此之外,ATL或MFC的CString还存在另外一个问题,已经有开发者发现了这个bug,WTL中将其修服了(其实似乎就是删掉了这一功能)。内容摘录如下:

 

有关CString的一个陷阱,小心了

文章出处:http://www.diybl.com/course/3_program/c++/cppjs/2008828/138377.html

 

 

发现有关CString的一个容易掉入的陷阱,重现问题的源码如下:

(注:编译器为VC9 SP1)

void Test(LPCTSTR str)

{

    if (str == NULL)

        AfxMessageBox(_T("Null Pointer"));

    else

        AfxMessageBox(_T("A Valid String"));

}

CString str;

Test(str.IsEmpty() ? NULL : str);

 

    相信一定有人跟我一样,会直觉上认为上面程序的运行结果为"Null Pointer"。但事实上,其结果为"A Valid String"。

    为什么我(们)会这样想呢?因为在我(们)的思维中,会习惯性认为NULL为空指针(事实上是int类型),而CString有到指针类型(LPCTSTR)的自动转换。因此,我们可能会认为,如果该布尔表达式的条件为真,则传递NULL参数;如果条件为假,则先把str自动类型转换为LPCTSTR类型,然后传递转换后的参数。一切想法那么自然,流畅。然后,它却是错误的。

    让我们来仔细的审视下这段代码,其实,关键的也就这一布尔表达式:

    str.IsEmpty() ? NULL : str

该布尔表达式的两个结果子表达式类型分别为int(NULL的类型)和CString(str的类型),而C++语言规定布尔表达式的类型为两个结果子表达的共同类型。然而,int和CString没有共同类型,也无法相互转换。因此,该表达式所在语句应当无法通过编译器编译。

    但这里,该语句却通过了编译器(VC9 SP1)的编译,并且运行结果为“A Valid String”。刚开始,我认为是编译器错误地执行了布尔表达式,把str作为了布尔表达式的结果(为此,我还跑到Microsoft Forum上去发帖询问,但无果)。后来,我到Microsoft Connect上发布了一条关于此问题的信息,很快有网友给出了意见,认为是使用NULL调用了CString的构造函数。我跟踪测试了一下代码,确实如此。编译器先用NULL构造一个CString对象,然后把该CString对象转换为LPCTSTR指针。后来,Microsoft也给出了官方对此问题的跟踪信息,并最后给出了Visual
C++ Compiler Team的官方解释。引用其文如下:

Hi: with a recent build of the compiler then I am getting the following error message:

t498875.cpp(20) : error C2446: ':' : no conversion from 'CString' to 'int'

     No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called

which is what I would expect. The C++ Standard requires that when faced with some code like:

    f(str.IsEmpty() ? NULL : str);

The compiler must convert "NULL" and "str" to a common type. Which in this case is either int or CString -- but there is no way to perform this conversion (in either direction).

Jonathan Caves

Visual C++ Compiler Team
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: