.Net Windbg 与汇编基础(学习笔记) 电脑版发表于:2022/9/19 13:59  ># .Net Windbg 与汇编基础(学习笔记) [TOC] ## 为什么要学习汇编? tn2>有时候再Debug下可以运行的逻辑,但在Release下却无法实现。 举例:主线程创建一个工作线程,在500ms后准备终止工作线程,在Release模式下无法终止。。。(代码如下) ```csharp internal class Program { static void Main(string[] args) { var worker = new Worker(); Console.WriteLine("Main thread: Start the work thread..."); var workerTask = Task.Run(worker.DoWork); // 等待 500 毫秒以确保工作线程已在执行 Thread.Sleep(500); Console.WriteLine("Main thread: request to terminate the work thread..."); worker.RequestStop(); workerTask.Wait(); Console.WriteLine("Main thread: The work thread is terminated"); Console.ReadLine(); } } public class Worker { private bool _shouldStop; public void DoWork() { bool work = false; while (!_shouldStop) { work = !work; // do sth. } Console.WriteLine("Work threads: being terminated..."); } public void RequestStop() { _shouldStop = true; } } ``` tn2>接下来我们通过Debug运行,通过改变_shouldStop变量的值,发现该线程正常的在500ms后退出。  tn2>然后我们通过Release运行,并进行发布却没有达到预期的效果。  tn2>随后我们通过windbg附加进程的方式进行调试。  tn2>首先我们来看一下所有线程栈。 ```bash ~*e !clrstack ```  tn2>我们可以看到指向了Program类中的DoWork方法第41行,我们可以点一下,也可以执行一下下面的命令看执行哪里。 ```bash !U /d 00007ff933d7bdef ```  tn2>接着我们可以下个断点。 ```bash bp 00007ff9`33d7bdf4 g ```  tn2>然后我们打开汇编窗口。  tn2>通过执行`t`命令不断的单步执行,我们发现主要执行的是如下几句汇编代码。  tn2>`test`汇编命令表示判断寄存器中`ecx`与`ecx`的值。 `je`如果判断的值相等就跳转到`00007FF933D7BDEA`这个位置。 我们先来看看`ecx`的值 ```bash r ecx ```  tn2>`0`永远等于`0`,所以这个问题的关键在于它把`_shouldStop`的值放到了寄存器中,无论你怎么改变值它寄存器中永远不会改变,所以解决方法就是让它不从寄存器从内存中去比较值。 我们可以看看当前内存中的`_shouldStop`值。 ```bash !name2ee Exmaple_2_1_1!Exmaple_2_1_1.Worker # 这样找也可以 加类名 !dumpheap -type Worker # 然后我们找到第一个地址,可以看到_shouldStop值为1 !DumpObj /d 00000251407eb1d0 ```  tn2>出现了这种情况,我们应该给`_shouldStop`添加上`volatile`关键字,表示该值是一个异变的结构。 ```csharp private volatile bool _shouldStop; ``` tn2>然后我们再次运行程序,发现就可以了。  tn2>我们再通过windbg的方式来看看。 ```csharp !name2ee Exmaple_2_1_1!Exmaple_2_1_1.Worker.DoWork !U /d 00007ff933d6bde0 ```  tn2>在第39行中我们发现与原来的汇编代码有所出入,这里是直接通过rcx+8与0进行比较(rcx表示类的地址,+8表示偏移8位)。 直接从内存中进行比较就没问题了。 ## 内存单元和CPU三大总线 >### 理解内存单元 tn2>Bit:计算机中最小的信息单元 (8bit=1Byte), Bitmap 算法就得益于它的威力。 Byte:计算机中最小的数据存储单元,简而言之,一个地址占用一个1byte。 我们通过vs来查看数据的格式大小。 ```csharp static void Main(string[] args) { Console.WriteLine("hello world!"); Console.ReadLine(); } ```  tn2>接下来,我们也可以通过windbg来进行查看。 对应的依次是:db,dw,dd, dq。  >### CPU三大总线 tn2>地址总线: 现在的 intel i7 的CPU上,一般是 48 根地址总线,每根线可以表示 0/1 两种状态(高电平,低电平), 所以它最多能表示 248 个地址,即地址范围是: 0 ~ 0000ffff\`ffffffff。 一个地址能存放一个byte,所以最大寻址空间为:248 * 1byte = 256T  tn2>我们可以通过`!address -summary`命令查看地址空间,用户空间占用126T。  tn2>这里用户态的最大寻址空间为`128T`可以看到我这里Free+MappendFile的总和就是用户态最大的存储地址空间,另外还有`128T`在内核态中。 如果你想看更详细的请执行`!address`命令  tn2>数据总线: 现在的 intel i7 的CPU上,一般是 64 根数据总线,它决定了一次性可以从内存中读取 64bit 的数据,也就是 8byte。  tn2>比如说:一个 long 类型,在 32 位操作系统上需要走两次内存,在 64bit 上只需要一次。 <br/> tn2>控制总线: 它决定了对计算机外部器件的控制能力,比如说对内存可以发起 “读”或 “写” 命令。  tn2>综上:CPU读写内存,需要经过 地址总线,数据总线,控制总线 的多次往返,那么如何规避这些不必要的开销是我们思考的问题! 比如合理利用 CPU 内部的“CPU 缓存 & CPU 寄存器” ##常见的寄存器 tn2>1.寄存器是寄宿于 CPU 中的信息存储部件,通过内部总线实现了高效计算,比读内存速度要快几个数量级。 2.在 高级调试 中,我们需要熟练掌握这 10 个寄存器 (32bit) ,大体上分为 4 类。  >### 数据寄存器 | 寄存器名称 | 描述 | | ------------ | ------------ | | EAX | 累加器 | | EBX | 基数寄存器 | | ECX | 计数寄存器 | | EDX | 数据寄存器 | tn2>JIT 在将 IL 代码转成 汇编代码的时候,一般会遵守一些约定成俗的规定,比如: 在数据的 `+ ,- ,*,/` 方面,优先会使用 eax 寄存器,在方法的返回值上面,在方法同样优先使用 eax 。 (举例代码如下) ```csharp static void Main(string[] args) { /* +,-,*,/ */ var a = 10; var b = a + 10; var c = a - 20; var d = ++a; //获取方法返回值 var age = GetAge(); } static int GetAge() { int age = 10; return age; } ```  tn2>为`[ebp-3Ch]`赋值操作 ```csharp mov dword ptr [ebp-3Ch],0Ah ``` tn2>先将eax寄存器附上`[ebp-3Ch]`(a)的值,然后做一个`add`相加`0Ah`(这里是16进制相当于10进制的10),然后再赋值给`[ebp-40h]`(b)。 ```bash mov eax,dword ptr [ebp-3Ch] add eax,0Ah mov dword ptr [ebp-40h],eax ``` tn2>与上不同的是它这里加的负数。 ```bash mov eax,dword ptr [ebp-3Ch] add eax,0FFFFFFECh mov dword ptr [ebp-44h],eax ``` tn2>然后我们来看方法返回值这里,返回是通过EAX寄存器来进行赋值的,我们可以通过按F10来进行调试。 ```bash call CLRStub[MethodDescPrestub]@a859fb0804a0ac18 (04A0AC18h) mov dword ptr [ebp-54h],eax mov eax,dword ptr [ebp-54h] mov dword ptr [ebp-4Ch],eax ```   tn2>可以清晰的看到EAX发生了改变。 >### 变址寄存器 | 寄存器名称 | 描述 | | ------------ | ------------ | | ESI | 源变址寄存器 | | EDI | 目的变址寄存器 | tn2>常用于做字符串的赋值,比如在 C 语言的 main 序幕代码中,就有一段初始化栈空间的操作,代码的意思就是从 edi 开始,依次将 eax 中的 0CCCCCCCCh 赋值 ecx=0x17(转换成10进制就是23次) 次,可以看到这段区间内都是 cc 符。(代码如下) ```c #include <iostream> int main() { int nums[20] = { 10,11,12,13 }; } ```    >### 栈指针寄存器 | 寄存器名称 | 描述 | | ------------ | ------------ | | EBP | 基址指针寄存器 | | ESP | 堆栈指针寄存器 | tn2>每一个方法都有一个属于自己的方法栈帧,这个栈帧的范围就是用 EBP 和 ESP 标识的。  tn2>我们可以通过刚刚的例子通过汇编代码用ESP算出EBP的大小。 ```bash mov ebp,esp sub esp,11Ch push ebx push esi push edi ```  tn2>当前esp为`00CFF85C`,然后加上`11C`,再加上的三个入栈的`ebx`、`esi`、`edi`每个占4字节,最后得出EBP的基栈地址`CFF984`。  >### 控制寄存器 | 寄存器名称 | 描述 | | ------------ | ------------ | | EIP | 指令指针寄存器 | | PSW | 状态标志寄存器 | tn2>EIP: 用来保存程序下一步需要执行的指令地址,跳跃的长度就是机器码的byte数。  tn2>PSW :用来保存运算(CMP,TEST)过程中出现的比如 ZF(零标志位), OF (溢出标志)标志位等。 ## 常见语句的汇编代码 >### 赋值语句 tn2>在这里a,b,c赋值时它们的值分别为`0,0,10`,我们来看汇编就一目了然,先将b放入eax中再给a赋值eax,然后再把c放入eax中再给b赋值eax,最后给c赋值`0AH`(十进制为10)。 ```csharp internal class Program { private static int a = b; private static int b = c; private static int c = 10; static void Main(string[] args) { Console.WriteLine($"a={a},b={b},c={c}"); var txt = Convert.ToInt32(Console.ReadLine()); if (txt == 2) { Console.WriteLine("txt==2"); } else { Console.WriteLine("txt!=2"); } } } ```  tn>mov:用来将源操作数复制到目的操作数当中,是一个数据传送指令。 >### 条件跳转语句 | 命令名称 | 格式 |描述 | | ------------ | ------------ |------------ | | CMP | CMP destination,source | 目的操作数减去源操作数的隐含减法操作,不修改任何操作数。 | | PSW | JE address (Jump Equals) | 即 ZF=1 时跳转。 | | JMP | | 无条件地址跳转指令。 | | CALL | | 函数调用指令。 |  ## 案例样本分析 tn2>在一个dump中发生栈溢出的情况,发现在某一个 IsMatched 方法中,汇编代码高达9w行,rsp +xxxx 高达 8w 行,导致默认的 1M 栈空间不够而溢出! 经过分析发现他用了一个超大的struct,而且还有嵌套 struct, 当一个方法的“参数”和“返回值”都是 struct 时,会在父方法和子方法的栈上分配大量的 栈空间。 观察下面代码看会产生多少struct。 ```csharp internal class Program { static void Main(string[] args) { Person person1 = new Person() { A = int.MaxValue, B = int.MaxValue, C = int.MaxValue, D = int.MaxValue }; Person person2 = person1; Console.WriteLine(person2.A); } static Person Test(Person person) { person.A = int.MaxValue; person.B = int.MaxValue; person.C = int.MaxValue; person.D = int.MaxValue; return person; } } public struct Person { public int A; public int B; public int C; public int D; } ``` tn2>通过EBP发现数据一共有六对,每4个`7fffffff`为一对,主要的结果如下:  