logo

Net 高级调试之五:如何在托管函数上设置断点


风晓
风晓 2023-12-29 11:45:49 21669 赞同 0 反对 0
分类: 资源
一、简介     今天是《Net 高级调试》的第五篇文章。今天这篇文章开始介绍如何在托管方法和非托管方法设置断点,我们要想调试程序,必须掌握调试的一些命令,动态调试的命令,我们在上一篇文章已经讲过了。光有命令也是不行的,要让这些调试命令有用,必须可以在方法上设置断点,然后,再使用调试命令,才能完成我们的调试任务。当然了,第一次看视频或者看书,是很迷糊的,不知道如何操作,还是那句老话,一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现,我这是第三遍。      如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。     调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。           操作系统:Windows Professional 10           调试工具:Windbg Preview(可以去Microsoft Store 去下载)           开发工具:Visual Studio 2022           Net 版本:Net Framework 4.8           CoreCLR源码:源码下载

二、基础知识
    
    1、非托管函数下断点
        其实对非托管函数下断点是十分方便的,因为C/C++函数在编译之后就成了【机器代码】了,函数名就进入了【符号表】,比如我们可以非常方便的 notepad 的 SaveFile 函数下断点。

        操作步骤如下:
            a、打开 notepad 。
            b、使用  x notepad!SaveFile 下断点。
            c、在 notepad 上保存一下文件,就会触发断点。
        
    2、托管函数下断点
        
        2.1、简介
            托管函数下断点是很难的,因为你要下断点的方法的机器码在内存中可能还没有生成,也就是 JIT 从来就没有对该方法进行编译过,所以在还没有生成的代码上下断点就比较麻烦。虽然比较麻烦,并不代表不能实现,我们还是有三种方法可以对托管函数下断点的。

        2.2、托管函数下断点的三种方式

            1)、在编译后的函数上下断点
                这是最简单的一种方式,既然方法已经编译完成,肯定就已经生成了机器代码,那在编译后的函数上下断点就容易很多了,和非托管的是一样的。

            2)、在未编译的函数上下断点
                a、使用 !bpmd assembly.exe(模块包含后缀名) namespace.ClassName.MethodName
                b、使用 sosex 扩展的 mbm 下断点(只能在 Net framework 下使用,Net Core 是不支持的)

            3)、对泛型方法下断点
                如果我们想对泛型类型的方法下断点,最首要的任务就是找到泛型类型的名称和方法的名称,找到之后,我们就可以下断点了。找泛型类型的名称和方法的名称有两种办法,第一种是通过命令,第二种是我们可以使用 ILSpy 找到。


三、调试过程
    废话不多说,这一节是具体的调试操作的过程,又可以说是眼见为实的过程,在开始之前,我还是要啰嗦两句,这一节分为两个部分,第一部分是测试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。第二部分就是根据具体的代码来证实我们学到的知识,是具体的眼见为实。

    1、测试源码        
        1.1、Example_5_1_1

 View Code

     
        1.2、Example_5_1_2

 View Code


        1.3、Example_5_1_3

 View Code



    2、眼见为实
        
        2.1、在非托管函数 Notepad 的 SaveFile 方法上下断点。
            测试程序:Notepad
            操作流程:我们先要打开 notepad,然后再打开 windbg,点击【文件】菜单,然后通过【attach to process】附加进程,最后点击【Attach】按钮完成附加进程的操作。现在 notepad 是不能操作的,因为断点断住了,所以我们执行【g】命令,运行一下。然后我们点击工具栏【break】按钮,中断,然后就可以调试了。
            我们使用【x】命令查找一下 notepad 的 SaveFile 方法。

1 0:002> x notepad!*SaveFile*
2 00007ff6`e46be780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,class wil::unique_any_t<class wil::details::unique_storage<struct wil::details::resource_policy<unsigned short *,void (__cdecl*)(void *),&void __cdecl CoTaskMemFree(void *),struct wistd::integral_constant<unsigned __int64,0>,unsigned short *,unsigned short *,0,std::nullptr_t> > > &,bool,unsigned short const *))
3 00007ff6`e46d508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW)
4 00007ff6`e46e50b0 notepad!_imp_GetSaveFileNameW = <no type information>

            使用【bp】命令对 notepad!SaveFile 函数下断点。下断点后,继续运行,使用【g】命令。

1 0:002> bp notepad!SaveFile
2 0:002> g

            此时,我们可以操作 notepad,随便输入一些内容,然后点击【保存】,就可以被断点断住。

1 .......
2 Breakpoint 0 hit
3 notepad!SaveFile:
4 00007ff6`e46be780 488bc4          mov     rax,rsp

            我们可以使用【k】命令显示调用栈也能说明问题。

复制代码
 1 0:000> k
 2  # Child-SP          RetAddr               Call Site
 3 00 00000002`1a1dec58 00007ff6`e46b9336     notepad!SaveFile
 4 01 00000002`1a1dec60 00007ff6`e46badf4     notepad!NPCommand+0x2d2
 5 02 00000002`1a1df240 00007ff8`23e0e338     notepad!NPWndProc+0x844
 6 03 00000002`1a1df570 00007ff8`23e0dd79     USER32!UserCallWinProcCheckWow+0x2f8
 7 04 00000002`1a1df700 00007ff6`e46bb30c     USER32!DispatchMessageWorker+0x249
 8 05 00000002`1a1df780 00007ff6`e46d3b66     notepad!wWinMain+0x29c
 9 06 00000002`1a1df830 00007ff8`23b86fd4     notepad!__scrt_common_main_seh+0x106
10 07 00000002`1a1df870 00007ff8`25bbcec1     KERNEL32!BaseThreadInitThunk+0x14
11 08 00000002`1a1df8a0 00000000`00000000     ntdll!RtlUserThreadStart+0x21
复制代码

            截图效果:
              



        2.2、在已经编译的托管函数上下断点。
            测试程序:Example_5_1_1
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_5_1_1.exe】项目,通过【g】命令,运行程序,调试器运行代【Debugger.Break()】次会暂停执行。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。我们将在【sum = Sum(100, 200);】这行下断点。
            当调试器暂停的时候,说明有一部分代码已经执行了。【var sum = Sum(10, 20);】就是这行代码已经被执行,也可以说是被 JIT 编译了。我们如何查看 Sum 方法被编译的代码呢?可以使用【!name2ee】命令。

复制代码
1 0:000> !name2ee Example_5_1_1!Example_5_1_1.Program.Sum
2 Module:      00fc4044
3 Assembly:    Example_5_1_1.exe
4 Token:       06000002
5 MethodDesc:  00fc4d64
6 Name:        Example_5_1_1.Program.Sum(Int32, Int32)
7 JITTED Code Address: 01010908
复制代码

            红色标注表示代码已经编译,我们也可以使用【!u】命令查看这个代码的汇编代码,汇编代码很多,所以折叠。

 View Code

            

            我们使用【bp】命令在地址【01010908】地址处下断点,【g】继续运行,就会在 Sum 方法的第一行断住。

0:000> bp 01010908

            继续运行后,            

复制代码
1 0:000> g
2 Breakpoint 0 hit
3 eax=00000000 ebx=00aff108 ecx=00000064 edx=000000c8 esi=02b524bc edi=00aff058
4 eip=01010908 esp=00aff03c ebp=00aff068 iopl=0         nv up ei pl zr na pe nc
5 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
6 Example_5_1_1!COM+_Entry_Point <PERF> (Example_5_1_1+0x920908):
7 01010908 55              push    ebp
复制代码

            效果截图:
              


            
        2.3、在未编译的托管函数上下断点。
            测试程序:Example_5_1_2
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_5_1_2.exe】项目,通过【g】命令,运行程序,调试器运行代【Debugger.Break()】次会暂停执行,此行代码在12。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。这次我们的任务依然是在 Sum 方法上下断点。
            先来一个截图吧,效果更明显:
            
              我们还是先使用【!name2ee】命令查找一下 Sum 方法。

复制代码
1 0:000> !name2ee Example_5_1_2!Example_5_1_2.Program.Sum
2 Module:      00f34044
3 Assembly:    Example_5_1_2.exe
4 Token:       06000002
5 MethodDesc:  00f34d64
6 Name:        Example_5_1_2.Program.Sum(Int32, Int32)
7 Not JITTED yet. Use !bpmd -md 00f34d64 to break on run.
复制代码

              红色标注说明,我们的代码还没有被JIT编译。我们听从他的建议,使用【!bpmd -md】下断点。

1 0:000> !bpmd -md 00f34d64
2 MethodDesc = 00f34d64
3 Adding pending breakpoints...

              我们可以使用【g】命令,继续运行,果然在断点处暂停。我这里是可以的,但是视频说是不可以的。

复制代码
 1 0:000> g
 2 (bb0.3798): CLR notification exception - code e0444143 (first chance)
 3 JITTED Example_5_1_2!Example_5_1_2.Program.Sum(Int32, Int32)
 4 Setting breakpoint: bp 00F8094C [Example_5_1_2.Program.Sum(Int32, Int32)]
 5 Breakpoint: JIT notification received for method Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 0100da40.
 6 Breakpoint 0 hit
 7 eax=00f80928 ebx=009ef098 ecx=0000000a edx=00000000 esi=02c224bc edi=009eefe8
 8 eip=00f8094c esp=009eefb8 ebp=009eefc8 iopl=0         nv up ei pl zr na pe nc
 9 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
10 Example_5_1_2!COM+_Entry_Point <PERF> (Example_5_1_2+0x73094c):
11 00f8094c 90              nop
复制代码

             截图效果:
              


            1)、使用 !bpmd moduleName namespace.ClassName.MethodName
                这个实现的原理是借助 JIT 的编译完成通知事件,在事件中判断当前编译的是否是我们下断点的方法,如果是就转成 bp 命令下断点,从输出的信息也可以看得出来。
                【!bpmd】命令格式一下两种都对:!bpmd Example_5_1_2.exe Example_5_1_2.Program.Sum 或者 !bpmd Example_5_1_2 Example_5_1_2.Program.Sum

1 0:000> !bpmd Example_5_1_2 Example_5_1_2.Program.Sum
2 Found 1 methods in module 011a4044...
3 MethodDesc = 011a4d64
4 Adding pending breakpoints...

                断点设置成功,【g】继续运行,成功在断点处暂停。

复制代码
 1 0:000> g
 2 (14c4.1b60): CLR notification exception - code e0444143 (first chance)(表示 JIT 已经编译好了,CLR 给 Windbg 发出一个异常通知)
 3 JITTED Example_5_1_2!Example_5_1_2.Program.Sum(Int32, Int32)
 4 Setting breakpoint: bp 02DD094C [Example_5_1_2.Program.Sum(Int32, Int32)](Windbg 拿到编译后的机器码地址,重新设置断点)
 5 Breakpoint: JIT notification received for method Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 011f0fe8.
 6 Breakpoint 0 hit
 7 eax=02dd0928 ebx=00efedd8 ecx=0000000a edx=00000000 esi=02f124bc edi=00efed28
 8 eip=02dd094c esp=00efecf8 ebp=00efed08 iopl=0         nv up ei pl zr na pe nc
 9 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
10 Example_5_1_2!COM+_Entry_Point <PERF> (Example_5_1_2+0x22d094c):
11 02dd094c 90              nop
复制代码

                截图效果:
                


            2)、使用 sosex 扩展的 mbm 下断点(只能在 Net framework 下使用,Net Core 是不支持的,必须使用.load命令加载 SOSEX.dll)
                mbm 是非托管命令 bm 的托管版本,对方法名下断点,还支持模糊匹配。
                

1 0:000> !mbm Example_5_1_2!Example_5_1_2.Program.Sum

                设置断点后,我们可以继续运行,执行【g】命令。

复制代码
1 0:000> g
2 Breakpoint: JIT notification received for method Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 00a61010.
3 Breakpoint set at Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 00a61010.
4 Breakpoint 5 hit
5 eax=00a20928 ebx=008ff050 ecx=0000000a edx=00000000 esi=029d24bc edi=008fef98
6 eip=00a2094c esp=008fef68 ebp=008fef78 iopl=0         nv up ei pl zr na pe nc
7 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
8 Example_5_1_2!COM+_Entry_Point <PERF> (Example_5_1_2+0x5a094c):
9 00a2094c 90              nop
复制代码

                成功断住,效果如图:

                
                【mbm】命令很强大,也可以支持模糊查找。这个操作是另外一个过程,需要重新运行调试程序。

1 0:000> !mbm Example_5_1_2!*Sum

                成功在断点出暂停。

复制代码
1 0:000> g
2 Breakpoint: JIT notification received for method Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 01720fb8.
3 Breakpoint set at Example_5_1_2.Program.Sum(Int32, Int32) in AppDomain 01720fb8.
4 Breakpoint 1 hit
5 eax=031c0928 ebx=0137f378 ecx=0000000a edx=00000000 esi=033d24bc edi=0137f2c8
6 eip=031c094c esp=0137f298 ebp=0137f2a8 iopl=0         nv up ei pl zr na pe nc
7 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
8 Example_5_1_2!COM+_Entry_Point <PERF> (Example_5_1_2+0x21e094c):
9 031c094c 90              nop
复制代码


              
        2.4、在泛型方法上下断点。
            测试程序:Example_5_1_3
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_5_1_3.exe】项目,通过【g】命令,运行程序,调试器运行代【Debugger.Break();】次会暂停执行。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            这次的任务是,我们要在泛型类型 MyList<T> 的 Add() 方法上下断点。
            我们想要在泛型类型的方法上下断点,首要的任务是找到泛型类型的名称和方法的名称,这是关键。
            效果如图:
            

            a、我们通过 Windbg 和 SOS 的命令找到类型的名称。
                编译程序集后,泛型类型一定在这个程序集的模块中。然后我们再在这个模块中打印出所有的类型,就可以找到这个类型了。
                我们现在这个程序集中查找模块信息,我们可以使用【!dumpdomain】命令。

复制代码
 1 0:000> !dumpdomain
 2 --------------------------------------
 3 System Domain:      7141caf8
 4 ....
 5 --------------------------------------
 6 Shared Domain:      7141c7a8
 7 ......
 8 
 9 --------------------------------------
10 Domain 1:           00a2da30
11 ......
12 
13 Assembly:           00a87ea8 [E:\Visual Studio 2022\Source\Projects\......\Example_5_1_3\bin\Debug\Example_5_1_3.exe]
14 ClassLoader:        00a873a8
15 SecurityDescriptor: 00a872a0
16   Module Name
17 01004044(模块地址)    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_5_1_3\bin\Debug\Example_5_1_3.exe
复制代码

              我们找到了模块,就可以将模块中所有的类型输出出来,可以使用【!dumpmodule -mt 】命令。

复制代码
 1 0:000> !dumpmodule -mt 01004044 
 2 Name:       E:\Visual Studio 2022\Source\Projects\...\Example_5_1_3\bin\Debug\Example_5_1_3.exe
 3 Attributes: PEFile 
 4 Assembly:   00a87ea8
 5 LoaderHeap:              00000000
 6 TypeDefToMethodTableMap: 01000038
 7 TypeRefToMethodTableMap: 01000048
 8 MethodDefToDescMap:      01000094
 9 FieldDefToDescMap:       010000a8
10 MemberRefToDescMap:      00000000
11 FileReferencesMap:       010000b8
12 AssemblyReferencesMap:   010000bc
13 MetaData start address:  006220a8 (1680 bytes)
14 
15 Types defined in this module
16 
17       MT  TypeDef Name
18 ------------------------------------------------------------------------------
19 01004d6c 0x02000002 Example_5_1_3.Program
20 01004de8 0x02000003 Example_5_1_3.MyList`1
21 
22 Types referenced in this module
23 
24       MT    TypeRef Name
25 ------------------------------------------------------------------------------
26 6f802734 0x02000010 System.Object
27 6f847540 0x02000011 System.Diagnostics.Debugger
28 6f808af0 0x02000012 System.Console
复制代码

              红色标注的就是我们要查找泛型类型真实的名称。有了类型,我们继续可以使用【!dumpmt -md ...】命令,输出它所有方法。

复制代码
 1 0:000> !dumpmt -md 01004de8
 2 EEClass:         01001334
 3 Module:          01004044
 4 Name:            Example_5_1_3.MyList`1
 5 mdToken:         02000003
 6 File:            E:\Visual Studio 2022\Source\Projects\......\Example_5_1_3\bin\Debug\Example_5_1_3.exe
 7 BaseSize:        0xc
 8 ComponentSize:   0x0
 9 Slots in VTable: 6
10 Number of IFaces in IFaceMap: 0
11 --------------------------------------
12 MethodDesc Table
13    Entry MethodDe    JIT Name
14 6fbf97b8 6f7fc838 PreJIT System.Object.ToString()
15 6fbf96a0 6f938978 PreJIT System.Object.Equals(System.Object)
16 6fc021f0 6f938998 PreJIT System.Object.GetHashCode()
17 6fbb4f2c 6f9389a0 PreJIT System.Object.Finalize()
18 02840458 01004dd4   NONE Example_5_1_3.MyList`1..ctor()
19 02840450 01004dcc   NONE Example_5_1_3.MyList`1.Add(!0)
复制代码

              红色标记就是我们要查找的 Add 方法,有了方法的地址,我们就可以使用【bpmd】命令为其下断点了。

1 0:000> !bpmd Example_5_1_3 Example_5_1_3.MyList`1.Add
2 Found 1 methods in module 01004044...
3 MethodDesc = 01004dcc
4 Adding pending breakpoints...

              断点设置成功后,我们使用【g】命令,程序继续运行,就可以在断点处暂停。

复制代码
 1 0:000> g
 2 (3ab4.2920): CLR notification exception - code e0444143 (first chance)
 3 JITTED Example_5_1_3!Example_5_1_3.MyList`1[[System.Int32, mscorlib]].Add(Int32)
 4 Setting breakpoint: bp 02840942 [Example_5_1_3.MyList`1[[System.Int32, mscorlib]].Add(Int32)]
 5 Breakpoint: JIT notification received for method Example_5_1_3.MyList`1[[System.Int32, mscorlib]].Add(Int32) in AppDomain 00a2da30.
 6 Breakpoint 0 hit
 7 eax=02840928 ebx=007bee20 ecx=029f26b0 edx=0000000a esi=00000000 edi=007bed90
 8 eip=02840942 esp=007bed58 ebp=007bed60 iopl=0         nv up ei pl zr na pe nc
 9 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
10 Example_5_1_3!COM+_Entry_Point <PERF> (Example_5_1_3+0x2220942):
11 02840942 90              nop
复制代码

              断点效果如图:
              


            b、我们可以使用 ILSpy 来查找泛型类型的名称和方法的名称。
              效果如图:
              

                上面就是类型的名称,我继续查找方法的名称,也很简单。
              效果如图:

          

              有了这些信息,我们就可以使用 Windbg 为程序设置断点了,操作过程和 a 的过程一样,就不多说了。


四、总结
    终于写完了,为什么说是终于,因为写这一篇文章,不是一天完成的。写文章,记录操作过程,作图例,所以时间就长了。今天介绍的是如何在方法上设置断点,有了断点,我们就可以使用上一篇讲的动态调试命令,我们就可以更容易完成调试任务,掌握这些调试技巧还是很有必要的。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓
粉丝 1 资源 2038 + 关注 私信
最近热门资源
工业防火墙为啥不走寻常路?  637
窃密软件无孔不入?迪普科技防护策略为隐私数据筑牢防线  278
最近下载排行榜
工业防火墙为啥不走寻常路? 0
窃密软件无孔不入?迪普科技防护策略为隐私数据筑牢防线 0
作者收入月榜
1

prtyaa 收益363.15元

2

风晓 收益207.84元

3

IT-feng 收益198.17元

4

zlj141319 收益178.22元

5

777 收益172.06元

6

1843880570 收益171.31元

7

信创来了 收益103.8元

8

Fhawking 收益99.6元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.05元

请使用微信扫码