C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > Visual Studio调试C++

Visual Studio调试C/C++教程指南

作者:-飞鹤-

VisualStudio是微软开发的一款集成开发环境软件,本文主要介绍了Visual Studio调试C/C++教程指南,熟悉地掌握基于VS的C/C++调试技术,可以大幅提升调试性能,感兴趣的可以了解一下

1. 前言

Visual Studio(VS)是微软开发的一款集成开发环境(IDE)软件,支持C/C++、C#、VB、Python等开发语言,开发桌面、Web等应用程序。VS功能极其强大,使用极其便利,用户数量最多,被誉为"宇宙第一IDE"。

熟悉地掌握基于VS的C/C++调试技术,可以大幅提升调试性能。随着VS版本的更新,其功能越来越强大,本文的内容是基于VS2019进行验证测试的,之前版本VS可能有少量特性不支持。

Visual Studio下载

2. 基础

2.1. 调试

代码调试主要指使用调试工具来检查和修复代码中的错误和问题。代码调试主要有运行调试、打印调试、内存分析、静态分析、性能分析等。

2.2. 符号文件

符号文件(Symbol File)是指在编译程序时生成的包含调试信息的文件。它们通常与可执行文件或动态链接库(DLL)配对存在,用于提供程序的调试信息。VC生成的符号文件为PDB(Program Database)文件。其中存储变量名、函数名、代码行号、类型信息和栈信息等。exe/dll与pdb文件是一一对应的。每次重新编译代码,都会生成新的pdb。

2.3. 调试器

Microsoft Visual C/C++的调试器名称叫做"Visual Studio Debugger"。在调试exe时,其会读取exe文件中记录的PDB路径信息(这个路径是开发电脑编译时生成的PDB路径),如果这个PDB路径不存在,那么调试器会在exe目录去找PDB,如果依然找不到PDB,则启用无PDB调试。无PDB调试只能查看汇编信息和寄存器信息。

调试方式

3. 本地调试

VS工程默认即为本地调试(Local Windows Debugger)。选定启动工程,按F5或通过菜单Debug->Start Debugging。

3.1. 远程调试

3.2.  附加调试

3.3. 外网调试

远程调试一般是针对局域网进行调试。但是有些时候,问题进程在外地,出差不方便或成本太高,非常需要一种能够穿透广域网进行调试的方法。最简单的方法是使用VPN将目标电脑远程连接到开发电脑,这样目标电脑和开发电脑就相当于处在同一个局域网,就可以使用普通的远程调试来进行外网目标电脑调试。

3.4. DLL调试

在DLL工程的属性中Debugging的Command中选择要执行的exe,然后在dll中设置相关断点。再按F5调试,即会中断在DLL工程的断点处。

4. 断点调试

int 3是x86-64架构CPU上的中断指令,用于在程序执行过程中触发软件中断。VS在给代码添加断点时,就是将指定行对应的代码修改为int 3指令,并且调试器接管代码。继续单步执行时,会还原int 3覆盖的代码。

4.1. 断点类型

4.1.1. 普通断点

在代码指定行按F9或右键菜单Breakpoint->Insert Breakpoint设置普通断点。

4.1.2. 条件断点

在断点上右键选择Conditions。

设置 i == 5, 然后点击Close。按F5执行,代码会停止在断点处,此时i==5。

指定当前断点触发指定次数时中断下来。

当前断点运行在指定线程时才中断下来。

4.1.3. 行为断点

行为断点(Actions Breakpoint),也称Tracepoint,因为断点触发时会在Output窗口打印信息。Continue Code Execution勾选表示不停止在断点处,如果选空表示停止在断点处。

Output窗口显示:The value of z is 0x0000001e. 0x2C74

$PID,是伪指令。可用的伪指令如下:

4.1.4. 数据断点

数据断点(Data Breakpoint),当然变量地址的内容发生改变时,即中断下来。如下图,Address编辑框可以直接填写变量的地址,也可以使用取地址符来获取变量的地址。数据断点只能针对有效数据设置断点,并且只能在已经开始调试之后,在Breakpoint窗口的菜单New->Data Breakpoint来设置。

4.1.5. 系统函数断点

例如想在CreateFile函数中下断点。可以使用dhb.exe在相应的

dbh.exe -s:srv*C:\Symbols*Symbol information -d C:\Windows\SysWOW64\kernel32.dll enum *CreateFile*

然后Breakpoints->New->Function Breakpoint:

运行就会断在系统API函数处,通过调用栈查找到调用的函数。

4.1.6. 软件断点

除了通过VS来添加断点外,我们也可以在代码中主动添加软件断点__debugbreak()/DebugBreak函数,或是断言ASSERT(0)。

__debugbreak()/DebugBreak是代码到此处立即中断,而断言则是根据参数逻辑值来决定是否中断。软件断点主要用来在代码潜在的异常出现时产生中断提示开发者。

4.2. 调试行为

4.2.1. 基本行为

工具栏或Debug菜单或鼠标右键都有调试行为的选项。

4.2.2. 高级行为

5. 调试窗口

5.1. Output 

Output Debug窗口主要输出调试过程的信息,主要包括:

其中Program Output是代码运行时输出的信息,主要通过Trace函数或OutputDebugString函数来将信息输出到Output窗口。如果是非调试状态下,OutputDebugString的输出信息则需要DbgView工具来接收并显示。

5.2. Locals

Locals窗口显示调试时当前执行代码所在函数所在的栈的局部变量的值和类型。

5.3. Autos

Autos窗口显示调试时当前执行代码所在函数所在的栈的上下文栈变量值和类型。

5.4. Watch

Watch窗口总共有4个,可以在菜单Debug->Windows->Wartch中选择。Watch窗口可以显示当前栈内存的局部变量,全局变量等,支持16进制形式显示,并支持实时修改变量值。

支持简单的表达式显示,如x+y, sizeof(x)等。

支持格式化显示,如(int*)(szBuff),4将Buff转换为int*,再格式化为4个元素显示。

Watch窗口还支持显示伪变量,如:

5.5. Memory

Memory窗口是用来显示地址对应的内容,Memory窗口有4个从菜单Debug->Windows->Memory中选择。

Adress编辑框填写变量地址,Columns选择每行显示的内容数量。

5.6. QuickWatch

QuickWatch窗口是快捷观察、修改变量的窗口。

5.7. Disassembly

参数传递、函数返回等一些复杂的语法形式,要想理解其深层执行逻辑,就需要单步执行汇编代码来观察其隐藏逻辑细节。

5.8. Registers

寄存器窗口,显示当线程的寄存器信息。

5.9. Call Stack

函数调用栈窗口,显示当前线程的函数栈调用情况。可以通过Threads窗口选定当前线程。

5.10. Immediate Window

Debug->Windows->Immediate窗口。立即窗口主要用来查看、修改变量,执行函数,表达式等。

6. 多线程

6.1. Threads

Threads是显示当前进程的所有运行的线程信息的窗口。双击线程所在行,即将当前代码窗口、调用栈窗口等相关窗口的内容更新为选定线程。

当进程有多个工作者线程时,且只想调试其中一个线程时,可以将不关心的线程使用Freeze冰冻起来。冰冻的线程在按F5运行时,依然冰冻着不运行。使用Thaw解冻线程,线程将恢复为正常可以调试的状态。

6.2. 条件断点

在多线程中,根据线程信息来设置相应的断点来观察期望的变量信息。

6.3. Parallel Watch

Debug->Window->Parallel Watch可以显示几个线程同时调用的函数变量的情况,更方便地调试多线程。

6.4. 线程结构图

打开Debug->Windows->Parallel Stack窗口,会显示所有线程的栈,并显示当前线程栈。相比Threads窗口更直观。

7. 参数配置

7.1. 增量链接

增量链接在Debug下默认打开,在Release下默认关闭。通过Linker->General->Enable Incremental Linking来开启和关闭。增量链接还需要配置C/C++->General->Debug Information Format->Program Database for Edit And Continue (/ZI)。

增量链接的用处是在断点单步调试代码的时候,编辑代码,然后继续单步执行时,VS会自动增量链接,不用重新编译源代码,然后继续单步执行代码进行调试。

增量链接是调试时使用,增量编译则是编译时只编译修改的源文件,两者不相同。

7.2. 优化级别

Debug版本下,默认关闭代码优化,此时代码的调试信息与代码源文件是一一对应,调试更方便。Release版本下,默认使用O2优化,此时代码优化程度非常大,调试信息与代码源文件无法一一对应。如果想调试Release版本,则需要关闭优化。

7.3 宏展开

一些复杂的宏,非常难以调试。如何查看宏预处理之后展开形成的代码呢?C/C++->Preporcessor->Preprocess to a File配置默认是关闭的,打开此配置为Yes(/P)表示将生成预处理后的文件,文件与源文件同名,后缀为.i。在.i文件中可以查看所有宏展开的结果,以及其他预处理的结果。

7.4. 显示链接细节

VS 链接器默认只显示一些关键的链接信息。可以通过Linker->General->Show Progress配置Display all progress messages(/VERBOSE)来显示详细的链接信息,可以更方便地分析一些链接异常的错误。

7.5. 警告

7.5.1. 编译警告

为了提升代码的可靠性,警告也需要认真对等。

7.5.2. 链接警告

Linker->All Options->Treat Linker Warning As Errors,根据需要将警告作为错误对待。

Dump调试

8.概念

Dump文件(Dump File),也叫转储文件,以.DMP为文件后缀。dump文件是进程在内存中的镜像文件,通过转换然后存储成以.DMP后缀的文件。dump文件根据存储时的选项不同,会生成不同大小的文件,其中记录信息也自然有所不同。

8.1. 转储文件生成

8.2. 调试转储文件

需要通过Set symbol paths设置符号文件路径,也即PDB文件路径。然后点击Debug with Native Only,代码即会中断在出错的地方,通过Call Stack窗口查看相关信息。

变量溢出

变量内存溢出分为上溢(Overflow)和下溢(Underflow)。内存溢出导致的异常是C++代码最为难以调试的Bug之一,因为内存溢出导致的异常往往不会立即出现,而是展现在后面的函数中。因为内存溢出可能覆盖了后面的代码,导致后面的代码执行异常。

AddressSanitizer (ASan) 是一种编译器和运行时技术,它通过和编译器配合,通过插桩技术,向内存的前后添加标识,来识别运行时产生的上溢和下溢异常。其对代码执行性能影响较大,VS中默认是关闭的,需要手动打开。

ASan可以识别栈内存、堆内存、静态内存的溢出,另外也能检测重复释放,释放后使用内存。

通过C/C++->General->Enable Address Sanitizer使能,此配置与编辑并继续、增量链接和/RTC不兼容。

更多信息:AddressSanitizer | Microsoft Learn

资源泄露

电脑上的内核资源是有限的,申请使用完了,就要释放,否则资源可能不够用,导致后续申请失败而运行异常。所以资源泄露也是比较严重的问题,需要解决。

10. 内存泄露

在调试运行进程时,退出调试后,可能会在VS的Output窗口中显示如下信息:

这就是内存泄露的提示信息。上面的信息有时会指出内存泄露的位置,有的时候指出的位置却不准确。第三方工具 Visual Leak Detector,可以用来检测内存泄露,其主要是通过重载内存申请和释放函数,然后记录内存申请的地址和详细源文件行号,最后退出时检测所有申请的地址是否释放,如果没有释放,则打印出内存申请的信息。

Visual Leak Detector的使用非常简单,下载安装,然后在工程的启动接口文件中添加#include <vld.h>即可。

详细信息见:Home · KindDragon/vld Wiki · GitHub

10.1. 句柄泄露

除了内存泄露外,句柄也是容易泄露的资源。

11. 静态调试

相比于动态调试(在代码运行时调试),静态调试是在代码编译时来调试,其效率更高。

11.1. 静态断言

静态断言static_assert是C++11中引入的新语法,可以在编译时进行一些判断,并打印相应信息。

static_assert(sizeof(int) == 4, "int must be 4 bytes"); // 检查 int 类型是否为 4 字节

template <typename T>
void process(T value) {
    static_assert(std::is_integral<T>::value, "T must be an integral type"); // 检查模板类型是否为整数类型
}

11.2. 静态分析

VS自带的静态分析,功能非常强大,能够发现很多隐藏的问题。开启静态分析之后,会在编译期间进行静态分析代码,所以会加大编译时间。建议定期开启静态分析检查代码,并修复相关问题。

另外还可以选择相应的静态分析规则:

12. 性能调试

VS提供了性能度量工具(Performance Profiler),帮助开发者优化代码和提高应用程序性能。Profiler的主要功能是诊断内存、CPU 使用率。Debug下的性能分析,因为有很多调试及优化的影响,所以很不准确,建议是在Release版本下进行性能调试分析。

通过Debug->Performance Profiler。C/C++更多用来分析CPU和Memory。

点击Start之后,目标程序开始执行,并开始监控性能,并通过Take Snapshot给当前进程拍摄快照。

12.1. 内存分析

通过Stop Collection或等进程结束,会显示如下信息。每个快照会保存详细的进程堆栈信息。

点击上面的内存分配次数、分配次数增量等数据,可以获取详细信息:

双击相应对象分配行,可以得到详细的对象信息包括调用堆栈信息,通过调用栈可以查看相应的代码:

12.2. CPU分析

内存分类统计:

内存分线程统计:

双击函数名,可以详细函数占比分析:

12.3. 性能提示

调试器在断点或单步执行操作中停止执行时,中断与上一个断点之间经过的时间会显示为在编辑器窗口中的提示。

更多信息见:在 Visual Studio 中度量性能 - Visual Studio (Windows) | Microsoft Learn

协同调试

VS提供了多人协作进行调试的功能,使用非常简单。

协作会话启动完成后,会显示如下信息。默认已经将协作邀请链接进行了拷贝。如果协作方只需要查看代码,不需要调试,可以点击Make read-only。

将协作邀请链接发给协作方并打开,弹出如下窗口。

打开VS Code,主持人开始调试,协作方也可以通过VS Code查看调试信息。

通过VS的File->Join Live Share Session来加入协作调试。

还可以进行协作的管理,以及协作时聊天。

14. 调试技巧

14.1. 小技巧

调试器也能在 Watch 窗口中显示格式化的内存值,高达 64 个字节。你能用下面的说明符在表达式(变量或内存地址)后来格式化数据。

14.2. 管理员权限调试

通过Linker->Manifest File->UAC Execution Level->requireAdministrator,启用管理员权限,然后启用调试,则被调试的进程也将获取管理员权限。

14.3. 生成调用栈信息

void printStackTrace() 
{
    HANDLE process = GetCurrentProcess();
    SymInitialize(process, nullptr, TRUE);

    void* stack[100];
    WORD frames = CaptureStackBackTrace(0, 100, stack, nullptr);

    SYMBOL_INFO* symbol = (SYMBOL_INFO*)malloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char));
    symbol->MaxNameLen = 255;
    symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

    for (WORD i = 0; i < frames; ++i) {
        SymFromAddr(process, (DWORD64)(stack[i]), 0, symbol);

        printf("[%d] %s\n", i, symbol->Name);
    }

    free(symbol);
}

14.4. 内存耗尽

Windows下的32位应用程序,只能申请2GB内存。所以在申请过的总内存过大时,可能超过2GB,导致程序异常。可以使用下面代码提示总内存过大的提示。

void NoMoreMemory()
{
    LPVOID lpMsgBuf;
    if (!FormatMessage( 
    FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
    NULL,
    0x00000008, // Memory error	
     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
    (LPTSTR) &lpMsgBuf,
    0,
    NULL ))
    {
    return;
    }
    
    // Display the string.
    MessageBox( NULL, (LPCTSTR)lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION );
    abort();
}

// Add to this function to the entry of EXE/DLL.
set_new_handler(NoMoreMemory);

14.5. 未初始化异常

14.5.1. Debug

VS在Debug下为了方便用户调试,编译器会强制将未初始化的变量强制赋值指定值做标记。

栈变量强制赋值0xCCCCCCCC,堆内存强制赋值为0xCDCDCDCD。

14.5.2. Release

在VS下,C/C++中的变量编译器不会对变量进行初始化。栈变量和堆内存都是随机的。养成变量初始化的习惯是提升代码质量的好习惯。

14.6. 调试运行时库代码

VS运行时库,有一些提供了代码,有一些没有提供。如CString的GetLength()函数。VS2012及之前的版本默认不会进入库函数,VS2015及之后版本默认在使用Step Into时会进入库函数。如果无法进入库函数,可以进入汇编调试,然后再Step Into就可以进入库函数了。

14.7. Debug和Release的差异

编译优化:

调试信息:

运行时检查:

断言和异常处理:

编译器定义宏:

编译器优化标志:

链接器优化:

运行时库:

14.8. 构建事件

有时需要在编译前后附加一些操作来简单调试,就可以使用编译事件配置,选择相应的构建事件然后配置命令行来执行需要附加的操作。

14.9. 日志

日志是调试工具的重要手段,除了使用Trace和OutputDebugString之外,还可以使用自定义的日志函数。

14.10. 调试窗口

VS自带的Spy++可以用来查看窗口信息,还可以用来监控窗口消息。

14.11. 查看DLL接口

dumpbin是VS自带的命令行工具,可以用来查看DLL的接口函数。通过Tools->Command Line来使用。

这是一个命令行工具,可以用于查看 DLL 文件的结构和内容。常用命令:

15. 总结

调试是手段,不是目的,不能为了调试而调试。目的是更高效地开发,为此,减少问题,减少调试才是最重要的。

到此这篇关于Visual Studio调试C/C++教程指南的文章就介绍到这了,更多相关Visual Studio调试C++内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文