聊一下volitile关键词在汇编代码上的理解

说到 volatile,它的中文含义叫 易变的,说直白点就是不稳定,任何人使用它都要当点心,为了说明问题,先上一段代码:.

    public class Worker
    {
        private bool _shouldStop;

        public void DoWork()
        {
            bool work = false;
            // 注意:这里会被编译器优化为 while(true)
            while (!_shouldStop)
            {
                work = !work; // do sth.
            }
            Console.WriteLine("工作线程:正在终止...");
        }

        public void RequestStop()
        {
            _shouldStop = true;
        }
    }

    public class Program
    {
        public static void Main()
        {
            var worker = new Worker();

            Console.WriteLine("主线程:启动工作线程...");
            var workerTask = Task.Run(worker.DoWork);

            // 等待 500 毫秒以确保工作线程已在执行
            Thread.Sleep(500);

            Console.WriteLine("主线程:请求终止工作线程...");
            worker.RequestStop();

            // 待待工作线程执行结束
            workerTask.Wait();
            //workerThread.Join();

            Console.WriteLine("主线程:工作线程已终止");
        }
    }

接下来我们将程序跑起来,可以看到程序无法退出,那什么原因呢?难道 _shouldStop 还没有别改为 true 吗?只能用 windbg 求证啦,首先我们反汇编下 DoWork() 方法。

0:009> !U /d 022e08db
Normal JIT generated code
ConsoleApp5.Worker.DoWork()
Begin 022e08d0, size 1c

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 21:
022e08d0 55              push    ebp
022e08d1 8bec            mov     ebp,esp

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 23:
022e08d3 0fb64104        movzx   eax,byte ptr [ecx+4]
022e08d7 85c0            test    eax,eax
022e08d9 7504            jne     022e08df
>>> 022e08db 85c0            test    eax,eax
022e08dd 74fc            je      022e08db

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 27:
022e08df 8b0d50234603    mov     ecx,dword ptr ds:[3462350h] ("工作线程:正在终止...")
022e08e5 e87a46e757      call    mscorlib_ni!System.Console.WriteLine(System.String)$##6000B79 (5a154f64)

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 28:
022e08ea 5d              pop     ebp
022e08eb c3              ret

接下来我们重点解读下 Progrom.cs:23 行的汇编指令。

1. movzx eax,byte ptr [ecx+4]

表示将 ecx+4 位置上的值给 eax,这里要提醒下,ecx 表示当前的 this 指针,也就是 worker 实例。

0:009> !do ecx
Name:        ConsoleApp5.Worker
MethodTable: 00964dd8
EEClass:     00961310
Size:        12(0xc) bytes
File:        D:\net5\ConsoleApp1\ConsoleApp5\bin\Release\ConsoleApp5.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
59cb878c  4000001        4       System.Boolean  1 instance        1 _shouldStop

所以 ecx+4 就是把 _shouldStop 值给提取出来了。

2. test eax,eax

test 指令表示将 eax 和 eax 进行逻辑与运算,将与结果 放到 ZF 标志位寄存器上,要知道的是,此时 windows 执行此指令时 _shouldStop 肯定还是 0 ,所以此时的 ZF=1 对吧。

3. jne 022e08df 和 je 022e08db

jne 表示当前的 ZF=0 时跳转到 022e08df 处,je 表示当前的 ZF=1 时跳转到 022e08db,按照方法反汇编的代码,此时会调转到指令 022e08db 85c0 test eax, eax 处,然后又执行了一次 test eax ,eax ,结果还是要走 je 022e08db 语句,形成了一个while循环

发现问题

从我刚才的描述中,是否发现了一个问题,后续的判断都是做 test eax,eax 操作,也就是程序无法退出时 _shouldStop 已经变更为 1 ,此时的 eax 在循环做 test 操作,并没有感知到 _shouldStop 的变化,不信的话,可以看下此时的 eax 寄存器值。

0:009> r eax
eax=00000000

解决方案

有了问题,接下来的解决方案是什么呢?对,用 volatile 关键词,目的就是让 cpu 感知到内存的变化,那如何做到感知呢?可以看看最终的汇编代码就好了。

接下来我们用 windbg 来启动 exe 程序,并且在 Console.WriteLine("工作线程:正在终止..."); 上下一个断点,也就是 27 行代码处。

0:000> !mbp Program.cs 27
The CLR has not yet been initialized in the process.
Breakpoint resolution will be attempted when the CLR is initialized.
0:000> g
Breakpoint: JIT notification received for method ConsoleApp5.Worker.DoWork() in AppDomain 004d2f40.
Breakpoint set at ConsoleApp5.Worker.DoWork() in AppDomain 004d2f40.
Breakpoint 2 hit
eax=023408d0 ebx=024a8240 ecx=024a2394 edx=024a48b4 esi=024a48ec edi=04eaf99c
eip=023408df esp=04eaf928 ebp=04eaf928 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
023408df 8b0d50234a03    mov     ecx,dword ptr ds:[34A2350h] ds:002b:034a2350=024a8294

为了看清楚点,我们这次再把 DoWork 方法的反汇编贴一下。

0:009> !U /d 023408df
Normal JIT generated code
ConsoleApp5.Worker.DoWork()
Begin 023408d0, size 1c

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 21:
023408d0 55              push    ebp
023408d1 8bec            mov     ebp,esp

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 23:
023408d3 80790400        cmp     byte ptr [ecx+4],0
023408d7 7506            jne     023408df
023408d9 80790400        cmp     byte ptr [ecx+4],0
023408dd 74fa            je      023408d9

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 27:
>>> 023408df 8b0d50234a03    mov     ecx,dword ptr ds:[34A2350h] ("工作线程:正在终止...")
023408e5 e87a46e157      call    mscorlib_ni!System.Console.WriteLine(System.String)$##6000B79 (5a154f64)

D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 28:
023408ea 5d              pop     ebp
023408eb c3              ret

对比之前的代码看,人家做了两点变更。

  1. 将 test 换成了 cmp

  2. 用寄存器比较 改成 取内存地址值做比较。

因为 ecx 就是 work实例地址,所以一直取它的话自然就更感知到 _shouldStop 的变更,不过话说不回来,这也是以牺牲性能为代价的,毕竟取内存还要过 地址总线,数据总线,控制总线,哈哈。。。

原理就是这么一个原理,啰嗦了这么多,相信大家应该也明白了吧。