商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何反向绘制出 .NET程序 异步方法调用栈(最新)

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

  发布于2026-07-23 阅读(0)

扫一扫,手机访问

一:背景

1. 讲故事

这个问题源于给训练营里一位朋友分析卡死dump时遇到的情况。当时需要在分析期间拿到某一个异步方法的调用栈,但程序跑在 .NET Framework 4.8 上,SOS 后续版本才有的 !dumpasync 命令用不了,这就有点尴尬了。不过转念一想,既然 !dumpasync 能把调用栈搞出来,理论上手动也能把它捞出来,于是就有了这篇内容。

二:异步调用栈研究

1. 一个简单的案例

为了模拟得更真实一点,搞一个简单的三层架构,最后在 DAL 层的 ReadAsync 之后给它断住,参考代码如下:

namespace Example_18_1_1.UI{    internal class Program    {        static void Main(string[] args)        {            Task.Run(() =>            {                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> 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> GetTop10CustomersAsync()        {            var customers = new List();            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; }    }}

从代码流程看,异步调用链是这样的:GetCustomersAsync -> GetCustomersForDisplayAsync -> GetTop10CustomersAsync。程序中断之后,用 WinDbg 附加,使用 !clrstack 观察当前调用栈。

0:017> !clrstackOS Thread Id: 0x3118 (17)        Child SP               IP Call Site000000ABD6CBEAF8 00007ffeb1e61db2 [HelperMethodFrame: 000000abd6cbeaf8] System.Diagnostics.Debugger.BreakInternal()000000ABD6CBEC00 00007ffdf818a91a System.Diagnostics.Debugger.Break() [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18]000000ABD6CBEC30 00007ffd9915079d Example_18_1_1.DAL.CustomerRepository+d__2.MoveNext() [D:\skyfly\18.20220727\src\Example\Example_18_1_1\Program.cs @ 115]000000ABD6CBEE50 00007ffdf827f455 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib]].ExecutionContextCallback(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs @ 286]000000ABD6CBEE80 00007ffdf808dde9 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 183]000000ABD6CBEEF0 00007ffdf827f593 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib]].MoveNext(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs @ 324]000000ABD6CBEF60 00007ffdf827f4ec System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib]].MoveNext() [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs @ 302]000000ABD6CBEF90 00007ffdf80a9a06 System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Runtime.CompilerServices.IAsyncStateMachineBox, Boolean) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs @ 795]000000ABD6CBEFF0 00007ffdf80a48eb System.Threading.Tasks.Task.RunContinuations(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3374]000000ABD6CBF0D0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3350]000000ABD6CBF110 00007ffdf8251350 System.Threading.Tasks.Task`1[[System.__Canon, System.Private.CoreLib]].TrySetResult(System.__Canon) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs @ 400]000000ABD6CBF160 00007ffdf8254fc3 System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].TrySetFromTask(System.Threading.Tasks.Task, Boolean)000000ABD6CBF1C0 00007ffdf825515b System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].ProcessInnerTask(System.Threading.Tasks.Task) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 6940]000000ABD6CBF200 00007ffdf8254ead System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].ProcessCompletedOuterTask(System.Threading.Tasks.Task)000000ABD6CBF240 00007ffdf8254d1b System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].Invoke(System.Threading.Tasks.Task) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 6802]000000ABD6CBF280 00007ffdf80a4e11 System.Threading.Tasks.Task.RunOrQueueCompletionAction(System.Threading.Tasks.ITaskCompletionAction, Boolean)000000ABD6CBF2C0 00007ffdf80a4c0a System.Threading.Tasks.Task.RunContinuations(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3392]000000ABD6CBF3A0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3350]000000ABD6CBF3E0 00007ffdf80a2e9f System.Threading.Tasks.Task.FinishStageThree() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2125]000000ABD6CBF410 00007ffdf80a2d0b System.Threading.Tasks.Task.FinishStageTwo() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2095]000000ABD6CBF460 00007ffdf80a33f6 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2350]000000ABD6CBF500 00007ffdf80a3293 System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2271]000000ABD6CBF540 00007ffdf80a323a System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2262]000000ABD6CBF570 00007ffdf80969df System.Threading.ThreadPoolWorkQueue.Dispatch()000000ABD6CBF610 00007ffdf809e566 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @ 107]000000ABD6CBF730 00007ffdf8082f0f System.Threading.Thread.StartCallback() [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 105]000000ABD6CBF9C0 00007ffdf8ccbde3 [DebuggerU2MCatchHandlerFrame: 000000abd6cbf9c0] 

卦中真的是眼花缭乱,找瞎了眼也没找到调用链上的三个方法名,只有一个 Example_18_1_1.DAL.CustomerRepository+d__2 状态机类,经过 ILSpy 反编译才能勉强看到是 GetTop10CustomersAsync 方法,截图如下:

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

所以 SOS 为了让调试者免去这个痛苦,新增了 !dumpasync 命令。

0:017> !dumpasyncSTACK 10000028b00029338 00007ffd993d1e00 (-1) Example_18_1_1.DAL.CustomerRepository+d__2 @ 7ffd991502a0  0000028b00029438 00007ffd993d3290 (0) Example_18_1_1.BLL.CustomerService+d__2 @ 7ffd9914d6c0    0000028b00029550 00007ffd993d3fe8 (0) Example_18_1_1.UI.Program+d__1 @ 7ffd9914b8f0

虽然能以屏蔽外部代码的方式显示异步调用栈,但这个 SOS 命令是 .NET Core 独有的。作为高级调试者,必须掌握手工绘制的能力。

2. 如何手工绘制

要想手工绘制,需要了解异步状态机的内部机制——子函数和父函数是通过 m_continuationObject 字段串联的。关于异步方法串联的底层机制,之前有过专门讨论,这里不再赘述,用一张图来表示吧。

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

本质上来说,就是 Box 之间形成了一个跨线程的由 m_continuationObject 串联出的单链表。有了思路之后,开始验证。使用 !dso 找到头节点 box。

0:017> !dsoOS Thread Id: 0x3118 (17)          SP/REG           Object Name             rbx     028b00029338 System.Runtime.CompilerServices.AsyncTaskMethodBuilder>+AsyncStateMachineBoxd__2>....0:017> !dumpobj /d 28b00029338Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib],[Example_18_1_1.DAL.CustomerRepository+d__2, Example_18_1_1]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name...00007ffd99125690  4000db9       20        System.Object  0 instance 0000028b00029438 m_continuationObject...0:017> !DumpObj /d 0000028b00029438Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib],[Example_18_1_1.BLL.CustomerService+d__2, Example_18_1_1]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name...00007ffd99125690  4000db9       20        System.Object  0 instance 0000028b00029550 m_continuationObject...0:017> !DumpObj /d 0000028b00029550Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Example_18_1_1.UI.Program+d__1, Example_18_1_1]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name...00007ffd99125690  4000db9       20        System.Object  0 instance 0000000000000000 m_continuationObject...00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e7f8 StateMachine...

上面三个 m_continuationObject 值正是 !dumpasync 输出的结果。最后一个 m_continuationObject=null 说明是异步执行流的最后一个节点,流程正在这里没出来。可以把这个异步状态机解包出来,即卦中的 StateMachine 字段,输出如下:

0:017> !do 0000028b0000e7f8Name:        Example_18_1_1.BLL.CustomerService+d__2Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd991c94b0  4000018       30         System.Int32  1 instance                0 <>1__state00007ffd9924fca0  4000019       38 ...Private.CoreLib]]  1 instance 0000028b0000e830 <>t__builder00007ffd99247298  400001a        8 ...L.CustomerService  0 instance 0000028b0000e7c8 <>4__this00007ffd992453b0  400001b       10 ... Example_18_1_1]]  0 instance 0000000000000000 5__100007ffd992453b0  400001c       18 ... Example_18_1_1]]  0 instance 0000000000000000 <>s__200007ffd99246d60  400001d       20 ... Example_18_1_1]]  0 instance 0000000000000000 <>s__300007ffd99245338  400001e       28 ..._1_1.DAL.Customer  0 instance 0000000000000000 5__400007ffd99245448  400001f       40 ...Private.CoreLib]]  1 instance 0000028b0000e838 <>u__1

再配上 ILSpy 反编译出来的状态机代码,截图如下:

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

可以根据这里的字段赋值情况来推测当前正执行哪一个阶段。

3. 父节点如何找到子节点

刚才我们是通过 子节点 -> 父节点 寻找法。在真实的 dump 分析中,可能还会存在反向的情况,即 父节点 -> 子节点 寻找法。但父节点在寻找目标子节点的过程中会存在多条链路,比如 GetTop10CustomersAsync 方法中存在五个 await 就对应着 4 条链路。

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

用状态机的话术就是下面的 4 个 <>u__xxxx

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

可能有些朋友还是有点懵,没关系,再绘制一张图。

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

最后通过 Windbg 来验证一下。

0:017> !do 0000028b00029550 Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Example_18_1_1.UI.Program+d__1, Example_18_1_1]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000de10 StateMachine0:017> !DumpObj /d 0000028b0000de10Name:        Example_18_1_1.UI.Program+d__1Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99245448  400002b       50 ...Private.CoreLib]]  1 instance 0000028b0000de60 <>u__10:017> !DumpVC /d 00007ffd99245448 0000028b0000de60Name:        System.Runtime.CompilerServices.TaskAwaiter`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99247db8  400139e        0 ...Private.CoreLib]]  0 instance 0000028b00029438 m_task0:017> !DumpObj /d 0000028b00029438Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib],[Example_18_1_1.BLL.CustomerService+d__2, Example_18_1_1]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e7f8 StateMachine0:017> !DumpObj /d 0000028b0000e7f8Name:        Example_18_1_1.BLL.CustomerService+d__200007ffd99245448  400001f       40 ...Private.CoreLib]]  1 instance 0000028b0000e838 <>u__10:017> !DumpVC /d 00007ffd99245448 0000028b0000e838Name:        System.Runtime.CompilerServices.TaskAwaiter`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib]]Fields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99247db8  400139e        0 ...Private.CoreLib]]  0 instance 0000028b00029338 m_task0:017> !DumpObj /d 0000028b00029338Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib],[Example_18_1_1.DAL.CustomerRepository+d__2, Example_18_1_1]]MethodTable: 00007ffd993d1e00EEClass:     00007ffd993c1810Tracked Type: falseSize:        96(0x60) bytesFile:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.36\System.Private.CoreLib.dllFields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e870 StateMachine0:017> !DumpObj /d 0000028b0000e870Name:        Example_18_1_1.DAL.CustomerRepository+d__2Fields:              MT    Field   Offset                 Type VT     Attr            Value Name...00007ffd992602f0  4000014       60 ...vices.TaskAwaiter  1 instance 0000028b0000e8d0 <>u__100007ffd99267a60  4000015       68 ....Data.SqlClient]]  1 instance 0000028b0000e8d8 <>u__200007ffd99260450  4000016       70 ...Private.CoreLib]]  1 instance 0000028b0000e8e0 <>u__300007ffd99260ae8  4000017       78 ....ValueTaskAwaiter  1 instance 0000028b0000e8e8 <>u__4

4. 有没有更快捷的方式

手工绘制虽然是兜底方案,但每次都要这样搞也确实太累。最近在思考有没有更好的方式,好巧不巧,昨天在知乎上刷到了这样一篇文章,hez2010 大佬的一句话点醒了我,截图如下:

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

点醒的是什么呢?SOS 解析托管代码的能力远不如官方的 Visual Studio,毕竟后者才是全球最专业的托管代码调试器。将生成好的 dump 丢到 VS 中,在 Stack 或者 Parallel Stack 中一定要屏蔽 外部代码(External Code),否则海量的 AsyncTaskMethodBuilderMoveNext 会淹死我们,截图如下:

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

三:总结

手工绘制异步调用栈需要对异步的底层构建有一个清晰的认识,调试师是痛苦的。要想进阶为资深,需要日积月累的底层知识沉淀。在自我学习的过程中,如果没有无数次“在绝望中寻找希望”的能力,很容易从入门到放弃……

本文转载于:https://www.jb51.net/aspnet/3412450n5.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注