引言
在前面的文章《Windows上获取当前调用堆栈信息,StackWalker的C语言实现》中实现了如何通过编程的方式获取调用堆栈的详细信息。但如果要分析的源代码所在的路径中包含中文字符时则会出现获取的源文件路径名不正确的问题。本人经过实验,找到了解决的方法。
中文路径造成的问题
故意将一个试验用的项目放在了中文目录下,如下图所示:
项目所在的路径名为:E:\temp\测试文件\StackWalker_c,其中包含中文目录。
该项目执行的结果如下图所示:
第一个用红笔标出的地方本应显示完整的源文件名:e:\temp\测试文件\StackWalker_c\StackWalker.c, 但实际上显示的却是
e:\temp\测试文件\stackwalker_c\stackwalk ,文件名的后面一部分丢失了。
第二个用红笔标出的地方本应显示完整的源文件名:e:\temp\测试文件\StackWalker_c\main.c, 但实际上显示的却是
e:\temp\测试文件\stackwalker_c\ma,文件名的后面一部分也丢失了。
当项目所在的路径全为英文时则不会出现这种情况。网上查到一篇文献,告诫用户不要将项目放在中文目录中,避免出现文件名不完整的情形。这当然也是一种处理办法,但却并不令人满意。windows操作系统都已经支持中文这么多年了,难道还要用这种逃避的办法去解决问题吗?
经过多次尝试,已经找到了造成问题的根源,修正后的项目的运行结果如下:
从上图可以看出,尽管源代码文件所在路径中有中文字符,但是显示的源代码文件的路径名仍然是完整准确的。
工作原理
在原来的项目中,有这样一行代码
self->pSGLFA = (tSGLFA)GetProcAddress(self->m_hDbhHelp, "SymGetLineFromAddr64");
也就是说是使用了SymGetLineFromAddr64 从模块的调试符号信息中提取源码所在的行与源码的文件名。但是这个方法在源码文件的路径中包含中文时却不能正常工作。
带着这个问题在网上查找,却没有找到一篇与此相关的文献,当然也可能是只使用了国内搜索引擎的原因,但我想国外的人也不大可能碰到这个中文引起的问题吧。
最后在微软的官方文档中关于SymGetLineFromAddr64的文档中查到了一句
To call the Unicode version of this function, define DBGHELP_TRANSLATE_TCHAR. SymGetLineFromAddrW64 is defined as follows in Dbghelp.h.
就是这一句话启发了我,顺着这个线索,将上述的代码
self->pSGLFA = (tSGLFA)GetProcAddress(self->m_hDbhHelp, "SymGetLineFromAddr64");
改成了
self->pSGLFA = (tSGLFA)GetProcAddress(self->m_hDbhHelp, "SymGetLineFromAddrW64");
并且在引入<dbghelp.h>之前加上了 #define DBGHELP_TRANSLATE_TCHAR,变成下面这个样子
#define DBGHELP_TRANSLATE_TCHAR
#include <dbghelp.h>
原本以为这样修改之后就可以正常工作了,但结果显示的源文件的路径名发生了变化,但是仍然不正确。
针对这个问题,对SymGetLineFromAddr64的调用过程进行单步调试跟踪,下图是调试的截图:
图中红笔标出的部分可以看出文件名确实不完整,复制这个文件名对应的指针(地址),通过内存查看窗口,查看内存中的情形如下图所示:
从红笔标出来的地方可见确实没有取得完整的文件路径名。
接下来将 SymGetLineFromAddr64 改成 SymGetLineFromAddrW64,并且添加了#define DBGHELP_TRANSLATE_TCHAR后再观察内存里面的值,这次的观察情况如下图所示:
红笔圈出来的内容,如果仔细观察,可以发现这是一个双字节的字符串,即utf16字符串。两个字节表示一个字符,上图中
65 00 表示字符‘e’ ,3a 00 表示字符’:’, 5c 00 表示字符 ‘’ 。
由于不能直接判断中文字符是否正确,因此打开了一个16进制编辑软件wxMEdit,新建了一个文件,将上图中怀疑是中文字符的内容:4b 6d d5 8b 87 65 f6 4e
输入16制编辑器,得到内容如下图所示
右边窗口仍然显示乱码,与Visual Studio 内存窗口显示的内容有些相似,
接下来在wxMEdit中切换文本的编码方式,原来是GBK,现在切换为UTF-16LE, 现在的显示界面如下图所示:
可以看到上图中的中文内容:”测试文件“正是当前测试项目所在的中文目录。
由此可以确定 SymGetLineFromAddrW64 确实将调试符号信息中的源文件路径名提取出来了,但是为什么最终的输出结果仍然是错误的呢?再仔细的检查了一下源代码后发现,原来StackWalker原始版本的作者是一个老外,根本就没有考虑到源代码的路径中可能会出现中文,所以发生了两点状况
- 根本就没有使用SymGetLineFromAddrW64,而是使用的SymGetLineFromAddr64
- 在复制SymGetLineFromAddr64函数的结果到用户对象的时候写了一个函数 MyStrCpy,其具体代码如下:
//MyStrCpy
void MyStrCpy(char* szDest, size_t nMaxDestSize, const char* szSrc)
{
if (nMaxDestSize <= 0)
return;
strncpy_s(szDest, nMaxDestSize, szSrc, _TRUNCATE);
// INFO: _TRUNCATE will ensure that it is null-terminated;
// but with older compilers (<1400) it uses "strncpy" and this does not!)
szDest[nMaxDestSize - 1] = 0;
} // MyStrCpy
仔细看一下以上代码就可以清楚的看出,源串使用的是const char* ,根本就没有考虑到双字节字符串的问题
找到了原因,解决问题的办法就很简单了,再写一个宽字符转ANSI字符的函数来代替上文中的MyStrCpy函数。
代码如下:
//WChar2ANSI
void WChar2ANSI(char* szDest, size_t nMaxDestSize, const PWSTR szSrc)
{
if (nMaxDestSize <= 0)
return;
memset(szDest, 0, nMaxDestSize);
WideCharToMultiByte(CP_ACP, 0, szSrc, -1, szDest, nMaxDestSize, NULL, FALSE);
} // WChar2ANSI
其核心功能就是一个操作系统API WideCharToMultiByte 。 其中参数 CP_ACP 指明转换结果为ANSI ,在我的win10电脑上其实就是GBK编码的字符串。
经过以上修正,成功地解决了项目放在中文目录下,通过SymGetLineFromAddr64无法正确地查看调用堆栈的问题。
补充说明:
刚才通过16进制编辑器wxMEdit确定了SymGetLineFromAddrW64得到的结果为 utf-16le编码格式,顺便 也用wxMEdit打开了项目中的调试符号信息文件:StackWalker.pdb,发现 StackWalker.pdb 采用的是utf-8方式保存的中文内容。
完整项目下载
完整的示例项目请在CSDN资源中 下载