2. WinDbg 主要功能

总目录

1. WinDbg概述
2. WinDbg主要功能
3. WinDbg程序调试示例
4. CPU寄存器及指令系统
5. CPU保护模式概述
6. 汇编语言不等于CPU指令
7. 用WinDbg观察托管程序架构
8. Windows PE/COFF文件格式简述
9. 让WinDbg自动打开DotNet Runtime源程序
10. WinDbg综合实战

前言

上一篇《1. WinDbg概述》中,我们通过示例介绍了WinDbg的下载、安装,介绍了如何查找命令,如何通过帮助系统学习命令的使用方法,以及如何加载WinDbg扩展。本篇文章,我们将概要介绍WinDbg的主要功能。

我们可以将WinDbg理解成Windows Application Debugger,即微软Windows操作系统应用程序调试器。

WinDbg是微软为其自己的程序员开发的工具,其前身可追溯到古老DOS年代的Debug.exe。我本人接触Debug在1992年,它是打包在微软的Dos 5.0之中的。当时的Debug程序功能还十分简陋,主要的Debug命令及其功能包括:

  1. 显示和修改寄存器内容的命令R
    功能:用于查看和修改CPU内部寄存器的值,包括标志位状态。
    格式:
    -R:显示CPU内部所有寄存器内容和标志位状态。
    -R 寄存器名:显示和修改指定寄存器的内容。
    -RF:显示和修改标志位状态。
  2. 显示内存单元内容的命令D
    功能:用于查看内存中的内容。
    格式:
    -D[address]:从指定地址开始连续显示内存单元的内容。
    -D[range]:显示指定范围内的内存单元内容。
  3. 修改内存单元内容的命令E
    功能:用于修改内存中的内容。
    格式:
    -E address [list]:用给定的内容表来替代指定范围的存储单元内容。
    -E address:逐个单元相继地修改内存内容。
  4. 汇编命令A
    功能:允许用户键入汇编语言语句,并将它们汇编成机器代码,存放在指定地址的内存中。
    格式:-A[address]
  5. 反汇编命令U
    功能:将内存中的机器指令翻译成汇编指令。
    格式:
    -U[address]:从指定地址开始反汇编32个字节。
    -U[range]:对指定范围内的存储单元进行反汇编。
  6. 执行命令G
    功能:从指定地址开始执行程序,直到遇到断点或程序结束。
    格式:-G[=address1][address2[address3…]]
  7. 跟踪命令T
    功能:逐条指令执行程序,并在每条指令执行后停下来,显示寄存器内容和状态值。
    格式:
    -T:从当前CS:IP开始执行一条指令。
    -T[=n]:从指定地址开始执行n条指令。

显然,经过了40多年的迭代,WinDbg的功能要远超只有十几K字节的Debug了,但很多后续功能只是锦上添花,其核心功能依旧离不开显示与修改寄存器、显示与修改内存、汇编与反汇编代码、汇编语言调试(单步、断点等) 这几个方面。

当然,随着应用程序越来越复杂,尤其是操作系统越来越复杂,WinDbg确实新增了许多功能,下面举两个例子,以便使大家对所谓的新增功能有一个初步直观的感受。

第一个例子用WinDbg查看notepad程序,第二个例子用WinDbg调试.NET程序。

notepad.exe示例

查询可执行文件映像

Windows可执行文件又称为映像文件,该文件被双击后,会被Windows操作系统加载到内存中,然后由操作系统引导该程序得以被执行。此处我们以Windows 11自带的32位版记事本为例(路径:C:\Windows\SysWOW64\notepad.exe),使用WinDbg将其打开并运行以后,我们可以使用lm命令(Loaded module)去查看 操作系统为了运行notepad.exe都加载了哪些模块:

0:007> lm
start    end        module name
001b0000 00201000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\2B40059F9CBA6BEC6A7041000A7683421\notepad.pdb
63170000 6326e000   textinputframework   (deferred)             
63270000 632c4000   oleacc     (deferred)             
632d0000 6336c000   efswrt     (deferred)             
63370000 63405000   TextShaping   (deferred)             
64220000 6440a000   twinapi_appcore   (deferred)             
67680000 678a7000   COMCTL32   (deferred)             
69b70000 69bf1000   uxtheme    (deferred)             
71a40000 71b07000   wintypes   (deferred)             
72fc0000 736b1000   windows_storage   (deferred)             
74e00000 74e13000   kernel_appcore   (deferred)             
759d0000 75a52000   clbcatq    (deferred)             
75be0000 75cd0000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\wkernel32.pdb\3B3998558694D03EC4EDD70D3848DD371\wkernel32.pdb
75cd0000 75d56000   sechost    (deferred)             
75d80000 75e1c000   OLEAUT32   (deferred)             
75e20000 75f32000   ucrtbase   (deferred)             
75f60000 765fd000   SHELL32    (deferred)             
76aa0000 76aba000   win32u     (deferred)             
76ac0000 76b84000   msvcrt     (deferred)             
76b90000 76c09000   msvcp_win   (deferred)             
76c10000 76dba000   USER32     (deferred)             
76dc0000 76dda000   bcrypt     (deferred)             
76de0000 77059000   KERNELBASE   (deferred)             
77060000 77160000   MSCTF      (deferred)             
771d0000 77233000   bcryptPrimitives   (deferred)             
77630000 776af000   advapi32   (deferred)             
776b0000 77792000   gdi32full   (deferred)             
777a0000 77862000   shcore     (deferred)             
77910000 779ca000   RPCRT4     (deferred)             
779d0000 77a1b000   shlwapi    (deferred)             
77a20000 77a45000   IMM32      (deferred)             
77a50000 77a73000   GDI32      (deferred)             
77ae0000 77d5c000   combase    (private pdb symbols)  C:\ProgramData\Dbg\sym\combase.pdb\2EEF30B58FF90662AD716E2D8CEFA54A1\combase.pdb
77d70000 77f22000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\wntdll.pdb\16D25128F0923461692F09D5A2DA14291\wntdll.pdb

通过简单一个命令,我们就知道了:原来为了能让notepad.exe在64位Windows环境下运行,Windows除了加载notepad.exe以外,还需要加载其他33个dll文件(这些dll基本都是Windows操作系统带的动态链接库),另外我们还可以知道,notepad.exe的映像文件被操作系统加载到了虚拟内存001b0000 ~ 00201000区域。那么,整个区域一共占多少字节?此时我们可以将WinDbg当作计算机用,只需通过问号操作符,后面加上表达式,就可以计算:

0:007> ? 00201000-001b0000
Evaluate expression: 331776 = 00051000

也就是说,十六进制的结束地址00201000减去起始001b0000,结果是十进制331776字节,转换为十六进制是51000h字节。

0:007> lmDvm notepad
Browse full module list
start    end        module name
001b0000 00201000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\2B40059F9CBA6BEC6A7041000A7683421\notepad.pdb
    Loaded symbol image file: C:\Windows\SysWOW64\notepad.exe
    Image path: notepad.exe
    Image name: notepad.exe
    Browse all global symbols  functions  data
    Image was built with /Brepro flag.
    Timestamp:        51680EDC (This is a reproducible build file hash, not a timestamp)
    CheckSum:         0005868F
    ImageSize:        00051000
    File version:     10.0.22621.3672
    Product version:  10.0.22621.3672
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    Information from resource tables:
        CompanyName:      Microsoft Corporation
        ProductName:      Microsoft® Windows® Operating System
        InternalName:     Notepad
        OriginalFilename: NOTEPAD.EXE
        ProductVersion:   10.0.22621.3672
        FileVersion:      10.0.22621.3672 (WinBuild.160101.0800)
        FileDescription:  Notepad
        LegalCopyright:   © Microsoft Corporation. All rights reserved.

前面说过,调试器可以使用d命令显示内存,而db就是以字节方式显示内存(display memory in byte),我们就用db命令看看notepad.exe加载到内存以后长什么模样:

0:007> db 001b0000 00201000
001b0000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
001b0010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
001b0020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
001b0030  00 00 00 00 00 00 00 00-00 00 00 00 00 01 00 00  ................
001b0040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
001b0050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
001b0060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS 
001b0070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......
......

原来exe文件就长这个模样!

想知道Windows PE/COFF映像文件的数据结构?这些结构都定义在ntdll中,可以使用以下命令查看PE文件包含哪些结构体:

0:000> dt ntdll!_Image*
          ntdll!_IMAGE_NT_HEADERS64
          ntdll!_IMAGE_DOS_HEADER
          ntdll!_IMAGE_FILE_HEADER
          ntdll!_IMAGE_OPTIONAL_HEADER64
          ntdll!_IMAGE_RUNTIME_FUNCTION_ENTRY
          ntdll!_IMAGE_DATA_DIRECTORY

使用dt命令就行:

0:007> dt ntdll!_IMAGE_DOS_HEADER 001b0000
   +0x000 e_magic          : 0x5a4d
   +0x002 e_cblp           : 0x90
   +0x004 e_cp             : 3
   +0x006 e_crlc           : 0
   +0x008 e_cparhdr        : 4
   +0x00a e_minalloc       : 0
   +0x00c e_maxalloc       : 0xffff
   +0x00e e_ss             : 0
   +0x010 e_sp             : 0xb8
   +0x012 e_csum           : 0
   +0x014 e_ip             : 0
   +0x016 e_cs             : 0
   +0x018 e_lfarlc         : 0x40
   +0x01a e_ovno           : 0
   +0x01c e_res            : [4] 0
   +0x024 e_oemid          : 0
   +0x026 e_oeminfo        : 0
   +0x028 e_res2           : [10] 0
   +0x03c e_lfanew         : 0n256

从以上列表得知,最开始的一个字(5A4D)是一个e_magic字段,或者理解为类型签名。

屏幕显示内容太多了,想清屏?输入 .cls并会出就可以了。

另外,也可以使用 !dh 扩展命令,显示notepad.exe的更多信息:

0:007> !dh notepad

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
     14C machine (i386)
       6 number of sections
51680EDC time date stamp Fri Apr 12 21:40:44 2013

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
     102 characteristics
            Executable
            32 bit word machine

OPTIONAL HEADER VALUES
     10B magic #
   14.30 linker version
   27A00 size of code
   25800 size of initialized data
       0 size of uninitialized data
   26E90 address of entry point
    1000 base of code
         ----- new -----
001b0000 image base
    1000 section alignment
     200 file alignment
       2 subsystem (Windows GUI)
   10.00 operating system version
   10.00 image version
   10.00 subsystem version
   51000 size of image
     400 size of headers
   5868F checksum
00040000 size of stack reserve
00011000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
    C140  DLL characteristics
            Dynamic base
            NX compatible
            Guard
            Terminal server aware
       0 [       0] address [size] of Export Directory
   2B5AC [     3E8] address [size] of Import Directory
   2F000 [   1E1D0] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
       0 [       0] address [size] of Security Directory
   4E000 [    25A8] address [size] of Base Relocation Directory
    5210 [      54] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
    1000 [      C0] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
   2B000 [     5A4] address [size] of Import Address Table Directory
   284B4 [      E0] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory


SECTION HEADER #1
   .text name
   27880 virtual size
    1000 virtual address
   27A00 size of raw data
     400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read


Debug Directories(3)
	Type       Size     Address  Pointer
	cv           24        59e0     4de0	Format: RSDS, guid, 1, notepad.pdb
	(   13)     440        5a04     4e04
	(   16)      24        5e44     5244

SECTION HEADER #2
   .data name
    1EE0 virtual size
   29000 virtual address
     A00 size of raw data
   27E00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #3
  .idata name
    2C7A virtual size
   2B000 virtual address
    2E00 size of raw data
   28800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #4
  .didat name
      7C virtual size
   2E000 virtual address
     200 size of raw data
   2B600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #5
   .rsrc name
   1E1D0 virtual size
   2F000 virtual address
   1E200 size of raw data
   2B800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #6
  .reloc name
    25A8 virtual size
   4E000 virtual address
    2600 size of raw data
   49A00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

当然,以上信息有可能看不懂,其实它讲的就是Windows PE文件(Windows映像文件)格式信息,如果对PE格式了解了以后,再看这些内容就会觉得非常亲切。但最起码,从上述 32 bit word machine 可以知道,这个notepad.exe是32位程序,另外从下面四行信息也可以知道,系统预留了40000h的栈空间,预留了100000h的堆空间,而目前真正已经提交的栈空间是11000h,堆空间是1000h。

00040000 size of stack reserve
00011000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit

如果我们想看看notepad.exe中都定义了哪些方法,可以使用x命令

0:007> x notepad!*
001b6620          notepad!`dynamic initializer for 'dismissString'' (void)
001bcf4c          notepad!ShowOpenSaveDialog (void)
001d4f99          notepad!StringLengthWorkerW (void)
001d70d1          notepad!find_pe_section (void)......

如果我们想看看上述第二个方法是如何实现的,可以用u命令:

0:007> uf notepad!ShowOpenSaveDialog
notepad!ShowOpenSaveDialog:
001bcf4c 8bff            mov     edi,edi
001bcf4e 55              push    ebp
001bcf4f 8bec            mov     ebp,esp
001bcf51 83ec18          sub     esp,18h
001bcf54 53              push    ebx
001bcf55 56              push    esi
001bcf56 57              push    edi
001bcf57 ff7508          push    dword ptr [ebp+8]
001bcf5a 8bfa            mov     edi,edx
001bcf5c 8bd9            mov     ebx,ecx
001bcf5e 57              push    edi
001bcf5f 8b07            mov     eax,dword ptr [edi]
001bcf61 8b7044          mov     esi,dword ptr [eax+44h]
001bcf64 8bce            mov     ecx,esi
001bcf66 ff15a4b51d00    call    dword ptr [notepad!__guard_check_icall_fptr (001db5a4)]
001bcf6c ffd6            call    esi
001bcf6e 8bf0            mov     esi,eax
001bcf70 85f6            test    esi,esi
001bcf72 0f88ee000000    js      notepad!ShowOpenSaveDialog+0x11a (001bd066)
001bcf78 8b07            mov     eax,dword ptr [edi]......

下面,我们再切换到主线程(0号线程),然后看一下主线程栈回朔:

0:007> ~0s
eax=00000000 ebx=01b50695 ecx=00000000 edx=00000000 esi=000dfee4 edi=027268a4
eip=76aa10dc esp=000dfe88 ebp=000dfec0 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
win32u!NtUserGetMessage+0xc:
76aa10dc c21000          ret     10h
0:000> k
 # ChildEBP RetAddr      
00 000dfe84 76c40900     win32u!NtUserGetMessage+0xc
01 000dfec0 001befdd     USER32!GetMessageW+0x30
02 000dff28 001d6e00     notepad!wWinMain+0x14a
03 000dff74 75bf7ba9     notepad!__scrt_common_main_seh+0xf8
04 000dff84 77ddc10b     KERNEL32!BaseThreadInitThunk+0x19
05 000dffdc 77ddc08f     ntdll!__RtlUserThreadStart+0x2b
06 000dffec 00000000     ntdll!_RtlUserThreadStart+0x1b

从栈顶数据win32u!NtUserGetMessage+0xc我们知道,此时notepad正在等待用户消息。

如果想看看整个进程的用户空间内存布局,可以使用 !address扩展命令,如果仅想知道内存的综合信息,也可以用 !address -summary:

0:007> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                     76          775db000 (   1.865 GB)           93.26%
MappedFile                               64           5926000 (  89.148 MB)  64.56%    4.35%
Image                                   154           2809000 (  40.035 MB)  28.99%    1.95%
<unknown>                                57            511000 (   5.066 MB)   3.67%    0.25%
Stack                                    24            200000 (   2.000 MB)   1.45%    0.10%
Heap                                     10            187000 (   1.527 MB)   1.11%    0.07%
Other                                     6             2d000 ( 180.000 kB)   0.13%    0.01%
TEB                                       8             1e000 ( 120.000 kB)   0.08%    0.01%
PEB                                       1              3000 (  12.000 kB)   0.01%    0.00%......

如果不想继续调试了,只需在命令行输入qd并回车即可。

以上,通过notepad示例,我们演示了调试普通Windows应用程序常用的一些命令。下面再通过一个C#示例,演示一下如何用WinDbg调试托管程序。

示例代码

示例代码使用C#语言,.NET 8.0框架。代码注释说明语句目的:

//file: Core.dll 使用.NET8.0框架,控制台应用程序, AnyCPU
using System.Diagnostics;
namespace BasicGrammar;

class Program
{
    static void Main()
    {
        Debugger.Break();//目的:在程序入口点下断点以方便调试
        int y = 100;     //目的:观察int类型数值的内存存储
        SimpleStr simpleStr = new SimpleStr();
        Console.WriteLine(y); //目的:观察int型参数的传递
        StrEmployee strEmployee = new StrEmployee();
        ClassEmployee classEmployee = new ClassEmployee();
        strEmployee.Name = "Struct Employee Name";  //目的:观察如何给值类型中的引用类型成员赋值
        classEmployee.Name = "Class Employee Name"; //目的:观察如何给引用类型中的引用类型成员赋值
        y = simpleStr.x + 1; //目的:观察如何获取值类型中值类型成员的值
        y = strEmployee.Age + 2; //目的:观察如何获取值类型中值类型成员的值
        y = classEmployee.Age + 3; //目的:观察如何获取引用类型中值类型成员的值
        ShowClassEmpName(classEmployee); //目的:观察如何传递引用类型参数
        ShowStructEmpName(strEmployee);  //目的:观察如何传递值类型参数给方法
    }

    public static void ShowStructEmpName(StrEmployee employee)
    {
        Console.WriteLine(employee.Name);
    }

    public static void ShowClassEmpName(ClassEmployee classEmployee)
    {  
        Console.WriteLine(classEmployee.Name); 
    }

    //struct定义值类型,且成员也是值类型
    public struct SimpleStr
    {
        public int x;
    }

    //struct定义值类型,但其中既有值类型(int)成员,也有引用类型(string)成员
    public struct StrEmployee
    {
        public string Name;
        public int Age;
    }

    //引用类型class
    public class ClassEmployee
    {
        public string? Name;
        public int Age;
    }
}

程序运行结果如下:

100
Class Employee Name
Struct Employee Name

加载Core.exe

1、在Windows开始菜单找到WinDbg打开。
2、选择** Start debugging -> Launch executable (advanced) -> 选择目标文件 Core.exe打开。**
主窗口显示如下信息:

......
Microsoft (R) Windows Debugger Version 10.0.27553.1004 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: E:\test\a\Core\bin\Debug\net8.0\Core.exe

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
Deferred                                       srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: srv*;srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is: 
......

ModLoad: 00007ff7`144e0000 00007ff7`14509000   apphost.exe
ModLoad: 00007ffe`e4bd0000 00007ffe`e4de7000   ntdll.dll
ModLoad: 00007ffe`e3390000 00007ffe`e3454000   C:\Windows\System32\KERNEL32.DLL
ModLoad: 00007ffe`e21b0000 00007ffe`e255c000   C:\Windows\System32\KERNELBASE.dll
ModLoad: 00007ffe`e4760000 00007ffe`e490e000   C:\Windows\System32\USER32.dll
ModLoad: 00007ffe`e26b0000 00007ffe`e26d6000   C:\Windows\System32\win32u.dll
ModLoad: 00007ffe`e2e20000 00007ffe`e2e49000   C:\Windows\System32\GDI32.dll
ModLoad: 00007ffe`e27f0000 00007ffe`e2909000   C:\Windows\System32\gdi32full.dll
ModLoad: 00007ffe`e26e0000 00007ffe`e277a000   C:\Windows\System32\msvcp_win.dll
ModLoad: 00007ffe`e2590000 00007ffe`e26a1000   C:\Windows\System32\ucrtbase.dll
ModLoad: 00007ffe`e3a20000 00007ffe`e427c000   C:\Windows\System32\SHELL32.dll
ModLoad: 00007ffe`e4ad0000 00007ffe`e4b82000   C:\Windows\System32\ADVAPI32.dll
ModLoad: 00007ffe`e3460000 00007ffe`e3507000   C:\Windows\System32\msvcrt.dll
ModLoad: 00007ffe`e2ed0000 00007ffe`e2f7a000   C:\Windows\System32\sechost.dll
ModLoad: 00007ffe`e2560000 00007ffe`e2588000   C:\Windows\System32\bcrypt.dll
ModLoad: 00007ffe`e4470000 00007ffe`e4585000   C:\Windows\System32\RPCRT4.dll
(6300.6904): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00007ffe`e4cabed4 cc              int     3

以上输出,是WinDbg其中过程中给用户提供的信息,主要包括:如WinDbg版本,符号服务器设置已经dll模块的加载顺序等。
最后三行是说程序加载过程中执行到ntdll模块的LdrpDoDebuggerBreak函数距离起始地址偏移0x30的位置时,遇到了断点指令int 3,此指令的虚拟地址是:00007ffe`e4cabed4。

备注:ntdll在加载应用程序时,如果检测到有调试器附加,则会跳转到包含这个int 3指令程序分支,及时将启动过程中断下来。

输入k命令查看当前(0号线程)的线程栈:

0:000> k
 # Child-SP          RetAddr               Call Site
00 00000000`001cf370 00007ffe`e4caeb2a     ntdll!LdrpDoDebuggerBreak+0x30
01 00000000`001cf3b0 00007ffe`e4c9a86e     ntdll!LdrpInitializeProcess+0x1cfa
02 00000000`001cf780 00007ffe`e4c44383     ntdll!_LdrpInitialize+0x564b2
03 00000000`001cf800 00007ffe`e4c442ae     ntdll!LdrpInitializeInternal+0x6b
04 00000000`001cfa80 00000000`00000000     ntdll!LdrInitializeThunk+0xe

【0:000>】 是命令提示符,其中:

  • 冒号前的数字0代表进程编号,从0开始编号
  • 冒号后面的000是当前线程号,也是从0开始连续编号的,且不同进程的线程号也不重复
  • 命令提示符后面为用户输入的命令,比如上面列表代表用户输入了k并回车的效果
    注意:此处的进程编号并不是我们在任务管理器中看到的进程编号。

如果此时希望继续执行启动过程,可以 输入g并回车 。g代表Go,是继续运行的意思。

0:000> g
ModLoad: 00007ffe`e3610000 00007ffe`e3641000   C:\Windows\System32\IMM32.DLL
ModLoad: 00007ffe`a0090000 00007ffe`a00e9000   C:\Program Files\dotnet\host\fxr\8.0.3\hostfxr.dll
ModLoad: 00007ffe`c7570000 00007ffe`c75d4000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\hostpolicy.dll
ModLoad: 00007ffd`fb110000 00007ffd`fb5f6000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\coreclr.dll
ModLoad: 00007ffe`e3650000 00007ffe`e37f5000   C:\Windows\System32\ole32.dll
ModLoad: 00007ffe`e2f80000 00007ffe`e3308000   C:\Windows\System32\combase.dll
ModLoad: 00007ffe`e4590000 00007ffe`e4667000   C:\Windows\System32\OLEAUT32.dll
ModLoad: 00007ffe`e2070000 00007ffe`e20eb000   C:\Windows\System32\bcryptPrimitives.dll
(6300.6904): Unknown exception - code 04242420 (first chance)
ModLoad: 00007ffd`b1630000 00007ffd`b22bc000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Private.CoreLib.dll
ModLoad: 00007ffd`fa5b0000 00007ffd`fa769000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\clrjit.dll
ModLoad: 00007ffe`e0f80000 00007ffe`e0f98000   C:\Windows\SYSTEM32\kernel.appcore.dll
ModLoad: 00000000`024b0000 00000000`024b8000   E:\test\a\Core\bin\Debug\net8.0\Core.dll
ModLoad: 00000000`024c0000 00000000`024ce000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Runtime.dll
ModLoad: 00007ffe`c7540000 00007ffe`c7568000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Console.dll
(6300.6904): Break instruction exception - code 80000003 (first chance)
KERNELBASE!wil::details::DebugBreak+0x2:
00007ffe`e22c8072 cc              int     3

因为我们的C#程序第一条语句就是Debugger.Break(),其实就是一条断点(int 3),所以启动过程继续到执行该断点以后又被断下。

不过,如果细心观察,我们会发现,虽然上面经历了两次断点,且每次都是通过 int 3 实现的,但两次断点并不属于同一个模块:
前一次是断在ntdll 模块的LdrpDoDebuggerBreak+0x30处,而本次则被断在KERNELBASE 模块的wil::details::DebugBreak+0x2处。也就是说,一定是在Main方法执行Debugger.Break()时,调用了wil::details::DebugBreak。

因为此时程序指针在KernelBASE中,如果想回到Main方法,或者采用单步模式一步一步StepOver回来,或者使用下面介绍的更简单的方法一步到位:

0:000> k
 # Child-SP          RetAddr               Call Site
00 00000000`001ceaa8 00007ffd`fb322359     KERNELBASE!wil::details::DebugBreak+0x2
01 00000000`001ceab0 00007ffd`b1a5550a     coreclr!DebugDebugger::Break+0x149 [D:\a\_work\1\s\src\coreclr\vm\debugdebugger.cpp @ 150] 
02 00000000`001cec30 00007ffd`9b6f196a     System_Private_CoreLib!System.Diagnostics.Debugger.Break+0xa [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18] 
03 00000000`001cec60 00007ffd`fb26b8c3     Core!BasicGrammar.Program.Main+0x3a [E:\test\a\Core\Program.cs @ 9] 
04 00000000`001cecd0 00007ffd`fb1a0b19     coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100] 
05 (Inline Function) --------`--------     coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67] 
......其它栈帧略

首先,我们使用k命令查看到03号栈帧是Main方法所在帧,返回03号帧的返回地址保存在02号栈帧中,即00007ffd`9b6f196a,所以我们可以将此地址加到g 命令后面作为参数,指示WinDbg运行到该地址时停止:

0:000> g 00007ffd`9b6f196a
Core!BasicGrammar.Program.Main+0x3a:
00007ffd`9b6f196a 90              nop

程序成功断到了我们设置的目标地址,此时C#源代码文件也被WinDbg自动加载了进来,如下面截图所示:
WinDbg
以上,稍加折腾,我们终于让程序中断到了C#程序入口点。这个过程和Visual Studio调试相比,确实要复杂很多。不过,因为WinDbg第一次中断比Visual Studio第一次中断早很多,所以使用WinDbg有助于我们学习研究.NET应用程序的启动过程,这是Visual Studio调试器无法提供的。

用户程序调试

接下来,我们就可以调试C#程序了,我们可以在C#源代码中下断点,进行源代码级调试,也可以直接在汇编窗口进行汇编级调试,切换的方法就是使用Home下的Source/Assembly切换开关。至于单步/运行/暂停等操作,则与Visual Studio调试器类似,如下图所示:

在这里插入图片描述
作为《WinDbg Step by Step》系列的第一篇,我们先不做太复杂操作,但为了能概览全局,我们介绍一下如何显示Main方法对应的汇编语言代码。这里我们要使用 !u 命令,并继续用上面g指令用过的返回03号栈帧的地址00007ffd`9b6f196a作为参数:
备注:!u命令比较智能,它并不要求其参数必须指定代码的首地址,而是可以使用代码区的任何一个地址。

0:000> !u 00007ffd`9b6f196a
Normal JIT generated code
BasicGrammar.Program.Main()
ilAddr is 00000000024B2050 pImport is 00000000055F7EA0
Begin 00007FFD9B6F1930, size d8

E:\test\a\Core\Program.cs @ 8:
00007ffd`9b6f1930 55              push    rbp
00007ffd`9b6f1931 4883ec60        sub     rsp,60h
00007ffd`9b6f1935 c5f877          vzeroupper
00007ffd`9b6f1938 488d6c2460      lea     rbp,[rsp+60h]
00007ffd`9b6f193d c5d857e4        vxorps  xmm4,xmm4,xmm4
00007ffd`9b6f1941 c5f97f65c0      vmovdqa xmmword ptr [rbp-40h],xmm4
00007ffd`9b6f1946 c5f97f65d0      vmovdqa xmmword ptr [rbp-30h],xmm4
00007ffd`9b6f194b c5f97f65e0      vmovdqa xmmword ptr [rbp-20h],xmm4
00007ffd`9b6f1950 c5f97f65f0      vmovdqa xmmword ptr [rbp-10h],xmm4
00007ffd`9b6f1955 833dccc9080000  cmp     dword ptr [00007ffd`9b77e328],0
00007ffd`9b6f195c 7405            je      Core!BasicGrammar.Program.Main+0x33 (00007ffd`9b6f1963)
00007ffd`9b6f195e e84dacc95f      call    coreclr!JIT_DbgIsJustMyCode (00007ffd`fb38c5b0)
00007ffd`9b6f1963 90              nop

E:\test\a\Core\Program.cs @ 9:
00007ffd`9b6f1964 ff15dedf0a00    call    qword ptr [00007ffd`9b79f948] (System.Diagnostics.Debugger.Break(), mdToken: 0000000006007B32)
00007ffd`9b6f196a 90              nop

E:\test\a\Core\Program.cs @ 10:
>>> 00007ffd`9b6f196b c745fc64000000  mov     dword ptr [rbp-4],64h

E:\test\a\Core\Program.cs @ 11:
00007ffd`9b6f1972 33c9            xor     ecx,ecx
00007ffd`9b6f1974 894df0          mov     dword ptr [rbp-10h],ecx

E:\test\a\Core\Program.cs @ 12:
00007ffd`9b6f1977 8b4dfc          mov     ecx,dword ptr [rbp-4]
00007ffd`9b6f197a ff15d02d0d00    call    qword ptr [00007ffd`9b7c4750]
00007ffd`9b6f1980 90              nop

E:\test\a\Core\Program.cs @ 13:
00007ffd`9b6f1981 c5f857c0        vxorps  xmm0,xmm0,xmm0
00007ffd`9b6f1985 c5fa7f45e0      vmovdqu xmmword ptr [rbp-20h],xmm0

E:\test\a\Core\Program.cs @ 14:
00007ffd`9b6f198a 48b908957a9bfd7f0000 mov rcx,7FFD9B7A9508h (MT: BasicGrammar.Program+ClassEmployee)
00007ffd`9b6f1994 e807aab75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`fb26c3a0)
00007ffd`9b6f1999 488945d0        mov     qword ptr [rbp-30h],rax
00007ffd`9b6f199d 488b4dd0        mov     rcx,qword ptr [rbp-30h]
00007ffd`9b6f19a1 ff1589df0a00    call    qword ptr [00007ffd`9b79f930]
00007ffd`9b6f19a7 488b4dd0        mov     rcx,qword ptr [rbp-30h]
00007ffd`9b6f19ab 48894dd8        mov     qword ptr [rbp-28h],rcx

E:\test\a\Core\Program.cs @ 15:
00007ffd`9b6f19af 48c745e0a004b502 mov     qword ptr [rbp-20h],2B504A0h

E:\test\a\Core\Program.cs @ 16:
00007ffd`9b6f19b7 488b4dd8        mov     rcx,qword ptr [rbp-28h]
00007ffd`9b6f19bb 48c74108e004b502 mov     qword ptr [rcx+8],2B504E0h

E:\test\a\Core\Program.cs @ 17:
00007ffd`9b6f19c3 8b4df0          mov     ecx,dword ptr [rbp-10h]
00007ffd`9b6f19c6 ffc1            inc     ecx
00007ffd`9b6f19c8 894dfc          mov     dword ptr [rbp-4],ecx

E:\test\a\Core\Program.cs @ 18:
00007ffd`9b6f19cb 8b4de8          mov     ecx,dword ptr [rbp-18h]
00007ffd`9b6f19ce 83c102          add     ecx,2
00007ffd`9b6f19d1 894dfc          mov     dword ptr [rbp-4],ecx

E:\test\a\Core\Program.cs @ 19:
00007ffd`9b6f19d4 488b4dd8        mov     rcx,qword ptr [rbp-28h]
00007ffd`9b6f19d8 8b4910          mov     ecx,dword ptr [rcx+10h]
00007ffd`9b6f19db 83c103          add     ecx,3
00007ffd`9b6f19de 894dfc          mov     dword ptr [rbp-4],ecx

E:\test\a\Core\Program.cs @ 20:
00007ffd`9b6f19e1 488b4dd8        mov     rcx,qword ptr [rbp-28h]
00007ffd`9b6f19e5 ff15f5510a00    call    qword ptr [00007ffd`9b796be0]
00007ffd`9b6f19eb 90              nop

E:\test\a\Core\Program.cs @ 21:
00007ffd`9b6f19ec c5fa6f45e0      vmovdqu xmm0,xmmword ptr [rbp-20h]
00007ffd`9b6f19f1 c5fa7f45c0      vmovdqu xmmword ptr [rbp-40h],xmm0
00007ffd`9b6f19f6 488d4dc0        lea     rcx,[rbp-40h]
00007ffd`9b6f19fa ff15c8510a00    call    qword ptr [00007ffd`9b796bc8]
00007ffd`9b6f1a00 90              nop

E:\test\a\Core\Program.cs @ 22:
00007ffd`9b6f1a01 90              nop
00007ffd`9b6f1a02 4883c460        add     rsp,60h
00007ffd`9b6f1a06 5d              pop     rbp
00007ffd`9b6f1a07 c3              ret

有了这个列表,我们就可以慢慢研究了,每一行C#语句都会对应1到多条汇编语句,逐个对照,就可以更深刻理解C#语言的底层实现了。比如源程序第10行 int y = 100 只对应了一条汇编语句:

E:\test\a\Core\Program.cs @ 10:
>>> 00007ffd`9b6f196b c745fc64000000  mov     dword ptr [rbp-4],64h

十六进制的64h,转换成十进制恰好是100。所以通过汇编语言我们知道,变量y其实是保存到以rbp-4作为指针的内存单元的,具体地址可以使用求值命令(?),让WinDbg自动帮我们计算:

0:000> ? #@rbp - 4
Evaluate expression: 1895612 = 00000000`001cecbc

我们还可以用!address命令查查这个地址的详细信息:

0:000> !address 00000000`001cecbc
Usage:                  Stack
Base Address:           00000000`001bf000
End Address:            00000000`001d0000
Region Size:            00000000`00011000 (  68.000 kB)
State:                  00001000          MEM_COMMIT
Protect:                00000004          PAGE_READWRITE
Type:                   00020000          MEM_PRIVATE
Allocation Base:        00000000`00050000
Allocation Protect:     00000004          PAGE_READWRITE
More info:              ~0k
Content source: 1 (target), length: 1344

很显然,命令输出的第一行就告诉了我们,这个地址在栈区(Stack),这与我们学习c#时知道的值类型保存在栈区刚好吻合。

作为练习,我们再分析一下C#第11行:

E:\test\a\Core\Program.cs @ 11:  //SimpleStr simpleStr = new SimpleStr();
00007ffd`9b6f1972 33c9            xor     ecx,ecx ;将cpu的ecx寄存器清零
00007ffd`9b6f1974 894df0          mov     dword ptr [rbp-10h],ecx ;将ecx的值保存到rbp-10h地址单元中

SimpleStr是一个struct类型(值类型),并且只有一个int型成员。该语句也只对应两行汇编代码,我为这两行代码写了注释(汇编语言注释以分号开始)。

对比以上C#第10行和第11行代码,可以看出,int y 保存在 [rbp - 4]指针指向的单元, SimpleStr simpleStr保存在 [rbp - 10h]指针指向的单元,两者都在栈区,两者都是值类型,符合我们所学知识。另外,通过以上代码我们知道,示例的SimpleStr和int在汇编语言级没有任何差别!

继续分析第12行代码:

E:\test\a\Core\Program.cs @ 12: //Console.WriteLine(y)
00007ffd`9b6f1977 8b4dfc          mov     ecx,dword ptr [rbp-4]
00007ffd`9b6f197a ff15d02d0d00    call    qword ptr [00007ffd`9b7c4750]

这里我们看到,Console.WriteLine的参数y并不是通过栈传递的,而是传递给了ecx寄存器,原因就是64位程序调用使用的是_fastcall调用约定。

第13行代码中strEmployee也是struct型(值类型),只不过这个类型有两个成员,一个是引用类型 string Name,一个是值类型 int Age。

E:\test\a\Core\Program.cs @ 13: StrEmployee strEmployee = new StrEmployee();
00007ffd`9b6f1981 c5f857c0        vxorps  xmm0,xmm0,xmm0
00007ffd`9b6f1985 c5fa7f45e0      vmovdqu xmmword ptr [rbp-20h],xmm0

上面两行汇编代码使用了x86-64架构下的VEX编码的AVX(Advanced Vector Extensions)指令。看似复杂,其实第一条指令就是给128位的xmm0寄存器清零,第二条就是将xmm0的内存保存到[rbp-20h]指针所指向的合计16字节存储单元中,其实也就是直接给Age赋值了0,并且给Name赋值了null。这16个字节依旧在栈区,合情合理。不过,似乎这两条汇编是分别为Age和Name进行的直接赋值,在汇编语言级别,其实strEmployee结构被丢掉了。

最后看类实例的创建,也即是第14行:

E:\test\a\Core\Program.cs @ 14: ClassEmployee classEmployee = new ClassEmployee();
00007ffd`9b6f198a 48b908957a9bfd7f0000 mov rcx,7FFD9B7A9508h (MT: BasicGrammar.Program+ClassEmployee)
00007ffd`9b6f1994 e807aab75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`fb26c3a0)
00007ffd`9b6f1999 488945d0        mov     qword ptr [rbp-30h],rax
00007ffd`9b6f199d 488b4dd0        mov     rcx,qword ptr [rbp-30h]
00007ffd`9b6f19a1 ff1589df0a00    call    qword ptr [00007ffd`9b79f930]
00007ffd`9b6f19a7 488b4dd0        mov     rcx,qword ptr [rbp-30h]
00007ffd`9b6f19ab 48894dd8        mov     qword ptr [rbp-28h],rcx       

这段代码复杂些:

第一行是给rcx赋值7FFD9B7A9508h,这个值其实就是CLR中BasicGrammar.Program.ClassEmployee的Method Table地址,也就是ClassEmployee构造方法的第一个参数实际是类型信息;

第二行汇编就是调用coreclr的内存分配方法,为ClassEmployee实例申请合适的内存,并将得到的内存首地址通过rax寄存器返回;

第三行汇编将实例地址保存到[rbp - 30h]单元,这其实就是实例的this指针;

第四行将上一行得到的this指针传给rcx作为构造方法的隐含参数(this指针)

第五行调用构造方法,为实例对象初始化。这行代码执行完毕以后,this指针不变,还是rbp-30h,但其指向的内容会根据构造函数的代码进行初始化。

第六行是将this指针赋值给rcx

第七行是将rcx中的this指针保存到 [rbp-28h]中,也就是说,classEmployee实例指针最终是保存到了栈上[rbp-28h]的地址中的,而其真实对象object则是第二行从堆中分配的内存。所谓的引用类型,其实就是指栈上有一个引用变量classEmployee,该值其实是一个指针,但在C#中被称为引用,主要区别在于引用是类型安全的,是对指针的扩展。而真正的对象则保存在堆中,通过引用来使用。这就是引用类型的本质。

通过以上分析,我们可以更清晰理解值类型和引用类型了:

** 值类型中无论有多少成员,每个成员的值都是直接分配到栈上的,除非成员本身是引用类型,这种情况下栈中保存引用,堆中保存对象实例。对于引用类型,则会同时在栈中和堆中分配空间,且栈中空间用于保存引用,如果是64位程序,栈中占用8字节,如果是32位程序则栈中占用4字节,而堆中会保存实例对象。**

总结

本篇通过两个示例,简单演示了一下WinDbg的基本使用。

WinDbg命令虽多,却不要求一次全部记住,随着使用时间的增加,慢慢就掌握了要领。最关键的是跟随操作,如果遇到不懂的命令,可以参考第一篇去查找帮助。

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值