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

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

一、简介
    今天是《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
 1 namespace Example_5_1_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var sum = Sum(10, 20);
 8 
 9             Debugger.Break();
10 
11             sum = Sum(100, 200);
12 
13             Console.WriteLine($"sum={sum}");
14         }
15 
16         private static int Sum(int a, int b)
17         {
18             var sum = a + b;
19 
20             return sum;
21         }
22     }
23 }
View Code      
        1.2、Example_5_1_2
 1 namespace Example_5_1_2
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Console.WriteLine("请查看:未编译形态的 Sum 方法");
 8             Debugger.Break();
 9 
10             var sum = Sum(10,20);
11             Console.WriteLine("请查看:已经编译形态的 Sum 方法");
12             Debugger.Break();
13 
14             sum = Sum(100, 200);
15 
16             Console.WriteLine($"sum={sum}");
17         }
18 
19         private static int Sum(int a, int b)
20         {
21             var sum = a + b;
22 
23             return sum;
24         }
25     }
26 }
View Code
        1.3、Example_5_1_3
 1 namespace Example_5_1_3
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Debugger.Break();
 8 
 9             var mylist = new MyList<int>();
10 
11             mylist.Add(10);
12 
13             Console.ReadLine();
14         }
15     }
16 
17     public class MyList<T>
18     {
19         public T[] arr = new T[10];
20 
21         public void Add(T t)
22         {
23             arr[0] = t;
24         }
25     }
26 }
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】命令查看这个代码的汇编代码,汇编代码很多,所以折叠。

 1 0:000> !u 01010908
 2 Normal JIT generated code
 3 Example_5_1_1.Program.Sum(Int32, Int32)
 4 Begin 01010908, size 3e
 5 
 6 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_5_1_1\Program.cs @ 20:
 7 >>> 01010908 55              push    ebp
 8 01010909 8bec            mov     ebp,esp
 9 0101090b 83ec10          sub     esp,10h
10 0101090e 894dfc          mov     dword ptr [ebp-4],ecx
11 01010911 8955f8          mov     dword ptr [ebp-8],edx
12 01010914 833df042fc0000  cmp     dword ptr ds:[0FC42F0h],0
13 0101091b 7405            je      01010922
14 0101091d e85ef40670      call    clr!JIT_DbgIsJustMyCode (7107fd80)
15 01010922 33d2            xor     edx,edx
16 01010924 8955f0          mov     dword ptr [ebp-10h],edx
17 01010927 33d2            xor     edx,edx
18 01010929 8955f4          mov     dword ptr [ebp-0Ch],edx
19 0101092c 90              nop
20 
21 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_5_1_1\Program.cs @ 21:
22 0101092d 8b45fc          mov     eax,dword ptr [ebp-4]
23 01010930 0345f8          add     eax,dword ptr [ebp-8]
24 01010933 8945f4          mov     dword ptr [ebp-0Ch],eax
25 
26 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_5_1_1\Program.cs @ 23:
27 01010936 8b45f4          mov     eax,dword ptr [ebp-0Ch]
28 01010939 8945f0          mov     dword ptr [ebp-10h],eax
29 0101093c 90              nop
30 0101093d eb00            jmp     0101093f
31 
32 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_5_1_1\Program.cs @ 24:
33 0101093f 8b45f0          mov     eax,dword ptr [ebp-10h]
34 01010942 8be5            mov     esp,ebp
35 01010944 5d              pop     ebp
36 01010945 c3              ret
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 的过程一样,就不多说了。


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