zyl910

优化技巧、硬件体系、图像处理、图形学、游戏编程、国际化与文本信息处理。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

作者:zyl910

  前面测试了各种编译器的执行结果,但为什么它们的执行结果是那样呢?这需要仔细分析。VC2005的测试结果比较典型,而且调试跟踪比较方便,于是本篇对VC2005的crt源码进行分析。


一、须知

  开发工具是VC2005,平台为32位的x86,编译模式为Debug,使用MBCS字符集。


二、cout输出窄字符串

2.1 已初始化locale

  “已初始化locale”是指——在输出前执行了初始化locale,即执行了下列语句——

    // init.
    locale::global(locale(""));
    wcout.imbue(locale(""));

 

  现在开始进行分析。
  “cout << psa”表示使用cout输出窄字符串。按F11单步跟踪,它依次进入了下列函数——
operator<<:[C++库] 流输出运算符。
basic_streambuf<char>::sputn:[C++库] 输出字符串(公开方法)。
basic_streambuf<char>::xsputn:[C++库] 输出字符串(内部实现)。循环对源串中的每一个char调用overflow。【注意#1】gbk编码的汉字是2个字节,会调用overflow 2次。
basic_filebuf<char>::overflow:[C++库] 数据溢出,即向文件写入一个字符。【注意#2】因为现在是char版,无需转换编码,直接调用_Fputc。
_Fputc<char>:[C++库]向文件写入一个char。
fputc:[C库] 向文件写入一个char。
_flsbuf:[C库] 刷新缓冲区并输出char。
_write:[C库] 向文件写数据。
_write_nolock:[C库] 向文件写数据(不加锁版)。【注意#3】条件判断存在漏洞,导致汉字的首字节无法输出。返回-1。

  此时的调用栈——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0012fb50, unsigned int cnt=0x00000001)  行170 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0012fb50, unsigned int cnt=0x00000001)  行74 + 0x11 字节 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x10311d20)  行189 + 0x11 字节 C
  msvcr80d.dll!fputc(int ch=0xffffffba, _iobuf * str=0x10311d20)  行52 + 0x4b 字节 C
  msvcp80d.dll!std::_Fputc<char>(char _Byte=0xba, _iobuf * _File=0x10311d20)  行81 + 0xf 字节 C++
  msvcp80d.dll!std::basic_filebuf<char,std::char_traits<char> >::overflow(int _Meta=0x000000ba)  行261 + 0x1c 字节 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::xsputn(const char * _Ptr=0x0041774d, int _Count=0x00000007)  行379 + 0x1a 字节 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::sputn(const char * _Ptr=0x0041774c, int _Count=0x00000008)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<std::char_traits<char> >(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...}, const char * _Val=0x0041774c)  行768 + 0x3e 字节 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a58)  行45 + 0x12 字节 C++

  发现_write_nolock函数存在Bug,代码摘录——

// C:\VS2005\VC\crt\src\write.c, 160 line:
        /* don't need double conversion if it's ANSI mode C locale */
        if (toConsole && !(isCLocale && (tmode == __IOINFO_TM_ANSI))) {
            UINT consoleCP = GetConsoleCP();
            char mboutbuf[MB_LEN_MAX];
            wchar_t tmpchar;
            int size = 0;
            int written = 0;
            char *pch;

            for (pch = (char *)buf; (unsigned)(pch - (char *)buf) < cnt; ) {
                BOOL bCR;

                if (tmode == __IOINFO_TM_ANSI) {
                    bCR = *pch == LF;
                    /*
                     * Here we need to do double convert. i.e. convert from
                     * multibyte to unicode and then from unicode to multibyte in
                     * Console codepage.
                     */
                    if (!isleadbyte(*pch)) {
                        if (mbtowc(&tmpchar, pch, 1) == -1) {
                            break;
                        }
                    } else if ((cnt - (pch - (char*)buf)) > 1) {
                        if (mbtowc(&tmpchar, pch, 2) == -1) {
                            break;
                        }
                        /*
                         * Increment pch to accomodate DBCS character.
                         */
                        ++pch;
                    } else {
                        break;
                    }
                    ++pch;
                } else if (tmode == __IOINFO_TM_UTF8 || tmode == __IOINFO_TM_UTF16LE) {
                    /*
                     * Note that bCR set above is not valid in case of UNICODE
                     * stream. We need to set it using unicode character.
                     */
                    tmpchar = *(wchar_t *)pch;
                    bCR = tmpchar == LF;
                    pch += 2;
                }

                if (tmode == __IOINFO_TM_ANSI)
                {
                    if( (size = WideCharToMultiByte(consoleCP,
                                                    0,
                                                    &tmpchar,
                                                    1,
                                                    mboutbuf,
                                                    sizeof(mboutbuf),
                                                    NULL,
                                                    NULL)) == 0) {
                        break;
                    } else {
                        if ( WriteFile( (HANDLE)_osfhnd(fh),
                                        mboutbuf,
                                        size,
                                        (LPDWORD)&written,
                                        NULL) ) {
                            charcount += written;
                            if (written < size)
                                break;
                        } else {
                            dosretval = GetLastError();
                            break;
                        }
                    }

                    if (bCR) {
                        size = 1;
                        mboutbuf[0] = CR;
                        if (WriteFile((HANDLE)_osfhnd(fh),
                                      mboutbuf,
                                      size,
                                      (LPDWORD)&written,
                                      NULL) ) {
                            if (written < size)
                                break;
                            lfcount ++;
                            charcount++;
                        } else {
                            dosretval = GetLastError();
                            break;
                        }
                    }
                }
                else if ( tmode == __IOINFO_TM_UTF8 || tmode == __IOINFO_TM_UTF16LE)
...


// C:\VS2005\VC\crt\src\write.c, 443 line:
        if (charcount == 0) {
                /* If nothing was written, first check if an o.s. error,
                   otherwise we return -1 and set errno to ENOSPC,
                   unless a device and first char was CTRL-Z */
                if (dosretval != 0) {
                        /* o.s. error happened, map error */
                        if (dosretval == ERROR_ACCESS_DENIED) {
                            /* wrong read/write mode should return EBADF, not
                               EACCES */
                                errno = EBADF;
                                _doserrno = dosretval;
                        }
                        else
                                _dosmaperr(dosretval);
                        return -1;
                }
...

 

  _write_nolock函数的主要处理流程是——
循环处理源串中的每一个char
{
 调用mbtowc将当前char转换为宽字符。利用isleadbyte函数判断当前char是不是多字节字符的首字节,再判断是否能凑够2个字节进行转换。
 调用WideCharToMultiByte将宽字符转为窄字符串。
 调用WriteFile将窄字符串写入文件。
}

  问题就是出在“调用mbtowc将当前char转换为宽字符”这一步——
因为先前在basic_streambuf<char>::xsputn函数中,就已经将源串分解为各个char了。gbk编码的汉字是2个字节,所以会先将汉字的首字节传递到_write_nolock函数。
因现在是首字节,所以“if (!isleadbyte(*pch))”判断为假。因现在只有一个字节,“else if ((cnt - (pch - (char*)buf)) > 1)”判断也为假。最终到else分支,执行break跳出循环。
跳出循环后,因为没有输出字符,于是进入“if (charcount == 0)”分支。因dosretval变量未初始化,所以该变量为非0值的可能性很高,于是进入了“if (dosretval != 0)”分支。最终执行“return -1”返回-1。

  函数返回时——
_write_nolock:【注意#3】条件判断存在漏洞,导致汉字的首字节无法输出。返回-1。
_write:返回_write_nolock的返回值,即返回-1。
_flsbuf:因_flsbuf的返回值(-1)与字符数不同(sizeof(TCHAR)),返回EOF(-1)。
fputc:返回_flsbuf的返回值,即返回EOF(-1)。
_Fputc<char>:因“fputc的返回值(EOF)与EOF不相等”的结果为假((fputc(_Byte, _File) != EOF)),返回false。
basic_filebuf<char>::overflow:因_Fputc返回false,返回_Traits::eof(),即EOF(-1)。
basic_streambuf<char>::xsputn:【注意#4】因overflow返回EOF(-1),跳出循环,返回实际输出的字符数。
basic_streambuf<char>::sputn:返回xsputn的返回值,即返回实际输出的字符数。
operator<<:【注意#5】因实际输出的字符数与源串字符数不同,设置流标记为bad。

  这就是“已初始化locale时,cout无法输出中文窄字符串”的原因。


2.2 未初始化locale

  “未初始化locale”是指——在输出前没有初始化locale,即将相关语句注释了——

    // init.
    //locale::global(locale(""));
    //wcout.imbue(locale(""));

 

  “cout << psa”仍会执行到_write_nolock函数。此时的调用栈——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0012fb98, unsigned int cnt=0x00000001)  行268 + 0x5 字节 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0012fb98, unsigned int cnt=0x00000001)  行74 + 0x11 字节 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x10311d20)  行189 + 0x11 字节 C
  msvcr80d.dll!fputc(int ch=0xffffffba, _iobuf * str=0x10311d20)  行52 + 0x4b 字节 C
  msvcp80d.dll!std::_Fputc<char>(char _Byte=0xba, _iobuf * _File=0x10311d20)  行81 + 0xf 字节 C++
  msvcp80d.dll!std::basic_filebuf<char,std::char_traits<char> >::overflow(int _Meta=0x000000ba)  行261 + 0x1c 字节 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::xsputn(const char * _Ptr=0x0041774d, int _Count=0x00000007)  行379 + 0x1a 字节 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::sputn(const char * _Ptr=0x0041774c, int _Count=0x00000008)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<std::char_traits<char> >(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...}, const char * _Val=0x0041774c)  行768 + 0x3e 字节 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行45 + 0x12 字节 C++

  在_write_nolock函数中,因为现在使用的是C默认locale(未初始化locale),所以执行的语句不同。代码摘录——

// C:\VS2005\VC\crt\src\write.c, 160 line:
        /* don't need double conversion if it's ANSI mode C locale */
        if (toConsole && !(isCLocale && (tmode == __IOINFO_TM_ANSI))) {
...
// C:\VS2005\VC\crt\src\write.c, 268 line:
        } else if ( _osfile(fh) & FTEXT ) {
            /* text mode, translate LF's to CR/LF's on output */

            dosretval = 0;          /* no OS error yet */

            if(tmode == __IOINFO_TM_ANSI) {
                char ch;                    /* current character */
                char *p = NULL, *q = NULL;  /* pointers into buf and lfbuf resp. */
                char lfbuf[BUF_SIZE];
                p = (char *)buf;        /* start at beginning of buffer */
                while ( (unsigned)(p - (char *)buf) < cnt ) {
                    q = lfbuf;      /* start at beginning of lfbuf */

                    /* fill the lf buf, except maybe last char */
                    while ( q - lfbuf < sizeof(lfbuf) - 1 &&
                            (unsigned)(p - (char *)buf) < cnt ) {
                        ch = *p++;
                        if ( ch == LF ) {
                            ++lfcount;
                            *q++ = CR;
                        }
                        *q++ = ch;
                    }

                    /* write the lf buf and update total */
                    if ( WriteFile( (HANDLE)_osfhnd(fh),
                                lfbuf,
                                (int)(q - lfbuf),
                                (LPDWORD)&written,
                                NULL) )
                    {
                        charcount += written;
                        if (written < q - lfbuf)
                            break;
                    }
                    else {
                        dosretval = GetLastError();
                        break;
                    }
                }

 

  因现在isCLocale为真,于是转到“else if ( _osfile(fh) & FTEXT )”分支。简单做了一下换行符处理后,便调用WriteFile写数据。操作成功。

  这就是“未初始化locale,cout能正常输出中文窄字符串”的原因。


2.3 其他测试

  修改了一下项目配置,改为Unicode字符集。进行调试,发现程序运行效果完全相同。这是因为_write_nolock是msvcr80d.dll中已经编译好代码,本项目的编译参数不会影响msvcr80d.dll的执行效果。
  再修改项目配置,改为静态链接。进行调试,发现程序运行效果完全相同。原理同上。
  

三、wcout输出宽字符串

3.1 已初始化locale

  “wcout << psw”表示使用cout输出窄字符串。按F11单步跟踪,它依次进入了下列函数——
operator<<:[C++库] 流输出运算符。
basic_streambuf<wchar_t>::sputn:[C++库] 输出字符串(公开方法)。
basic_streambuf<wchar_t>::xsputn:[C++库] 输出字符串(内部实现)。循环对源串中的每一个wchar_t调用overflow。【注意#1】汉字一般是1个wchar_t,会调用overflow 1次。
basic_filebuf<wchar_t>::overflow:[C++库] 数据溢出,即向文件写入一个字符。【注意#2】因为现在是wchar_t版,需要进行编码转换。
codecvt<wchar_t,char,int>::out:[C++库] 将wchar_t串转为char串(公开方法)。
codecvt<wchar_t,char,int>::do_out:[C++库] 将wchar_t串转为串(内部实现)。
_Wcrtomb:[C库] 调用WideCharToMultiByte将wchar_t字符转换为多字节串。

  此时的调用栈——
> msvcp80d.dll!_Wcrtomb(char * s=0x0018fc18, wchar_t wchar=L'汉', int * pst=0x6ad750ec, const _Cvtvec * ploc=0x00264cf0)  行111 C
  msvcp80d.dll!std::codecvt<wchar_t,char,int>::do_out(int & _State=0, const wchar_t * _First1=0x0018fc38, const wchar_t * _Last1=0x0018fc3a, const wchar_t * & _Mid1=0x0018fc38, char * _First2=0x0018fc18, char * _Last2=0x0018fc20, char * & _Mid2=0x0018fc18)  行1000 + 0x1f 字节 C++
  msvcp80d.dll!std::codecvt<wchar_t,char,int>::out(int & _State=0, const wchar_t * _First1=0x0018fc38, const wchar_t * _Last1=0x0018fc3a, const wchar_t * & _Mid1=0x0018fc38, char * _First2=0x0018fc18, char * _Last2=0x0018fc20, char * & _Mid2=0x0018fc18)  行897 C++
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=27721)  行273 + 0x90 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x004187e2, int _Count=5)  行379 + 0x1a 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x004187e0, int _Count=6)  行170 C++
  tcharall_cpp_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x004187e0)  行853 + 0x3e 字节 C++
   wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字节 C++

  在_Wcrtomb函数中,它会调用WideCharToMultiByte这个Windows API进行编码转换。
  编码转换成功后,又会回到overflow函数。它会调用fwrite输出转换后的char串,依次进入了下列函数——
fwrite:[C库] 向文件写入数据。
_fwrite_nolock:[C库] 向文件写入数据(不加锁版)。【注意#3】循环对数据的每一个char调用_flsbuf。
_flsbuf(int ch, _iobuf* str) // [C库] 刷新缓冲区并输出char。
_write(int fh, const void* buf, unsigned int cnt) // [C库] 向文件写数据。
_write_nolock(int fh, const void* buf, unsigned int cnt) // [C库] 向文件写数据(不加锁版)。

  此时的调用栈——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0018fae0, unsigned int cnt=0x00000001)  行470 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0018fae0, unsigned int cnt=0x00000001)  行74 + 0x11 字节 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x67cc1d20)  行189 + 0x11 字节 C
  msvcr80d.dll!_fwrite_nolock(const void * buffer=0x0018fc18, unsigned int size=0x00000001, unsigned int num=0x00000002, _iobuf * stream=0x67cc1d20)  行194 + 0xd 字节 C
  msvcr80d.dll!fwrite(const void * buffer=0x0018fc18, unsigned int size=0x00000001, unsigned int count=0x00000002, _iobuf * stream=0x67cc1d20)  行83 + 0x15 字节 C
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=0x6c49)  行280 + 0x59 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x004187e2, int _Count=0x00000005)  行379 + 0x1a 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x004187e0, int _Count=0x00000006)  行170 C++
  tcharall_cpp_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x004187e0)  行853 + 0x3e 字节 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字节 C++

  在_write_nolock函数中,又遇到了同样的问题——
因为先前在_fwrite_nolock函数中,就已经将源串分解为各个char了。gbk编码的汉字是2个字节,所以会先将汉字的首字节传递到_write_nolock函数。
因现在是首字节,所以“if (!isleadbyte(*pch))”判断为假。因现在只有一个字节,“else if ((cnt - (pch - (char*)buf)) > 1)”判断也为假。最终到else分支,执行break跳出循环。
跳出循环后,因为没有输出字符,于是进入“if (charcount == 0)”分支。因dosretval变量未初始化,所以该变量为非0值的可能性很高,于是进入了“if (dosretval != 0)”分支。最终执行“return -1”返回-1。

  函数返回时——
_write_nolock:【注意#4】条件判断存在漏洞,导致汉字的首字节无法输出。返回-1。
_write:返回_write_nolock的返回值,即返回-1。
_flsbuf:因_flsbuf的返回值(-1)与字符数不同(sizeof(TCHAR)),返回EOF(-1)。
_fwrite_nolock:因_flsbuf返回EOF(-1),跳出循环,返回实际输出的字符数(0)。
fwrite:返回_fwrite_nolock的返回值,即返回0。
basic_filebuf<wchar_t>::overflow:因fwrite的返回值(0)与编码转换后的字符数不同,返回_Traits::eof(),即WEOF(-1)。
basic_streambuf<wchar_t>::xsputn:【注意#5】因overflow返回WEOF(-1),跳出循环,返回实际输出的字符数。
basic_streambuf<wchar_t>::sputn:返回xsputn的返回值,即返回实际输出的字符数。
operator<<:【注意#6】因实际输出的字符数与源字符数不同,设置流标记为bad。

  这就是“已初始化locale时,cout无法输出中文窄字符串”的原因。虽然basic_filebuf<wchar_t>::overflow能正常的将宽字符转为窄字符串,但_write_nolock的Bug造成了无法输出。


3.2 未初始化locale

  未初始化locale时,“wcout << psw”的执行路径与先前不同,依次进入了下列函数——
operator<<:[C++库] 流输出运算符。
basic_streambuf<wchar_t>::sputn:[C++库] 输出字符串(公开方法)。
basic_streambuf<wchar_t>::xsputn:[C++库] 输出字符串(内部实现)。循环对源串中的每一个wchar_t调用overflow。【注意#1】汉字一般是1个wchar_t,会调用overflow 1次。
basic_filebuf<wchar_t>::overflow:[C++库] 数据溢出,即向文件写入一个字符。【注意#2】因为现在是“未初始化locale”,不做编码转换,直接调用_Fputc<wchar_t>。
_Fputc<wchar_t>:[C++库] 输出 wchar_t。
fputwc:[C库] 输出 wchar_t(公开方法)。
_fputwc_nolock:[C库] 输出 wchar_t(内部实现)。【注意#3】因为现在是wchar_t版,需要进行编码转换。
wctomb_s:[C库] (缓冲安全版)将宽字符转为多字节字符(公开方法)。
_wctomb_s_l:[C库] (缓冲安全版)将宽字符转为多字节字符(内部实现)。

  此时的调用栈——
> msvcr80d.dll!_wctomb_s_l(int * pRetValue=0x0012fbac, char * dst=0x0012fba0, unsigned int sizeInBytes=0x00000005, wchar_t wchar=L'汉', localeinfo_struct * plocinfo=0x00000000)  行81 C++
  msvcr80d.dll!wctomb_s(int * pRetValue=0x0012fbac, char * dst=0x0012fba0, unsigned int sizeInBytes=0x00000005, wchar_t wchar=L'汉')  行145 + 0x18 字节 C++
  msvcr80d.dll!_fputwc_nolock(wchar_t ch=L'汉', _iobuf * str=0x10311d20)  行133 + 0x14 字节 C
  msvcr80d.dll!fputwc(wchar_t ch=L'汉', _iobuf * str=0x10311d20)  行60 + 0xe 字节 C
  msvcp80d.dll!std::_Fputc<wchar_t>(wchar_t _Wchar=L'汉', _iobuf * _File=0x10311d20)  行86 + 0xf 字节 C++
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=0x6c49)  行261 + 0x1c 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x0041773e, int _Count=0x00000005)  行379 + 0x1a 字节 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x0041773c, int _Count=0x00000006)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x0041773c)  行853 + 0x3e 字节 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字节 C++

  在_wctomb_s_l函数中,因为现在使用的是C默认locale(未初始化locale),对于编码大于255的字符会报错。代码摘录——

// C:\VS8_2005\VC\crt\src\wctomb.c, 79 line:
    if ( _loc_update.GetLocaleT()->locinfo->lc_handle[LC_CTYPE] == _CLOCALEHANDLE )
    {
        if ( wchar > 255 )  /* validate high byte */
        {
            if (dst != NULL && sizeInBytes > 0)
            {
                memset(dst, 0, sizeInBytes);
            }
            errno = EILSEQ;
            return errno;
        }

 

  函数返回时——
_wctomb_s_l:【注意#4】因现在是C地区,而汉字的unicode码>255,于是返回EILSEQ。
wctomb_s:同_wctomb_s_l,返回EILSEQ。
_fputwc_nolock:因wctomb_s的返回值非0,返回WEOF(-1)。
fputwc:返回WEOF(-1)。
_Fputc<wchar_t>:判断条件为“return (::fputwc(_Wchar, _File) != WEOF);”,返回false。
basic_filebuf<wchar_t>::overflow:因_Fputc返回false,返回WEOF(-1)。
basic_streambuf<wchar_t>::xsputn:【注意#5】因overflow返回WEOF(-1),跳出循环,返回实际输出的字符数。
basic_streambuf<wchar_t>::sputn:返回xsputn的返回值,即返回实际输出的字符数。
operator<<:【注意#6】因实际输出的字符数与源字符数不同,设置流标记为bad。

  这就是“未初始化locale时,cout无法输出中文窄字符串”的原因。主要因为C默认locale不支持编码大于255的字符。


四、printf输出窄字符串

4.1 已初始化locale

  “printf("\t%s\n", psa)”表示使用printf输出窄字符串。按F11单步跟踪,它依次进入了下列函数——
printf:[C库] 带格式输出。
_output_l:[C库] 根据locale信息进行带格式输出。对格式字符串进行解析,根据“%s”提取窄字符串,然后调用write_string输出窄字符串。
write_string:[C库] 写窄字符串。循环对源串中的每一个字符调用write_char。
write_char:[C库] 写窄字符。

  此时的调用栈——
> msvcr80d.dll!write_char(char ch=0xd7, _iobuf * f=0x10311d20, int * pnumwritten=0x0012fba8)  行2442 C++
  msvcr80d.dll!write_string(char * string=0x0041774f, int len=0x00000004, _iobuf * f=0x10311d20, int * pnumwritten=0x0012fba8)  行2570 + 0x19 字节 C++
  msvcr80d.dll!_output_l(_iobuf * stream=0x10311d20, const char * format=0x00417823, localeinfo_struct * plocinfo=0x00000000, char * argptr=0x0012fe54)  行2260 + 0x18 字节 C++
  msvcr80d.dll!printf(const char * format=0x00417820, ...)  行63 + 0x18 字节 C
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行50 + 0x13 字节 C++

  write_char函数的源码如下——

// C:\VS8_2005\VC\crt\src\output.c, 2428 line:
LOCAL(void) write_char (
    _TCHAR ch,
    FILE *f,
    int *pnumwritten
    )
{
    if ( (f->_flag & _IOSTRG) && f->_base == NULL)
    {
        ++(*pnumwritten);
        return;
    }
#ifdef _UNICODE
    if (_putwc_nolock(ch, f) == WEOF)
#else  /* _UNICODE */
    if (_putc_nolock(ch, f) == EOF)
#endif  /* _UNICODE */
        *pnumwritten = -1;
    else
        ++(*pnumwritten);
}

 

  可见,因现在采用的是MBCS字符集,它是调用_putc_nolock函数来输出字符的。
  在VS2005中,_putc_nolock函数无法按F11单步跟踪进去。而且“C:\VS8_2005\VC\crt\src”目录下也找不到_putc_nolock函数的源码。
  虽然无法看见_putc_nolock函数的源码,但根据测试结果可以知道,它能正常的处理窄字符串。


4.2 未初始化locale

  未初始化locale时,“printf("\t%s\n", psa)”的执行路径与先前相同,最终调用_putc_nolock逐个逐个的输出窄字符。


五、printf输出宽字符串

5.1 已初始化locale

  “printf("\t%ls\n", psw)”表示使用printf输出宽字符串。按F11单步跟踪,它依次进入了下列函数——
printf:[C库] 带格式输出。
_output_l:[C库] 根据locale信息进行带格式输出。对格式字符串进行解析,根据“%ls”提取宽字符串,随后调用wctomb_s进行编码转换。
wctomb_s:[C库] (缓冲安全版)将宽字符转为多字节字符(公开方法)。
_wctomb_s_l:[C库] (缓冲安全版)将宽字符转为多字节字符(内部实现)。

  此时的调用栈——
> msvcr80d.dll!_wctomb_s_l(int * pRetValue=0x0012fb2c, char * dst=0x0012fb24, unsigned int sizeInBytes=0x00000006, wchar_t wchar=L'W', localeinfo_struct * plocinfo=0x00000000)  行115 C++
  msvcr80d.dll!wctomb_s(int * pRetValue=0x0012fb2c, char * dst=0x0012fb24, unsigned int sizeInBytes=0x00000006, wchar_t wchar=L'W')  行145 + 0x18 字节 C++
  msvcr80d.dll!_output_l(_iobuf * stream=0x10311d20, const char * format=0x0041781c, localeinfo_struct * plocinfo=0x00000000, char * argptr=0x0012fe54)  行2252 + 0x2d 字节 C++
  msvcr80d.dll!printf(const char * format=0x00417818, ...)  行63 + 0x18 字节 C
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行51 + 0x13 字节 C++

  在_wctomb_s_l函数中,因为现在已初始化locale,所以它能能正确的将宽字符串转为窄字符串。
  编码转换成功后,又会回到_output_l函数。它会调用write_string输出转换后的窄字符串,依次进入了下列函数——
write_string:[C库] 写窄字符串。循环对源串中的每一个字符调用write_char。
write_char:[C库] 写窄字符。调用_putc_nolock函数正常的输出窄字符串。


5.2 未初始化locale

  未初始化locale时,“printf("\t%ls\n", psw)”的执行路径与先前大致相同,也调用_wctomb_s_l进行编码转换。
  在_wctomb_s_l函数中,因为现在使用的是C默认locale(未初始化locale),对于编码大于255的字符会报错,于是造成可宽字符串不能输出。


六、总结

  总结一下不能输出时的原因——
已初始化locale时,cout无法输出中文窄字符串:因为_write_nolock函数中的条件判断存在漏洞,导致汉字的首字节无法输出。
已初始化locale时,wcout无法输出中文宽字符串:因为_write_nolock函数中的条件判断存在漏洞,导致汉字的首字节无法输出。
未初始化locale时,wcout无法输出中文宽字符串:因为在C默认locale时的_wctomb_s_l函数不支持编码大于255的字符。
未初始化locale时,printf无法输出中文宽字符串:因为在C默认locale时的_wctomb_s_l函数不支持编码大于255的字符。

  其中前2条是bug,而后2条是C标准中规定的。

 

参考资料——
《ISO/IEC 9899:1999》(C99). ISO/IEC,1999. www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
《C++ International Standard - ISO IEC 14882 Second edition 2003》(C++03). ISO/IEC,2003-10-15.
《C++标准程序库—自修教程与参考手册》. Nicolai M.Josuttis 著,侯捷、孟岩 译. 华中科技大学出版社,2002-09.
《[C] 跨平台使用TCHAR——让Linux等平台也支持tchar.h,解决跨平台时的格式控制字符问题,多国语言的同时显示》. http://www.cnblogs.com/zyl910/archive/2013/01/17/tcharall.html
《[C++] cout、wcout无法正常输出中文字符问题的深入调查(1):各种编译器测试》. http://www.cnblogs.com/zyl910/archive/2013/01/20/wchar_crtbug_01.html

 

 

posted on 2013-01-22 21:13  zyl910  阅读(4892)  评论(0编辑  收藏  举报