WinDbg调试C#值类型方法表
一. 基础部分
1. 十六进制
如:0x2F
一共 8 位,4 位一个
如果是 64 位(x64平台)的话,按照 4 位一个十六进制的符号,那么有 16 个,也就是 8 组;同理,如果是 32 位(x86平台)的,那么有 8 个,也就是 4 组。
2. 大小端表示法
可以查看下面的文章
- 计算机:小端表示法(低地址放低位数据)
- 通讯协议:大端表示法(低地址放高位数据)
例如:0x 00 00 00 01 28 F2 A1 2B
低地址 ------------------------> 高地址
用小端表示法:
有这样表示的(比较方便人查看):
2B | A1 | F2 | 28 | 01 | 00 | 00 | 00
也要这样表示的,完全是相反的:
B2 | 1A | 2F | 82 | 10 | 00 | 00 | 00
用大端表示法:
00 | 00 | 00 | 01 | 28 | F2 | A1 | 2B
3. 值类型与引用类型
- 值类型:直接放在栈上,按值类型对象的成员来放,会被 CLR 优化存放的顺序(为了对齐内存)
- 引用类型:类型保存的位置在堆上,在栈上存放这个引用类型所保存在堆上的地址,同理在堆上的成员存放顺序也会被 CLR 优化
二. 调试所用的代码
先来看一下今天用来调试值类型方法表的程序:
- 值类型的类对象
public struct Student { public string Name; public int Age; public string Address; public Student(string name, int age, string address = "") { this.Name = name; this.Age = age; this.Address = address; } public void PrintStudentInformation() { string info = string.Format("姓名:{0} 年龄:{1} 住址:{2}", this.Name, this.Age, this.Address); Console.WriteLine(info); } public string GetName() { return this.Name; } }
- 上端调用代码
Student student = new Student("恋如雨止", 26, "嘉兴市"); student.PrintStudentInformation(); object obj = (object)student; Console.ReadKey();
三. 调式流程
1. 打开程序 Demo.exe ,程序停在 Console.ReadKey()。用 WinDbg 附加到进程之后打开,还是老步骤先用 .chain 看 sos.dll 是不是存在。如下图:

如果不在那么就使用 .loadby sos clr
2. 我们再来设置一下调试的符号,我们把工程 Debug 目录下面的 .pdb 后缀拷贝到符号文件目录。

我们这边的符号文件的目录为:" c:\mysymbol "

我们把 .pdb 文件拷贝到这个符号文件目录中

3. 使用 !threads 来看一下线程

可以看到有两个线程
- 一个是 Main 方法的主线程
- 一个是 GC 的终结线程
4. 我们接着使用 ~0s 切换到 Main 方法的线程。

5. 然后,看一下局部的变量,输入 !clrstack -l 。

看到局部变量的地址:
0x000000540affef98 = 0x0000019680002cf8
0x000000540affef80 = 0x0000019680006b38
6. 我们使用 !do 或者使用 !dumpobj 来查曾局部变量的方法表地址。这边如果使用 /d 的设置,会给我们在显示的结果中给出跳转地址,当然也可以不使用这个开关。我们分别查看一下这两个局部变量的地址是什么?
输入 !do 0x0000019680002cf8

输入 !do 0x0000019680006b38

一个类型是 Demo.Student 而另外一个是 System.String。明显地,我们可以知道这个 Demo.Student 是值类型经过装箱之后的状态,即
object obj = (object)student;
后保存在局部的变量。对于装箱后的 Demo.Student 的这个类型,它的方法表是:
MethodTable: 00007ffb10055b28
6. 通过 !dumpmt -m 00007ffb10055b28 来查看一下 Demo.Student 的方法表。

方法表的内容为:
MethodDesc Table
Entry MethodDesc JIT Name
00007ffb5b1fd540 00007ffb5a5f6140 PreJIT System.ValueType.ToString()
00007ffb5a9982b0 00007ffb5a5f6138 PreJIT System.ValueType.Equals(System.Object)
00007ffb5ab46e98 00007ffb5a5f6188 NONE System.ValueType.GetHashCode()
00007ffb5aa3be70 00007ffb5a5f0d20 PreJIT System.Object.Finalize()
00007ffb10160940 00007ffb10055ae0 JIT Demo.Student..ctor(System.String, Int32, System.String)
00007ffb101609b0 00007ffb10055af0 JIT Demo.Student.PrintStudentInformation()
00007ffb101604a8 00007ffb10055b00 NONE Demo.Student.GetName()
7. 那么另外一个 System.String 类型的局部变量是什么呢?还有我们保存的
Student student = new Student("恋如雨止", 26, "嘉兴市");
去哪里了呢?
上面讲过,值类型的对象是直接把数据内容保存在栈中的。我们先看一下:

LOCALS:
0x000000540affef98 = 0x0000019680002cf8
0x000000540affef80 = 0x0000019680006b38
这边的Locals,左边的栈上的地址,而 " = " 右边的地址是左边栈上地址中内容所指向堆中的地址。
0x000000540affef98 是值类型对象在栈中的起始的地址,而右边的 0x0000019680002cf8 是这个值类型对象的一个 string 类型字段所在堆中的地址。
8. 我们查看一下这个值类型对象的内存,通过 alt + 5 或者点击上面的按钮,打开 " 内存(Memory)" 的窗体,输入栈中的起始地址后可以看到内存的分布:

我们在查看一下,红色和蓝色处的地址索引 0x0000019680002cf8 和 0x0000019680002d20。

分别是 " 恋如雨止 " 和 " 嘉兴市 " 这个就是我们初始化值类型的 string (当然这两个 string 是保存在字符串的驻留池中的,在系统域中)
四. 其他的方法
我们使用对象的类型来查看一下 Demo.Student。
1. 输入 !name2ee Demo Demo.Student,如下图:

方法表的地址为:
0x0000019680002cf8 和 0x0000019680002d20
和上面装箱对象查出来的方法表的地址是一样的。
2. 输入 !dumpmt -md 00007ffb10055b28 得到和上面一样的结果。
五. 代码程序下载
六. 结束语
那么问题来了,值类型装箱和拆箱究竟发生了什么,请关注博主之后的文章!~

浙公网安备 33010602011771号