调试优化x64代码的挑战
如果到目前为止您还没有机会调试优化的x64代码,请不要再等待太久,也不要落后于时代!由于类似x64 fastcall的调用约定加上大量的通用寄存器,在调用堆栈中的任意点查找变量值确实非常困难。
在本文中,我想详细介绍一些我最喜欢的调试优化x64代码的技术。但是在深入研究这些技术之前,让我们先对x64调用约定有一个快速的概述。
x64调用约定
熟悉x86平台上fastcall调用约定的人将认识到与x64调用约定的相似之处。通常,您必须了解x86平台上的多个调用约定,而在x64平台上,目前只有一个。在这种情况下,通过__declspec(naked)调用(当然不包括直接调用)可以实现编码
我不会详细介绍x64呼叫约定的所有细微差别,因此我建议您查看以下链接(http://msdn.microsoft.com/en-us/library/ms794533.aspx). 但是通常,函数的前四个参数是通过寄存器rcx、rdx、r8和r9传递的。如果函数接受四个以上的参数,则这些参数将传递到堆栈上。(熟悉x86 fastcall调用约定的人,其中前两个参数是在ecx和edx中传递的,熟悉这种约定的人会认识到它们的相似之处)。
为了帮助说明x64调用约定是如何工作的,我创建了一些简单的示例代码。虽然代码是人为设计的,与真实世界中的代码相去甚远,但它演示了在实际世界中可能遇到的一些场景。代码如下所示。
#include <stdlib.h>#include<stdio.h>#include<windows.h>__declspec(noinline)voidFunctionWith4Params(int param1, int param2, intparam3,intparam4 )
{
size_t lotsOfLocalVariables1=rand();
size_t lotsOfLocalVariables2=rand();
size_t lotsOfLocalVariables3=rand();
size_t lotsOfLocalVariables4=rand();
size_t lotsOfLocalVariables5=rand();
size_t lotsOfLocalVariables6=rand();
DebugBreak();
printf("Entering FunctionWith4Params( %X, %X, %X, %X )\n",
param1, param2, param3, param4 );
printf("Local variables: %X, %X, %X, %X, %X, %X \n",
lotsOfLocalVariables1, lotsOfLocalVariables2,
lotsOfLocalVariables3, lotsOfLocalVariables4,
lotsOfLocalVariables5, lotsOfLocalVariables6 );
}
__declspec(noinline)voidFunctionWith5Params(int param1, int param2, intparam3,int param4, intparam5 )
{
FunctionWith4Params( param5, param4, param3, param2 );
FunctionWith4Params( rand(), rand(), rand(), rand() );
}
__declspec(noinline)voidFunctionWith6Params(int param1, int param2, intparam3,int param4, int param5, intparam6 )
{
size_t someLocalVariable1=rand();
size_t someLocalVariable2=rand();
printf("Entering %s( %X, %X, %X, %X, %X, %X )\n","FunctionWith6Params",
param1, param2, param3, param4, param5, param6 );
FunctionWith5Params( rand(), rand(), rand(),
param1, rand() );
printf("someLocalVariable1 = %X, someLocalVariable2 = %X\n",
someLocalVariable1, someLocalVariable2 );
}intmain(int /*argc*/, TCHAR** /*argv*/)
{//I use the rand() function throughout this code to keep//the compiler from optimizing too much. If I had used//constant values, the compiler would have optimized all//of these away. int params[] ={ rand(), rand(), rand(),
rand(), rand(), rand() };
FunctionWith6Params(params[0], params[1], params[2],params[3], params[4], params[5] );return 0;
}
设置微软符号服务器的又一方法
通过注册表设置:HKLM\SOFTWARE\Microsoft\Symbol Server Proxy\Web Directories\symbols下,设置SymbolPath,类型为REG_EXPAND_SZ。可以通过命令行直接添加:
reg add "HKLM\SOFTWARE\Microsoft\Symbol Server Proxy\Web
Directories\symbols" /v SymbolPath /t REG_EXPAND_SZ /f /d
c:\windows\symbols;SRV*d:\symbols*http://msdl.microsoft.com/download/symbols
.Net中的异常处理:高级异常
这种处理的有效性主要取决于所选择的语言和平台,因此,详细了解它们的正确用法和行为非常重要,这样我们的用户和其他开发人员在诊断代码中的问题时免受痛苦。
在本文中,我们将了解C和.NET在错误处理方面的作用。
词汇表
CLR:公共语言运行时的缩写,是.NET运行时,它负责执行用所有.NET语言编译的应用程序。除了虚拟机和实时编译器之外,它还具有额外的职责,如内存管理、安全性等。
BCL:Base Class Library的缩写,是.NET framework的核心库。除了直接使用CLR操作之外,它还公开了原始数据类型和构建和运行应用程序的基本功能。也称为mscorlib。
FCL:Framework类库的缩写,是我们大多数人在.NET中所知道的“框架”。使用BCL作为构建块,它公开了大量具有各种特性的名称空间,比如系统IO, 系统安全, 系统文本,等等。
TPL:Task Parallel Library的缩写,是一个包含由异步关键字和API提供的功能的库。它是随着.NET版本4.5和C#5一起发布的。
SEH:Structured Exception Handling的缩写,是Windows的原生异常子系统,它在操作系统级别处理软件和硬件异常
MDA:托管调试助手的缩写。这些是特殊的调试扩展,向VisualStudio调试器提供与CLR执行状态相关的信息,后者由内部助手和资产公开。
重新审查异常
在.NET中,尤其是在C#中,异常是使用try、catch和finally块来处理的。首先,try块将包含预期引发异常的代码,其次是catch块,它将指定异常类型和在try块内引发与指定类型匹配的异常时将执行的代码块:
Random rnd = newRandom();try{
Console.WriteLine(1 / rnd.Next(-100, 101));
}catch(DivideByZeroException ex) {
Debug.WriteLine(“A division by zero attempt has occurred”);
}
windbg是如何搜索符号文件的?
来个样例
我的符号目录设置是:
用我们在windows下调试必须用到的ntdll.dll模块来讲下windbg加载符号文件的过程。windbg加载符号文件时,会首先根据配置的符号目录信息,在本地符号目录中查找对应的符号文件。一个典型的搜索过程如下:
F:\Debug_Symbol\Symbols32\
F:\Debug_Symbol\Symbols32\pingme.txt
F:\Debug_Symbol\Symbols32\flat.txt
F:\Debug_Symbol\Symbols32\index2.txt
F:\Debug_Symbol\Symbols32\ntdll.pdb\2505F15902821D2C6931BBFF1B941EBF1\ntdll.pdb
F:\Debug_Symbol\Symbols32\ntdll.pdb\2505F15902821D2C6931BBFF1B941EBF1\ntdll.pd_
F:\Debug_Symbol\Symbols32\ntdll.pdb\2505F15902821D2C6931BBFF1B941EBF1\file.ptr
首先解释一下路径中的那一串字母和数字混合的东西是什么玩意儿,这个字符串是编译器根据编译时的时间、版本、程序类型等信息生成的一个类似GUID一样的东西(VC6编译的符号文件其内部编号是编译时间的绝对秒,就是
time
函数返回的32位从1970年1月1日0点开始的秒数,后面加上程序的特征,例如目标机器的类型、程序类型等;VC7.0、7.1、8.0、9.0
编译的符号文件编号是一个
GUID,这可能是为了避免多线程同时编译相同特征的程序引发内部编号冲突),存储在PE文件的DebugDirecotry数据目录指向的数据中,暂且称之为pdb的索引串,对于每个编译出来的文件而言它是唯一的。同名文件的不同版本,它的这个索引串也不同。
过程详解
下面我来逐一解释下上面看到的这个搜索过程:
- 调试器先检查符号目录是否存在
- 检查符号目录下是否存在flat.txt、pingme.txt或index2.txt。这三个文件的存在与否,决定了搜索过程中的一些细节。
如果存在pingme.txt,说明该目录下存在自动下载的符号文件。那么windbg将按照自动下载时的存放路径来检查符号文件是否存在。若没有pingme.txt,将不会采用这种路径来搜索。具体搜索方式参考第3条。
如果存在flat.txt(即使同时也存在pingme.txt),将忽略上面这种采用pdb索引串的快捷搜索方式,只以文件名和文件类型等信息进行搜索。
如果存在index2.txt,将按照文件名称分组进行搜索。分组方式是:使用符号文件名称的前两个字母最为一级目录,符号文件的名称作为二级目录,符号文件的编号作为三级目录,如此可对大量的文件进行分级索引,避免Symbols 目录下的子目录过多。比如以下路径:
F:\Debug_Symbol\Symbols32\ke\kernel32.pdb\
F:\Debug_Symbol\Symbols32\nt\ntdll.pdb\
F:\Debug_Symbol\Symbols32\nt\ntkrnlpa.pdb\ - 按pdb索引搜索(要求pingme.txt存在)
对于windbg自动下载的符号文件,会以"符号目录+符号文件名+pdb索引串+符号文件名的方式"为路径存储符号文件,这样,在下次需要查找该符号时,可以直接从PE文件中取得pdb索引串,然后构造出这样一个路径来快速加载符号文件。这就是搜索路径"F:\Debug_Symbol\Symbols32\ntdll.pdb\2505F15902821D2C6931BBFF1B941EBF1\ntdll.pdb"的由来。当存在pingme.txt时,将优先采用这种方式搜索。当然,自动下载符号的目录一般会自动创建一个pingme.txt的。 - 检查是否存在压缩的符号文件
windbg从符号服务器下载的符号文件,有些可能是压缩形式(文件名以_结束,需要用expand.exe解压缩),所以windbg会检查ntdll.pd_的存在,若存在就会将其解压缩。file.ptr可能也是某种方式的临时文件,暂时我无法完全解释它。 - 以符号文件名作为文件夹名进行搜索
如果以上都没有找到,那么就检查符号目录下ntdll.pdb这个文件夹是否存在,注意这里是文件夹。如果该文件夹存在,就会继续查找F:\Debug_Symbol\Symbols32\ntdll.pdb\ntdll.pdb。若文件夹不存在,就会直接在符号目录下查找符号文件ntdll.pdb(注意是文件)。 - 以目标文件的类型作为分类搜索
如果仍然没有找到,那么将根据PE文件的类型(dll,exe,sys,ocx等)作为子目录进行查找(安装的符号文件一般是以这种路径形式存放的)。 - 以目标文件Debug信息中指定的符号路径进行搜索
对于我们自己编译的驱动,通常是包含了pdb文件的全路径的,随便用一个编辑器打开一个sys文件都可以看到文件中出现的pdb路径信息。 - 搜索windbg所在路径
- 到符号服务器查找符号
如果以上都没有找到的话,也就是说本地符号库中无法找到匹配的符号文件,如果符号设置中允许自动到符号服务器下载符号(比如出现了"SRV*F:\Debug_Symbol\Symbols32*http://msdl.microsoft.com/download/symbols"这样的配置),那么windbg就会根据PE文件的pdb索引串到符号服务器上查找是否有与该pdb索引串匹配的符号文件,若有,就将其下载到本地,若没有,那就是真的没有了,windbg将返回"ERROR: Symbol file could not be found."
!address -summary报错"The current target does not provide full memory information. No meaningful summary available. "
今天分析一个dmp文件时,想看下内存的使用情况,于是执行!address -summary,结果却有如下输出:
0:000> !address -summary
The current target does not provide full memory information. No meaningful summary available.
Use !address with no arguments to display the available virtual memory map of the target.
之前收集到的dmp文件没有这样问题,于是问了下支持的同事,这个dmp文件的获取方法是怎样的。支持的同事反应不是自己抓的,是我们的程序自动产生的。看来程序在生成dmp时,Minidump的的选项设置有问题。
用工具打开dmp看看
赶紧查看一下代码
果然设置选项不够,赶紧加上MiniDumpWithFullMemoryInfo选项,制造一个异常,在看看
用Windbg加载,并执行!address -summary
0:000> !address -summary
Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 123 75782000 ( 1.835 GB) 91.78%
<unknown> 124 5954000 ( 89.328 MB) 53.04% 4.36%
Image 306 3c17000 ( 60.090 MB) 35.68% 2.93%
Heap 42 c96000 ( 12.586 MB) 7.47% 0.61%
Stack 18 600000 ( 6.000 MB) 3.56% 0.29%
Other 8 5c000 ( 368.000 kB) 0.21% 0.02%
TEB 6 e000 ( 56.000 kB) 0.03% 0.00%
PEB 1 3000 ( 12.000 kB) 0.01% 0.00%
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED 57 5497000 ( 84.590 MB) 50.22% 4.13%
MEM_IMAGE 327 3c83000 ( 60.512 MB) 35.93% 2.95%
MEM_PRIVATE 121 1754000 ( 23.328 MB) 13.85% 1.14%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 123 75782000 ( 1.835 GB) 91.78%
MEM_COMMIT 447 8197000 ( 129.590 MB) 76.94% 6.33%
MEM_RESERVE 58 26d7000 ( 38.840 MB) 23.06% 1.90%
--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READONLY 188 3f7c000 ( 63.484 MB) 37.69% 3.10%
PAGE_EXECUTE_READ 68 2f22000 ( 47.133 MB) 27.98% 2.30%
PAGE_READWRITE 144 11be000 ( 17.742 MB) 10.53% 0.87%
PAGE_WRITECOPY 29 666666000 ( 1.066 MB) 0.63% 0.05%
PAGE_READWRITE|PAGE_GUARD 12 1e000 ( 120.000 kB) 0.07% 0.01%
PAGE_EXECUTE_READWRITE 4 a000 ( 40.000 kB) 0.02% 0.00%
PAGE_NOACCESS 2 2000 ( 8.000 kB) 0.00% 0.00%
--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
Free 7eb0000 56d90000 ( 1.357 GB)
<unknown> 4e00000 2000000 ( 32.000 MB)
Image 75231000 54d000 ( 5.301 MB)
Heap 7a79000 376000 ( 3.461 MB)
Stack 37e0000 fd000 (1012.000 kB)
Other 7fcc0000 33000 ( 204.000 kB)
TEB e19000 3000 ( 12.000 kB)
PEB e16000 3000 ( 12.000 kB)
看到了内存的摘要信息了。
虽然我们希望dmp文件尽量小,但一些必要的信息还是需要的。