Visual Studio高级调试技巧
1. 设置软件断点,运行到目标位置启动调试器
方法①:使用汇编指令(注:x64 c++不支持嵌入汇编)
_asm int 3
方法②:编译器提供的方法
__debugbreak();
方法③:使用windows API
DebugBreak();
WerFault.exe进程(Windows Error Reporting)弹出ConsoleTest.exe已停止工作:
要想出现“调试程序”选项,需要将Windows Error Reporting注册表信息设置成如下图所示(注:特别是红框的内容)
如果在注册表AeDebug的Debugger项配置了VSJitDebugger路径,且VSJitDebugger安装正常
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug // 注:64位系统上的32位程序使用该注册表项
点击“调试程序”选项就会弹出Visual Studio实时调试器对话框,选择对应的调试器后,点击“是”就可以启动调试器并中断到软件断点位置了
需要注意的是,软件断点也是一种异常,一旦被处理,就不会传到WerFault.exe进程上,那么这种方法也就失效了!
下面两种情况软件断点异常会被处理:
① 被SEH异常捕获并处理
② 被自定义的全局异常函数处理
注:可以将上面两种情况中的EXCEPTION_EXECUTE_HANDLER修改为EXCEPTION_CONTINUE_SEARCH来指明异常未得到处理
2. 修改变量
注:在悬停出来小面板、Locals窗口、Autos窗口、Watch窗口、或Quick Watch窗口中进行修改;也可以在Immediate窗口中执行:bFlag=false
3. 格式化变量
注:d,i:有符号的十进制数
u:无符号的十进制数
o:无符号的八进制数
x:十六进制数(字母小写)
X:十六进制数(字母大写)
更多详见:Format specifiers for C++ in the Visual Studio debugger
4. 修改内存
注:在内存窗口中,将光标定位到要修改的地方,直接按0-9输入十六进制;要输入a-f则需通过右键菜单中的“Edit Value”进行输入
5. 格式化显示内存
6. 设置下一个运行位置
注:直接拖动黄色箭头到想要的运行位置
示例中:传入的bFlag为true,代码开始运行到断点处(43行),然后重新把黄色箭头拖回39行,此时bFlag的值为false,按F10会进入else分支
注:(1)跳过中间所有指令。意味着:printf("True\n")及CTest的析构函数均不会被执行
(2)当拖动箭头到一个新的函数中时,vs会将原来的函数从栈中弹出,将新函数压入栈顶;
由于新函数与上层函数没有调用关系,输出类型的参数及返回值很有可能写坏上层函数的栈数据
(3)该调试技巧为一种事后行为,应谨慎使用,最好是只在函数内局部使用
7. 编辑然后继续运行
(1)不能在64位代码上使用
(2)使用“Program Database for Edit & Continue (/ZI)”生成pdb文件
(3)仅适用于函数内部改变(若要修改函数原型或增加新函数,只能选择重启程序)
8. 变量的一些特殊查看方法
以$和@开头的伪变量:(注:$和@两个符号是一样的,随便用哪个都可以)
$err -- 获取GetLastError()的返回值 注:linux中,系统api的最近错误码可通过errno全局变量来获取
$err,hr -- 获取GetLastError()的返回值并解释返回值的含义
@eax -- 查看eax的值(64位为@rax)
@esp+4 -- 函数的第一个参数地址
$handles -- 查看打开的句柄数
$pid -- 当前进程id
$tid -- 当前线程id
$vframe -- 当前栈帧的ebp
$clk -- 以时钟周期为单位显示时间
$ReturnValue -- 查看函数的返回值
Message,wm --以windows消息的宏形式显示 如:Message为15时,显示为WM_PAINT(注:Message为unsigned int类型)
hResult,hr --hResult为0x80070005时,显示为E_ACCESSDENIED(注:hResult为void*类型)
pArray,10 --从pArray地址起显示后续10个int类型的数据(注:pArray为int*类型)
(pArray+5),3 --从pArray[5]地址起显示后续3个int类型的数据(注:pArray为int*类型)
更多详见:Pseudovariables in the Visual Studio debugger
9. 获取简单类型的函数返回值
注1:不能为inline函数
注2:执行函数的下一条语句时,查看eax或伪变量ReturnValue的值
10. 使用指针类型转换查看某个地址的变量
注:有时候,尽管对象仍然存在,在调试符号越界后,watch窗口中的变量是被禁用的,不能再查看(也不能更新)。
若知道对象的地址,则可以将地址转换为该对象类型的指针,放在watch窗中来继续观察它。
11. 查看其他模块的全局变量
{,,UE4Editor-Core.dll}GIsEditor // 当前是否为编辑器
{,,UE4Editor-Core.dll}GIsClient // 当前是否为客户端
{,,UE4Editor-Core.dll}GFrameCounter // 当前帧序号 1100
{,,UE4Editor-Core.dll}GFrameNumber // 当前帧序号 1101
{,,UE4Editor-Core.dll}GFrameNumberRenderThread // 当前渲染线程帧序号 1102
{,,UE4Editor-Core.dll}GPlayInEditorID // PIE ID(适用于多玩家,也可作为断点条件)
{,,UE4Editor-Engine-Win64-Debug}::GPlayInEditorContextString // PIE窗口名称(适用于多玩家)
注:UE4Editor-Core.dll为全局变量GIsEditor、GIsClient、GFrameCounter所在的模块
还要另外一个方法是:通过看汇编代码,找到全局变量对应的地址,然后在Memory中查看对应的值或在Watch窗口里面将地址转换成对应的类型
12. 查看函数指针地址具体对应哪个函数
注:如果某个地址为函数,通过void*类型转换可看到函数名称
13. 在Watch窗口中执行表达式
14. 在菜单“Debug” -- “QuickWatch”中执行表达式
15. Immediate窗口
① 求类型或对象的大小
? sizeof(int) 4 ? sizeof(lua_State) 208 ? sizeof(L) 8
② 输出字符串
? "go out" "go out" [0]: 103 'g' [1]: 111 'o' [2]: 32 ' ' [3]: 111 'o' [4]: 117 'u' [5]: 116 't' [6]: 0 '\0' ? "go \nout",sb // 带上sb,回车符会换行打印 go out [0]: 103 'g' [1]: 111 'o' [2]: 32 ' ' [3]: 10 '\n' [4]: 111 'o' [5]: 117 'u' [6]: 116 't' [7]: 0 '\0'
③打印变量
? n 100 ? g_rc {w=0.000000000 h=0.000000000 } w: 0.000000000 h: 0.000000000 ? L 0x00000218c5702128 {next=0x0000000000000000 <NULL> tt=8 '\b' marked=0 '\0' ...} next: 0x0000000000000000 <NULL> tt: 8 '\b' marked: 0 '\0' nci: 7 status: 0 '\0' top: 0x00000218c570d7a0 {value_={gc=0x00007ff790a97a30 {LuaTest.exe!Square(lua_State *)} {next=0x48575508244c8948 {...} ...} ...} ...} l_G: 0x00000218c57021f8 {frealloc=0x00007ff790a4ab60 {LuaTest.exe!l_alloc(void *, void *, unsigned __int64, unsigned __int64)} ...} ci: 0x00000218c5702188 {func=0x00000218c570d760 {value_={gc=0xcdcdcdcdcdcdcdcd {next=??? tt=??? marked=??? } ...} ...} ...} oldpc: 0xcdcdcdcdcdcdcdcd {???} stack_last: 0x00000218c570db50 {value_={gc=0xcdcdcdcdcdcdcdcd {next=??? tt=??? marked=??? } p=0xcdcdcdcdcdcdcdcd ...} ...} stack: 0x00000218c570d760 {value_={gc=0xcdcdcdcdcdcdcdcd {next=??? tt=??? marked=??? } p=0xcdcdcdcdcdcdcdcd ...} ...} openupval: 0x0000000000000000 <NULL> gclist: 0x0000000000000000 <NULL> twups: 0x00000218c5702128 {next=0x0000000000000000 <NULL> tt=8 '\b' marked=0 '\0' ...} errorJmp: 0x0000000000000000 <NULL> base_ci: {func=0x00000218c570d760 {value_={gc=0xcdcdcdcdcdcdcdcd {next=??? tt=??? marked=??? } p=0xcdcdcdcdcdcdcdcd ...} ...} ...} hook: 0x0000000000000000 errfunc: 0 stacksize: 68 basehookcount: 0 hookcount: 0 nny: 1 nCcalls: 0 hookmask: 0 allowhook: 1 '\x1'
④修改变量
? n=120 //修改整型变量n为120 120 ? g_rc.w=10 //修改结构体变量CRect g_rc的成员w为10 10.0000000
⑤执行当前栈帧所在模块中的全局函数
int ImmediateTest1() { return 100; } const char* ImmediateTest2() { return "Hello World!"; } const char* ImmediateTest3() { return "Hello World!\nThis is China.\nLet's go."; } std::vector<std::string> ImmediateTest4() { std::vector<std::string> output; output.push_back("Hello World!"); output.push_back("This is China."); output.push_back("Let's go."); return output; } const char* VSDumpLuaStateTValue(lua_State* L, int n) { static char s_szBuffer[256] = { 0 }; memset(s_szBuffer, 0, 256); // ... ... return s_szBuffer; }
输出结果如下:
? ImmediateTest1() 100 ? ImmediateTest2() 0x00007ff790aabb30 "Hello World!" ? ImmediateTest3(),sb Hello World! This is China. Let's go. ? ImmediateTest4() { size=3 } [capacity]: 3 [allocator]: allocator [0]: "Hello World!" [1]: "This is China." [2]: "Let's go." [Raw View]: {_Mypair=allocator } ? VSDumpLuaStateTValue(L, -1) 0x00007ff790ac0380 "3[-1]: table:0x00000218C5706520"
注1:Immediate窗口中一行的最大字符串长度为230个字符。
注2:格式化符说明
b: unsigned binary integer
bb: unsigned binary integer (without leading 0b)
bstr: BSTR string
c: single character
d: decimal integer
e: scientific notation
en: enum
env: Environment block (double-null terminated string)
f: floating point
g: shorter of scientific or floating point
h: hexadecimal integer
H: hexadecimal integer
hb: hexadecimal integer (without leading 0x)
Hb: hexadecimal integer (without leading 0x)
hr: HRESULT or Win32 error code.
hv: Pointer type - indicates that the pointer value being inspected is the result of the heap allocation of an array
na: Suppresses the memory address of a pointer to an object.
nd: Displays only the base class information, ignoring derived classes
nr: Suppress "[Raw View]" item
nvo: numeric value only
o: unsigned octal integer
s: const char* string
s32: UTF-32 string
s32b: UTF-32 string (no quatation marks)
s8: UTF-8 string
s8b: UTF-8 string (no quatation marks)
sb: const char* string (no quatation marks)
su: Unicode(UTF-16 encoding) string
sub: Unicode(UTF-16 encoding) string (no quatation marks)
wc: Window class flag
wm: Windows message numbers
x: hexadecimal integer
X: hexadecimal integer
xb: hexadecimal integer (without leading 0x)
Xb: hexadecimal integer (without leading 0x)
⑥ 执行当前栈帧ue4全局函数
?{,,UE4Editor-Core-Win64-Debug}::PrintScriptCallstack() // 打印蓝图调用堆栈 [2021.08.13-17.43.23:445][462]LogOutputDevice: Warning: Script Stack (2 frames): ThirdPersonExampleMap_C.ExecuteUbergraph_ThirdPersonExampleMap ThirdPersonExampleMap_C.ReceiveBeginPlay <void>
16. Command窗口
通过命令来完成vs中的功能(不仅仅在调试状态时使用),另外其调试相关命令与windbg保持一致。
? nLocal //查看变量nLocal的值 ?? nLocal //将nLocal添加到Quick Watch窗口中 ? nLocal=100 //修改nLocal的值为100 ? MySum(20,30) //调用全局函数MySum,并返回结果 k //打印当前线程堆栈 ~ //查看线程情况 ~*k //打印出所有线程的堆栈信息 watch //打开watch窗口 memory2 //打开memory2窗口 g //继续执行,F5功能 q //结束调试
17. 内存断点
注:纯Native的c++项目工程才能使用内存断点,开了CLR的c++项目工程则不行;另外,需在调试中断时,才能新建内存断点
(1)在84行断点停住后,查看&s.Age的地址为0x0042FCEC
(2)点击"Debug"-"New Breakpoint"-"New Data Breakpoint...",在弹出的对话框Address填入:0x0042FCEC,长度为4即可
(3)当运行到88行时,由于Scores数组越界引发了s.Age的内存修改,触发了内存断点
18. 条件断点
宏变量名 | 含义 |
$ADDRESS | 当前指令 |
$CALLER | 调用函数名 |
$CALLSTACK | “调用堆栈” |
$FUNCTION | 当前函数名 |
$PID | 进程 ID |
$PNAME | 进程名称 |
$TID | 线程 ID |
$TNAME | 线程名 |
断点说明:
(1)设置断点条件:i>6;且被命中次数>=2时才断住程序,所以第一次断住时i=8
(2)命中时,在Output窗口中打印当前函数名及线程ID(也可以打印相关变量的值,详见"When Breakpoint Is Hit"面板上的说明);在Command窗口中打印出堆栈信息
(3)若不想断住程序,可以把"When Breakpoint Is Hit"对话框中的"Continue execution"勾选上
注1:对于字符串的条件断点,不能写如下条件pStr=="Hello"(pStr为char*类型),应该写成:pStr[0]=='H' && pStr[1]=='e' && pStr[2]=='l' && pStr[3]=='l' && pStr[4]=='o' && pStr[5]=='\0'
vs2010及以上版本中,条件断点中可使用字符串:strcmp(pStr, "Hello")==0
支持的字符串函数有:strlen, wcslen, strnlen, wcsnlen, strcmp, wcscmp, _stricmp, _wcsicmp, strncmp, wcsncmp, _strnicmp, _wcsnicmp, strchr, wcschr, strstr, wcsstr.
ue的FString、FName的条件断点:
wcsstr((wchar_t*)MyString.Data.AllocatorInstance.Data, L"SearchSubString") // FString MyString strstr(((FNameEntry&)GNameBlocksDebug[MyFName.DisplayIndex.Value >> FNameDebugVisualizer::OffsetBits][FNameDebugVisualizer::EntryStride *(MyFName.DisplayIndex.Value & FNameDebugVisualizer::OffsetMask)]).AnsiName, "SearchSubString") // FName MyFName
strstr(((FNameEntry&)GNameBlocksDebug[MyFName.ComparisonIndex.Value >> FNameDebugVisualizer::OffsetBits][FNameDebugVisualizer::EntryStride *(MyFName.ComparisonIndex.Value & FNameDebugVisualizer::OffsetMask)]).AnsiName, "SearchSubString") // 新FName
注2:也可以创建自己的宏,具体方法:"Tools"-"Macros"-"Macro Explorer",然后在下图:MyMacros-Module1上右键快捷菜单中选择"New macro",
如ChangeExpression宏函数会在Output窗口的Debugger过滤器下打印出"Hello World",然后修改变量code的值为1000
编写自己的宏时,可以参考大量vs已有的宏(见:Samples节点下)
Public Module Module1 Function GetOutputWindowPane(ByVal Name As String, Optional ByVal show As Boolean = True) Dim window As Window Dim outputWindow As OutputWindow Dim outputWindowPane As OutputWindowPane window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput) If show Then window.Visible = show outputWindow = window.Object Try outputWindowPane = outputWindow.OutputWindowPanes.Item(Name) Catch ex As Exception outputWindowPane = outputWindow.OutputWindowPanes.Add(Name) End Try outputWindowPane.Activate() Return outputWindowPane End Function Sub ChangeExpression() Dim bppane As EnvDTE.OutputWindowPane
bppane = GetOutputWindowPane("Debugger") bppane.OutputString("Hello World") DTE.Debugger.ExecuteStatement("code = 1000;") End Sub End Module
注3:如果弹出以下框,表明vs没有开启macro权限(Tools -- Options... -- Environment -- Add-in/Macro Security中勾选“Allow macros to run”)
注4:2014.2月windows系统更新后,各个版本的vs的宏失效。
vs2005sp1补丁 vs2008sp1补丁 vs2010sp1补丁
19.在windows API上打断点
(1)例如:对SetWindowText打断点。首先当前程序字符集为未设置或多字节,则SetWindowTextA;为Unicode则为SetWindowTextW。下面以SetWindowTextA为例。
(2)调试运行程序断住后,打开Modules窗口可以看到所有已经加载的模块,找到windows API所在的模块,右击鼠标执行"Load Symbols From" - "Microsoft Symbol Servers"下载并加载对应模块的pdb
(3)新建一个Break At Function断点,填入:{,,user32.dll}_SetWindowTextA@8。可以看到,VS里面的符号跟windbg相比多了一些字符,其中‘_’表示stdcall类型,后面‘@8’表示所有参数的字节数的和。
有些函数Symbol Name与导出函数名可能不一致,例如GetDC(HWND),其Symbol Name为NtUserGetDC,最后断点应填入:{,,user32.dll}_NtUserGetDC@4
注:查找windows API符号名可以使用windbg的x命令或者使用pdb解析工具(symView)
也可以直接使用地址对windows API打断点(这种方式不需要符号的支持):如对GetDC打断点,可以用Dependency查看其在user32.dll中导出函数地址(Entry Point列):0x000172CC
然后在Modules窗口中获得user32.dll模块起始地址0x75840000,最后对两个值相加后的绝对地址处直接设置断点:{,,user32.dll}0x758572CC
通过以上方法也可以给malloc函数(vs2008 debug其dll为msvcr90d.dll,release的dll为msvcr90.dll;在UE4 vs2017的dll为ucrtbase.dll)打上断点
20. 异常(First-chance)时断住程序
在调试程序时,Output窗口有时会出现“First-chance exception in xxx.exe...”这样的信息。
一般来说,这是由于程序中发生了异常,被调试器捕获而产生的输出。
在调试器中运行程序时,如果程序产生异常,调试器会首先获得通知(即First-chance exception)。
若程序没有捕获该异常,则在结束进程之前,操作系统会再次通知调试器(即Second-chance exception,Last-chance exception)。
(1)在"Debug"-"Exceptions...",弹出如下对话框:点击Add按钮,新增一个int类型的C++ Exceptions异常,并勾选Thrown
(2)当int、int*、int&的异常被catch到时,会断住程序进入调试状态(注:以上void类型对应:char*、void*的异常)
21. 单步调试自动跳过不必进入的函数
注:仅适用于Native c++)
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0\NativeDE\StepOver] "1"="\\scope:CString.*\\:\\:.*=NoStepInto"
注1:如果是32位windows,删除上面路径中的Wow6432Node
注2:不进入任何CString的方法(前面的1表示优先级,该值越大优先级越高)
++++++++++++++++++++++++++++++
NoStepInto 不可进入匹配函数
StepInto 可进入匹配函数
特殊字符串:
\cid 代表一个C/C++标识符
\funct 代表一个C/C++函数名
\scope 代表一个函数的作用范围(命名空间+类名 如:ATL::CFoo::CBar::)
\anything 代表一个字符串
\oper 代表一个C/C++操作符
正则表达式:
\ 转义字符 如:要使用 “\” 本身, 则应该使用“\\”
\: 代表字符:
. 匹配任意字符
* 其左边的字符被匹配任意次(0次或多次)。如:be*匹配“b”,“be”或“bee”
.* 匹配0个或多个字符
更多例子:
例1:不进入重载操作符函数:
10 \scope:operator\oper:=NoStepInto
例2:除了CComBSTRs的非操作符函数,不进入任何ATL::开头的函数:
20 ATL\:\:CComBSTR::\funct:=StepInto
10 ATL\:\:.*=NoStepInto
例3:除了全局模版函数外,不进入任何模板函数:
20 \scope:\funct:=StepInto
10 .*[\<\>].NoStepInto
++++++++++++++++++++++++++++++
22. 使用OutputDebugString进行日志调试
(1)调试状态时,会将日志输出到Debug过滤器的Output窗口中
(2)非调试状态时,可采用DbgView.exe(DebugView)来捕捉程序日志
23. 使用autoexp.dat自定义调试时变量的显示格式
文件所在位置:Microsoft Visual Studio 9.0\Common7\Packages\Debugger\autoexp.dat
在autoexp.dat中的[Visualizer]域可以对各种类型变量的显示格式进行配置,来优化变量在调试时显示,提高效率。
注1:在vs中要让autoexp.dat生效需要去掉"Tools"-"Options..."对话框中,
"Debugging"-"General"-"Show raw structure of objects in variables windows"的勾选
注2:vs2012版本后,autoexp.dat被废弃,改用.natvis文件,该文件需要安装到"%USERPROFILE%\Documents\Visual Studio 2012\Visualizers"目录中
如UE4引擎源码中的%EngineDir%\Engine\Extras\VisualStudioDebugging\UE4.natvis
可以修改一下,来支持ULevel对象名称显示
<Type Name="ULevel"> <DisplayString>(Name={NamePrivate}-{OutPrivate->NamePrivate})</DisplayString> </Type>
(1) STL之string、vector、map
①原始显示结果:
②配置了autoexp.dat的显示结果:
-->对应的配置内容如下:
; std::string -- char std::basic_string<char,*>{ preview ( #if(($e._Myres) < ($e._BUF_SIZE)) ( [$e._Bx._Buf,s]) #else ( [$e._Bx._Ptr,s])) stringview ( #if(($e._Myres) < ($e._BUF_SIZE)) ( [$e._Bx._Buf,sb]) #else ( [$e._Bx._Ptr,sb])) children ( #if(($e._Myres) < ($e._BUF_SIZE)) ( #([actual members]: [$e,!] , #array( expr: $e._Bx._Buf[$i], size: $e._Mysize)) ) #else ( #([actual members]: [$e,!], #array( expr: $e._Bx._Ptr[$i], size: $e._Mysize)) ) ) } ;------------------------------------------------------------------------------ ; std::vector ;------------------------------------------------------------------------------ std::vector<*>{ children ( #array ( expr : ($e._Myfirst)[$i], size : $e._Mylast-$e._Myfirst ) ) preview ( #( "[", $e._Mylast - $e._Myfirst , "](", #array ( expr : ($e._Myfirst)[$i], size : $e._Mylast-$e._Myfirst ), ")" ) ) } ;------------------------------------------------------------------------------ ; std::map ;------------------------------------------------------------------------------ std::map<*>{ children ( #tree ( head : $e._Myhead->_Parent, skip : $e._Myhead, size : $e._Mysize, left : _Left, right : _Right ) : $e._Myval ) preview ( #( "[", $e._Mysize, "](", #tree ( head : $e._Myhead->_Parent, skip : $e._Myhead, size : $e._Mysize, left : _Left, right : _Right ) : $e._Myval, ")" ) ) }
(2) 自定义类MyArray
①原始显示结果:
②配置了autoexp.dat的显示结果:
-->对应的配置内容如下:
MyArray{ preview ( #( "[size is ", $c.m_nSize, "] m_pData is (", #array ( expr: ($c.m_pData)[$i], size: $c.m_nSize ), ")..." ) ) stringview ( #( "Hello MyArray!!!" ) ) children ( #( #array ( expr: ($c.m_pData)[$i], size: $c.m_nSize ) ) ) }
注1:双引号中字符串不能含有冒号,如:"[size is "不能写成"size: "
注2:多个类型使用 | 进行连接。如:MyArray|ArrayEx
注3:preview、stringview及children。对于不需要的部分可以不用定义,且三个部分没有先后顺序之分。
注4:格式的定义的最外层用大括号{},其中的每个部分使用小括号()。
注5:格式定义出错时,运行VS会弹出提示窗口,对于格式配置错误的类型,在调试期间无法正常显示。
注6:最外层的左边的大括号{必须紧挨着最后一个类型名,否则无论后面的格式正确与否,都无法正常显示。
注7:符号;为行注释符。
注8:一些字符串的语义
$c表示当前所定义数据结构的对象,#array表示用数组形式显示内容,$i表示数组中的每个元素的索引,$e表示数组中的每个元素的值
注9:array结构必须同时包含expr和size两个部分,缺少其中一个部分都将导致信息无法正确显示。
注10:可使用#switch、#if进行条件分支判断,要注意的是:#switch结构不能用于#array结构中,否则可能导致VS挂死。