刘胜荣 發表於 2025-5-12 09:10:07

如何反向绘制出 .NET程序 异步方法调用栈(最新)

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一:背景</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1. 讲故事</a></li></ul><li><a href="#_label1">二:异步调用栈研究</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_1">1. 一个简单的案例</a></li><li><a href="#_lab2_1_2">2. 如何手工绘制</a></li><li><a href="#_lab2_1_3">3. 父节点如何找到子节点</a></li><li><a href="#_lab2_1_4">4. 有没有更快捷的方式</a></li></ul><li><a href="#_label2">三:总结</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一:背景</h2>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>1. 讲故事</h3>
<p>这个问题源于给训练营里的一位朋友分析的卡死dump,在分析期间我需要知道某一个异步方法的调用栈,但程序是 .framework 4.8 ,没有sos后续版本独有的&nbsp;<code>!dumpasync</code>&nbsp;命令,所以这就比较搞了,但转念一想,既然&nbsp;<code>!dumpasync</code>&nbsp;能把调用栈搞出来,按理说我也可以给他捞出来,所以就有了此篇。</p>
<p class="maodian"><a name="_label1"></a></p><h2>二:异步调用栈研究</h2>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>1. 一个简单的案例</h3>
<p>为了模拟的真实一点,搞一个简单的三层架构,最后在 DAL 层的 ReadAsync 之后给它断住,参考代码如下:</p>
<div class="jb51code"><pre class="brush:csharp;">namespace Example_18_1_1.UI
{
    internal class Program
    {
      static void Main(string[] args)
      {
            Task.Run(() =&gt;
            {
                var task = GetCustomersAsync();
                Console.WriteLine(task.IsCompleted);
            });
            Console.ReadLine();
      }
      static async Task GetCustomersAsync()
      {
            string connectionString = @"Server=(localdb)\MyInstance;Database=MyDatabase;Integrated Security=true;";
            try
            {
                Console.WriteLine("Starting async database query...");
                // 初始化服务
                var customerService = new CustomerService(connectionString);
                // 获取并显示客户数据
                var customers = await customerService.GetCustomersForDisplayAsync();
                foreach (var customer in customers)
                {
                  Console.WriteLine($"Customer: ID={customer.Id}, Name={customer.Name}");
                }
                Console.WriteLine("Query completed successfully.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
      }
    }
}
namespace Example_18_1_1.BLL
{
    public class CustomerService
    {
      private readonly CustomerRepository _repository;
      public CustomerService(string connectionString)
      {
            _repository = new CustomerRepository(connectionString);
      }
      public async Task&lt;IEnumerable&lt;Customer&gt;&gt; GetCustomersForDisplayAsync()
      {
            // 这里可以添加业务逻辑,如验证、转换等
            var customers = await _repository.GetTop10CustomersAsync();
            // 示例业务逻辑:确保名称不为null
            foreach (var customer in customers)
            {
                customer.Name ??= "Unknown";
            }
            return customers;
      }
    }
}
namespace Example_18_1_1.DAL
{
    public class CustomerRepository
    {
      private readonly string _connectionString;
      public CustomerRepository(string connectionString)
      {
            _connectionString = connectionString;
      }
      public async Task&lt;IEnumerable&lt;Customer&gt;&gt; GetTop10CustomersAsync()
      {
            var customers = new List&lt;Customer&gt;();
            await using (var connection = new SqlConnection(_connectionString))
            {
                await connection.OpenAsync();
                var command = new SqlCommand("SELECT TOP 10 * FROM Customers", connection);
                await using (var reader = await command.ExecuteReaderAsync())
                {
                  while (await reader.ReadAsync())
                  {
                        customers.Add(new Customer
                        {
                            Id = Convert.ToInt32(reader["Id"]),
                            Name = Convert.ToString(reader["Name"])
                        });
                        Debugger.Break();
                  }
                }
            }
            return customers;
      }
    }
    public class Customer
    {
      public int Id { get; set; }
      public string Name { get; set; }
    }
}</pre></div>
<p>从代码流程看,异步调用链是这样的&nbsp;<code>GetCustomersAsync -&gt; GetCustomersForDisplayAsync -&gt; GetTop10CustomersAsync</code>&nbsp;一个过程,在程序中断之后,我们用 WinDbg 附加,使用&nbsp;<code>!clrstack</code>&nbsp;观察当前调用栈。</p>
<div class="jb51code"><pre class="brush:csharp;">0:017&gt; !clrstack
OS Thread Id: 0x3118 (17)
      Child SP               IP Call Site
000000ABD6CBEAF8 00007ffeb1e61db2 System.Diagnostics.Debugger.BreakInternal()
000000ABD6CBEC00 00007ffdf818a91a System.Diagnostics.Debugger.Break()
000000ABD6CBEC30 00007ffd9915079d Example_18_1_1.DAL.CustomerRepository+d__2.MoveNext()
000000ABD6CBEE50 00007ffdf827f455 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[,].ExecutionContextCallback(System.Object)
000000ABD6CBEE80 00007ffdf808dde9 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
000000ABD6CBEEF0 00007ffdf827f593 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[,].MoveNext(System.Threading.Thread)
000000ABD6CBEF60 00007ffdf827f4ec System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[,].MoveNext()
000000ABD6CBEF90 00007ffdf80a9a06 System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Runtime.CompilerServices.IAsyncStateMachineBox, Boolean)
000000ABD6CBEFF0 00007ffdf80a48eb System.Threading.Tasks.Task.RunContinuations(System.Object)
000000ABD6CBF0D0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations()
000000ABD6CBF110 00007ffdf8251350 System.Threading.Tasks.Task`1[].TrySetResult(System.__Canon)
000000ABD6CBF160 00007ffdf8254fc3 System.Threading.Tasks.UnwrapPromise`1[].TrySetFromTask(System.Threading.Tasks.Task, Boolean)
000000ABD6CBF1C0 00007ffdf825515b System.Threading.Tasks.UnwrapPromise`1[].ProcessInnerTask(System.Threading.Tasks.Task)
000000ABD6CBF200 00007ffdf8254ead System.Threading.Tasks.UnwrapPromise`1[].ProcessCompletedOuterTask(System.Threading.Tasks.Task)
000000ABD6CBF240 00007ffdf8254d1b System.Threading.Tasks.UnwrapPromise`1[].Invoke(System.Threading.Tasks.Task)
000000ABD6CBF280 00007ffdf80a4e11 System.Threading.Tasks.Task.RunOrQueueCompletionAction(System.Threading.Tasks.ITaskCompletionAction, Boolean)
000000ABD6CBF2C0 00007ffdf80a4c0a System.Threading.Tasks.Task.RunContinuations(System.Object)
000000ABD6CBF3A0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations()
000000ABD6CBF3E0 00007ffdf80a2e9f System.Threading.Tasks.Task.FinishStageThree()
000000ABD6CBF410 00007ffdf80a2d0b System.Threading.Tasks.Task.FinishStageTwo()
000000ABD6CBF460 00007ffdf80a33f6 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
000000ABD6CBF500 00007ffdf80a3293 System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread)
000000ABD6CBF540 00007ffdf80a323a System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread)
000000ABD6CBF570 00007ffdf80969df System.Threading.ThreadPoolWorkQueue.Dispatch()
000000ABD6CBF610 00007ffdf809e566 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
000000ABD6CBF730 00007ffdf8082f0f System.Threading.Thread.StartCallback()
000000ABD6CBF9C0 00007ffdf8ccbde3 </pre></div>
<p>卦中真的是眼花缭乱,找瞎了眼也没找到调用链上的三个方法名,只有一个&nbsp;<code>Example_18_1_1.DAL.CustomerRepository+d__2</code>&nbsp;状态机类,经过 ILSpy反编译才能勉强的看到是&nbsp;<code>GetTop10CustomersAsync</code>&nbsp;方法,截图如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851061.png" /></p>
<p>所以sos为了让调试者免去这个痛苦,新增了&nbsp;<code>!dumpasync</code>&nbsp;命令。</p>
<div class="jb51code"><pre class="brush:bash;">0:017&gt; !dumpasync
STACK 1
0000028b00029338 00007ffd993d1e00 (-1) Example_18_1_1.DAL.CustomerRepository+&lt;GetTop10CustomersAsync&gt;d__2 @ 7ffd991502a0
0000028b00029438 00007ffd993d3290 (0) Example_18_1_1.BLL.CustomerService+&lt;GetCustomersForDisplayAsync&gt;d__2 @ 7ffd9914d6c0
    0000028b00029550 00007ffd993d3fe8 (0) Example_18_1_1.UI.Program+&lt;GetCustomersAsync&gt;d__1 @ 7ffd9914b8f0</pre></div>
<p>虽然能以&nbsp;<code>屏蔽外部代码</code>&nbsp;的方式显示出了异步调用栈,但这个sos 命令是&nbsp;<code>.netcore</code>&nbsp;独有的,所以作为高级调试者,我们必须具有手工绘制的能力。</p>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2. 如何手工绘制</h3>
<p>要想手工绘制,需要了解异步状态机的内部机制,即子函数和父函数是通过&nbsp;<code>m_continuationObject</code>&nbsp;字段串联的,去年我写过一篇关于异步方法串联的文章,可以参考下 (<a href="https://www.jb51.net/program/341250jj0.htm" target="_blank">https://www.jb51.nethttps://www.jb51.net/program/341250jj0.htm</a>)[聊一聊 C#异步 任务延续的三种底层玩法],这里就不具体说了,用一张图来表示吧。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851062.png" /></p>
<p>本质上来说就是 Box 之间形成了一个跨线程的由<code>m_continuationObject</code>串联出的单链表,有了思路之后,我们开始验证吧,使用&nbsp;<code>!dso</code>&nbsp;找到头节点 box。</p>
<div class="jb51code"><pre class="brush:plain;">0:017&gt; !dso
OS Thread Id: 0x3118 (17)
          SP/REG         Object Name
             rbx   028b00029338 System.Runtime.CompilerServices.AsyncTaskMethodBuilder&lt;System.Collections.Generic.IEnumerable&lt;Example_18_1_1.DAL.Customer&gt;&gt;+AsyncStateMachineBox&lt;Example_18_1_1.DAL.CustomerRepository+&lt;GetTop10CustomersAsync&gt;d__2&gt;
....
0:017&gt; !dumpobj /d 28b00029338
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[], System.Private.CoreLib],]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
...
00007ffd991256904000db9       20      System.Object0 instance 0000028b00029438 m_continuationObject
...
0:017&gt; !DumpObj /d 0000028b00029438
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[], System.Private.CoreLib],]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
...
00007ffd991256904000db9       20      System.Object0 instance 0000028b00029550 m_continuationObject
...
0:017&gt; !DumpObj /d 0000028b00029550
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[,]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
...
00007ffd991256904000db9       20      System.Object0 instance 0000000000000000 m_continuationObject
...
00007ffd991257084001337       48       System.__Canon0 instance 0000028b0000e7f8 StateMachine
...</pre></div>
<p>上面三个 m_continuationObject 值即是&nbsp;<code>!dumpasync</code>&nbsp;输出的结果,最后一个 m_continuationObject=null 说明为异步执行流的最后一个节点,流程正在这里没出来,可以把这个异步状态机给解包出来,即卦中的 StateMachine 字段,输出如下:</p>
<div class="jb51code"><pre class="brush:plain;">0:017&gt; !do 0000028b0000e7f8
Name:      Example_18_1_1.BLL.CustomerService+&lt;GetCustomersForDisplayAsync&gt;d__2
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd991c94b04000018       30         System.Int321 instance                0 &lt;&gt;1__state
00007ffd9924fca04000019       38 ...Private.CoreLib]]1 instance 0000028b0000e830 &lt;&gt;t__builder
00007ffd99247298400001a      8 ...L.CustomerService0 instance 0000028b0000e7c8 &lt;&gt;4__this
00007ffd992453b0400001b       10 ... Example_18_1_1]]0 instance 0000000000000000 &lt;customers&gt;5__1
00007ffd992453b0400001c       18 ... Example_18_1_1]]0 instance 0000000000000000 &lt;&gt;s__2
00007ffd99246d60400001d       20 ... Example_18_1_1]]0 instance 0000000000000000 &lt;&gt;s__3
00007ffd99245338400001e       28 ..._1_1.DAL.Customer0 instance 0000000000000000 &lt;customer&gt;5__4
00007ffd99245448400001f       40 ...Private.CoreLib]]1 instance 0000028b0000e838 &lt;&gt;u__1</pre></div>
<p>再配上 ILSpy 反编译出来的状态机代码,截图如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851063.png" /></p>
<p>可以根据这里面的字段赋值情况来推测当前正执行哪一个阶段。</p>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>3. 父节点如何找到子节点</h3>
<p>刚才我们是通过&nbsp;<code>子节点 -&gt; 父节点</code>&nbsp;寻找法,在真实的dump分析中,可能还会存在反向的情况,即&nbsp;<code>父节点 -&gt; 子节点</code>&nbsp;寻找法,但父节点寻找目标子节点的过程中会存在多条链路,比如 GetTop10CustomersAsync 方法中存在五个 await 就对应着 4条链路。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851064.png" /></p>
<p>用状态机的话术就是下面的4个&nbsp;<code>&lt;&gt;u__xxxx</code>。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851065.png" /></p>
<p>可能有些朋友还是有点懵,没关系,我也绘制一张图。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851066.png" /></p>
<p>最后通过 windbg 来验证一下。</p>
<div class="jb51code"><pre class="brush:plain;">0:017&gt; !do 0000028b00029550
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[,]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd991257084001337       48       System.__Canon0 instance 0000028b0000de10 StateMachine
0:017&gt; !DumpObj /d 0000028b0000de10
Name:      Example_18_1_1.UI.Program+&lt;GetCustomersAsync&gt;d__1
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd99245448400002b       50 ...Private.CoreLib]]1 instance 0000028b0000de60 &lt;&gt;u__1
0:017&gt; !DumpVC /d 00007ffd99245448 0000028b0000de60
Name:      System.Runtime.CompilerServices.TaskAwaiter`1[], System.Private.CoreLib]]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd99247db8400139e      0 ...Private.CoreLib]]0 instance 0000028b00029438 m_task
0:017&gt; !DumpObj /d 0000028b00029438
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[], System.Private.CoreLib],]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd991257084001337       48       System.__Canon0 instance 0000028b0000e7f8 StateMachine
0:017&gt; !DumpObj /d 0000028b0000e7f8
Name:      Example_18_1_1.BLL.CustomerService+&lt;GetCustomersForDisplayAsync&gt;d__2
00007ffd99245448400001f       40 ...Private.CoreLib]]1 instance 0000028b0000e838 &lt;&gt;u__1
0:017&gt; !DumpVC /d 00007ffd99245448 0000028b0000e838
Name:      System.Runtime.CompilerServices.TaskAwaiter`1[], System.Private.CoreLib]]
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd99247db8400139e      0 ...Private.CoreLib]]0 instance 0000028b00029338 m_task
0:017&gt; !DumpObj /d 0000028b00029338
Name:      System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[], System.Private.CoreLib],]
MethodTable: 00007ffd993d1e00
EEClass:   00007ffd993c1810
Tracked Type: false
Size:      96(0x60) bytes
File:      C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.36\System.Private.CoreLib.dll
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ffd991257084001337       48       System.__Canon0 instance 0000028b0000e870 StateMachine
0:017&gt; !DumpObj /d 0000028b0000e870
Name:      Example_18_1_1.DAL.CustomerRepository+&lt;GetTop10CustomersAsync&gt;d__2
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
...
00007ffd992602f04000014       60 ...vices.TaskAwaiter1 instance 0000028b0000e8d0 &lt;&gt;u__1
00007ffd99267a604000015       68 ....Data.SqlClient]]1 instance 0000028b0000e8d8 &lt;&gt;u__2
00007ffd992604504000016       70 ...Private.CoreLib]]1 instance 0000028b0000e8e0 &lt;&gt;u__3
00007ffd99260ae84000017       78 ....ValueTaskAwaiter1 instance 0000028b0000e8e8 &lt;&gt;u__4</pre></div>
<p class="maodian"><a name="_lab2_1_4"></a></p><h3>4. 有没有更快捷的方式</h3>
<p>手工绘制虽然是兜底方案,但每次都要这样搞也确实太累,所以最近我在思考有没有更好的方式,好巧不巧,昨天在知乎上刷到了这样的一篇文章,hez2010大佬的话突然点醒了我,截图如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851067.png" /></p>
<p>哈哈,点醒了我什么呢?即 sos 解析托管代码的能力远不如官方的&nbsp;<code>Visual Studio</code>,毕竟后者才是全球最专业的托管代码调试器,将生成好的dump丢到 VS 中,在 Stack 或者 Parallel Stack 中一定要屏蔽&nbsp;<code>外部代码(External Code)</code>&nbsp;,否则海量的 AsyncTaskMethodBuilder 和 MoveNext 会淹死我们,截图如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202505/202505120851068.png" /></p>
<p class="maodian"><a name="_label2"></a></p><h2>三:总结</h2>
<p>手工绘制异步调用栈需要对异步的底层构建有一个清晰的认识,调试师是痛苦的,要想进阶为资深,需要日积月累的底层知识沉淀,在自我学习的过程中如果没有无数次的<code>在绝望中寻找希望</code>的能力,很容易从入门到放弃。。。</p>
頁: [1]
查看完整版本: 如何反向绘制出 .NET程序 异步方法调用栈(最新)