64位整型变量错误使用int类型对应的格式化符%d导致软件崩溃问题的排查与分析(借助deepseek辅助分析)
目录
1、在Windbg中查看到要打印的dwPlayid值为0xffffffff9b931362,深入分析产生崩溃的原因
1.1、在Windbg中查看上述函数中传入的dwPlayid值(在Windbg中查看变量的值)
1.2、查看格式化字符串的内部代码实现,搞清楚格式化字符串函数内部如何通过格式化符到栈上找到对应的待格式化的参数值的
1.3、传入的dwPlayid值为0xffffffff9b931362,为什么会引发崩溃?
2、如果传入的dwPlayid值是0x000000009b931362,为什么不会崩溃呢?
3、如果传入的dwPlayid值是0x000000019b931362(高4字节值是0x00000001),则会产生崩溃
4、解决办法
C++软件异常排查从入门到精通系列教程(核心精品专栏,订阅量已达8000多个,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(重点专栏,专栏文章已更新500多篇,订阅量已达6000多个,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/140824370C++ 软件开发从入门到实战(重点专栏,专栏文章已更新300多篇,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_2276111.html 某天软件发生异常崩溃,软件中安装的异常捕获模块感知到,生成了包含异常上下文的dump文件。取来dump文件进行详细分析,发现是某接口中输出日志的代码有问题,一个64位整型(unsigned __int64)变量错误使用了32位int类型对应的格式化符%d,问题代码如下所示:
代码中要将无符号64位整型(unsigned __int64)变量dwPlayid的值打印出来,错误使用了32位int类型对应的格式化符%d,导致访问了不该访问的内存,引发内存访问违例,导致程序发生崩溃。
1、在Windbg中查看到要打印的dwPlayid值为0xffffffff9b931362,深入分析产生崩溃的原因
根据Windbg中显示的函数调用堆栈,引发崩溃的代码就在打印日志的这行上:
仔细一看,要打印的无符号64位整型变量dwPlayid,在打印日志的这行代码中错误地使用了32位int类型对应的格式化符%d,即待格式化变量类型与对应的格式化符不一致,这应该就是引发崩溃的原因了。
当前的这个崩溃,只在某台电脑上才有,而且频繁地出现,其他人那边从未报过这样的问题,很是奇怪!按讲待格式化参数类型与对应的格式化符不一致,应该每次进入当前的函数就会崩溃,为啥在别的客户电脑上没有这个问题呢?
1.1、在Windbg中查看上述函数中传入的dwPlayid值(在Windbg中查看变量的值)
用Windbg打开dump文件,切换到发生异常的线程,输入kn命令查看异常发生时的函数调用堆栈,然后通过查看堆栈中的模块时间戳,找到对应时间点的pdb文件添加到Windbg中,然后重新使用kn命令查看函数调用堆栈,堆栈中就能看到具体的函数名及代码的行号了,点击每行行数调用前面的序号,就可以查看到函数中相关变量的值(包括函数中局部变量以及所在C++类对象的成员变量的值)。关于如何用Windbg静态分析dump文件,此处就不再赘述了,可以查看我的文章:
使用Windbg分析dump文件定位软件异常的方法与操作步骤https://blog.csdn.net/chenlycly/article/details/146005441使用Windbg调试目标进程的一般步骤及要点详解
https://blog.csdn.net/chenlycly/article/details/131029795 按讲待格式化参数类型与对应的格式化符不一致,一般都会触发异常,为啥在其他客户的电脑上没有发生过崩溃呢?(我们要多问问为什么,然后在由此触发的好奇心驱使下去深入研究其中的细节,好奇心是强大的内驱力!)要搞清楚这个原因,我们先查看一下待格式化参数dwPlayid的值,然后详细分析一下引发崩溃的原因。
点击堆栈中每行记录最前面的序号超链接,展开当前行函数的栈变量,然后点击函数所在类对象的this指针,就能看到传入的dwPlayid值了,如下所示:
待格式化的参数dwPlayid值为0xffffffff9b931362。在Windbg查看变量的值,可能是分析问题的关键线索,非常有用,关于如何在Windbg中查看变量的值以及通过查看变量的值去快速定位问题的实战分析案例,可以查看我之前写的文章:
通过查看Windbg中变量值去定位C++软件异常问题https://blog.csdn.net/chenlycly/article/details/125731044通过查看Windbg中变量值去定位C++软件异常的又一典型案例分享
https://blog.csdn.net/chenlycly/article/details/125793532
1.2、查看格式化字符串的内部代码实现,搞清楚格式化字符串函数内部如何通过格式化符到栈上找到对应的待格式化的参数值的
上述打印日志的接口,内部主要有两个作用:
1)对传入的参数,进行格式化,构建出要打印的完整字符串;
2)将要打印的日志输出到调试窗口或者文件中。
要分析出待格式化参数类型与对应的格式化符不一致导致的崩溃原因,首先我们可以从打印日志的函数内部怎么去解析处理待格式化内容的。当代码运行到打印日志的接口时,会把待格式化的参数压到栈上传递给函数内部(函数调用时主要通过栈传递参数值的,即将参数值压到栈上,被调用函数到栈上读取传入参数的值),格式化字符串函数内部先找到格式化串中的第一个格式化符(从左到右依次查找格式化符),从栈上找到对应的要格式化的参数值(从栈内存上去读参数值),同时将内存指针向后偏移(偏移第一个格式化符对应的内存长度),然后再找第二个格式化符对应的参数值。
以前看过格式化函数内部的代码实现,详细看过如何通过格式化符依次从栈内存上找到对应的待格式化参数的值,基本所有的格式化字符串函数(比如sprintf、_stprintf、_stprintf_s、vsprintf和_vstprintf_s等)实现思路都是差不多的。对这点有兴趣的话,可以去查看VC6版本的MFC常用字符串类CString的Format函数的内部实现:(代码相对复杂,可以静下心来看一看)
void CString::Format(LPCTSTR lpszFormat, ...)
{assert(IsValidString(lpszFormat));va_list argList;va_start(argList, lpszFormat);FormatV(lpszFormat, argList);va_end(argList);
}// CString::Format函数内部调用FormatV,到FormatV中查看如何根据格式化符到栈上找到对应的待格式化参数值的
void CString::FormatV(LPCTSTR lpszFormat, va_list argList)
{assert(IsValidString(lpszFormat));va_list argListSave = argList;// make a guess at the maximum length of the resulting stringint nMaxLen = 0;for (LPCTSTR lpsz = lpszFormat; *lpsz != '\0'; lpsz = _tcsinc(lpsz)){// handle '%' character, but watch out for '%%'if (*lpsz != '%' || *(lpsz = _tcsinc(lpsz)) == '%'){nMaxLen += _tclen(lpsz);continue;}int nItemLen = 0;// handle '%' character with formatint nWidth = 0;for (; *lpsz != '\0'; lpsz = _tcsinc(lpsz)){// check for valid flagsif (*lpsz == '#')nMaxLen += 2; // for '0x'else if (*lpsz == '*')nWidth = va_arg(argList, int);else if (*lpsz == '-' || *lpsz == '+' || *lpsz == '0' ||*lpsz == ' ');else // hit non-flag characterbreak;}// get width and skip itif (nWidth == 0){// width indicated bynWidth = _ttoi(lpsz);for (; *lpsz != '\0' && _istdigit(*lpsz); lpsz = _tcsinc(lpsz));}assert(nWidth >= 0);int nPrecision = 0;if (*lpsz == '.'){// skip past '.' separator (width.precision)lpsz = _tcsinc(lpsz);// get precision and skip itif (*lpsz == '*'){nPrecision = va_arg(argList, int);lpsz = _tcsinc(lpsz);}else{nPrecision = _ttoi(lpsz);for (; *lpsz != '\0' && _istdigit(*lpsz); lpsz = _tcsinc(lpsz));}assert(nPrecision >= 0);}// should be on type modifier or specifierint nModifier = 0;if (_tcsncmp(lpsz, _T("I64"), 3) == 0){lpsz += 3;nModifier = FORCE_INT64;
#if !defined(_X86_) && !defined(_ALPHA_)// __int64 is only available on X86 and ALPHA platformsassert(FALSE);
#endif}else{switch (*lpsz){// modifiers that affect sizecase 'h':nModifier = FORCE_ANSI;lpsz = _tcsinc(lpsz);break;case 'l':nModifier = FORCE_UNICODE;lpsz = _tcsinc(lpsz);break;// modifiers that do not affect sizecase 'F':case 'N':case 'L':lpsz = _tcsinc(lpsz);break;}}// now should be on specifierswitch (*lpsz | nModifier){// single characterscase 'c':case 'C':nItemLen = 2;va_arg(argList, TCHAR_ARG);break;case 'c'|FORCE_ANSI:case 'C'|FORCE_ANSI:nItemLen = 2;va_arg(argList, CHAR_ARG);break;case 'c'|FORCE_UNICODE:case 'C'|FORCE_UNICODE:nItemLen = 2;va_arg(argList, WCHAR_ARG);break;// stringscase 's':{LPCTSTR pstrNextArg = va_arg(argList, LPCTSTR);if (pstrNextArg == NULL)nItemLen = 6; // "(null)"else{nItemLen = lstrlen(pstrNextArg);nItemLen = max(1, nItemLen);}}break;case 'S':{
#ifndef _UNICODELPWSTR pstrNextArg = va_arg(argList, LPWSTR);if (pstrNextArg == NULL)nItemLen = 6; // "(null)"else{nItemLen = wcslen(pstrNextArg);nItemLen = max(1, nItemLen);}
#elseLPCSTR pstrNextArg = va_arg(argList, LPCSTR);if (pstrNextArg == NULL)nItemLen = 6; // "(null)"else{nItemLen = lstrlenA(pstrNextArg);nItemLen = max(1, nItemLen);}
#endif}break;case 's'|FORCE_ANSI:case 'S'|FORCE_ANSI:{LPCSTR pstrNextArg = va_arg(argList, LPCSTR);if (pstrNextArg == NULL)nItemLen = 6; // "(null)"else{nItemLen = lstrlenA(pstrNextArg);nItemLen = max(1, nItemLen);}}break;case 's'|FORCE_UNICODE:case 'S'|FORCE_UNICODE:{LPWSTR pstrNextArg = va_arg(argList, LPWSTR);if (pstrNextArg == NULL)nItemLen = 6; // "(null)"else{nItemLen = wcslen(pstrNextArg);nItemLen = max(1, nItemLen);}}break;}// adjust nItemLen for stringsif (nItemLen != 0){if (nPrecision != 0)nItemLen = min(nItemLen, nPrecision);nItemLen = max(nItemLen, nWidth);}else{switch (*lpsz){// integerscase 'd':case 'i':case 'u':case 'x':case 'X':case 'o':if (nModifier & FORCE_INT64)va_arg(argList, __int64);elseva_arg(argList, int);nItemLen = 32;nItemLen = max(nItemLen, nWidth+nPrecision);break;case 'e':case 'g':case 'G':va_arg(argList, DOUBLE_ARG);nItemLen = 128;nItemLen = max(nItemLen, nWidth+nPrecision);break;case 'f':{double f;LPTSTR pszTemp;// 312 == strlen("-1+(309 zeroes).")// 309 zeroes == max precision of a double// 6 == adjustment in case precision is not specified,// which means that the precision defaults to 6pszTemp = (LPTSTR)_alloca(max(nWidth, 312+nPrecision+6));f = va_arg(argList, double);_stprintf( pszTemp, _T( "%*.*f" ), nWidth, nPrecision+6, f );nItemLen = _tcslen(pszTemp);}break;case 'p':va_arg(argList, void*);nItemLen = 32;nItemLen = max(nItemLen, nWidth+nPrecision);break;// no outputcase 'n':va_arg(argList, int*);break;default:assert(FALSE); // unknown formatting option}}// adjust nMaxLen for output nItemLennMaxLen += nItemLen;}GetBuffer(nMaxLen);//VERIFY(_vstprintf(m_pchData, lpszFormat, argListSave) <= GetAllocLength());// 将上一句的VERIFY代码注释掉,重新写// 此处去掉了VERIFYint nWriteCount = GetAllocLength() + 1;int nLen = _vstprintf(m_pchData, nWriteCount, lpszFormat, argListSave);assert( nLen <= GetAllocLength() );ReleaseBuffer();va_end(argListSave);
}
上述代码中可以看出如何解析处格式化串中的格式化符,对于从何根据这些格式化符到栈上找到对应的格式化参数值,代码中的va_arg宏,网上有对该宏详细的说明,此处我就不展开了。
为什么要看VC6这么老的版本MFC字符串类CString的Format函数的内部实现呢?因为VC6版本的CString类没有使用模版,代码看起来更简单直接。
1.3、传入的dwPlayid值为0xffffffff9b931362,为什么会引发崩溃?
对于MtLogHint日志打印函数,我们传入了两个参数,一个是64位整型的dwPlayid,一个字符串对象strStreamId,即这两个参数的值将被格式化,分别对应格式化符%d和%s。在该函数被调用时,函数默认是C调用约定,参数值依次从右到左依次压栈,即先把strStreamId变量中的字符串内容压到栈上,然后再将dwPlayid的值压到栈上。
线程是分配栈内存的单元,线程中函数占用的栈内存都位于给该线程分配的栈空间上,函数中局部变量的值是存在栈内存上,函数调用时的参数值也是通过栈内存传递的。
栈的使用方向,是从大地址向小地址使用的,所以当strStreamId和dwPlayid的值被依次压到栈上后,进入MtLogHint内部,内部负责格式化字符串的代码会先解析到%d,然后到栈内存上读取出4字节的内存,把内存中的值作为%d格式化符对应的值格式化到字符串中,同时将刚取出的4字节偏移掉,然后解析到第二个格式化符%s,从当前栈内存上拿出4字节(当前是32位程序)作为待格式化的字符串的首地址,格式化到最终的字符串中。
对于64位整型dwPlayid,压到栈上的数据为0xffffffff9b931362,占8个字节,而第一个格式化符为%d,此时待格式化参数类型64整型与格式化符%d的内存长度不一致,这是本案例的问题所在。当前机器是小头序,0xffffffff9b931362数据的低位存放在低(小)内存地址上,所以对于第一个格式化符%d,取出的4字节的内存值为0x9b931362,然后解析第二个格式化符,在偏移4字节的内存上取出4个字节的内容作为%s对应的字符串的首地址,即从栈上取出0xffffffff9b931362值的高4字节值0xffffffff,即0xffffffff作为格式化符%s对应的字符串首地址。这样就会到内存地址0xffffffff处去读取内存中的值,而对于32程序,0xffffffff属于内核态的内存地址,当前用户态的代码是禁止访问内核态地址的,所以产生了内存访问违例,导致程序崩溃。
关于如何从栈上解析出格式化符对应的参数值的更详细图文说明,可以查看我之前写的一篇关于格式化符错误使用引发崩溃的典型案例:
UINT64整型数据在格式化时使用了不匹配的格式化符%d导致其他参数无法打印的问题排查https://blog.csdn.net/chenlycly/article/details/132549186
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:【C++软件异常与异常排查从入门到精通系列教程】(该精品技术专栏的订阅量已达到10000多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,已经更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,详细介绍分析C++软件问题的常用分析工具,以图文并茂的方式给出具体的项目问题实战分析实例(详细讲述分析排查过程,很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:【C/C++实战进阶】(该专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达8000多个,专栏文章已经更新到500多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、数据结构与算法、C++11及以上新特性(开源代码中可能会用到很多新特性(比如WebRTC开源库),日常编码中也会用到部分新特性,面试时也会频繁地涉及到,学习新特性很有必要)、常用C++开源库的介绍与使用(比如SQLite、libcurl、libwebsockets、libevent、jsoncpp/RapidJson、Redis、RabbitMQ、MongoDB、MQTT、ZooKeeper、OpenCV、FFmpeg、SDL、GStreamer、Live555、ReactOS等)、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(引发C++软件异常的常见原因分析与总结、排查C++软件异常的手段与方法、分析C++软件异常的基础知识、使用常用软件分析工具分析C++软件问题、多个项目实战问题分析案例分享等)、设计模式(单例模式、工厂模式、观察者模式、状态模式等)、网络基础知识与网络问题分析进阶内容(实战问题分析实例分享)等。本专栏的内容都是建立在项目实践的基础上,来源于项目实战,服务于项目实战,很有实战参考价值!
专栏3:【分析C++软件问题的实用软件与高效工具实战案例集锦】
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro以及内存泄漏检测工具等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏4:【VC++常用功能代码封装】
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:【C/C++软件开发从入门到实战】(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,专栏文章已经更新到300多篇,持续更新中!欢迎订阅!)
C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
2、如果传入的dwPlayid值是0x000000009b931362,为什么不会崩溃呢?
当前崩溃案例中引发崩溃的dwPlayid值为0xffffffff9b931362,是因为待格式化参数dwPlayid是64位整型与格式化符%d导致的。0xffffffff9b931362其实是程序中的异常值,正常的dwPlayid值一般是不超过4字节的,即高4字节都是0,比如0x000000009b931362,按上面的分析过程,当解析到第二个格式化符%s时,会把高4字节的值0x00000000作为待格式化的字符串的首地址,这样就会去访问内存地址为0x00000000的内存,而这个内存很小,是禁止访问的,也会触发内存访问违例,引发程序崩溃。而实际运行时并没有产生崩溃。
于是到deepseek中搜索了一下,才得知处理0x00000000内存时不产生崩溃的原因:
这是微软特意添加的保护:当微软检测到是格式化符%s对应的地址是0x00000000,为了防止程序发生崩溃,不会去读该地址,会直接输出“null”字符串。因为不去读取0x00000000,所以就不会触发内存违例,就不会崩溃。
有了大模型,有了deepseek,我们遇到问题不用到搜索引擎中费力的搜索了,直接在大模型中找到直击要点的答案,太便利了!
微软对格式化符%s对应的地址是否是0x00000000的检测,是放置在C/C++运行时库中的,其实就是我们代码中最终调用的格式化字符串C/C++运行时库函数sprintf、_stprintf、_stprintf_s、vsprintf和_vstprintf_s等中。
上述说明就能很好的解释本案例中的崩溃问题只在某台电脑上才有(该电脑上dwPlayid会出现类似0xffffffff9b931362这样的异常值),而其他人那边从未报过这样的问题(其他电脑上dwPlayid都是类似0x000000009b931362这样的正常值)。
至于为什么会产生0xffffffff9b931362这个异常值,我们也已经查清楚了,原因非常特别,这么多年是第一次遇到,后面有时间再分享,此处就不展开了。
3、如果传入的dwPlayid值是0x000000019b931362(高4字节值是0x00000001),则会产生崩溃
如果传入dwPlayid值0x000000019b931362(高4字节值是0x00000001),则第二个格式化符%s对应的待格式化字符串的首地址为0x00000001,就要读取0x00000001内存地址中的字符串,这个小地址是系统禁止访问的,就会触发内存访问违例,引发崩溃。
4、解决办法
解决办法很简单,将64位整型变量dwPlayid对应的格式化符换成%llu或者%I64u就可以了。最后,我们在分析问题时要尽量多挖掘和思考问题中隐含的多个细节和疑问点,这样可以获取更多的认知和知识点,积累更多的实战分析经验。