总目录
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命令及其功能包括:
- 显示和修改寄存器内容的命令R
功能:用于查看和修改CPU内部寄存器的值,包括标志位状态。
格式:
-R:显示CPU内部所有寄存器内容和标志位状态。
-R 寄存器名:显示和修改指定寄存器的内容。
-RF:显示和修改标志位状态。 - 显示内存单元内容的命令D
功能:用于查看内存中的内容。
格式:
-D[address]:从指定地址开始连续显示内存单元的内容。
-D[range]:显示指定范围内的内存单元内容。 - 修改内存单元内容的命令E
功能:用于修改内存中的内容。
格式:
-E address [list]:用给定的内容表来替代指定范围的存储单元内容。
-E address:逐个单元相继地修改内存内容。 - 汇编命令A
功能:允许用户键入汇编语言语句,并将它们汇编成机器代码,存放在指定地址的内存中。
格式:-A[address] - 反汇编命令U
功能:将内存中的机器指令翻译成汇编指令。
格式:
-U[address]:从指定地址开始反汇编32个字节。
-U[range]:对指定范围内的存储单元进行反汇编。 - 执行命令G
功能:从指定地址开始执行程序,直到遇到断点或程序结束。
格式:-G[=address1][address2[address3…]] - 跟踪命令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自动加载了进来,如下面截图所示:
以上,稍加折腾,我们终于让程序中断到了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命令虽多,却不要求一次全部记住,随着使用时间的增加,慢慢就掌握了要领。最关键的是跟随操作,如果遇到不懂的命令,可以参考第一篇去查找帮助。