2024年3月

SSK - 吵架键盘模拟器
SuperSimulator of Keyboard
调用系统底层,能够模拟所有键盘操作

本程序结合
快Key(QuicKeys智能登录助手)
一起使用,能够创造更多奇迹!

【下载】

点击下载 SSK:超级键盘模拟器 :https://files.cnblogs.com/files/BigSystemsView/SSK-%E8%B6%85%E7%BA%A7%E9%94%AE%E7%9B%98%E6%A8%A1%E6%8B%9F%E5%99%A8_V1_0_1-%E5%8F%91%E5%B8%83%E6%96%87%E4%BB%B6%E5%8C%85-%E5%8C%85%E5%90%AB%E5%BF%ABKey.zip?t=1709874567&download=true

* 已包含快Key安装文件包


【版权与免责声明】
①本软件为开源软件,可自由拷贝,自由使用,自由分享,自由修改,自由发布。
②作者不对使用者因使用本软件而对其自身或其他任何人所造成的任何伤害或损失负责?开始使用本软件即表示使用者接受此约定。
③作者拥有对于本软件的最终解释权。

【联系信息】
本软件不自动更新,安装文件、更新文件、源文件和交流信息等可通过【大系统观开放论坛】微信公众号获得。


【版本】:1.0.1

【更新日期】:2024.3.8

【作者】:阿色

【功能】

模拟键盘输入。底层模拟,可以实现完全硬件水平的实际按键模拟,实际键盘的任何操作都可模拟,包括简单的普通字符,以及组合键 Win+V 键等。
支持的组合键:Shift、Ctrl、Alt、Win、Menu、Apps。功能键F1-F24、方向键、Esc、Tab等,以及声音控制等都可模拟,只要存在keycode即可。
本程序结合
快Key(QuicKeys智能登录助手)
一起使用,能够创造更多奇迹!

【运行环境】:MicroSoft Windows 10 测试通过

【用法】

使用命令行方式调用,形如:
SSK 控制键+字符键功能键 [延迟时间][:&:接续键] [/H]
控制键:Shift/Ctrl/Alt/Win/Menu/Apps,不区分大小写和前后顺序
字符键功能键:普通字符键、Fn功能键,及所有具有键值的键
[延迟时间]:模拟前等待的时间(秒),默认为0,可略。当前键相应慢时,接续键应考虑延迟
[:&:接续键]:多次连续按键用:&:连接,可无
[/H]:延迟时隐藏准备图标,默认不隐藏,可略
* 不带参数执行将显示使用说明。

【示例】

SSK WinV: 按Win v
SSK SHIFTabc123: 按Shift abc123
SSK CtrlS 0.5:等待0.5秒再按Ctrl+S
SSK WINCtrlAlt{F4}:按Win+Ctrl+Alt+F4
SSK CtrlShiftABC{ESC}:按Ctrl+Shift+ABC+Esc
SSK 12{SPACE}W{HOME}:按12+空格+W+Home
SSK Menu 10 /H:等10秒后按一下Menu,隐藏延迟图标
SSK Ctrl{KC=172}:按Ctrl+KeyCode为172的键(浏览器)
SSK {Ctrl}{Alt}:单击Ctrl,然后单击Alt,不作为组合键
SSK CTRLs:&&:WinD:按Ctrl+S,抬起后,再按Win+D

【连续按键】

①非组合键(无Shift/Ctrl/...)可连续使用,空格用{SPACE}代替,
如:SSK abc123{SPACE}daqing{BS}{BS}ng
上面指令将输入abc123 daqing,然后退格2次,再输入ng
②如使用组合键,则需要用:&&:隔开各个组合键,如:
SSK CtrlV:&&:abc123:&&:CtrlS
③更复杂按键可用.bat文件模拟,文件中依次调用SSK,如:
@echo off
notepad.exe
SSK ...
......
SSK ...


【键盘代码】

字符键/数字键:直接使用,如 U、9、A5:j
功能键/特殊键:用{ }表示,如 {F4}、{TAB}、{ESC}
控制键:①作为组合键:使用缩写,如 Ctrl、Win、Shift;②不作为组合键:用{ },如 {Ctrl},表示单击
各键直接相连,中间无其它字符,如 CtrlWin{F2}M


各功能键/特殊键/控制键对应代码如下:

空格SPACE {SPACE}
制表TAB {TAB}
退格BACKSPACE {BS}
回车ENTER {ENTER}
大写CAPS LOCK {CAPSLOCK}

跳出ESC {ESC}
帮助HELP {HELP}
功能F1 {F1}...F24 {F24}
打印屏幕PRINT SCREEN {PRTSC}
滚动锁SCROLL LOCK {SCROLLLOCK}
中断BREAK/PAUSE {BREAK}

插入INS/INSERT {INS}
删除DEL/DELETE {DEL}
开头HOME {HOME}
结束END {END}
上页PAGE UP {PGUP}
下页PAGE DOWN {PGDN}

上箭头UP ARROW {UP}
下箭头DOWN ARROW {DOWN}
左箭头LEFT ARROW {LEFT}
右箭头RIGHT ARROW {RIGHT}

数字NUM LOCK {NUMLOCK}
小键盘+ {ADD}
小键盘- {SUB}
小键盘* {MULT}
小键盘/ {DIV}

音量加 {VOL+}
音量减 {VOL-}
静音 {MUTE}
启停 {START}
浏览器 {BROW}
邮件 {EMAIL}
搜索 {SEARCH}
收藏 {COLL}

其他 {KC=number}


* Shift/Ctrl/Alt/Win/Menu/Apps 作为非组合键使用时,需加{ },如{Win}、{Alt}表示单击Win键、Alt键

* { }内不区分大小写
* 如果上述代码不全或不对应,可使用键值(Key Code)形式,如:{KC=175} 表示音量加键

【下载】

点击下载 SSK:超级键盘模拟器 :https://files.cnblogs.com/files/BigSystemsView/SSK-%E8%B6%85%E7%BA%A7%E9%94%AE%E7%9B%98%E6%A8%A1%E6%8B%9F%E5%99%A8_V1_0_1-%E5%8F%91%E5%B8%83%E6%96%87%E4%BB%B6%E5%8C%85-%E5%8C%85%E5%90%AB%E5%BF%ABKey.zip?t=1709874567&download=true

* 已包含快Key安装文件包

一、介绍
这是我的《
Advanced .Net Debugging
》这个系列的第四篇文章。今天这篇文章的标题虽然叫做“基本调试任务”,但是这章的内容还是挺多的。由于内容太多,故原书的第三章内容我分两篇文章来写。上一篇我们了解了一些调试技巧,比如:单步调试、下断点、过程调试等,这篇文章主要涉及的内容是对象的转储,内存的转储,值类型的转储,引用类型的转储、数组的转储、异常的转储等。
第一次说到“转储”,可能大家不知道什么意思,其实就是把我们想要的内容输出出来或者说是打印出来,方便我们分析问题。
SOSEX扩展的内容我就省略了,因为我这个系列的是基于 .NET 8 版本来写的,SOSEX是基于 .NET Framework 版本的,如果大家想了解其内容,可以查看我的【高级调试】系列
(我当前写的是《
Advanced .Net Debugging
》系列,是不一样的),当然,也可以看原书。【高级调试】系列主要是集中在 .NET Framework 版本的。如果我们想成为一名合格程序员,这些调试技巧都是必须要掌握的。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)

下载地址:可以去Microsoft Store 去下载

开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源码:
源码下载

二、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
2.1、ExampleCore_3_1_6


1 usingSystem.Diagnostics;2 
3 namespaceExampleCore_3_1_64 {5     public classObjTypes6 {7         public structCoordinate8 {9             public intxCord;10             public intyCord;11             public intzCord;12 
13             public Coordinate(int x, int y, intz)14 {15                 xCord =x;16                 yCord =y;17                 zCord =z;18 }19 }20 
21         privateCoordinate coordinate;22 
23         int[] intArray = [1, 2, 3, 4, 5];24 
25         string[] strArray = ["Welcome", "to", "Advanced", ".NET", "Debugging"];26 
27         static void Main(string[] args)28 {29             Coordinate point = new Coordinate(100, 100, 100);30             Console.WriteLine("Press any key to continue(AddCoordinate)");31 Console.ReadKey();32             ObjTypes ob = newObjTypes();33 ob.AddCoordinate(point);34 
35             Console.WriteLine("Press any key to continue(Arrays)");36 Console.ReadKey();37 ob.PrintArrays();38 
39             Console.WriteLine("Press any key to continue(Generics)");40 Console.ReadKey();41             Comparer<int> c = new Comparer<int>();42             Console.WriteLine("Greater:{0}", c.GreaterThan(5, 10));43 
44             Console.WriteLine("Preaa any key to continue(Exception)");45 Console.ReadKey();46             ob.ThrowException(null);47 }48        
49         public voidAddCoordinate(Coordinate coord)50 {51             coordinate.xCord +=coord.xCord;52             coordinate.yCord +=coord.yCord;53             coordinate.zCord +=coord.zCord;54 
55             Console.WriteLine("x:{0},y:{1},z:{2}", coordinate.xCord, coordinate.yCord, coordinate.zCord);56 }57 
58         public voidPrintArrays()59 {60             foreach (int i inintArray)61 {62                 Console.WriteLine("Int:{0}", i);63 }64             foreach (string s instrArray)65 {66                 Console.WriteLine("Str:{0}", s);67 }68 }69 
70         public void ThrowException(ObjTypes?obj)71 {72             if (obj == null)73 {74                 throw new ArgumentException("Obj cannot be null");75 }76 }77 }78     public class Comparer<T> whereT : IComparable79 {80         publicT GreaterThan(T d, T d2)81 {82             int ret =d.CompareTo(d2);83             if (ret > 0)84 {85                 returnd;86 }87             else
88 {89                 returnd2;90 }91 }92 
93         publicT LessThan(T d, T d2)94 {95             int ret =d.CompareTo(d2);96             if (ret < 0)97 {98                 returnd;99 }100             else
101 {102                 returnd2;103 }104 }105 }106 }

View Code


2.2、ExampleCore_3_1_7


1 namespaceExampleCore_3_1_72 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             var person = newPerson();8 Console.ReadLine();9 }10 }11     internal classPerson12 {13         public int Age = 20;14 
15         public string Name = "jack";16 }17 }

View Code


2.3、ExampleCore_3_1_8


1 namespaceExampleCore_3_1_82 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             Console.WriteLine("Welcome to .NET Advanced Debugging!");8 
9             Person person = new Person() { Name = "PatrickLiu", Age = 32, HomeAddress = new Address() { Country = "China", Province = "冀州", City = "直隶总督府", Region = "广平大街23号", PostalCode = "213339"} };10 
11             Console.WriteLine($"名称:{person.Name},地址:{person.HomeAddress}");12 
13 Console.Read();14 }15 }16 
17     public classPerson18 {19         public int Age { get; set; }20         public string? Name { get; set; }21         public Address? HomeAddress { get; set; }22 }23 
24     public classAddress25 {26         public string? Country { get; set; }27         public string? Province { get; set; }28         public string? City { get; set; }29         public string? Region { get; set; }30         public string? PostalCode { get; set; }31         public override stringToString()32 {33             return $"{Country}-{Province}-{City}-{Region}-{PostalCode}";34 }35 }36 }

View Code


三、基础知识
本节的内容也很多,本来打算这篇文章分为:3.1、3.2、3.3、3.4、3.5、3.6 共 6 节就将原书的第三章剩下的内容全部写完,但是内容太多,就只保留一节了。便于学习和阅读。下一篇,怎么排版再定吧。
3.1、对象检查
本节,我们将介绍一些命令用来分析程序的状态,以确定程序的故障。我们先来介绍非托管调试器中一些常用的命令,然后在介绍在 SOS 调试扩展中针对托管代码调试的命令。
3.1.1、内存转储
A、基础知识
在调试器中有很多命令都可以转储内存的内容,这个方式非常底层,从内存地址上观察地址上的内容。最常使用的命令是【d(显示内存)】,比如:【dp】命令。根据转储的数据类型不同,命令【d】也有很多不同的变化,比如:du,dw,db,da 等,如果想了解更多,可以查看 Windbg 的帮助文档,命令是【.hh】。
其他一些变化形式:
。du 命令把被转储的内存视作为 Unicode 字符。
。da 命令把被转储的内存视作为 ASCII字符。
。dw 命令把被转储的内存视作为字(word)。
。db 命令把被转储的内存视作为字节值和 ASCII 字符。
。dq 命令把被转储的内存视作为四字值(quad word)。

B、眼见为实:
1)、NTSD 调试
调试源码:ExampleCore_3_1_7
我们编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】,输入命令:NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.EXE。

打开【NTSD】调试器窗口。

继续使用【g】命令,运行调试器,等调试器卡住后,按【ctrl+c】组合键键入调试器的中断模式。
切换到主线程,执行命令【~0s】。

1 0:009> ~0s2 coreclr!GetThreadNULLOk+0x1e [inlined in coreclr!CrstBase::Enter+0x32]:3 00007ff9`bd119fa2 488b34c8        mov     rsi,qword ptr [rax+rcx*8] ds:0000026c`600cb0b0=0000026c600ba2d0

使用【!clrstack -a】查看托管代码的线程调用栈。

1 0:000> !clrstack -a2 OS Thread Id: 0x954 (0)3 Child SP               IP Call Site4 000000785C77DCE8 00007ff9bd119fa2 [ExternalMethodFrame: 000000785c77dce8]5 ......(省略了)6 000000785C77E4D0 00007FF95D621987 ExampleCore_3_1_7.Program.Main(System.String[])7 PARAMETERS:8         args (0x000000785C77E520) = 0x0000026c64808ea0
9 LOCALS:10         0x000000785C77E508 = 0x0000026c64809640

0x0000026c64809640
红色标注的就是 Person 类型的局部变量 person 的地址。我们可以使用【!dumpobj /d 0x0000026c64809640】查看 person 的详情。

1 0:000> !dumpobj /d 0x0000026c64809640
2 Name:        ExampleCore_3_1_7.Person3 MethodTable: 00007ff95d6d93e0(方法表地址)4 EEClass:     00007ff95d6e1f185 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff95d591188  4000001       10         System.Int32  1 instance               20Age11 00007ff95d60ec08  4000002        8        System.String  0instance 000002acf67a04a0 Name12 0:000>

同样,我们可以使用【dp】命令也能看到 person 的详情信息,只是不是很直观。

1 0:000> dp 0x0000026c64809640
2 0000026c`6480964000007ff9`5d6d93e0 000002ac`f67a04a03 0000026c`64809650  00000000`00000014 00000000`00000000
4 0000026c`64809660  00007ff9`5d666666fa8 00000000`00000000
5 0000026c`64809670  00000000`0000000000007ff9`5d60ec086 0000026c`648096800065006b`0000000c 006c0065`006e00727 0000026c`64809690  0064002e`00320033 00000000`006c006c8 0000026c`648096a0  00000000`0000000000007ff9`5d700d689 0000026c`648096b0  00000000`00000000 00000000`00000000

上面使用【!dumpobj】和【dp】命令我们找到的方法表地址都是一样的。
000002ac`f67a04a0
这个项就是 Name 域的地址,因为 Name 是引用类型,所以这里是一个地址。我们可以继续使用【!dumpobj /d 000002ac`f67a04a0】来验证。

1 0:000> !dumpobj /d 000002ac`f67a04a02 Name:        System.String3 MethodTable: 00007ff95d60ec084 EEClass:     00007ff95d5ea5005 Tracked Type: false
6 Size:        30(0x1e) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      jack(我们赋的值)9 Fields:10 MT    Field   Offset                 Type VT     Attr            Value Name11 00007ff95d591188  400033b        8         System.Int32  1 instance                4_stringLength12 00007ff95d59b538  400033c        c          System.Char  1instance               6a _firstChar13 00007ff95d60ec08  400033a       c8        System.String  0   static 000002acf67a0008 Empty

我们可以使用【dumpobj】命令,当然也可以使用【dp】命令,都可以看到想看到的信息,不过是【dp】可读性差很多。
如果我们查看命令如何使用,可以使用【.hh】命令。

1 0:000> .hh

效果如图:

就能打开调试的帮助文件。

我们可以打开【索引】选项,查看我们想要查看的命令。


2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_7
我们编译项目,打开 Windbg,点击【文件】----》【Launch executable】附加程序 ExampleCore_3_1_7.exe,打开调试器的界面,程序已经处于中断状态。由于显示的内容太多,我们可以使用【.cls】命令清空调试器的界面。我们使用【g】命令继续运行程序,调试器会在【Console.ReadLine()】这行代码处卡住,我们点击【break】按钮,就可以调试程序了。
查看是否在主线程,如果不是,切换到主线程,执行命令【~0s】。

1 0:006> ~0s2 ntdll!NtReadFile+0x14:3 00007ffa`9576ae54 c3              ret

我们查看一下当前托管的线程栈,执行命令【!clrstack -a】。

1 0:000> !clrstack -a2 OS Thread Id: 0x8fc (0)3 Child SP               IP Call Site4 000000D30A57E230 00007ffa9576ae54 [InlinedCallFrame: 000000d30a57e230]5 000000D30A57E230 00007ff9e4f076eb [InlinedCallFrame: 000000d30a57e230]6 ......(省略了)7 
8 000000D30A57E580 00007ff95e471987 ExampleCore_3_1_7.Program.Main(System.String[]) [E:\Visual Studio\..\ExampleCore_3_1_7\Program.cs @ 8]9 PARAMETERS:10         args (0x000000D30A57E5D0) = 0x0000021dc3808ea0
11 LOCALS:12         0x000000D30A57E5B8 = 0x0000021dc3809640

0x0000021dc3809640
红色标注的就是 Person 类型的局部变量 person 的地址。我们直接使用【dp】命令转储出 person 的值。

1 0:000> dp 0x0000021dc3809640
2 0000021d`c3809640  00007ff9`5e5293e0 0000025e`558d04a03 0000021d`c3809650  00000000`00000014 00000000`00000000
4 0000021d`c3809660  00007ff9`5e3a5fa8 00000000`00000000
5 0000021d`c3809670  00000000`0000000000007ff9`5e45ec086 0000021d`c3809680  0065006b`0000000c 006c0065`006e00727 0000021d`c3809690  0064002e`00320033 00000000`006c006c8 0000021d`c38096a0  00000000`0000000000007ff9`5e550d689 0000021d`c38096b0  00000000`00000000 00000000`00000000

最左边的一列给出了每行内存的起始地址,后面是内存的内容。
00007ff9`5e5293e0
红色标注的就是方法表。我们可以验证。执行命令【!dumpheap -type Person】。

1 0:000> !dumpheap -type Person2 Address               MT           Size3     021dc3809640     7ff95e5293e0             32 
4 
5 Statistics:6 MT Count TotalSize Class Name7 7ff95e5293e0     1        32ExampleCore_3_1_7.Person8 Total 1 objects, 32 bytes

7ff95e5293e0

00007ff9`5e5293e0
两个值是一样的。

虽然直接输出内存的内容很有用,但是阅读起来就很麻烦。当我们调试托管代码的时候,使用 SOS 扩展命令会提供更直接的信息。
如果我们想查看有关调试器的各种命令。我们可以使用【.hh】帮助文件。

1 windbg> .hh

同样能打开调试器的帮助窗口。

3.1.2、值类型的转储
A、基础知识
我们知道.NET 的类型分为值类型和引用类型,那我们如何判断一个指针指向的是否是值类型呢,最佳的方式就是使用【dumpobj】命令,但它只对引用类型有效。【dumpobj】命令的参数是一个指向引用类型的指针,如果指针指向的是值类型,【dumpobj】命令就会输出:<Note:this object has an  invalid CLASS field>Invalid object。
B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_6
1.1)、查看独立的值类型
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器窗口。

输出内容太多,使用【.cls】命令,清理一下屏幕。然后使用【g】命令,运行调试器。调试器输出:Press any key to continue(AddCoordinate),如图:

输入【ctrl+c】组合键进入中断模式。我们现在查看一下托管线程栈,可以使用【!clrstack -a】命令。

1 0:002> !clrstack -a2 OS Thread Id: 0x1764 (2)3 Unable to walk the managed stack. The current thread islikely not a4 managed thread. You can run !threads to get a list of managed threads in
5 the process6 Failed to start stack walk: 80070057

如图:

该命令执行错误,提示不是一个有效的托管线程,由于我们是手动中断程序的执行,调试器的线程上下文是在调试器线程上,它是一个非托管线程,因此,在执行该命令之前,需要切换到托管线程的上下文中。执行命令【~0s】。

1 0:002> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffb`2fdaae74 c3              ret4 0:000>

我们再次执行【!clrstack -a】命令。

1 0:000> !clrstack -a2 OS Thread Id: 0x2c1c (0)3 Child SP               IP Call Site4 000000CD41F7E5E8 00007ffb2fdaae74 [InlinedCallFrame: 000000cd41f7e5e8]5 000000CD41F7E5E8 00007ffb1b68787a [InlinedCallFrame: 000000cd41f7e5e8]6 。。。。。。(省略了)7 000000CD41F7E770 00007FF9F55919B6 ExampleCore_3_1_6.ObjTypes.Main(System.String[])8 PARAMETERS:9         args (0x000000CD41F7E840) = 0x000002a492808ea0
10 LOCALS:11         0x000000CD41F7E820 = 0x0000006400000064
12         0x000000CD41F7E818 = 0x0000000000000000
13         0x000000CD41F7E810 = 0x0000000000000000

这时,【clrstack】命令输出了托管线程的栈回溯,包括每个栈帧的局部变量和参数。我们主要关注【
ExampleCore_3_1_6.ObjTypes.Main
】栈帧和地址【
0x000000CD41F7E820
】上的局部变量。【
0x000000CD41F7E820
】这个地址我们不知道它指向的是一个值类型还是引用类型。我们可以使用【dumpobj】命令做一个测试,因为该命令只对引用类型实例起作用。

1 0:000> !dumpobj 0x000000CD41F7E820
2 <Note: this object has an invalid CLASS field>
3 Invalid object

从这个输出结果可以知道,这个指针指向的肯定不是引用类型。我们可以通过命令【r】验证一下,可以观察【rsp】寄存器,它保存的是当前栈指针。

1 0:000>r2 rax=0000000000000007 rbx=000000cd41f7e550 rcx=00000000000001c83 rdx=0000000000000000 rsi=0000000000000000 rdi=00000000000001c84 rip=00007ffb2fdaae74 rsp=000000cd41f7e388 rbp=000000cd41f7e4905  r8=000000cd41f7e388  r9=000000cd41f7e490 r10=0000000000000000
6 r11=0000000000000246 r12=000000cd41f7e8a0 r13=0000000000000004
7 r14=0000000000000003 r15=000000cd41f7ea008 iopl=0nv up ei pl zr na po nc9 cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000244
10 ntdll!NtDeviceIoControlFile+0x14:11 00007ffb`2fdaae74 c3              ret

【rsp】寄存器的值是:000000CD41F7E388,我们正在分析的地址是:0x000000CD41F7E820,连个地址非常接近,说明我们分析的地址是栈地址,也就是这个地址存储的是值类型的值。

我们可以使用【dp】命令查看一下具体的内容。

1 0:000> dp 0x000000CD41F7E820
2 000000cd`41f7e820  00000064`00000064 00000000`00000064
3 000000cd`41f7e830  000000cd`41f7e860 00007ffa`550da1a34 000000cd`41f7e840  000002a4`92808ea0 000000cd`41f7ee885 000000cd`41f7e850  000000cd`41f7ee88 000000cd`41f7ea796 000000cd`41f7e860  000000cd`41f7e910 00000000`0000001d7 000000cd`41f7e870  000000cd`41f7ea88 00007ffa`550614c98 000000cd`41f7e880  00000000`00000000 00000000`00000130
9 000000cd`41f7e890  000000cd`41f7ea88 00007ffa`54f8a456

00000064
的十进制的值就是100。后面有三个域的值是0x64。

1 0:000> ? 00000064
2 Evaluate expression: 100 = 00000000`00000064                  


1.2)、查看嵌入在引用类型中的值类型。
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器窗口。

输出内容太多,使用【.cls】命令,清理一下屏幕。然后使用【g】命令,运行调试器。调试器输出:Press any key to continue(AddCoordinate),如图:

按组合键【ctrl+c】,进入中断模式。我们直接使用【!bpmd ExampleCore_3_1_6 ExampleCore_3_1_6.ObjTypes.AddCoordinate】命令,在 ObjTypes 类型上的 AddCoordinate 的方法上设置断点。

1 0:009> !bpmd ExampleCore_3_1_6 ExampleCore_3_1_6.ObjTypes.AddCoordinate2 MethodDesc =00007FF9F56301083 Adding pending breakpoints...

我们继续运行调试器。

0:000>g
(131c.3ca0): CLR notification exception
-code e0444143 (first chance)
JITTED ExampleCore_3_1_6
!ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)
Setting breakpoint: bp 00007FF9F43B1CD0 [ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)]
。。。

重新设置断点,除非并中断执行。我们执行【!clrstack -a】命令查看一下托管线程栈。

1 0:000> !clrstack -a2 OS Thread Id: 0x3df8 (0)3 Child SP               IP Call Site4 00000085A277E468 00007ffb2d7f9202 [HelperMethodFrame: 00000085a277e468] System.Diagnostics.Debugger.BreakInternal()5 00000085A277E570 00007FFA4FC654DA System.Diagnostics.Debugger.Break()6 
7 00000085A277E5A0 00007FF9F0AB1D1A ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)8 PARAMETERS:9         this (0x00000085A277E600) = 0x00000264de809ce0
10         coord (0x00000085A277E628) = 0x0000006400000064
11 
12 00000085A277E600 00007FF9F0AB1A11 ExampleCore_3_1_6.ObjTypes.Main(System.String[])13 PARAMETERS:14         args (0x00000085A277E6D0) = 0x00000264de808ea0
15 LOCALS:16         0x00000085A277E6B0 = 0x0000006400000064
17         0x00000085A277E6A8 = 0x00000264de809ce0
18         0x00000085A277E6A0 = 0x0000000000000000

我们主要关注【
ExampleCore_3_1_6.ObjTypes.AddCoordinate
】栈帧和【this】指针。【this】指针指向的是当前的引用类型实例。我们可以使用【dumpobj】命令输出看一下。

1 0:000> !dumpobj /d 0x00000264de809ce0
2 Name:        ExampleCore_3_1_6.ObjTypes3 MethodTable: 00007ff9f0b602384 EEClass:     00007ff9f0b4fcb85 Tracked Type: false
6 Size:        48(0x30) bytes7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9f0b601c8  4000001       18 ...jTypes+Coordinate  1 instance 00000264de809cf8 coordinate11 00007ff9f0a99df8  4000002        8       System.Int32[]  0instance 00000264de809d10 intArray12 00007ff9f0b3fac8  4000003       10      System.String[]  0 instance 00000264de809d80 strArray

红色标注的【coordinate】就是引用类型包含一个值类型的例子。【MT、Field、Offset、Type、VT、Attr、Value和 Name】这些域具体的意思,可以查看图表。

我们有两种方式可以查看【coordinate】变量的具体值。第一种,使用【dp】命令。命令的参数分别是引用类型对象的地址(0x00000264de809ce0)和偏移(0x18)。

1 0:000> dp 0x00000264de809ce0+0x18
2 00000264`de809cf8  00000000`00000000 00000000`00000000
3 00000264`de809d08  00000000`0000000000007ff9`f0a99df84 00000264`de809d18  00000000`00000005 00000002`00000001
5 00000264`de809d28  00000004`00000003 00000000`00000005
6 00000264`de809d38  00000000`0000000000007ff9`f0bb44687 00000264`de809d48  00000000`00000000 00000000`00000000
8 00000264`de809d58  00000000`00000000 00000000`00000000
9 00000264`de809d68  00000000`00000000 00007ff9`f0bb3de0

第二种就是使用【!dumpvc mt addr】直接查看值类型,更直接。

1 0:000> !dumpvc 00007ff9f0b601c8  00000264de809cf82 Name:        ExampleCore_3_1_6.ObjTypes+Coordinate3 MethodTable: 00007ff9f0b601c84 EEClass:     00007ff9f0b4fd305 Size:        32(0x20) bytes6 File:        E:\Visual Studio 2022\Source\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll7 Fields:8 MT    Field   Offset                 Type VT     Attr            Value Name9 00007ff9f0a21188  4000005        0         System.Int32  1 instance                0xCord10 00007ff9f0a21188  4000006        4         System.Int32  1 instance                0yCord11 00007ff9f0a21188  4000007        8         System.Int32  1 instance                0 zCord


2)、Windbg Preview 调试。
调试源码:ExampleCore_3_1_6
2.1)、查看独立的值类型
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,控制台程序输出:Press any key to continue(AddCoordinate),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。


首先,我们希望显示出托管调用栈以及相关的局部变量。使用【!clrstack -a】命令获取这些信息。

1 0:001> !clrstack -a2 OS Thread Id: 0x239c (1)3 Unable to walk the managed stack. The current thread islikely not a4 managed thread. You can run !clrthreads to get a list of managed threads in
5 the process6 Failed to start stack walk: 80070057

【clrstack】命令出错了,错误原因是当前的线程上下文并不是一个有效的托管线程。由于我们手动的中断程序的执行,调试器的线程上下文是在调试器线程上,而这个线程是非托管的线程。因此,在执行【clrstack】命令之前,首先必须切换到托管线程上下文。使用【~】命令将上下文切换到线程0。

1 0:001> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`9576ae74 c3              ret

然后,再次执行【clrstack】命令。

1 0:000> !clrstack -a2 OS Thread Id: 0x584 (0)3 Child SP               IP Call Site4 000000F06737E948 00007ffa9576ae74 [InlinedCallFrame: 000000f06737e948]5 000000F06737E948 00007ffa4f72787a [InlinedCallFrame: 000000f06737e948]6 ......(省略了)7 
8 000000F06737EAD0 00007ff95dd019b6 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 29]9 PARAMETERS:10         args (0x000000F06737EBA0) = 0x0000020f54808ea0
11 LOCALS:12         0x000000F06737EB80 = 0x0000006400000064
13         0x000000F06737EB78 = 0x0000000000000000
14         0x000000F06737EB70 = 0x0000000000000000

这就是【clrstack】命令输出的托管线程的栈回溯,包括每个栈帧的局部变量和参数。在调用栈中,我们主要关注的是 Main 栈帧和位于地址
0x000000F06737EB80
上的局部变量(红色标注的)。
由于我们不知道这个局部变量指向的是值类型还是引用类型,因此我们可以使用【dumpobj】做一下判断。

1 0:000> !dumpobj 0x000000F06737EB80
2 <Note: this object has an invalid CLASS field>
3 Invalid object

从命令的输出结果中可以看出,该地址的肯定不是引用类型。我们来验证,值类型被保存在栈上,如果发现某个地址位于当前栈指针的附近,那就可以证明是值类型了。我们使用【r】命令,观察
rsp
寄存器(保存的是当前栈指针)。

1 0:000>r2 rax=0000000000000007 rbx=000000f06737e8b0 rcx=0000000000000058
3 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000058
4 rip=00007ffa9576ae74 rsp=000000f06737e6e8 rbp=000000f06737e7f05  r8=000000f06737e6e8  r9=000000f06737e7f0 r10=0000000000000000
6 r11=0000000000000130 r12=000000f06737ec00 r13=0000000000000004
7 r14=0000000000000003 r15=000000f06737ed608 iopl=0nv up ei pl zr na po nc9 cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000244
10 ntdll!NtDeviceIoControlFile+0x14:11 00007ffa`9576ae74 c3              ret


rsp
寄存器中包含的值:
000000f06737e6e8,它非常接近我们分析的地址:
0x000000F06737EB80
。说明我们分析的地址就是一个栈地址,我们使用【dp】查看一下详情。

1 0:000> dp 0x000000F06737EB80
2 000000f0`6737eb80  00000064`00000064 00000000`00000064
3 000000f0`6737eb90  000000f0`6737ebc0 00007ff9`bd85a1a34 000000f0`6737eba0  0000020f`54808ea0 000000f0`6737f1e85 000000f0`6737ebb0  000000f0`6737f1e8 000000f0`6737edd96 000000f0`6737ebc0  000000f0`6737ec70 00000000`0000001d7 000000f0`6737ebd0  000000f0`6737ede8 00007ff9`bd7e14c98 000000f0`6737ebe0  00000000`00000000 00000000`00000130
9 000000f0`6737ebf0  000000f0`6737ede8 00007ff9`bd70a456

00000064
红色标注的十进制的值就是100。

1 0:000> ? 00000064
2 Evaluate expression: 100 = 00000000`00000064

有三个
00000064
,分别对应 Coordinate 类型的各个域。

2.2)、查看引用类型中的值类型

2.1 的例子

我们看到了如何显示在函数内声明的值类型的内容。通常,值类型被嵌入在引用类型中并被保存在托管堆上。在这种情况下,我们不能直接使用内存转储命令,而需要借助一些辅助命令来转储值类型。
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,控制台程序输出:Press any key to continue(AddCoordinate),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。


由于我们是人工中断程序的执行,当前的线程上下文是调试器的线程上下文不是托管线程上下文,我们必须切换到托管线程的上下文,执行命令【~0s】。

1 0:007> !bpmd ExampleCore_3_1_6 ExampleCore_3_1_6.ObjTypes.AddCoordinate2 MethodDesc =00007FF9EDAA01083 Adding pending breakpoints...

断点设置好后,我们继续执行调试器,执行命令【g】。

1 0:007>g2 (3310.3864): CLR notification exception -code e0444143 (first chance)3 JITTED ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)4 Setting breakpoint: bp 00007FF9ED9F1CE0 [ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)]5 Breakpoint 0hit6 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.AddCoordinate:7 00007ff9`ed9f1ce0 55              push    rbp

当前我们的调试器线程上下文也自动切换到托管线程的上下文,可以直接执行【!clrstack -a】命令。

1 0:000> !clrstack -a2 OS Thread Id: 0x3864 (0)3 Child SP               IP Call Site4 000000DEBDF7E968 00007ff9ed9f1ce0 ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate) [E:\Visual Studio\.\ExampleCore_3_1_6\Program.cs @ 48]5 PARAMETERS:6         this (<CLR reg>) = 0x0000027eb4409ce0
7         coord (<CLR reg>) = 0x000000debdf7e998
8 
9 000000DEBDF7E970 00007ff9ed9f1a11 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\..\ExampleCore_3_1_6\Program.cs @ 31]10 PARAMETERS:11         args (0x000000DEBDF7EA40) = 0x0000027eb4408ea0
12 LOCALS:13         0x000000DEBDF7EA20 = 0x0000006400000064
14         0x000000DEBDF7EA18 = 0x0000027eb4409ce0
15         0x000000DEBDF7EA10 = 0x0000000000000000

0x0000027eb4409ce0
红色标注的是 this 指针。this 指针指向当前的对象实例。我们使用【!dumpobj】命令查看一下 this 指针。

1 0:000> !dumpobj /d 0x0000027eb4409ce0
2 Name:        ExampleCore_3_1_6.ObjTypes3 MethodTable: 00007ff9edaa02384 EEClass:     00007ff9eda8fca85 Tracked Type: false
6 Size:        48(0x30) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9edaa01c8  4000001       18 ...jTypes+Coordinate  1instance 0000027eb4409cf8 coordinate11 00007ff9ed9d9df8  4000002        8       System.Int32[]  0instance 0000027eb4409d10 intArray12 00007ff9eda7fac8  4000003       10      System.String[]  0 instance 0000027eb4409d80 strArray

Fields 是最重要的信息,它包含了这个对象的元数据。各个域的表示信息可以看下表。

如果要显示这个域本身的内容,有两种方式。其中一种是使用【dp】命令,参数是引用类型实例的地址(000001d9b1c09ce0)和偏移(0x18)。

1 0:000> dp 000001d9b1c09ce0+0x18
2 000001d9`b1c09cf8  00000000`00000000 00000000`00000000
3 000001d9`b1c09d08  00000000`0000000000007ff9`ebf49df84 000001d9`b1c09d18  00000000`00000005 00000002`00000001
5 000001d9`b1c09d28  00000004`00000003 00000000`00000005
6 000001d9`b1c09d38  00000000`0000000000007ff9`ec0644687 000001d9`b1c09d48  00000000`00000000 00000000`00000000
8 000001d9`b1c09d58  00000000`00000000 00000000`00000000
9 000001d9`b1c09d68  00000000`00000000 00007ff9`ec063de0

如果我们想查看【...jTypes+Coordinate】的具体的值,可以执行命令【!dumpvc mt addr】。

1 0:000> !dumpvc 00007ff9edaa01c8 0000027eb4409cf82 Name:        ExampleCore_3_1_6.ObjTypes+Coordinate3 MethodTable: 00007ff9edaa01c84 EEClass:     00007ff9eda8fd205 Size:        32(0x20) bytes6 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll7 Fields:8 MT    Field   Offset                 Type VT     Attr            Value Name9 00007ff9ed961188  4000005        0         System.Int32  1 instance                0xCord10 00007ff9ed961188  4000006        4         System.Int32  1 instance                0yCord11 00007ff9ed961188  4000007        8         System.Int32  1 instance                0 zCord

3.1.3、引用类型的转储
A、基础知识
如果我们想将引用类型转储输出,很简单,直接使用【dumpobj】命令就可以了。【dumpobj】命令的参数可以直接跟引用类型的地址,如果不想输出域的内容,可以使用 -nofields 命令开关。这个命令也有一个简写形式就是【do】。
B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_7
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.exe】。

打开【NTSD】调试器窗口。

我们使用【g】命令,运行调试器,等到调试器暂停,我们按【ctr+c】组合键进入中断模式。

由于我们手动中断调试器,需要执行线程上下文的切换,切换到托管线程的上下文,执行命令【~os】。

1 0:002> ~0s2 coreclr!LookupMap<MethodTable *>::GetValueAt+0x3 [inlined in coreclr!ClassLoader::LoadTypeDefThrowing+0x89]:3 00007ffa`536a5c49 48f7d2          not     rdx

现在就可以使用【!clrstack -a】命令查看托管线程栈了。

1 0:000> !clrstack -a2 OS Thread Id: 0x290c (0)3 Child SP               IP Call Site4 00000068AEBEDE68 00007ffa536a5c49 [ExternalMethodFrame: 00000068aebede68]5 。。。。。。(省略了)6 00000068AEBEE650 00007FF9F3C51987 ExampleCore_3_1_7.Program.Main(System.String[])7 PARAMETERS:8         args (0x00000068AEBEE6A0) = 0x000001afc2408ea0
9 LOCALS:10         0x00000068AEBEE688 = 0x000001afc2409640
                      0x000001afc2409640 红色标注的就是 Person 类型的局部变量 person。
                      废话不多说,直接使用【!dumpobj】或者【!do】命令查看 Person 的详情了。
1 0:000> !dumpobj 0x000001afc2409640
2 Name:        ExampleCore_3_1_7.Person3 MethodTable: 00007ff9f3d093e04 EEClass:     00007ff9f3d11f185 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9f3bc1188  4000001       10         System.Int32  1 instance               20Age11 00007ff9f3c3ec08  4000002        8        System.String  0instance 000001f0546704a0 Name12 
13 
14 0:000> !do 0x000001afc2409640
15 Name:        ExampleCore_3_1_7.Person16 MethodTable: 00007ff9f3d093e017 EEClass:     00007ff9f3d11f1818 Tracked Type: false
19 Size:        32(0x20) bytes20 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll21 Fields:22 MT    Field   Offset                 Type VT     Attr            Value Name23 00007ff9f3bc1188  4000001       10         System.Int32  1 instance               20Age24 00007ff9f3c3ec08  4000002        8        System.String  0 instance 000001f0546704a0 Name

如果不想输出【Fields】的内容,可以使用 -nofields 命令开关。

1 0:000> !dumpobj -nofields 0x000001afc2409640
2 Name:        ExampleCore_3_1_7.Person3 MethodTable: 00007ff9f3d093e04 EEClass:     00007ff9f3d11f185 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll8 
9 0:000> !do -nofields 0x000001afc2409640
10 Name:        ExampleCore_3_1_7.Person11 MethodTable: 00007ff9f3d093e012 EEClass:     00007ff9f3d11f1813 Tracked Type: false
14 Size:        32(0x20) bytes15 File:        E:\Visual Studio 2022\...ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll16 0:000>


2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_7
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。

由于我们手动中断了调试器,当前的线程上下文是调试器的,不是托管线程的,我们必须使用【~0s】命令,切换到托管线程。

1 0:006> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffb`2fdaae74 c3              ret

查看当前的托管线程的调用栈,使用【!clrstack -a】命令。

1 0:000> !clrstack -a2 OS Thread Id: 0x99c (0)3 Child SP               IP Call Site4 0000006E2117E200 00007ffb2fdaae54 [InlinedCallFrame: 0000006e2117e200]5 0000006E2117E200 00007ffaeadb76eb [InlinedCallFrame: 0000006e2117e200]6 。。。。。。(省略了)7 
8 0000006E2117E550 00007ff9f3461987 ExampleCore_3_1_7.Program.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_7\Program.cs @ 8]9 PARAMETERS:10         args (0x0000006E2117E5A0) = 0x000001da19808ea0
11 LOCALS:12         0x0000006E2117E588 = 0x000001da19809640

0x000001da19809640
这个地址就是引用类型 Person 的局部变量 person。我们可以直接使用【!dumpobj】或者【!do】命令。

1 0:000> !DumpObj /d 000001da198096402 Name:        ExampleCore_3_1_7.Person3 MethodTable: 00007ff9f35193e04 EEClass:     00007ff9f3521f185 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9f33d1188  4000001       10         System.Int32  1 instance               20Age11 00007ff9f344ec08  4000002        8        System.String  0instance 0000021aab9904a0 Name12 
13 
14 0:000> !do000001da1980964015 Name:        ExampleCore_3_1_7.Person16 MethodTable: 00007ff9f35193e017 EEClass:     00007ff9f3521f1818 Tracked Type: false
19 Size:        32(0x20) bytes20 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll21 Fields:22 MT    Field   Offset                 Type VT     Attr            Value Name23 00007ff9f33d1188  4000001       10         System.Int32  1 instance               20Age24 00007ff9f344ec08  4000002        8        System.String  0 instance 0000021aab9904a0 Name

我们可以使用 -nofields 命令开关去掉域的内容。

1 0:000> !DumpObj -nofields 000001da198096402 Name:        ExampleCore_3_1_7.Person3 MethodTable: 00007ff9f35193e04 EEClass:     00007ff9f3521f185 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll8 
9 
10 0:000> !do -nofields 000001da1980964011 Name:        ExampleCore_3_1_7.Person12 MethodTable: 00007ff9f35193e013 EEClass:     00007ff9f3521f1814 Tracked Type: false
15 Size:        32(0x20) bytes16 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll


3.1.4、数组的转储
A、基础知识
CLR 将数组作为第一级(first class)引用类型(因为所有数组都是从 System.Array 继承下来的),因此我们可以直接使用【!dumpobj】命令或者【!dumparray】命令转储出数组的在内存中的内容。
数组本身是引用类型,但是它的元素是可以区分为:值类型和引用类型的。由此,我们可以把数组称为值类型的数组和引用类型的数组。虽然有这个区别,但是它们的内存布局是一致的,只不过在查看元素的时候有区别,如果是值类型的元素,可以使用【!dumpvc】命令,如果是引用类型,可以使用【!dumpobj】命令查看。
数组的内存布局如图:

这里的概念不多,但是,要演示的内容很多,而且,内容很简单,就不多说了。

B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_6
1.1)、值类型数组
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器窗口。

继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Arrays),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。

我们进入中断模式后,需要切换线程到托管线程,执行命令【~0s】。

1 0:002> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`eeaeae74 c3              ret

现在,我们就可以查看托管线程栈了,执行命令【!clrstack -a】。

0:000> !clrstack -a
OS Thread Id:
0x3394 (0)
Child SP IP Call Site
00000090F577E5E8 00007ffaeeaeae74 [InlinedCallFrame: 00000090f577e5e8]
00000090F577E5E8 00007ffadb81787a [InlinedCallFrame: 00000090f577e5e8]
。。。。。。(省略了)

00000090F577E770 00007FF9BE1E1A2D ExampleCore_3_1_6.ObjTypes.Main(System.String[])
PARAMETERS:
args (
0x00000090F577E840) = 0x000002282e008ea0LOCALS:0x00000090F577E820 = 0x0000006400000064 0x00000090F577E818 = 0x000002282e009ce0 0x00000090F577E810 = 0x0000000000000000

0x000002282e009ce0
红色标注的就是 ObjTypes 类型的指针,我们可以使用【!do】命令查看它。

1 0:000> !do 0x000002282e009ce0
2 Name:        ExampleCore_3_1_6.ObjTypes3 MethodTable: 00007ff9be2902384 EEClass:     00007ff9be27fca85 Tracked Type: false
6 Size:        48(0x30) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9be2901c8  4000001       18 ...jTypes+Coordinate  1instance 000002282e009cf8 coordinate11 00007ff9be1c9df8  4000002        8       System.Int32[]  0instance 000002282e009d10 intArray12 00007ff9be26fac8  4000003       10      System.String[]  0instance 000002282e009d80 strArray13 0:000>

这里我们开始关注 intArray 值类型数组,也就是红色标注的。intArray 数组的地址是:000002282e009d10,有了地址,我们就可以使用【!dumpobj 000002282e009d10】命令转储出详情。

1 0:000> !dumpobj 000002282e009d102 Name:        System.Int32[]3 MethodTable: 00007ff9be1c9df84 EEClass:     00007ff9be1c9d785 Tracked Type: false
6 Size:        44(0x2c) bytes7 Array:       Rank 1, Number of elements 5, Type Int328 Fields:9 None

验证了我们的说法,输出了数组的名称,维度(Rank),元素个数(Number of elements)等。我们依然可以使用【dp
000002282e009d10
】命令输出详情。

我们使用【!dumpmt 00007ff9`be1c9df8】命令是不是数组的方法表。

1 0:000> !dumpmt 00007ff9`be1c9df82 EEClass:         00007FF9BE1C9D783 Module:          00007FF9BE0840004 Name:            System.Int32[]5 mdToken:         0000000002000000
6 File:            C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll7 BaseSize:        0x18
8 ComponentSize:   0x4
9 DynamicStatics:  false
10 ContainsPointers false
11 Slots in VTable: 28
12 Number of IFaces in IFaceMap: 6

当然,我们可以直接使用【!dumparray
000002282e009d10
】命令查看 intArray 数组详情,这个命令更直观。

1 0:000> !dumparray 000002282e009d102 Name:        System.Int32[]3 MethodTable: 00007ff9be1c9df84 EEClass:     00007ff9be1c9d785 Size:        44(0x2c) bytes6 Array:       Rank 1, Number of elements 5, Type Int327 Element Methodtable: 00007ff9be1511888 [0] 000002282e009d209 [1] 000002282e009d2410 [2] 000002282e009d2811 [3] 000002282e009d2c12 [4] 000002282e009d3013 
14 0:000> !dumpvc 00007ff9be151188 000002282e009d2015 Name:        System.Int3216 MethodTable: 00007ff9be15118817 EEClass:     00007ff9be141e2018 Size:        24(0x18) bytes19 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll20 Fields:21 MT    Field   Offset                 Type VT     Attr            Value Name22 00007ff9be151188  400051d        0         System.Int32  1 instance                1m_value23 
24 0:000> !dumpvc 00007ff9be151188 000002282e009d2425 Name:        System.Int3226 MethodTable: 00007ff9be15118827 EEClass:     00007ff9be141e2028 Size:        24(0x18) bytes29 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll30 Fields:31 MT    Field   Offset                 Type VT     Attr            Value Name32 00007ff9be151188  400051d        0         System.Int32  1 instance                2m_value33 
34 0:000> !dumpvc 00007ff9be151188 000002282e009d2835 Name:        System.Int3236 MethodTable: 00007ff9be15118837 EEClass:     00007ff9be141e2038 Size:        24(0x18) bytes39 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll40 Fields:41 MT    Field   Offset                 Type VT     Attr            Value Name42 00007ff9be151188  400051d        0         System.Int32  1 instance                3m_value43 
44 0:000> !dumpvc 00007ff9be151188 000002282e009d2c45 Name:        System.Int3246 MethodTable: 00007ff9be15118847 EEClass:     00007ff9be141e2048 Size:        24(0x18) bytes49 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll50 Fields:51 MT    Field   Offset                 Type VT     Attr            Value Name52 00007ff9be151188  400051d        0         System.Int32  1 instance                4m_value53 
54 0:000> !dumpvc 00007ff9be151188 000002282e009d3055 Name:        System.Int3256 MethodTable: 00007ff9be15118857 EEClass:     00007ff9be141e2058 Size:        24(0x18) bytes59 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll60 Fields:61 MT    Field   Offset                 Type VT     Attr            Value Name62 00007ff9be151188  400051d        0         System.Int32  1 instance                5m_value63 0:000>


1.2)、引用类型数组
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器窗口。

继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Arrays),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。

我们进入中断模式后,需要切换线程到托管线程,执行命令【~0s】。

1 0:002> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`eeaeae74 c3              ret

现在,我们就可以查看托管线程栈了,执行命令【!clrstack -a】。

1 0:000> !clrstack -a2 OS Thread Id: 0x3394 (0)3 Child SP               IP Call Site4 00000090F577E5E8 00007ffaeeaeae74 [InlinedCallFrame: 00000090f577e5e8]5 00000090F577E5E8 00007ffadb81787a [InlinedCallFrame: 00000090f577e5e8]6 。。。。。。(省略了)7 
8 00000090F577E770 00007FF9BE1E1A2D ExampleCore_3_1_6.ObjTypes.Main(System.String[])9 PARAMETERS:10         args (0x00000090F577E840) = 0x000002282e008ea0
11 LOCALS:12         0x00000090F577E820 = 0x0000006400000064
13         0x00000090F577E818 = 0x000002282e009ce0
14         0x00000090F577E810 = 0x0000000000000000

0x000002282e009ce0
红色标注的就是 ObjTypes 类型的指针,我们可以使用【!do】命令查看它。

1 0:000> !do 0x000002282e009ce0
2 Name:        ExampleCore_3_1_6.ObjTypes3 MethodTable: 00007ff9be2902384 EEClass:     00007ff9be27fca85 Tracked Type: false
6 Size:        48(0x30) bytes7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9be2901c8  4000001       18 ...jTypes+Coordinate  1instance 000002282e009cf8 coordinate11 00007ff9be1c9df8  4000002        8       System.Int32[]  0instance 000002282e009d10 intArray12 00007ff9be26fac8  4000003       10      System.String[]  0 instance 000002282e009d80 strArray

这里我们开始关注 strArray 引用类型数组,也就是红色标注的。我们先使用【dp
000002282e009d80
】命令转储一下。

执行【!dumpmt 00007ff9`be26fac8】命令查看 strArray 数组的方法表。

1 0:000> !dumpmt 00007ff9`be26fac82 EEClass:         00007FF9BE11C4403 Module:          00007FF9BE0840004 Name:            System.String[]5 mdToken:         0000000002000000
6 File:            C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll7 BaseSize:        0x18
8 ComponentSize:   0x8
9 DynamicStatics:  false
10 ContainsPointers true
11 Slots in VTable: 28
12 Number of IFaces in IFaceMap: 6

我们可以使用【!dumpobj】命令查看查看元素的具体信息。

1 0:000> !do -nofields 00000268`c0280b782 Name:        System.String3 MethodTable: 00007ff9be1cec084 EEClass:     00007ff9be1aa5005 Tracked Type: false
6 Size:        36(0x24) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      Welcome9 
10 0:000> !do -nofields 00000268`c0280ba011 Name:        System.String12 MethodTable: 00007ff9be1cec0813 EEClass:     00007ff9be1aa50014 Tracked Type: false
15 Size:        26(0x1a) bytes16 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll17 String:      to18 
19 0:000> !do -nofields 00000268`c0280bc020 Name:        System.String21 MethodTable: 00007ff9be1cec0822 EEClass:     00007ff9be1aa50023 Tracked Type: false
24 Size:        38(0x26) bytes25 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll26 String:      Advanced27 
28 0:000> !do -nofields 00000268`c0280be829 Name:        System.String30 MethodTable: 00007ff9be1cec0831 EEClass:     00007ff9be1aa50032 Tracked Type: false
33 Size:        30(0x1e) bytes34 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll35 String:      .NET36 
37 0:000> !do -nofields 00000268`c0280c0838 Name:        System.String39 MethodTable: 00007ff9be1cec0840 EEClass:     00007ff9be1aa50041 Tracked Type: false
42 Size:        40(0x28) bytes43 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll44 String:      Debugging

当然,还有一个更容易,更直接的命令,就是【!dumparray】可以输出所有数组的信息。

内容很简单,就不赘述了。

1 0:000> !do -nofields 00000268c0280b782 Name:        System.String3 MethodTable: 00007ff9be1cec084 EEClass:     00007ff9be1aa5005 Tracked Type: false
6 Size:        36(0x24) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      Welcome

可以继续使用【!do】命令查看元素的内容,这里就不演示了。


2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_6
2.1)、值类型数组
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Arrays),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。

1 0:001> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`eeaeae74 c3              ret

现在我们需要查看一下托管线程的调用栈,执行命令【!clrstack -a】。

1 0:000> !clrstack -a2 OS Thread Id: 0x3144 (0)3 Child SP               IP Call Site4 0000000C91F7E668 00007ffaeeaeae74 [InlinedCallFrame: 0000000c91f7e668]5 0000000C91F7E668 00007ffa375c787a [InlinedCallFrame: 0000000c91f7e668]6 。。。。。。(省略了)7 
8 0000000C91F7E7F0 00007ff9b6fa1a2d ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 36]9 PARAMETERS:10         args (0x0000000C91F7E8C0) = 0x000001cfe3408ea0
11 LOCALS:12         0x0000000C91F7E8A0 = 0x0000006400000064
13         0x0000000C91F7E898 = 0x000001cfe3409ce0
14         0x0000000C91F7E890 = 0x0000000000000000

0x000001cfe3409ce0
标红的就是 ObjTypes 类型的指针,执行命令【
!do 0x000001cfe3409ce0
】确认一下。

1 0:000> !do 0x000001cfe3409ce0
2 Name:        ExampleCore_3_1_6.ObjTypes3 MethodTable: 00007ff9b70502384 EEClass:     00007ff9b703fca85 Tracked Type: false
6 Size:        48(0x30) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff9b70501c8  4000001       18 ...jTypes+Coordinate  1instance 000001cfe3409cf8 coordinate11 00007ff9b6f89df8  4000002        8       System.Int32[]  0instance 000001cfe3409d10 intArray12 00007ff9b702fac8  4000003       10      System.String[]  0 instance 000001cfe3409d80 strArray

我们看到了
ExampleCore_3_1_6.ObjTypes
类型有两个数组的域,分别是:
intArray
和 strArray。我们获取了
intArray
数组的地址:
000001cfe3409d10
,我们使用【!dumpobj
000001cfe3409d10
】命令查看一下 intArray 数组的详情。

1 0:000> !dumpobj 000001cfe3409d102 Name:        System.Int32[]3 MethodTable: 00007ff9b6f89df84 EEClass:     00007ff9b6f89d785 Tracked Type: false
6 Size:        44(0x2c) bytes7 Array:       Rank 1, Number of elements 5, Type Int32 (Print Array)8 Fields:9 None

【!dumpobj】命令输出了数组的名称(
Name
),方法表(
MethodTable
),数组的维度(Rank),数组的类型(
Type
)和数组的元素个数(Number of elements),但是我们没有看到数组的元素内容。如果想查看数组的内容,我们可以使用【dp】命令。

1 0:000>dp 000001cfe3409d102 000001cf`e3409d10  00007ff9`b6f89df8 00000000`00000005
3 000001cf`e3409d20  00000002`00000001 00000004`00000003
4 000001cf`e3409d30  00000000`00000005 00000000`00000000
5 000001cf`e3409d40  00007ff9`b70a4468 00000000`00000000
6 000001cf`e3409d50  00000000`00000000 00000000`00000000
7 000001cf`e3409d60  00000000`00000000 00000000`00000000
8 000001cf`e3409d70  00007ff9`b70a3de0 00000000`00000000
9 000001cf`e3409d80  00007ff9`b702fac8 00000000`00000005

00007ff9`b6f89df8
第一个值就是数组本身的方法表的地址

00000000`00000005

表示的数组元素个数,是 5 个元素。
00000002`00000001

00000004`00000003

00000000
`00000005

就是数组的元素内容,分别是:1,2,3,4,5。
00007ff9`b6f89df8
这个地址是数组本身的方法表,我们可以执行【!dumpmt
00007ff9`b6f89df8
】命令验证一下。

1 0:000> !dumpmt 00007ff9`b6f89df82 EEClass:             00007ff9b6f89d783 Module:              00007ff9b6e440004 Name:                System.Int32[]5 mdToken:             0000000002000000
6 File:                C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize:            0x18
9 ComponentSize:       0x4
10 DynamicStatics:      false
11 ContainsPointers:    false
12 Slots in VTable:     28
13 Number of IFaces in IFaceMap: 6                     


2.2)、引用类型数组
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Arrays),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。

1 0:001> ~0s
2 ntdll!NtDeviceIoControlFile+0x14:
3 00007ffa`eeaeae74 c3 ret

现在我们需要查看一下托管线程的调用栈,执行命令【!clrstack -a】。

1 0:000> !clrstack -a
2 OS Thread Id: 0x3144 (0)
3 Child SP IP Call Site
4 0000000C91F7E668 00007ffaeeaeae74 [InlinedCallFrame: 0000000c91f7e668]
5 0000000C91F7E668 00007ffa375c787a [InlinedCallFrame: 0000000c91f7e668]
6 。。。。。。(省略了)
7
8 0000000C91F7E7F0 00007ff9b6fa1a2d ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 36]
9 PARAMETERS:
10 args (0x0000000C91F7E8C0) = 0x000001cfe3408ea0
11 LOCALS:
12 0x0000000C91F7E8A0 = 0x0000006400000064
13 0x0000000C91F7E898 = 0x000001cfe3409ce0
14 0x0000000C91F7E890 = 0x0000000000000000

0x000001cfe3409ce0
标红的就是 ObjTypes 类型的指针,执行命令【
!do 0x000001cfe3409ce0】确认一下。

1 0:000> !do 0x000001cfe3409ce0
2 Name: ExampleCore_3_1_6.ObjTypes
3 MethodTable: 00007ff9b7050238
4 EEClass: 00007ff9b703fca8
5 Tracked Type: false
6 Size: 48(0x30) bytes
7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff9b70501c8 4000001 18 ...jTypes+Coordinate 1instance 000001cfe3409cf8 coordinate
11 00007ff9b6f89df8 4000002 8 System.Int32[] 0instance 000001cfe3409d10 intArray12 00007ff9b702fac8 4000003 10 System.String[] 0 instance 000001cfe3409d80 strArray

我们看到了
ExampleCore_3_1_6.ObjTypes 类型有两个数组的域,分别是:
intArray 和 strArray。这次我们关注 strArray 字符串数据,我们获取了
strArray
数组的地址:
000001cfe3409d80
,我们使用【!dumpobj
000001cfe3409d80
】命令查看一下
strArray
数组的详情。

1 0:000> !dumpobj /d 000001cfe3409d802 Name:        System.String[]3 MethodTable: 00007ff9b702fac84 EEClass:     00007ff9b6edc4405 Tracked Type: false
6 Size:        64(0x40) bytes7 Array:       Rank 1, Number of elements 5, Type CLASS (Print Array)8 Fields:9 None

我们也可以使用【dp
000001cfe3409d80
】命令查看它的显示。

0:000>dp 000001cfe3409d80
000001cf`e3409d80 00007ff9`b702fac8
00000000`00000005000001cf`e3409d9000000210`75660b78 00000210`75660ba0
000001cf`e3409da0
00000210`75660bc0 00000210`75660be8
000001cf`e3409db0
00000210`75660c08 00000000`00000000000001cf`e3409dc0 00007ff9`b6f6666668800000000`00000064000001cf`e3409dd000000000`0000000000007ff9`b6f66666688
000001cf`e3409de0
00000000`00000064 00000000`00000000000001cf`e3409df0 00007ff9`b6f6666668800000000`00000064

00007ff9`b702fac8
就是 strArray 数组的方法表,我们可以使用【!dumpmt
00007ff9`b702fac8
】验证这点。

1 0:000> !dumpmt 00007ff9`b702fac82 EEClass:             00007ff9b6edc4403 Module:              00007ff9b6e440004 Name:                System.String[]5 mdToken:             0000000002000000
6 File:                C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize:            0x18
9 ComponentSize:       0x8
10 DynamicStatics:      false
11 ContainsPointers:    true
12 Slots in VTable:     28
13 Number of IFaces in IFaceMap: 6

【dp
000001cfe3409d80
】命令的
00000000`
00000005

这个值就是 strArray 数组的元素个数。【dp
000001cfe3409d80】
命令的5个输出就是数组元素的值,由于它们是引用类型,我们可以依次使用【!dumpobj】命令输出它们的内容。

0:000> !dumpobj 00000210`75660b78
Name: System.String
MethodTable: 00007ff9b6f8ec08
EEClass: 00007ff9b6f6a500
Tracked Type:
falseSize:36(0x24) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.2\System.Private.CoreLib.dll
String: Welcome
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9b6f66666688 400033b
8 System.Int32 1 instance 7_stringLength
00007ff9b6f1b538 400033c c System.Char
1 instance 57_firstChar
00007ff9b6f8ec08 400033a c8 System.String
0 static 0000021075660008Empty0:000> !dumpobj 00000210`75660ba0
Name: System.String
MethodTable: 00007ff9b6f8ec08
EEClass: 00007ff9b6f6a500
Tracked Type:
falseSize:26(0x1a) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.2\System.Private.CoreLib.dll
String: to
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9b6f66666688 400033b
8 System.Int32 1 instance 2_stringLength
00007ff9b6f1b538 400033c c System.Char
1 instance 74_firstChar
00007ff9b6f8ec08 400033a c8 System.String
0 static 0000021075660008Empty0:000> !dumpobj 00000210`75660bc0
Name: System.String
MethodTable: 00007ff9b6f8ec08
EEClass: 00007ff9b6f6a500
Tracked Type:
falseSize:38(0x26) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.2\System.Private.CoreLib.dll
String: Advanced
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9b6f66666688 400033b
8 System.Int32 1 instance 8_stringLength
00007ff9b6f1b538 400033c c System.Char
1 instance 41_firstChar
00007ff9b6f8ec08 400033a c8 System.String
0 static 0000021075660008Empty0:000> !dumpobj 00000210`75660be8
Name: System.String
MethodTable: 00007ff9b6f8ec08
EEClass: 00007ff9b6f6a500
Tracked Type:
falseSize:30(0x1e) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.2\System.Private.CoreLib.dll
String: .NET
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9b6f66666688 400033b
8 System.Int32 1 instance 4_stringLength
00007ff9b6f1b538 400033c c System.Char
1instance 2e _firstChar
00007ff9b6f8ec08 400033a c8 System.String
0 static 0000021075660008Empty0:000> !dumpobj 00000210`75660c08
Name: System.String
MethodTable: 00007ff9b6f8ec08
EEClass: 00007ff9b6f6a500
Tracked Type:
falseSize:40(0x28) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.2\System.Private.CoreLib.dll
String: Debugging
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9b6f66666688 400033b
8 System.Int32 1 instance 9_stringLength
00007ff9b6f1b538 400033c c System.Char
1 instance 44_firstChar
00007ff9b6f8ec08 400033a c8 System.String
0 static 0000021075660008 Empty

红色标注的就是数组元素具体的值。
上面的命令有些繁琐,我们可以直接使用【!dumparray】命令获取我们想要的数据。
我们直接输出 intArray,使用【!dumparray】命令。

1 0:000> !dumparray 000001cfe3409d102 Name:        System.Int32[]3 MethodTable: 00007ff9b6f89df84 EEClass:     00007ff9b6f89d785 Size:        44(0x2c) bytes6 Array:       Rank 1, Number of elements 5, Type Int327 Element Methodtable: 00007ff9b6f666666888 [0] 000001cfe3409d209 [1] 000001cfe3409d2410 [2] 000001cfe3409d2811 [3] 000001cfe3409d2c12 [4] 000001cfe3409d3013 
14 0:000> !DumpVC /d 00007ff9b6f66666688 000001cfe3409d2015 Name:        System.Int3216 MethodTable: 00007ff9b6f6666668817 EEClass:     00007ff9b6f01e2018 Size:        24(0x18) bytes19 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll20 Fields:21 MT    Field   Offset                 Type VT     Attr            Value Name22 00007ff9b6f66666688  400051d        0         System.Int32  1 instance                1m_value23 
24 0:000> !DumpVC /d 00007ff9b6f66666688 000001cfe3409d2425 Name:        System.Int3226 MethodTable: 00007ff9b6f6666668827 EEClass:     00007ff9b6f01e2028 Size:        24(0x18) bytes29 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll30 Fields:31 MT    Field   Offset                 Type VT     Attr            Value Name32 00007ff9b6f66666688  400051d        0         System.Int32  1 instance                2m_value33 
34 0:000> !DumpVC /d 00007ff9b6f66666688 000001cfe3409d2835 Name:        System.Int3236 MethodTable: 00007ff9b6f6666668837 EEClass:     00007ff9b6f01e2038 Size:        24(0x18) bytes39 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll40 Fields:41 MT    Field   Offset                 Type VT     Attr            Value Name42 00007ff9b6f66666688  400051d        0         System.Int32  1 instance                3m_value43 
44 0:000> !DumpVC /d 00007ff9b6f66666688 000001cfe3409d2c45 Name:        System.Int3246 MethodTable: 00007ff9b6f6666668847 EEClass:     00007ff9b6f01e2048 Size:        24(0x18) bytes49 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll50 Fields:51 MT    Field   Offset                 Type VT     Attr            Value Name52 00007ff9b6f66666688  400051d        0         System.Int32  1 instance                4m_value53 
54 0:000> !DumpVC /d 00007ff9b6f66666688 000001cfe3409d3055 Name:        System.Int3256 MethodTable: 00007ff9b6f6666668857 EEClass:     00007ff9b6f01e2058 Size:        24(0x18) bytes59 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll60 Fields:61 MT    Field   Offset                 Type VT     Attr            Value Name62 00007ff9b6f66666688  400051d        0         System.Int32  1 instance                5 m_value

我们直接输出strArray,使用【!dumparray】命令。

1 0:000> !dumparray 000001cfe3409d802 Name:        System.String[]3 MethodTable: 00007ff9b702fac84 EEClass:     00007ff9b6edc4405 Size:        64(0x40) bytes6 Array:       Rank 1, Number of elements 5, Type CLASS7 Element Methodtable: 00007ff9b6f8ec088 [0] 0000021075660b789 [1] 0000021075660ba010 [2] 0000021075660bc011 [3] 0000021075660be812 [4] 0000021075660c0813 
14 0:000> !DumpObj /d 0000021075660b7815 Name:        System.String16 MethodTable: 00007ff9b6f8ec0817 EEClass:     00007ff9b6f6a50018 Tracked Type: false
19 Size:        36(0x24) bytes20 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll21 String:      Welcome22 Fields:23 MT    Field   Offset                 Type VT     Attr            Value Name24 00007ff9b6f66666688  400033b        8         System.Int32  1 instance                7_stringLength25 00007ff9b6f1b538  400033c        c          System.Char  1 instance               57_firstChar26 00007ff9b6f8ec08  400033a       c8        System.String  0   static 0000021075660008Empty27 
28 0:000> !DumpObj /d 0000021075660ba029 Name:        System.String30 MethodTable: 00007ff9b6f8ec0831 EEClass:     00007ff9b6f6a50032 Tracked Type: false
33 Size:        26(0x1a) bytes34 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll35 String:      to36 Fields:37 MT    Field   Offset                 Type VT     Attr            Value Name38 00007ff9b6f66666688  400033b        8         System.Int32  1 instance                2_stringLength39 00007ff9b6f1b538  400033c        c          System.Char  1 instance               74_firstChar40 00007ff9b6f8ec08  400033a       c8        System.String  0   static 0000021075660008Empty41 
42 0:000> !DumpObj /d 0000021075660bc043 Name:        System.String44 MethodTable: 00007ff9b6f8ec0845 EEClass:     00007ff9b6f6a50046 Tracked Type: false
47 Size:        38(0x26) bytes48 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll49 String:      Advanced50 Fields:51 MT    Field   Offset                 Type VT     Attr            Value Name52 00007ff9b6f66666688  400033b        8         System.Int32  1 instance                8_stringLength53 00007ff9b6f1b538  400033c        c          System.Char  1 instance               41_firstChar54 00007ff9b6f8ec08  400033a       c8        System.String  0   static 0000021075660008Empty55 
56 0:000> !DumpObj /d 0000021075660be857 Name:        System.String58 MethodTable: 00007ff9b6f8ec0859 EEClass:     00007ff9b6f6a50060 Tracked Type: false
61 Size:        30(0x1e) bytes62 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll63 String:      .NET64 Fields:65 MT    Field   Offset                 Type VT     Attr            Value Name66 00007ff9b6f66666688  400033b        8         System.Int32  1 instance                4_stringLength67 00007ff9b6f1b538  400033c        c          System.Char  1instance               2e _firstChar68 00007ff9b6f8ec08  400033a       c8        System.String  0   static 0000021075660008Empty69 
70 0:000> !DumpObj /d 0000021075660c0871 Name:        System.String72 MethodTable: 00007ff9b6f8ec0873 EEClass:     00007ff9b6f6a50074 Tracked Type: false
75 Size:        40(0x28) bytes76 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll77 String:      Debugging78 Fields:79 MT    Field   Offset                 Type VT     Attr            Value Name80 00007ff9b6f66666688  400033b        8         System.Int32  1 instance                9_stringLength81 00007ff9b6f1b538  400033c        c          System.Char  1 instance               44_firstChar82 00007ff9b6f8ec08  400033a       c8        System.String  0   static 0000021075660008 Empty



3.1.5、栈上对象的转储
A、基础知识
我们可以使用【!clrstack】命令找出每个栈帧的局部变量和参数,但是有时候需要对栈进行更深入的分析,就可以使用【!dumpstackobjects】命令。它能对栈进行遍历,并输出栈上所有的托管对象。
命令格式:!DumpStackObjects [-verify] [top stack [bottom stack]],verify 选项表示对找到的每个托管对象进行一个验证过程,这对对象是否被破坏来说非常有用。如果想对这个命令的输出进行限制,需要指定一个范围(栈顶[top stack]和栈低[bottom stack])。
这个命令确实有点长,当然也有一个缩写形式:dso。

B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_6
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器窗口。

继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Generics),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。

我们进入中断模式后,需要切换线程到托管线程,执行命令【~0s】。

1 0:002> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`eeaeae74 c3              ret

我们直接执行【!dso】或者【!dumpstackobjects】命令。

1 0:000> !dso2 OS Thread Id: 0x1d8c (0)3 RSP/REG          Object           Name4 000000F45E99E8A8 00000235eb220b30 Interop+INPUT_RECORD5 000000F45E99E918 00000235eb220b30 Interop+INPUT_RECORD6 000000F45E99E920 000001f559009c98 System.Object7 000000F45E99E950 00000235eb220560 System.String    Press any key to continue(Generics)8 000000F45E99E9B8 000001f559009c38 System.IO.TextWriter+SyncTextWriter9 000000F45E99E9C0 000001f559009ce0 ExampleCore_3_1_6.ObjTypes10 000000F45E99EA28 000001f559009ce0 ExampleCore_3_1_6.ObjTypes11 000000F45E99EA48 000001f559009ce0 ExampleCore_3_1_6.ObjTypes12 000000F45E99EA70 000001f559008ea0 System.String[]13 000000F45E99EB18 000001f559008ea0 System.String[]14 000000F45E99ED10 000001f559008ea0 System.String[]15 000000F45E99ED18 000001f559008ea0 System.String[]16 000000F45E99EE30 000001f559008ea0 System.String[]17 000000F45E99EEB0 000001f559008eb8 System.String    E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll18 000000F45E99EEC0 000001f559008ea0 System.String[]19 000000F45E99EED0 000001f559008e80 System.String[]20 000000F45E99EF08 000001f559008eb8 System.String    E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll21 000000F45E99F0B8 000001f559008ea0 System.String[]

在输出结果中有 3 列:
SP/
REG
:该列表式托管对象所在的栈地址。
Object
:是托管对象的地址。我们可以使用【!dumpobj】命令查看对象的详情。
Name
:该列表示托管对象的名称。
在输出的内容中,我们看到有几行是完全相同的,这也在预料之中,因为对象可能从一个函数传递另一个函数,其中每个栈帧都包含了对同一个对象的引用。

2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_6
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Generics),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。

1 0:007> ~0s2 ntdll!NtDeviceIoControlFile+0x14:3 00007ffa`eeaeae74 c3              ret

直接执行命令【!
DumpStackObjects
】。

1 0:000> !DumpStackObjects2 OS Thread Id: 0x3a04 (0)3           SP/REG           Object Name4 00c21e57e918     026cd5cb1378 System.String5     00c21e57ea28     026cd5cb0b30 Interop+INPUT_RECORD6     00c21e57ea98     026cd5cb0b30 Interop+INPUT_RECORD7 00c21e57eaa0     022c43c09c98 System.Object8 00c21e57ead0     026cd5cb0560 System.String9     00c21e57eb38     022c43c09c38 System.IO.TextWriter+SyncTextWriter10 00c21e57eb40     022c43c09ce0 ExampleCore_3_1_6.ObjTypes11 00c21e57eba8     022c43c09ce0 ExampleCore_3_1_6.ObjTypes12 00c21e57ebc8     022c43c09ce0 ExampleCore_3_1_6.ObjTypes13 00c21e57ebf0     022c43c08ea0 System.String[]14 00c21e57ec98     022c43c08ea0 System.String[]15 00c21e57ee90     022c43c08ea0 System.String[]16 00c21e57ee98     022c43c08ea0 System.String[]17 00c21e57efb0     022c43c08ea0 System.String[]18 00c21e57f030     022c43c08eb8 System.String19 00c21e57f040     022c43c08ea0 System.String[]20 00c21e57f050     022c43c08e80 System.String[]21 00c21e57f088     022c43c08eb8 System.String22     00c21e57f238     022c43c08ea0 System.String[]

在输出结果中有 3 列:
SP/
REG

:该列表式托管对象所在的栈地址。
Object
:是托管对象的地址。我们可以使用【!dumpobj】命令查看对象的详情。
Name
:该列表示托管对象的名称。
在输出的内容中,我们看到有几行是完全相同的,这也在预料之中,因为对象可能从一个函数传递另一个函数,其中每个栈帧都包含了对同一个对象的引用。


3.1.6、找出对象的大小
A、基础知识
我们能找到一个对象的准确大小,对于我们排查一些问题是很有用的。我们知道【!dumpobj】命令可以输出一个对象的大小。如图:

这种方法对我们找到单个对象的确切大小是非常有用的。但是,对象有时候会引用其他对象,而这些其他对象有可能引用另外的对象,以此类推。此时,知道对象的总体大小(包括遍历每个类型域的大小)能够有助于了解一些非常大和复杂的对象。我们可以使用【!objsize】命令。


B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_8
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.exe】。

打开【NTSD】调试器窗口。

我们通过【g】命令直接运行调试器,知道调试器输出:名称:PatrickLiu,地址:China-冀州-直隶总督府-广平大街23号-213339。此时,调试器卡住,我们按【ctrl+c】进入调试器中断模式。

我们切换到托管线程0下,执行命令【~0s】。

1 0:001> ~0s2 ucrtbase!strcmp+0x49:3 00007ffa`ec654e69 4e8d0c10        lea     r9,[rax+r10]

我们使用【!clrstack -a】命令查看一下托管线程的调用栈。

1 0:000> !clrstack -a2 OS Thread Id: 0x2798 (0)3 Child SP               IP Call Site4 0000006CF777E238 00007ffaec654e69 [ExternalMethodFrame: 0000006cf777e238]5 。。。。。。(省略了)6 0000006CF777E9A0 00007FF996FC1B34 ExampleCore_3_1_8.Program.Main(System.String[])7 PARAMETERS:8         args (0x0000006CF777EA40) = 0x0000023e84c08ea0
9 LOCALS:10         0x0000006CF777EA28 = 0x0000023e84c09c98
11         0x0000006CF777EA00 = 0x0000000000000000

0x0000023e84c09c98
红色标注的就是 Person 类型的局部变量 person 的地址,我们使用【!dumpobj
0x0000023e84c09c98
】查看一下它的详情。

1 0:000> !dumpobj 0x0000023e84c09c98
2 Name:        ExampleCore_3_1_8.Person3 MethodTable: 00007ff9970794804 EEClass:     00007ff9970820085 Tracked Type: false
6 Size:        40(0x28) bytes7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff996f31188  4000001       18         System.Int32  1 instance               32 <Age>k__BackingField11 00007ff996faec08  4000002        8        System.String  0 instance 0000027f16be0500 <Name>k__BackingField12 00007ff99707d9a0  4000003       10 ...ore_3_1_8.Address  0 instance 0000023e84c09cc0 <HomeAddress>k__BackingField

红色标注就是它的大小。我们再使用【!ObjSize
0x0000023e84c09c98
】命令查看一下它的大小。

1 0:000> !ObjSize 0x0000023e84c09c98
2 sizeof(0000023E84C09C98) = 320 (0x140) bytes (ExampleCore_3_1_8.Person)

【!objsize】命令和【!dumpobj】命令输出的大小区别还是挺大的。
【!objsize】命令在【NTSD】调试器模式下,如果没有指定任何地址,它会输出当前进程中所有托管线程上的所有托管对象的大小。Windbg Preview 执行错误:
ystem.ArgumentException: Could not parse target object address: 0

1 0:000> !ObjSize2 Thread 2798 ([ExternalMethodFrame: 0000006cf777e238] ): 0000006cf777e810 -> 0000023e84c0b148: 4120 (0x1018) bytes (System.Byte[])3 Thread 2798 ([ExternalMethodFrame: 0000006cf777e238] ): 0000006cf777e818 -> 0000027f16be0a90: 32 (0x20) bytes (System.String)4 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): rbp: 0000023e84c0b0b8: 232 (0xe8) bytes (System.Text.DecoderDBCS)5 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): rsi: 0000023e84c0b148: 4120 (0x1018) bytes (System.Byte[])6 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): r15: 0000023e84c0c178: 32792 (0x8018) bytes (System.Char[])7 Thread 2798 (System.IO.StreamReader.ReadBuffer() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 604]): rbx: 0000023e84c0b058: 37416 (0x9228) bytes (System.IO.StreamReader)8 Thread 2798 (System.IO.StreamReader.Read() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 338]): rbx: 0000023e84c0b058: 37416 (0x9228) bytes (System.IO.StreamReader)9 Thread 2798 (System.IO.SyncTextReader.Read() [/_/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @ 53]): rbp+10: 0000006cf777e970 -> 0000023e84c14190: 37440 (0x9240) bytes (System.IO.SyncTextReader)10 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-58: 0000006cf777e9d8 -> 0000006cf777ea00 (interior): 0 (0x0) bytes (unknown type)11 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-48: 0000006cf777e9e8 -> 0000006cf777ea00 (interior): 0 (0x0) bytes (unknown type)12 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-68: 0000006cf777e9c8 -> 0000023e84c0a9d8: 120 (0x78) bytes (System.String)13 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-60: 0000006cf777e9d0 -> 0000023e84c09cc0: 232 (0xe8) bytes (ExampleCore_3_1_8.Address)14 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-50: 0000006cf777e9e0 -> 0000027f16be0500: 48 (0x30) bytes (System.String)15 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-40: 0000006cf777e9f0 -> 0000023e84c09cc0: 232 (0xe8) bytes (ExampleCore_3_1_8.Address)16 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-38: 0000006cf777e9f8 -> 0000023e84c09c98: 320 (0x140) bytes (ExampleCore_3_1_8.Person)17 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-8: 0000006cf777ea28 -> 0000023e84c09c98: 320 (0x140) bytes (ExampleCore_3_1_8.Person)18 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp+10: 0000006cf777ea40 -> 0000023e84c08ea0: 24 (0x18) bytes (System.String[])19 Failed to enumerate GC references.20 Failed to walk thread 31d421 Handle (strong): 0000023E804A1378 -> 0000023E84C0A2D8: 1024 (0x400) bytes (System.Object[])22 Handle (strong): 0000023E804A1380 -> 0000023E84C0A228: 176 (0xb0) bytes (System.Object[])23 Handle (strong): 0000023E804A1388 -> 0000023E84C0A180: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider)24 Handle (strong): 0000023E804A1390 -> 0000023E84C0A0D0: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider)25 Handle (strong): 0000023E804A1398 -> 0000023E82402020: 8232 (0x2028) bytes (System.Object[])26 Handle (strong): 0000023E804A13A0 -> 0000023E84C092F0: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider)27 Handle (strong): 0000023E804A13A8 -> 0000023E84C095B8: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider)28 Handle (strong): 0000023E804A13B0 -> 0000023E84C09508: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider)29 Handle (strong): 0000023E804A13B8 -> 0000023E84C09240: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider)30 Handle (strong): 0000023E804A13C8 -> 0000023E84C00188: 128 (0x80) bytes (System.ExecutionEngineException)31 Handle (strong): 0000023E804A13D0 -> 0000023E84C00108: 128 (0x80) bytes (System.StackOverflowException)32 Handle (strong): 0000023E804A13D8 -> 0000023E84C00088: 128 (0x80) bytes (System.OutOfMemoryException)33 Handle (strong): 0000023E804A13E0 -> 0000023E84C00028: 96 (0x60) bytes (System.Int32[])34 Handle (strong): 0000023E804A13E8 -> 0000023E82400028: 87704 (0x15698) bytes (System.Object[])35 Handle (pinned): 0000023E804A15F8 -> 0000023E84C00208: 24 (0x18) bytes (System.Object)


2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_8
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:名称:PatrickLiu,地址:China-冀州-直隶总督府-广平大街23号-213339,我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
切换到托管线程,执行命令【~0s】。

1 0:007> ~0s2 ntdll!NtReadFile+0x14:3 00007ffa`eeaeae54 c3              ret

我们执行命令【!clrstack -a】查看托管线程栈。

1 0:000> !clrstack -a2 OS Thread Id: 0x3cf8 (0)3 Child SP               IP Call Site4 000000CD8377E2C0 00007ffaeeaeae54 [InlinedCallFrame: 000000cd8377e2c0]5 000000CD8377E2C0 00007ffa2d8276eb [InlinedCallFrame: 000000cd8377e2c0]6 。。。。。。(省略了)7 
8 000000CD8377E590 00007ff997991b34 ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_8\Program.cs @ 13]9 PARAMETERS:10         args (0x000000CD8377E630) = 0x0000024827408ea0
11 LOCALS:12         0x000000CD8377E618 = 0x0000024827409c98
13         0x000000CD8377E5F0 = 0x0000000000000000

0x0000024827409c98
红色标注的就是 Person 类型的局部变量 person 的地址。我们可以使用【!dumpobj
0x0000024827409c98
】命令查看这个对象的大小。

1 0:000> !dumpobj 0x0000024827409c98
2 Name:        ExampleCore_3_1_8.Person3 MethodTable: 00007ff997a494804 EEClass:     00007ff997a520085 Tracked Type: false
6 Size:        40(0x28) bytes(这里就是大小)7 File:        E:\Visual Studio 2022\...\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff997901188  4000001       18         System.Int32  1 instance               32 <Age>k__BackingField11 00007ff99797ec08  4000002        8        System.String  0 instance 00000288b93b0500 <Name>k__BackingField12 00007ff997a4d9a0  4000003       10 ...ore_3_1_8.Address  0 instance 0000024827409cc0 <HomeAddress>k__BackingField

我们使用【!objsize
0x0000024827409c98
】这个命令看看它的大小。

1 0:000> !objsize 0x0000024827409c98
2 Objects which 24827409c98(ExampleCore_3_1_8.Person) transitively keep alive:3 
4 Address               MT           Size5     024827409c98     7ff997a49480             40 
6     0288b93b0500     7ff99797ec08             42 
7     024827409cc0     7ff997a4d9a0             56 
8     0288b93b0530     7ff99797ec08             32 
9     0288b93b0550     7ff99797ec08             26 
10     0288b93b0570     7ff99797ec08             32 
11     0288b93b0590     7ff99797ec08             36 
12     0288b93b05b8     7ff99797ec08             34 
13 
14 Statistics:15 MT Count TotalSize Class Name16 7ff997a49480     1        40ExampleCore_3_1_8.Person17 7ff997a4d9a0     1        56ExampleCore_3_1_8.Address18 7ff99797ec08     6       202System.String19 Total 8 objects, 298 bytes

这个结果还是很详细的。我们看到了【!objsize】命令得到的是 298 bytes,而【!dumpobj】得到的大小是 40 bytes,这个差别还是挺大的。

3.1.7、异常的转储
A、基础知识
Windows 的异常模型采用的是结构化异常处理(Structured Exception Handling,SEH)。同样,.NET异常模型也是构建在 Windows SEH模型之上的,并提供了基于对象的异常模型。CLR 在每个异常内携带的额外信息都被保存到托管堆上。所有 CLR 异常都以 SEH 异常的形式出现,错误码是:0x
e0434352
。既然所有的异常的错误码都是 0x
e0434352
,我们是如何区分异常的不同的呢?答案就是依靠异常中保存的扩展信息。
(说明,原书错误码是:0xe0434f4d,平台是 .NET Framework,我的错误码是:0xe0434352,平台是.NET 8.0)

在 CLR 看来,异常也是一种引用类型,所以也可以使用【!dumpobj】命令输出它的信息。这个命令有些繁琐,如果我们只希望显示异常的类型、栈回溯(包括内部栈回溯)以及信息,我们就可以使用【printexception】命令。这个命令的参数托管异常的地址。当然,我们在分析异常时,【threads】命令也会经常用得到,它能显示出系统中各个托管线程的信息,包括该线程抛出的最后一个异常。
【StopOnException】命令是在抛出特定异常时设置一个断点,和异常信息的转储关系不大。
命令的格式:
!StopOnException [-derived] [-create|-create2] <Exception> [<Pseudo-register number>],create 和 create2 这两个开关控制着断点是在第一次出现指定的异常时触发,还是第二次出现时触发。derived 开关会增加断点的范围,不仅包含指定的异常,而且还包括从指定异常派生的任意异常。pseudo-register 是可选的,表示命令将使用哪个伪寄存器设置断点,如果没有指定伪寄存器,默认是 $t1。
例子:
!StopOnException -create System.ArgumentException,表示当第一次抛出 System.ArgumentException 异常时设置断点。
!StopOnException -derived System.Exception ,表示对从 System.Exception 派生下来的任意异常都设置一个断点。

B、眼见为实
1)、NTSD 调试
调试源码:ExampleCore_3_1_6
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。

打开【NTSD】调试器。

使用【g】命令运行调试器,直到调试器抛出异常,调试器中断执行。

我们使用【kb】命令,查看托管调用栈的栈回溯。

0:000>kb
RetAddr : Args to Child : Call Site
00007ffb`00a9ca6f : 000001b8`72c0aca8 0000002c`9517e710 0000002c`9517ea00 00007ffb`00a7bbb4 : KERNELBASE
!RaiseException+0x6900007ffb`00a9c129 :00000000`70000185 00007ffb`00d961c8 0000002c`9517eb88 0000002c`9517ed28 : coreclr!RaiseTheExceptionInternalOnly+0x26b00007ffa`a0fc2416 : 000001b8`72c0aca8 000001f9`04c913c8 000001b8`700fa1e800000000`00000040 : coreclr!IL_Throw+0xb900007ffa`a0fc1afd : 000001b8`72c09ce000000000`00000000 000001b8`6e7af410 000001b8`72c09c38 : 0x00007ffa`a0fc2416
00007ffb`00b2a1a3 : 000001b8`72c08ea0 0000002c`9517f128 0000002c`9517f128 0000002c`9517ed19 :
0x00007ffa`a0fc1afd
00007ffb`00ab14c9 :
00000000`00000000 00000000`00000130 0000002c`9517ed28 00007ffb`009da456 : coreclr!CallDescrWorkerInternal+0x83(Inline Function) :--------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x5600007ffb`009d75ac : 0000002c`9517eda800000000`00000000 00000000`00000048 00007ffb`00ac28a6 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1(Inline Function) :--------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb00007ffb`009d6f7a : 000001b8`72c08ea0 000001b8`72c08ea000000000`00000000 0000002c`9517f128 : coreclr!RunMainInternal+0x11c00007ffb`009d6b17 : 000001b8`6e7af410 000001b8`00000000 000001b8`6e7af410 00000000`00000000 : coreclr!RunMain+0xd200007ffb`009d7321 :00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf00007ffb`00ae7768 :00000000`00000001 0000002c`9517f301 0000002c`9517f350 00007ffb`86ff23ea : coreclr!CorHost2::ExecuteAssembly+0x28100007ffb`87012c36 : 000001b8`6e780fd0 000001b8`6e78031000000000`00000000 000001b8`6e780310 : coreclr!coreclr_execute_assembly+0xd8(Inline Function) :--------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a00007ffb`87012f1c : 000001b8`6e76bcc8 0000002c`9517f579 00007ffb`8704c9c0 000001b8`6e76bcc8 : hostpolicy!run_app_for_context+0x59600007ffb`8701385a :00000000`00000000 000001b8`6e76bcc0 000001b8`6e76bcc0 00000000`00000000 : hostpolicy!run_app+0x3c00007ffb`b3e2b5c9 : 000001b8`6e77f438 000001b8`6e77f32000000000`00000000 0000002c`9517f679 : hostpolicy!corehost_main+0x15a00007ffb`b3e2e066 : 000001b8`6e77e330 0000002c`9517fa0000000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e900007ffb`b3e302ec : 00007ffb`b3e625f8 000001b8`6e77cb40 0000002c`9517f940 0000002c`9517f8f0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 00007ffb`b3e2e644 : 0000002c`9517fa00 0000002c`9517fa20 0000002c`9517f971 000001b8`6e77cf60 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c00007ffb`b3e285a0 : 0000002c`9517fa20 000001b8`6e77c44000000000`00000001 000001b8`6e760000 : hostfxr!fx_muxer_t::execute+0x494 *** WARNING: Unable to verify checksum forapphost.exe
00007ff6`5faef998 : 00007ffb`d241f4e8 00007ffb`b3e29b10 0000002c`9517fbc0 000001b8`6e77c130 : hostfxr
!hostfxr_main_startupinfo+0xa000007ff6`5faefda6 : 00007ff6`5fafb6c000000000`00000007 000001b8`6e76bcc0 00000000`0000005e : apphost!exe_start+0x87800007ff6`5faf12e8 :00000000`00000000 00000000`00000000 000001b8`6e76bcc0 00000000`00000000 : apphost!wmain+0x146(Inline Function) :--------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x2200007ffb`d33f6fd4 :00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c00007ffb`d4b7cec1 :00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21 0:000>

接下来,我们找到
KERNELBASE
!RaiseException

函数(
000001b8`72c0aca8
)的第一个参数,在该地址上执行【!dumpobj】命令,查看异常详情。

1 0:000> !DumpObj 000001b8`72c0aca82 Name:        System.ArgumentException3 MethodTable: 00007ffaa10467b04 EEClass:     00007ffaa0f8cdf85 Tracked Type: false
6 Size:        136(0x88) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 0000000000000000  4000264        8 ...ection.MethodBase  0 instance 0000000000000000_exceptionMethod11 00007ffaa0faec08  4000265       10        System.String  0instance 000001f904c913c8 _message12 00007ffaa1043060  4000266       18 ...tions.IDictionary  0 instance 0000000000000000_data13 00007ffaa1040820  4000267       20     System.Exception  0 instance 0000000000000000_innerException14 00007ffaa0faec08  4000268       28        System.String  0 instance 0000000000000000_helpURL15 00007ffaa10aa858  4000269       30        System.Byte[]  0instance 000001b872c0ad78 _stackTrace16 00007ffaa10aa858  400026a       38        System.Byte[]  0 instance 0000000000000000_watsonBuckets17 00007ffaa0faec08  400026b       40        System.String  0 instance 0000000000000000_stackTraceString18 00007ffaa0faec08  400026c       48        System.String  0 instance 0000000000000000_remoteStackTraceString19 00007ffaa0efc4d8  400026d       50      System.Object[]  0 instance 0000000000000000_dynamicMethods20 00007ffaa0faec08  400026e       58        System.String  0 instance 0000000000000000_source21 00007ffaa0fa8b78  400026f       60       System.UIntPtr  1instance 00007FFAA0FC2415 _ipForWatsonBuckets22 00007ffaa0fa70a0  4000270       68        System.IntPtr  1 instance 0000000000000000_xptrs23 00007ffaa0f31188  4000271       70         System.Int32  1 instance       -532462766_xcode24 00007ffaa0f31188  4000272       74         System.Int32  1 instance      -2147024809_HResult25 00007ffaa0faec08  4000383       78        System.String  0 instance 0000000000000000_paramName26 0:000>

以上就是异常的详情,如果我们想查看具体提示消息,可以针对 _message 域的地址
000001f904c913c8
再次执行【!DumpObj】命令。

1 0:000> !dumpobj 000001f904c913c82 Name:        System.String3 MethodTable: 00007ffaa0faec084 EEClass:     00007ffaa0f8a5005 Tracked Type: false
6 Size:        58(0x3a) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      Obj cannot be null
9 Fields:10 MT    Field   Offset                 Type VT     Attr            Value Name11 00007ffaa0f31188  400033b        8         System.Int32  1 instance               18_stringLength12 00007ffaa0f3b538  400033c        c          System.Char  1instance               4f _firstChar13 00007ffaa0faec08  400033a       c8        System.String  0   static 000001f904c90008 Empty

【!dumpobj】命令有些繁琐,我们可以直接使用【!PrintException】命令输出异常信息。

1 0:000> !PrintException 000001b8`72c0aca82 WARNING: SOS needs to be upgraded for thisversion of the runtime. Some commands may not work correctly.3 For more information see https://go.microsoft.com/fwlink/?linkid=2135652
4 
5 Exception object: 000001b872c0aca86 Exception type:   System.ArgumentException7 Message:          Obj cannot be null
8 InnerException:   <none>
9 StackTrace (generated):10 SP               IP               Function11     0000002C9517E9C0 00007FFAA0FC2415 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException(ExampleCore_3_1_6.ObjTypes)+0x85
12     0000002C9517EA10 00007FFAA0FC1AFC ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main(System.String[])+0x1cc
13 
14 StackTraceString: <none>
15 HResult: 80070057

在这里看的很清楚,包括异常的类型(
Exception type

System.ArgumentException
),异常的地址(Exception
object
: 000001b872c0aca8

),异常抛出的方法(ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException)等。
当然,我们可以使用【!threads】或者【!t】命令输出所有托管线程的信息,包括托管线程抛出的最有一个异常。

【!threads】命令执行的效果。

1 0:000> !threads2 ThreadCount:      3
3 UnstartedThread:  0
4 BackgroundThread: 2
5 PendingThread:    0
6 DeadThread:       0
7 Hosted Runtime:   no8 Lock9 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception10    0    1     1190 000001B86E7AF410    2a020 Preemptive  000001B872C0ADF0:000001B872C0C638 000001B86E7F1530 -00001MTA System.ArgumentException 000001b872c0aca811    6    2     3b64 000001B8700EDDA0    21220 Preemptive  0000000000000000:0000000000000000 000001B86E7F1530 -00001Ukn (Finalizer)12    7    3     1630 000001B86E7BBF00    2b220 Preemptive  0000000000000000:0000000000000000 000001B86E7F1530 -00001 MTA

红色标注的就是 ID 为 1 的托管线程抛出的异常(
System.ArgumentException
),异常的地址:
000001b872c0aca8


2)、Windbg Preview 调试
调试源码:ExampleCore_3_1_6
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序抛出一个异常,调试器停止执行。

1 0:000>g2 ModLoad: 00007ffb`d4ac0000 00007ffb`d4af0000   C:\Windows\System32\IMM32.DLL3 ModLoad: 00007ffb`c8d40000 00007ffb`c8d99000   C:\Program Files\dotnet\host\fxr\8.0.2\hostfxr.dll4 ModLoad: 00007ffb`c8cd0000 00007ffb`c8d34000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\hostpolicy.dll5 ModLoad: 00007ffa`fcab0000 00007ffa`fcf98000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\coreclr.dll6 ModLoad: 00007ffb`d3b60000 00007ffb`d3c89000   C:\Windows\System32\ole32.dll7 ModLoad: 00007ffb`d34a0000 00007ffb`d37f4000   C:\Windows\System32\combase.dll8 ModLoad: 00007ffb`d3200000 00007ffb`d32d5000   C:\Windows\System32\OLEAUT32.dll9 ModLoad: 00007ffb`d1fc0000 00007ffb`d203f000   C:\Windows\System32\bcryptPrimitives.dll10 (2e10.1988): Unknown exception - code 04242420(first chance)11 ModLoad: 00007ffa`fb810000 00007ffa`fc49c000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll12 ModLoad: 00007ffa`fb650000 00007ffa`fb809000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll13 ModLoad: 00007ffb`d1f00000 00007ffb`d1f13000   C:\Windows\System32\kernel.appcore.dll14 ModLoad: 00000293`f6f10000 00000293`f6f18000   E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll15 ModLoad: 00000293`f6f30000 00000293`f6f3e000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll16 ModLoad: 00007ffb`c8c70000 00007ffb`c8c98000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll17 ModLoad: 00007ffb`79470000 00007ffb`79482000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Threading.dll18 ModLoad: 00000293`f6f40000 00000293`f6f48000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Text.Encoding.Extensions.dll19 ModLoad: 00007ffb`5dc90000 00007ffb`5dca5000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.InteropServices.dll20 (2e10.1988): CLR exception - code e0434352 (first chance)21 (2e10.1988): CLR exception - code e0434352 (!!! second chance !!!)22 KERNELBASE!RaiseException+0x69:23 00007ffb`d2563e49 0f1f440000      nop     dword ptr [rax+rax]

我们使用【kb】命令查看调用栈。

1 0:000>kb2 # RetAddr               : Args to Child                                                           : Call Site3 00 00007ffa`fcb7ca6f     : 00000293`fb40aca8 000000d7`0477e1f0 000000d7`0477e4e0 00007ffa`fcb5bbb4 : KERNELBASE!RaiseException+0x69
4 01 00007ffa`fcb7c129     : 00000000`70000185 00007ffa`fce761c8 000000d7`0477e668 000000d7`0477e808 : coreclr!RaiseTheExceptionInternalOnly+0x26b [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 2795]5 02 00007ffa`9d0a24b6     : 00000293`fb40aca8 000002d4`8d5813c8 00000293`f6fbe1e8 00000000`00000040 : coreclr!IL_Throw+0xb9 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 4247]6 03 00007ffa`9d0a1afd     : 00000293`fb409ce0 00000000`00000000 00000293`f6fa6280 00000293`fb409c38 : ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException+0x86 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\Program.cs @ 76]7 04 00007ffa`fcc0a1a3     : 00000293`fb408ea0 000000d7`0477ec08 000000d7`0477ec08 000000d7`0477e7f9 : ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main+0x1cd [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\Program.cs @ 46]8 05 00007ffa`fcb914c9     : 00000000`00000000 00000000`00000130 000000d7`0477e808 00007ffa`fcaba456 : coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100]9 06 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67]10 07 00007ffa`fcab75ac     : 000000d7`0477e888 00000000`00000000 00000000`00000048 00007ffa`fcba28a6 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 570]11 08 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb [D:\a\_work\1\s\src\coreclr\vm\callhelpers.h @ 458]12 09 00007ffa`fcab6f7a     : 00000293`fb408ea0 00000293`fb408ea0 00000000`00000000 000000d7`0477ec08 : coreclr!RunMainInternal+0x11c [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1304]13 0a 00007ffa`fcab6b17     : 00000293`f6fa6280 00000293`00000000 00000293`f6fa6280 00000000`00000000 : coreclr!RunMain+0xd2 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1375]14 0b 00007ffa`fcab7321     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1504]15 0c 00007ffa`fcbc7768     : 00000000`00000001 000000d7`0477ee01 000000d7`0477ee30 00007ffb`c8cd23ea : coreclr!CorHost2::ExecuteAssembly+0x281 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349]16 0d 00007ffb`c8cf2c36     : 00000293`f6f7a900 00000293`f6f7a660 00000000`00000000 00000293`f6f7a660 : coreclr!coreclr_execute_assembly+0xd8 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 504]17 0e (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 109]18 0f 00007ffb`c8cf2f1c     : 00000293`f6f67da8 000000d7`0477f059 00007ffb`c8d2c9c0 00000293`f6f67da8 : hostpolicy!run_app_for_context+0x596 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256]19 10 00007ffb`c8cf385a     : 00000000`00000000 00000293`f6f67da0 00000293`f6f67da0 00000000`00000000 : hostpolicy!run_app+0x3c [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285]20 11 00007ffb`c8d4b5c9     : 00000293`f6f769e8 00000293`f6f768d0 00000000`00000000 000000d7`0477f159 : hostpolicy!corehost_main+0x15a [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426]21 12 00007ffb`c8d4e066     : 00000293`f6f77540 000000d7`0477f4e0 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145]22 13 00007ffb`c8d502ec     : 00007ffb`c8d825f8 00000293`f6f780d0 000000d7`0477f420 000000d7`0477f3d0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532]
23 14 00007ffb`c8d4e644     : 000000d7`0477f4e0 000000d7`0477f500 000000d7`0477f451 00000293`f6f78401 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007]24 15 00007ffb`c8d485a0     : 000000d7`0477f500 00000293`f6f76690 00000000`00000001 00000293`f6f60000 : hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578]25 16 00007ff7`6c9ff998     : 00007ffb`d241f4e8 00007ffb`c8d49b10 000000d7`0477f6a0 00000293`f6f76380 : hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62]26 17 00007ff7`6c9ffda6     : 00007ff7`6ca0b6c0 00000000`00000007 00000293`f6f67da0 00000000`0000005e : apphost!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240]27 18 00007ff7`6ca012e8     : 00000000`00000000 00000000`00000000 00000293`f6f67da0 00000000`00000000 : apphost!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311]28 19 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]29 1a 00007ffb`d33f6fd4     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]30 1b 00007ffb`d4b7cec1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
31 1c 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

接下来,找出
KERNELBASE!RaiseException
函数的(
000001f8`4c80aca8
)第一个参数,并在这个地址上执行【!dumpobj
000001f8`4c80aca8
】命令。

1 0:000> !DumpObj 000001f8`4c80aca82 Name:        System.ArgumentException3 MethodTable: 00007ffaa06e67b04 EEClass:     00007ffaa062cdf85 Tracked Type: false
6 Size:        136(0x88) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 0000000000000000  4000264        8 ...ection.MethodBase  0 instance 0000000000000000_exceptionMethod11 00007ffaa064ec08  4000265       10        System.String  0instance 00000238deaf13c8 _message12 00007ffaa06e3060  4000266       18 ...tions.IDictionary  0 instance 0000000000000000_data13 00007ffaa06e0820  4000267       20     System.Exception  0 instance 0000000000000000_innerException14 00007ffaa064ec08  4000268       28        System.String  0 instance 0000000000000000_helpURL15 00007ffaa074a858  4000269       30        System.Byte[]  0instance 000001f84c80ad78 _stackTrace16 00007ffaa074a858  400026a       38        System.Byte[]  0 instance 0000000000000000_watsonBuckets17 00007ffaa064ec08  400026b       40        System.String  0 instance 0000000000000000_stackTraceString18 00007ffaa064ec08  400026c       48        System.String  0 instance 0000000000000000_remoteStackTraceString19 00007ffaa059c4d8  400026d       50      System.Object[]  0 instance 0000000000000000_dynamicMethods20 00007ffaa064ec08  400026e       58        System.String  0 instance 0000000000000000_source21 00007ffaa0648b78  400026f       60       System.UIntPtr  1instance 00007FFAA0662415 _ipForWatsonBuckets22 00007ffaa06470a0  4000270       68        System.IntPtr  1 instance 0000000000000000_xptrs23 00007ffaa05d1188  4000271       70         System.Int32  1 instance       -532462766_xcode24 00007ffaa05d1188  4000272       74         System.Int32  1 instance      -2147024809_HResult25 00007ffaa064ec08  4000383       78        System.String  0 instance 0000000000000000 _paramName

从【!dumpobj】命令的输出中,我们可以看到托管代码异常的所有信息,包括异常的类型(
System.ArgumentException
)以及异常相关的所有域。我们可以针对标红的
_message
再次执行【!dumpobj
00000238deaf13c8
】命令,查看异常的具体提示信息。

1 0:000> !dumpobj 00000238deaf13c82 Name:        System.String3 MethodTable: 00007ffaa064ec084 EEClass:     00007ffaa062a5005 Tracked Type: false
6 Size:        58(0x3a) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      Obj cannot be null
9 Fields:10 MT    Field   Offset                 Type VT     Attr            Value Name11 00007ffaa05d1188  400033b        8         System.Int32  1 instance               18_stringLength12 00007ffaa05db538  400033c        c          System.Char  1instance               4f _firstChar13 00007ffaa064ec08  400033a       c8        System.String  0   static 00000238deaf0008 Empty

简写方式输出。

1 0:000> !DumpObj -nofields 00000238deaf13c82 Name:        System.String3 MethodTable: 00007ffaa064ec084 EEClass:     00007ffaa062a5005 Tracked Type: false
6 Size:        58(0x3a) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll8 String:      Obj cannot be null

上面我们使用【!dumpobj】命令可以查看异常信息,但是比较繁琐。我们可以使用【!PrintException】命令输出异常信息。

1 0:000> !PrintException 000001f8`4c80aca82 Exception object: 000001f84c80aca83 Exception type:   System.ArgumentException4 Message:          Obj cannot be null
5 InnerException:   <none>
6 StackTrace (generated):7 SP               IP               Function8     0000007BA577E6D0 00007FFAA0662415 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException(ExampleCore_3_1_6.ObjTypes)+0x85
9     0000007BA577E720 00007FFAA0661AFC ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main(System.String[])+0x1cc
10 
11 StackTraceString: <none>
12 HResult: 80070057

这里输出的内容更清晰,是哪个方法抛出的异常都可以看到(ThrowException)。我们分析异常的时候,可以使用【!threads】或者【!t】命令查看所有托管线程信息,包括该线程抛出的最后一个异常。

1 0:000> !threads2 ThreadCount:      3
3 UnstartedThread:  0
4 BackgroundThread: 2
5 PendingThread:    0
6 DeadThread:       0
7 Hosted Runtime:   no8 Lock9 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception10    0    1     379c 000001F8485263D0    2a020 Preemptive  000001F84C80ADF0:000001F84C80C638 000001f848568920 -00001MTA System.ArgumentException 000001f84c80aca811    6    2     10d4 000001F8485D7C40    21220 Preemptive  0000000000000000:0000000000000000 000001f848568920 -00001Ukn (Finalizer)12    7    3     3204 000001F8485346F0    2b220 Preemptive  0000000000000000:0000000000000000 000001f848568920 -00001MTA13 
14 
15 0:000> !t16 ThreadCount:      3
17 UnstartedThread:  0
18 BackgroundThread: 2
19 PendingThread:    0
20 DeadThread:       0
21 Hosted Runtime:   no22 Lock23 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception24    0    1     379c 000001F8485263D0    2a020 Preemptive  000001F84C80ADF0:000001F84C80C638 000001f848568920 -00001MTA System.ArgumentException 000001f84c80aca825    6    2     10d4 000001F8485D7C40    21220 Preemptive  0000000000000000:0000000000000000 000001f848568920 -00001Ukn (Finalizer)26    7    3     3204 000001F8485346F0    2b220 Preemptive  0000000000000000:0000000000000000 000001f848568920 -00001 MTA 

我们看到了 ID 为 1 的线程抛出一个类型为
System.ArgumentException
异常,异常的地址:
000001f84c80aca8
,我们可以针对这个地址使用【!DumpObj】或者【!PrintException】命令查看异常的详情。


四、总结
这篇文章的第二部分终于写完了,考虑到内容太多,分多篇写作了。只有所有代码自己都写了,都测试了,才会明白里面发生的事情。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

上一篇文章
Python中那些简单又好用的特性和用法
发出后,群里的小伙伴又给补充了几个好用的用法,结合生产实用经验汇总整理如下,各位看官如有需要请自取

  1. 反射,反射是一种机制,可以在运行时获取、检查和修改对象的属性和方法。Python提供了一些内置的函数和属性,可以用于实现反射操作

dir()函数:返回对象的所有属性和方法的列表

class MyClass:
    def __init__(self):
        self.name = "运维咖啡吧"
        self.age = 25

my_obj = MyClass()
print(dir(my_obj))

getattr()函数:获取对象的属性或方法

class MyClass:
    def __init__(self):
        self.name = "运维咖啡吧"
        self.age = 25

my_obj = MyClass()
name = getattr(my_obj, "name")
print(name)

setattr()函数:设置对象的属性或方法

class MyClass:
    def __init__(self):
        self.name = "运维咖啡吧"
        self.age = 25

my_obj = MyClass()
setattr(my_obj, "name", "blog.ops-coffee.cn")
print(my_obj.name)

hasattr()函数:检查对象是否具有指定的属性或方法

class MyClass:
    def __init__(self):
        self.name = "运维咖啡吧"
        self.age = 25

my_obj = MyClass()
has_name = hasattr(my_obj, "name")
print(has_name)

这些函数和属性可以帮助你在运行时动态地访问和修改对象的属性和方法。反射为编写更灵活和通用的代码提供了一种机制,可以根据需要操作对象的属性和方法

  1. 交换变量,这是一种非常常见和方便的操作,可以在需要时快速交换变量的值
a = 10
b = 20

a, b = b, a

print(a, b)  # 输出: 20 10
  1. 可迭代对象解包,它允许你将可迭代对象的元素解包到单独的变量中。这对于同时处理多个值非常有用
# 解包元组
a, b, c = (1, 2, 3)
print(a, b, c)   # 输出: 1 2 3

# 解包列表
x, y, z = [4, 5, 6]
print(x, y, z)   # 输出: 4 5 6
  1. 深复制和浅复制,当处理复杂的数据结构时,深复制和浅复制允许你创建对象的副本。深复制会创建一个全新的对象,而浅复制会创建一个新对象,但共享内部数据。
import copy

# 创建一个列表
original_list = [1, 2, [3, 4]]

# 浅复制
shallow_copy = copy.copy(original_list)

# 深复制
deep_copy = copy.deepcopy(original_list)

# 修改原始列表中的嵌套列表
original_list[2][0] = 5

print(original_list)   # 输出: [1, 2, [5, 4]]
print(shallow_copy)    # 输出: [1, 2, [5, 4]]
print(deep_copy)       # 输出: [1, 2, [3, 4]]
  1. with语句的多个上下文管理器:在一个with语句中,你可以同时使用多个上下文管理器,以便在代码块执行前后执行多个操作
class CustomContextManager1:
    def __enter__(self):
        print("Entering CustomContextManager1")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting CustomContextManager1")

class CustomContextManager2:
    def __enter__(self):
        print("Entering CustomContextManager2")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting CustomContextManager2")

# 使用多个上下文管理器
with CustomContextManager1(), CustomContextManager2():
    # 在这里执行一些操作

比较常用的是一次打开多个文件进行操作,例如同时打开文件1和文件2,读取文件1内容处理后写入文件2

with open("file1.txt") as file1, open("file2.txt") as file2:
    # 在这里可以使用file1和file2进行操作
  1. 装饰器,通过装饰器来复用函数,简化操作
# 创建一个装饰器函数,用于计算函数执行时间
import time

def calculate_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"函数 {func.__name__} 执行时间为: {end_time - start_time} 秒")
        return result
    return wrapper

# 应用装饰器到函数
@calculate_time
def long_running_function():
    time.sleep(2)

long_running_function()
  1. 使用字典来代替冗长的if/else判断,提升代码可读性
def operation_add(a, b):
    return a + b

def operation_subtract(a, b):
    return a - b

def operation_multiply(a, b):
    return a * b

def operation_divide(a, b):
    return a / b

# 定义一个操作字典,将操作名映射到对应的函数上
operations = {
    "add": operation_add,
    "subtract": operation_subtract,
    "multiply": operation_multiply,
    "divide": operation_divide
}

# 使用字典进行操作选择和调用相应的函数
result = operations["multiply"](2, 3)
print("结果:", result)
  1. 字符串格式化,使用占位符和格式化字符串来构建新的字符串
name = "Alice"
age = 25
message = "My name is {} and I am {} years old.".format(name, age)
# 输出: "My name is Alice and I am 25 years old."
  1. 使用
    lamba
    函数进行简单的函数定义
add = lambda x, y: x + y
result = add(2, 3)
# 输出: 5
  1. 使用
    reversed
    函数反转可迭代对象
numbers = [1, 2, 3, 4, 5]
reversed_numbers = list(reversed(numbers))
# 输出: [5, 4, 3, 2, 1]
  1. 使用
    map
    函数对可迭代对象中的每个元素应用指定的函数
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
# 输出: [1, 4, 9, 16, 25]
  1. 使用
    filter
    函数根据指定的条件筛选可迭代对象中的元素
numbers = [1, 2, 3, 4, 5]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
# 输出: [2, 4]
  1. 生成器函数,使用
    yield
    关键字来生成一个值,并且在每次生成值后暂停执行,等待下一次调用
# 下面是一个简单的生成器函数示例,用于生成斐波那契数列
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print(next(fib))  # 输出:0
print(next(fib))  # 输出:1
print(next(fib))  # 输出:1

生成器函数可以一次生成一个值,而不是一次生成所有值,这使得生成器函数非常适合处理大量数据或无限序列

  1. 使用
    functools
    模块中的
    partial
    函数进行函数参数固定:partial函数可以固定函数的部分参数,生成一个新的函数
from functools import partial

def power(x, y):
    return x ** y

square = partial(power, y=2)
cube = partial(power, y=3)

print(square(5))  # 输出 25
print(cube(5))  # 输出 125
  1. 使用
    try...else
    语句在
    try
    块没有引发异常时执行特定的代码
try:
    # 可能会引发异常的代码
    result = 10 / 2
except ZeroDivisionError:
    # 如果发生ZeroDivisionError异常
    print("除数不能为零!")
else:
    # 如果没有发生异常
    print("结果为:", result)

类似的还有try...finally无论是否发生异常,都会执行

try:
    # 可能会引发异常的代码
    f = open('example.txt', 'r')
    # 其他操作
except IOError:
    # 如果发生IOError异常
    print("无法打开文件")
else:
    # 如果没有发生异常
    print(f.read())
finally:
    # 无论是否发生异常,都会执行的清理代码
    f.close()
  1. 通过
    signal
    解耦代码
import signal
import os

# 定义一个事件处理函数
def handle_event(signum, frame):
    if signum == signal.SIGUSR1:
        print("接收到用户定义信号1,执行事件A")
        # 执行事件A的逻辑
    elif signum == signal.SIGUSR2:
        print("接收到用户定义信号2,执行事件B")
        # 执行事件B的逻辑
    else:
        print(f"接收到未知信号: {signum}")

# 注册事件处理函数
signal.signal(signal.SIGUSR1, handle_event)  # 注册处理用户定义信号1的事件处理函数
signal.signal(signal.SIGUSR2, handle_event)  # 注册处理用户定义信号2的事件处理函数

# 模拟发送事件信号
def send_event1():
    # 发送用户定义信号1
    print("发送用户定义信号1")
    os.kill(os.getpid(), signal.SIGUSR1)

def send_event2():
    # 发送用户定义信号2
    print("发送用户定义信号2")
    os.kill(os.getpid(), signal.SIGUSR2)

# 执行事件
send_event1()  # 执行事件1
send_event2()  # 执行事件2

两篇文章一共总结了32个用法,先这么多,欢迎大伙继续补充

原文

代码

摘要

为了解决传统的关系抽取(RE)方法只能识别两个实体之间的关系,而忽略了同一上下文中多个关系之间的相互依赖性,即关系的关系(relation of relations,RoR)的问题,本文提出了一种新的RE范式,它将所有关系的预测作为一个整体进行优化。本文设计了一种数据驱动的方法,利用
图神经网络和关系矩阵Transformer自动学习RoR
,无需人工规则。在两个公开的数据集ACE05和SemEval 2018任务7.2上,本文的模型分别比最先进的方法提高了+1.12%和+2.55%,达到了显著的改进效果。

1 Introduction


概述:
图1中的句子涉及到七个实体。在广泛使用的ACE 05数据集(Walker et al., 2006)中,有
99.76%
的数据实例涉及到两个以上的实体,而且每个文本中平均存在9.21个关系。
传统方法:
采用了一种简化的设置,即只对每两个实体之间的关系进行分类(Zeng et al., 2014; Luan et al., 2018; Li and Ji, 2014; Gormley et al., 2015; Miwa and Bansal, 2016)。这意味着对于图1中有7个实体的句子,大多数以前的方法需要执行49个独立的关系分类任务(如果考虑自反关系)。由于现有的方法需要对输入的实体进行显式标注,因此无法减少这个数量的分类任务。例如,要预测实体对(obstetricians, California)之间的关系,输入需要转换为“...<e1> obstetricians <\e1> in <e2> California <\e2> will pay $60,000 in Los Angeles ...”。
传统方法的问题:
它不仅效率低下,而且忽略了同一上下文中多个关系之间的相互依赖性。例如,在图1中的49个关系中,如果已经知道了关系(Miami, is part of, south Florida),其中“is part of”是定义在两个对象上的关系,那么Miami就不太可能与其他任何人际关系有关,比如“is the father of...”。
RoR:
在ACE 05数据集中,每个文本中平均存在9.21个关系,这意味着每个关系都可能对同一文本中的其他关系有影响。将同一文本中多个关系之间的频繁出现的相互依赖性称为“关系的关系”(RoR)。
为了捕捉RoR,本文提出了一种新的关系抽取(RE)范式,它将同一文本中所有关系的预测作为一个整体来处理。本文的工作与(
Wang et al., 2019
)不同,后者仍然将每个实体对的关系视为独立的分类任务,但以牺牲准确性为代价节省了计算能力,通过一次编码所有实体。相反,本文新提出的范式不是关于计算成本和准确性之间的权衡,而是通过捕捉RoR来提高性能。

2 New formulation of RE

传统的任务定义:
把每个文本序列和其中的两个实体之间的关系作为一个独立的分类问题来处理的。即:
给定一个文本序列

,以及

中的两个实体提及



,还有一个预定义的关系类型集合

,任务是预测



之间属于哪种关系类型。这样的 RE 问题可以归结为一个经典的句子
分类任务
本文的任务定义:
给定一个文本序列

,以及

中的所有实体提及

,模型需要预测

中任意两个实体

之间的关系

,其中


。本文使用一个矩阵

来表示

中所有可能的关系,如图 2 所示。

3 Statistical analysis of RoR

本文的案例研究基于 ACE05 数据集 (Walker et al., 2006)。下面将介绍两种 RoR 的形式:
  1. biRoR,只考虑两个关系之间的相互作用。
  2. multiRoR,考虑三个或更多关系之间的复杂关联。

3.1 Data overview

ACE05 (Walker et al., 2006) 是最广泛使用的关系抽取数据集。它的文本来自多种来源,包括新闻节目、报纸、新闻报道和音频转录。它有 6 种关系类型,如表 1 所示。对于这些关系,有 7 种有效的实体类型:设施 (FAC)、地缘政治实体 (GPE)、位置 (LOC)、组织 (ORG)、人 (PER)、车辆 (VEH)、武器 (WEA)。

3.2 BiRoR: Interdependency of two relations

3.2.1 Entity type-constrained biRoR(受实体类型约束)

给定两个实体和它们之间的关系类型


,可以判断

是否不可能与另一种关系类型

同时出现。为了说明这种方法,用 ACE05 数据集中的七种实体类型举例。表 1 显示了不同的关系类型允许的实体类型参数。根据这些信息,可以得出 12 条不兼容的规则。比如,同一个实体不能同时是 Per-Soc 的 arg0 和 Part-Whole 的 arg0,因为 Per-Soc 的 arg0 必须是人 (PER),而 Part-Whole 不能包含 PER。所有的不兼容规则都列在附录 A.1 中。

3.2.2 Semantic-constrained biRoR(受语义约束)

一个关系的含义可以决定它是否能够与另一个关系共存或者必须与另一个关系互斥。例如,Art 关系可以表示一个人(arg0)拥有一个设施(arg1),其中 arg1 必须是一个设施。如果已经存在这样的关系,那么同一个设施就不能再与一个城市构成 Part-Whole 关系,因为从语义上讲,城市(比如波士顿)不可能是一个设施的一部分。这种不兼容性不是由实体类型强制规定的,而是由关系的语义所暗示的。
语义也可以暗示一个关系是否应该具有对称性。例如,Per-Soc 关系总是对称的,因为家庭和朋友是可以互换的关系,而 Org-Aff 关系总是非对称的(Walker et al., 2005)。因此,如果一个关系

,那么

也等于

。而如果

,那么

3.2.3 Empirical biRoR

本文对图 3 中的每对关系进行了相关性分析。从图中可以看出,3.2.1 节提出的不兼容性规则在红色的负相关区域得到了验证,3.2.2 节描述的 Per-Soc 和 Phys 的对称性在深蓝色的高相关区域得到了体现。此外,图中还显示了一些其他的相关性,例如 Part-Whole 和 Phys 关系经常同时出现。

3.3 MultiRoR: Correlation of 3+ relations

3.3.1 Entity type-constrained multiRoR

biRoR 是一种只涉及两个关系的规则,但还有一些更复杂的规则,它们可以作用于多个关系,称之为 multiRoR。multiRoR 是一种基于实体类型的规则,它把 3.2.1 节中介绍的两个关系的不兼容性推广到 3 个或更多的关系。举个例子,如果一个实体同时是 Org-Aff 和 Phys 关系的 arg1,那么根据实体类型的约束,它就不能再是 Art 关系的 arg1(因为它必须是 GPE)。图 4 显示了,当一个实体的关系数量增加时,所有可能的多个关系组合中,无效的组合所占的比例也会迅速上升,当有 7 个关系时,甚至高达 83%。

3.3.2 Numerically correlated multiRoR

从数据集中的所有关系矩阵 R 中,可以发现不同类别的关系之间存在数值相关性。这种相关性不是针对两个具体的关系,而是针对两类关系的总数,因此属于 multiRoR 的范畴(每一类关系都可能有多次出现)。图 5 中的相关性图显示了,Per-Soc 和 Gen-Aff 两类关系的总数呈现出明显的正相关,而 Art 和 Org-Aff 两类关系的总数则呈现出负相关。

4 Method

手工制定各种类型的RoR规则是一种简单的方法,但是它不适用于具有不同特征的数据集,也无法识别一些难以人工发现的RoR。因此,本文的目标是设计一个不依赖于手工规则的RoR学习模型。 训练策略如图6所示。接下来,将详细介绍三个关键的模型组件:
  1. 实体和关系的初始嵌入。
  2. 基于GNN的biRoR学习器。
  3. 学习multiRoR的矩阵变换器。

4.1 Initialization of entities and relations

在准备阶段,首先为每个实体生成嵌入。利用预训练的BERT模型(Devlin et al., 2019)处理文本,并通过对BERT最后一层的每个词的隐藏状态取平均,得到每个实体的表示。本文的框架可以灵活地采用其他获取预训练嵌入的方法,例如(Yang et al., 2019; Liu et al., 2019)。接着,
将两个相关实体的嵌入拼接起来,并通过一个前馈层,得到每个关系的初始嵌入

4.2 BiRoR Learner

本文的目标是利用图神经网络(GNN)来学习文本序列中不同关系之间的相互作用,实现双向关系推理(biRoR)。给定一个包含

个实体的文本序列

,将其表示为一个图

,其中

是由所有的实体和关系组成的节点集合,

是由每个关系节点和其对应的两个实体节点组成的边集合。
采用图神经网络来捕捉节点之间的信息传递,特别是在关系节点之间。图神经网络的第一层以第4.1节中得到的节点嵌入(包括实体嵌入和关系嵌入)为输入。在第

层,图神经网络通过从邻居节点收集隐藏状态来更新当前层的节点表示,进而得到第

层的节点表示。具体地,有
其中

表示第

层中第

个节点的隐藏状态,

表示一个前馈网络,

表示一个权重矩阵,

表示节点

的邻居节点集合,

表示节点

对节点

的注意力权重。这个注意力权重

由下式计算:
其中



分别表示计算注意力时的键和查询权重矩阵。

4.3 MultiRoR Learner

图神经网络(GNN)具有强大的建模节点间交互的能力,这与双向关系推理(biRoR)相对应,但它在捕捉更复杂的多重关系推理(multiRoR)方面并不那么强大。例如,如果一个实体同时具有Org-Aff和Phys两种关系,那么它就不能具有Art关系。但是GNN结构并不一定能够捕捉涉及嵌套条件的这种复杂的multiRoR。因此,需要另一个模块来对关系矩阵

进行建模,它考虑了所有关系之间的动态,以便捕捉multiRoR。本文提出了一个简单而有效的模块,即关系矩阵Transformer。由于关系矩阵

中的每个关系

都需要关注所有其他关系,本文定制了基于Transformer的编码器架构来构建关系矩阵Transformer,它允许所有元素之间进行广泛的相互注意力(Vaswani et al., 2017)。将Transformer中的位置编码定制为两部分:行编码和列编码,每一部分都是一个从位置索引到

维向量空间的可学习映射。关系矩阵Transformer将位置编码,即行和列嵌入的和,加到4.1节中得到的关系的初始表示上。接着,关系矩阵Transformer学习所有关系之间的动态,并通过一个变换后的矩阵

输出所有关系的新特征,从而捕捉multiRoR。最后,将GNN和关系矩阵Transformer学习到的关系嵌入相加,然后将它们输入到最终的分类层,以获得每个关系的类型。

Prompt进阶系列1:LangGPT(从编程语言反思LLM的结构化可复用提示设计框架)

大语言模型 (Large Language Models, LLMs) 在不同领域都表现出了优异的性能。然而,对于非AI专家来说,制定高质量的提示来引导 LLMs 是目前AI应用领域的一项重要挑战。现有的提示工程研究已经提出了一些零散的优化原则,也有些研究设计了基于经验的提示优化器。然而,这些研究缺乏结构化的设计模板,学习成本高,可复用性低。受到编程语言的结构化和可复用性的启发,提出了双层提示设计框架 LangGPT,作为面向 LLM 的编程语言。LangGPT 具有易于学习的规范结构,并为迁移和复用提供了扩展结构。实验表明,与 baseline 相比,LangGPT 极大地增强了 LLM 生成高质量回复的能力。此外,也证明了 LangGPT 框架能有效地指导 LLM 自动化地生成高质量提示。

1. 提示优化介绍

以 ChatGPT 为代表的大型语言模型(Large Language Models, LLMs)[Achiam 等人,2023;Schulman 等人,2023] 可以基于强大的语言理解能力、推理能力和生成能力执行各种各样的任务[Sun 等人,2023b;Sun 等人,2023c;Yu 等人,2023]。此外,注入领域知识还能让 LLM 执行与领域相关的特定任务 [Wang 等人,2023a;Li 等人,2023b;Zhang 等人,2023;Ren 等人,2023]。要充分发挥 LLM 的这些能力,就需要构造高质量的提示[Eric,2022;Chen 等人,2023;Gajula,2023]。因此,提示工程 (Prompt Engineering) 吸引了许多研究人员的关注[Varshney 和 Surla,2023;Mesk´o,2023;Wang,2023]。

  • 提示工程是典型的经验科学,主要涉及提示设计和提示优化。由于 LLM 具备自然语言理解能力,可以通过非结构化的自然语言指令直接要求 LLM 执行任务。在此基础上,研究人员首先探索并总结出了一些优化提示的技巧。[Bsharat等人,2023年]介绍了26条指导原则,旨在让 LLMs 表现得更好。除了这些直接作用于提示的优化技巧外,一些研究人员还基于历史数据的提出了提示优化方法。[Sun 等人,2023a] 引导 LLM 从不正确的推理中得出特定实例的新提示,然后总结每个实例的对应提示,作为优化原始提示的参考。[Pryzant等人,2023年]定义了自然语言"梯度",利用小批量数据求解该"梯度"并利用波束搜索和 Bandit 选择程序,按照梯度的相反语义方向编辑当前提示。[Fan 等人,2023 年]对一个大型提示数据库进行分析,并提出了一个自动提示优化框架。

  • 基于历史数据的直接提示优化原则和方法需要丰富的经验。因此,这些方法通常只在特定任务或领域中表现良好。为了提高通用性,一些研究人员提出了自适应提示优化方法。[Guo 等人,2023 年] 将 LLM 与进化算法联系起来,提出了一种新的离散提示优化框架,称为 EvoPrompt。[Li等人,2023a]设计了一种多轮对话对齐策略,并利用GPT-4[Achiam等人,2023]生成易读的提示集。同时,他们还提出了一种高效的提示筛选指标,能以线性复杂度筛选出高质量的提示。[Wang等人,2023b]引入了PromptAgent,它可以反思模型错误并产生建设性的错误反馈,以诱导精确的专家级见解和深入的指导。[Hao等人,2022] 和 [Cheng等人,2023] 还从协调人类和 LLM 偏好风格的角度优化了提示。

  • 提示优化可以大大提高 LLM 的性能,但由于缺乏系统性设计,高质量提示的可复用性很差。因此,一些研究人员设计了构建提示的规则。[Nigh, 2023]收集了大量高质量提示,并总结了用于提示设计的 CRISPE 规则。[ZamfirescuPereira等人,2023年]以基于LLM的聊天机器人设计工具原型为设计探针,支持非人工智能专家参与"终端用户提示工程"。此外,一些研究人员为不同领域的应用设计了提示构建规则。[Cao 等人,2023 年]为 ChatGPT 提出了各种利用深度学习执行程序修复任务的提示模板。[Yeh等人,2022年]将生物医学关系提取任务重新表述为简单提示表述下的 cloze 测试任务,从而系统地生成全面的提示语。[Liu和Chilton,2022年]在文生图任务的五个实验中评估了涵盖51个主题和51种风格的5493个生成提示,并总结了提示设计指南。

  • 这些方法以AI专家大量的使用经验为基础,主要依赖列出的设计规则,缺乏系统性。此外,这些规则具有较强的领域相关性和模型相关性,但通用性、灵活性和可复用性较低。为了进一步释放 LLM 的性能,一些研究人员定义了 Agent。Agent 赋予 LLMs 使用工具、获取领域知识、保留长期或短期记忆以及制定计划的能力[Xu 等人,2023;Xi 等人,2023;Park 等人,2023]。虽然Agent [Chase,2022;Hong 等人,2023;Wu 等人,2023]系统地设计了提示的关键组件并预留了灵活的自定义接口,但学习成本非常高。此外,非 AI 专家很难修改 Agent 设计,可复用性较差。

为了推广基于 LLM 的应用,进一步激发 LLM 的潜力,希望设计一个高质量、可复用的提示模板。该模板应具有通用性和可扩展性。此外,该模板还应该易学易用。受 prompt 是 LLM 时代的编程语言这一观点的启发 [Alouani, 2023; Mund, 2023],设计了一种面向 LLM 的编程语言,即一种 prompt 设计框架
Lang
uage for
GPT
-like LLMs (LangGPT)。LangGPT 具有编程语言的系统性、规范性和可复用性的特点,并保留了自然语言的灵活性和可扩展性。首先分析了自然语言和编程语言之间的差异,以确定提示模板应具备的属性。LangGPT 设计为双层结构,由模块和内部元素组成。LangGPT 中的模块可分为两类:内置模块和扩展模块。对于内置模块,详细设计了每个模块的必要内部元素,并给出了示例模板。此外,对于扩展模块,统一了基本内部元素的设计。实验证明,在引导 LLM 方面,LangGPT 比基线提示更好。此外,LLM 可以利用这一框架自动生成提示,就像生成代码一样。

这项工作的主要贡献为:

  • 提出了双层结构的提示设计框架 LangGPT,以提高提示的通用性和可复用性。还在 LangGPT 的基础上编写了详细的提示设计规则,从而降低了提示设计的学习成本,使 LLM 能够自动生成高质量的提示语。此外,提供了基于 LangGPT 的常见场景的通用提示模板,进一步简化了提示设计成本。
  • 通过实验证明,基于 LangGPT 设计的提示可以更好地引导 LLM 执行任务。同时,举例说明了 LangGPT 可以帮助 LLM 生成高质量的提示。
  • 建立了一个基于 LangGPT 的在线社区,提供使用文档和提示设计界面。此外,收集和分享优秀的提示案例也促进了 LLM 应用的交流。在社区中进行了用户体验调查,验证了 LangGPT 的易用性和可复用性。

2. 参考编程语言构造提示设计规则

与自然语言相比,编程语言更具标准化和可复用性。为了设计高质量的可复用的提示,分析了自然语言和编程语言之间的差异,并提出了提示设计原则。

2.1 自然语言与编程语言的区别

自然语言主要用于交流,而编程语言则是为机器执行任务定义的指令[GeeksforGeeks, 2023]。不同的应用目的导致这两种语言的创造和演变背景截然不同。

  1. 目标受众
    :自然语言是由人类对人类说的[Grune等人,2012],而编程语言是由人类为机器准备的[Chakray,2018]。
  2. 结构
    :计算机只能理解固定的指令,这要求编程语言具有严格、严谨的语法和语义。相反,自然语言具有宽松而灵活的语法和语义,允许创造和变化,并具有高度的容错性。
  3. 模糊性
    :自然语言更加模棱两可,但人类有能力明确自然语言表达的意思。例如,顺序混乱和打字错误等问题对阅读的影响较小,发音错误也可以理解[Chakray,2018;Aho,2007]。编程语言比较准确,因为它们需要为计算机提供明确的指令[GeeksforGeek,2023]。
  4. 演变与更新
    :自然语言通过人类的使用和交流,随着时间的推移自然演变[Sipser,1996]。自然语言在增加新词与新的意义,以及摒弃过时用法方面是比较灵活的[Fromkin等人,2018年]。相反地,编程语言是专门为与机器通信而设计的[Sebesta,2012],新的语法规则和功能需要明确的升级或正式发布[Pratt等人,1984]。

总而言之,这两种语言的主要区别在于,自然语言更模糊、更灵活,而编程语言更规范、更精确。LLM本质上执行大量计算,与机器有许多相似之处。因此,借鉴了编程语言的特点,结合自然语言的优势,提出了一种面向 LLM 的自然语言编程框架--LangGPT。

2.2 提示设计规则

参考编程语言的设计思想来优化提示。在分析了编程语言和自然语言的区别之后,提出了提示的设计原则:

  • (1)
    提示应具有规范化的格式
    。灵活和模糊的自然语言对于LLM来说是很难理解的。格式规范的提示使用户的目的和要求更加突出。
  • (2)
    提示的结构应该是可扩展的
    。自定义结构便于用户根据自己的领域和任务设计合适的提示。
  • (3)
    具体要求必须明确、完整
    。指令和附加要求都应明确和完整,以避免误解或偏见。
  • (4)
    语言要灵活
    。在需求明确的情况下,灵活的语言可以更好地适应不同的领域。此外,灵活的语言也便于用户学习。

3 面向大语言模型的自然语言编程框架

基于这些设计规则,提出了一种面向 LLM 的双层自然语言编程框架--LangGPT。

3.1 整体双层结构

为了系统地设计符合规则的提示,充分参考了面向对象编程语言的设计思想和结构[Rentsch,1982;Lutz,2010]。将提示视为一个软件项目,并将提示设计过程类比为软件开发过程。对应关系如图1所示。

图 1:编程语言和自然语言提示之间的类比。依据层次结构分析了两种语言之间的相似性。不同大小的圆圈表示不同的层。较小的圆圈表示更靠近内层,对应于较深的颜色。

通过类比分析可以发现,自然语言提示与编程语言具有相似的多层次结构。因此,参考编程语言的结构,提出了提示设计的双层结构,并定义了提示的模块和元素的概念。

一个完整的提示包含几个模块。模块类似于编程语言中的类,每个模块代表要求LLM的一个方面。例如,可以在Constraint、Goal、Profile等方面增加提示。在一个模块中,包括许多内部元素。元素类似于编程语言中的函数和属性,表示对LLM的直接和特定指令的内容。例如“输出不应超过500字”可以是提示中属于Constraint模块的元素。

双层结构可以很好地标准化提示的格式。然而,如果过于严格地要求提示遵循预定义的内置模块和基本元素,无法利用自然语言的灵活性的优势。此外,还会降低LangGPT对不同领域不同任务的通用性,不利于高质量提示的复用。为了解决这些问题,将模块和元素的类型进行了划分。首先定义了内置模块和基本元素作为预定义的双层提示模板。此外,还构建了支持自定义的扩展模块和自定义元素。为内置模块和扩展模块提供Markdown[Gruber,2012]和JSON[Pezoa等人,2016]格式。此外,为不同的模块编写了基本元素,并定义了编写自定义元素的规则。

3.2 内置模块的构造

该模块作为完整提示和指令单元之间的连接部分,对于控制提示的结构有非常重要的作用。

为几乎所有提示所需的关键方面定义了内置模块。此外,还针对与应用程序相关的某些场景定义了内置模块,以便于学习和使用。表1展示了为某些场景定义的内置模块。

  • Prof. (Profile)指出了用户对LLM在角色方面的要求,包括个人简介、人物肖像等。
  • Cons. (Constraint)表示约束或突出,即不允许LLM超出的范围以及生成回复时必须满足的要求等。
  • Goal列出了用户想要实现的目标,这就是LLM需要完成的目标。Init. (Initialization)即初始化,以告知LLM即将开始对话。有时,该模块中还会给出指定的第一句话。
  • Ex. (Example)给出了输入-输出对作为供LLM学习的例子。
  • Wkflo. (Workflow)指示了执行任务时的工作流程,类似于 CoT 方法 [Wei 等人., 2023]。当任务需求比较复杂时,往往需要实例化这个模块。
  • Skill用于向LLM表明他们拥有的技能。经过工具学习的LLM,可以指导他们调用工具,更准确地执行任务。另外,计划在未来的工作中提供使用该模块下工具的能力,参考代理工具的设计[Chase, 2022;洪等人,2023]。
  • Sug. (Suggestion)包括对LLM的建议和行为规划。该模块重点列出常见场景,并告诉LLM在此类情况下可以采取的行为或应对措施。
  • Bkgrd. (Background)表示LLM在执行任务时需要具备的背景信息和记忆。
  • Style限定了LLM生成回复的风格。
  • Outf. (Output Format)定义了LLM的输出格式。指定输出格式可以提高某些任务中结果提取的效率和准确性。

表 1:内置模块定义的状态。该表列出了目前定义的8类应用场景以及为这些场景定义的模块。 ✓表示已经针对该场景设计了相应的模块。相反, ✗ 表示没有设计。

3.3 内部基本元素

  • 提示通常包含三个目的:
    1. 向LLM传递某种信息;
    2. 让LLM执行某个有输出或无输出的任务;
    3. 前两者的结合。

第一个与编程语言中属性或变量的定义非常相似。相应地,后两类类似于编程语言中的函数。因此,构建了这三种类型的基本元素。使用“⟨PROPERTY⟩ 是⟨VALUE⟩”语句来模拟赋值操作。对于后两种情况,需要指定输入信息、任务和输出,其中输入和输出可以省略。使用“对于给定的⟨VALUE⟩的⟨PROPERTY⟩,请执行以下操作:⟨ACTIONS⟩;返回⟨RESULT⟩。”的形式来模拟函数。在提供的基本元素模式中,
尖括号
中包含的内容需要根据模块和使用场景来填充。需要注意的是,提供的模式仅指定了编辑内部元素的想法。为了提高提示的通用性和灵活性,可以调整语言来表达关键信息。在表 2 中,展示了一些模块中基本元素的一些示例。

表2:写作场景中内置模块的基本内部元素示例。该提示属于写作场景,其目的是为给定的文章生成标题。选择了五个模块作为示例——Profile、Goal、Constraint、Workflow和Style——并展示了每个模块的一个内部元素。特别是,对于Workflow模块,展示了一个类似函数的基本元素。

3.4 扩展模块和自定义元素

定义的内置模块已经尽可能全面地涵盖了提示的许多方面。此外,还添加了LangGPT涵盖的应用场景和模块。但受限于自身的能力和领域知识,无法考虑LLM的所有应用场景。

因此,除了内置模块和基本元素之外,还定义了扩展模块和自定义元素,以提高提示的通用性和复用性。扩展模块和自定义元素的设计流程如图2所示。

这个过程可以帮助用户快速分析他们的需求并提出提示中需要包含的要点。基于LangGPT提供的设计模板以及设计流程,可以优化高质量提示以快速匹配应用场景。

图 2:扩展模块和自定义元素的设计流程。对于内置模块无法覆盖所有方面的应用场景,可以根据需要定义新的扩展模块。定义了扩展模块后,还应该根据扩展模块的要求来设计内部元素。另外,如果内置模块能够满足应用场景需求,但基本元素无法满足所有需求,可以直接在内置模块中添加自定义元素。需要注意的是,扩展模块和内置模块应尽可能相互排斥,以最大限度地减少修改成本。

4. 实验

为了验证提出的LangGPT的先进性,在两个方面进行了实验:LLM执行任务的效果和LangGPT的可用性调研。在的实验中,选择了两种类型的应用场景:写作和角色扮演。对于每个场景,选择了五个由社区用户构建和共享的具体的任务助手。

4.1 Baseline

为了进行比较,选择了两个baseline。

  • 直接指令
    :提示仅包含对LLM提出的说明以及所需的必要信息。
  • CRISPE
    :提示设计规则 [Night,2023]。该框架从宏观角度前瞻性地定义了提示的组成部分。它需要一个完整的提示,包含Capacity和Role、Insight、Statement、Personality和Experiment。

直接指令提示非常直观,可最大限度地节省token。但这种方式内容太少,往往不能全面传达需求,也难以抓住重点。 CRISPE提供了一个相对完整的设计框架,而不是一些零散的规则。但它没有清晰的结构化设计,不利于学习和高质量提示的复用。另外,CRISPE对提示中包含的元素有严格的规范,泛化能力较低。由于这些方法在提示设计上的缺点,设计了LangGPT。然而,除了这些直观的优势之外,还需要验证提示能否引导LLM执行任务,这才是提示设计最本质的目的。

4.2 大语言模型

选择了表 3 中所示的这些LLM进行评估,实验中的所有任务均于2024年1月18日前完成。

表 3:实验中使用的LLM。“unk”表示不知道这些模型的规模。

4.3 评估指标

由于任务场景的复杂性,缺乏客观的评价指标。因此,使用人工评估和LLM评估来评估LLM执行任务的能力。为了更好地保证评估的合理性,针对两种场景设定了评估标准。

对于写作任务,要求评估者从3个维度评估LLM执行任务的有效性:文本连贯性、格式规范和内容丰富度。生成主题连贯的内容的能力很重要。然而,发现LLM在生成任务中通常能够保持主题一致性,因此没有针对这方面设计评估指标。对于角色扮演任务,同样设计了3个维度的评价指标:语言风格、角色相关性和主题一致性。对于每个评估指标,定义了从0到5的分数来表示不同的级别,并解释了每个整数分数对应的情况的描述。此外,允许评估者在不同级别之间打 .5 分。

该评估框架还可以指导高性能LLM,例如GPT-4和Ernie Bot-4,自动评估其表现。由于LLM作为评估者的一致性较低[Liu et al., 2023],实验中也发现LLM评分的区分度很低,因此仅采用了人工评估的结果。

4.4 结果

提示引导LLM执行任务能力最直观的体现是LLM在执行提示引导的任务时的表现。因此,评估了LLM在两种情况下的表现,结果如表 4 及表 5 所示。

表 4:引导LLM执行任务的不同提示的结果。粗体字体表示在当前场景中效果最佳的提示类型。 I、C、L 分别表示三种提示符,即Instruction-only、CRISPE、LangGPT。 s1到s3分别代表两种场景的三个评估指标。对于写作场景来说,s1是文本连贯性,s2是格式规范,s3是内容丰富度。对于角色扮演场景,s1是语言风格,s2是角色相关性,s3是主题一致性。

从结果可以看出,LangGPT更擅长引导LLM执行任务。此外,还注意到一个有趣的现象。某些具有特别严格安全限制的LLM将拒绝回答,例如关于吹牛和暴躁等话题的问题或请求。 LangGPT可以让这些LLM认识到这只是一种说话方式,而
不是真正具有破坏性的行为
,并引导LLM生成回复。其他两个baseline都无法做到这一点。

为了评估 LangGPT 的易用性,在在线社区进行了一项用户调查。该社区已经运行了六个多月,已经聚集了来自各行各业的数千名用户,包括制造、建筑、信息技术、金融和娱乐。因此,调查结果的客观性能够得到保证。设计了一份关于 LangGPT 体验的完整调查问卷,以确保答案的质量。调查问卷包括关于易用性的评分问题。用户评分结果如图3所示。从图3可以看出,87.81%的用户给出了3分或更高的分数,这表明用户对LangGPT的易用性表示认可。此外,LangGPT在用户调查中的总体满意度得分为8.48分(满分10分)。

图 3:用户调查中的易用性评分。最低分是0分,表示非常难用,最高分是5分,表示非常容易使用。 “:”用于分隔分数和百分比。

表 5:不同LLM在两种情况下的平均得分。

4.5 案例研究

为了更直观地展示LangGPT的效果,从实验中筛选了具体案例。除了直接的效果比较之外,还尝试使用LangGPT来指导LLM生成质量提示。

提示引导LLM

在的实验中,使用三种提示来引导LLM扮演马屁精,图 4 中给出了一个示例。

在此示例中,直接指令提示和 CRISPE 提示引导的 ChatGPT 都只是回复用户的话语。相比之下,LangGPT 引导的 ChatGPT 在针对用户给定主题进行了更加夸张的吹嘘。此外,它从更广泛的角度表达赞美,也更有个性。

LangGPT引导LLM编写提示

在实验中,还尝试让LLM使用 LangGPT 构建提示。图 5 展示了一个示例。

图 4:马屁精的案例。 ChatGPT-3.5 在三种不同提示下对用户的回复。需要注意的是,明远大学是虚构的。

图 5:ChatGPT-3.5 使用 LangGPT 生成提示的示例。为了便于演示,一些模块被省略。

如果直接要求LLM生成有关 MBTI 评估的提示,他们可能会拒绝。然而,LangGPT 可以指导LLM生成无害的高质量提示。

5 结论

在本文中,提出了 LangGPT,一个用于提示设计的结构化且可扩展的框架。 LangGPT具有类似于面向对象编程语言的系统结构,易于学习和复用。实验表明,LangGPT 在指导LLM执行任务方面比baseline方法表现更好。还在LangGPT构建的社区中进行了用户调查,以验证LangGPT的易用性和可复用性。在未来的工作中,将进一步优化LangGPT的设计,减少LangGPTtoken消耗。此外,还将添加对LLM使用第三方工具和自定义工具的支持。

  • 伦理声明

在LLM的应用过程中,可能会出现伦理争议,但LangGPT的设计及本文的写作过程避免了可能出现的伦理问题。

更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。