找回密码
 立即注册
首页 资源区 代码 如何反向绘制出 .NET程序 异步方法调用栈 ...

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

猷咎 2025-5-29 10:54:32
一:背景

1. 讲故事

这个问题源于给训练营里的一位朋友分析的卡死dump,在分析期间我需要知道某一个异步方法的调用栈,但程序是 .framework 4.8 ,没有sos后续版本独有的 !dumpasync 命令,所以这就比较搞了,但转念一想,既然 !dumpasync 能把调用栈搞出来,按理说我也可以给他捞出来,所以就有了此篇。
二:异步调用栈研究

1. 一个简单的案例

为了模拟的真实一点,搞一个简单的三层架构,最后在 DAL 层的 ReadAsync 之后给它断住,参考代码如下:
  1. namespace Example_18_1_1.UI
  2. {
  3.     internal class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.             Task.Run(() =>
  8.             {
  9.                 var task = GetCustomersAsync();
  10.                 Console.WriteLine(task.IsCompleted);
  11.             });
  12.             Console.ReadLine();
  13.         }
  14.         static async Task GetCustomersAsync()
  15.         {
  16.             string connectionString = @"Server=(localdb)\MyInstance;Database=MyDatabase;Integrated Security=true;";
  17.             try
  18.             {
  19.                 Console.WriteLine("Starting async database query...");
  20.                 // 初始化服务
  21.                 var customerService = new CustomerService(connectionString);
  22.                 // 获取并显示客户数据
  23.                 var customers = await customerService.GetCustomersForDisplayAsync();
  24.                 foreach (var customer in customers)
  25.                 {
  26.                     Console.WriteLine($"Customer: ID={customer.Id}, Name={customer.Name}");
  27.                 }
  28.                 Console.WriteLine("Query completed successfully.");
  29.             }
  30.             catch (Exception ex)
  31.             {
  32.                 Console.WriteLine($"Error: {ex.Message}");
  33.             }
  34.         }
  35.     }
  36. }
  37. namespace Example_18_1_1.BLL
  38. {
  39.     public class CustomerService
  40.     {
  41.         private readonly CustomerRepository _repository;
  42.         public CustomerService(string connectionString)
  43.         {
  44.             _repository = new CustomerRepository(connectionString);
  45.         }
  46.         public async Task<IEnumerable<Customer>> GetCustomersForDisplayAsync()
  47.         {
  48.             // 这里可以添加业务逻辑,如验证、转换等
  49.             var customers = await _repository.GetTop10CustomersAsync();
  50.             // 示例业务逻辑:确保名称不为null
  51.             foreach (var customer in customers)
  52.             {
  53.                 customer.Name ??= "Unknown";
  54.             }
  55.             return customers;
  56.         }
  57.     }
  58. }
  59. namespace Example_18_1_1.DAL
  60. {
  61.     public class CustomerRepository
  62.     {
  63.         private readonly string _connectionString;
  64.         public CustomerRepository(string connectionString)
  65.         {
  66.             _connectionString = connectionString;
  67.         }
  68.         public async Task<IEnumerable<Customer>> GetTop10CustomersAsync()
  69.         {
  70.             var customers = new List<Customer>();
  71.             await using (var connection = new SqlConnection(_connectionString))
  72.             {
  73.                 await connection.OpenAsync();
  74.                 var command = new SqlCommand("SELECT TOP 10 * FROM Customers", connection);
  75.                 await using (var reader = await command.ExecuteReaderAsync())
  76.                 {
  77.                     while (await reader.ReadAsync())
  78.                     {
  79.                         customers.Add(new Customer
  80.                         {
  81.                             Id = Convert.ToInt32(reader["Id"]),
  82.                             Name = Convert.ToString(reader["Name"])
  83.                         });
  84.                         Debugger.Break();
  85.                     }
  86.                 }
  87.             }
  88.             return customers;
  89.         }
  90.     }
  91.     public class Customer
  92.     {
  93.         public int Id { get; set; }
  94.         public string Name { get; set; }
  95.     }
  96. }
复制代码
从代码流程看,异步调用链是这样的 GetCustomersAsync -> GetCustomersForDisplayAsync -> GetTop10CustomersAsync 一个过程,在程序中断之后,我们用 WinDbg 附加,使用 !clrstack 观察当前调用栈。
  1. 0:017> !clrstack
  2. OS Thread Id: 0x3118 (17)
  3.         Child SP               IP Call Site
  4. 000000ABD6CBEAF8 00007ffeb1e61db2 [HelperMethodFrame: 000000abd6cbeaf8] System.Diagnostics.Debugger.BreakInternal()
  5. 000000ABD6CBEC00 00007ffdf818a91a System.Diagnostics.Debugger.Break() [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18]
  6. 000000ABD6CBEC30 00007ffd9915079d Example_18_1_1.DAL.CustomerRepository+d__2.MoveNext() [D:\skyfly\18.20220727\src\Example\Example_18_1_1\Program.cs @ 115]
  7. 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]
  8. 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]
  9. 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]
  10. 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]
  11. 000000ABD6CBEF90 00007ffdf80a9a06 System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Runtime.CompilerServices.IAsyncStateMachineBox, Boolean) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs @ 795]
  12. 000000ABD6CBEFF0 00007ffdf80a48eb System.Threading.Tasks.Task.RunContinuations(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3374]
  13. 000000ABD6CBF0D0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3350]
  14. 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]
  15. 000000ABD6CBF160 00007ffdf8254fc3 System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].TrySetFromTask(System.Threading.Tasks.Task, Boolean)
  16. 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]
  17. 000000ABD6CBF200 00007ffdf8254ead System.Threading.Tasks.UnwrapPromise`1[[System.__Canon, System.Private.CoreLib]].ProcessCompletedOuterTask(System.Threading.Tasks.Task)
  18. 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]
  19. 000000ABD6CBF280 00007ffdf80a4e11 System.Threading.Tasks.Task.RunOrQueueCompletionAction(System.Threading.Tasks.ITaskCompletionAction, Boolean)
  20. 000000ABD6CBF2C0 00007ffdf80a4c0a System.Threading.Tasks.Task.RunContinuations(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3392]
  21. 000000ABD6CBF3A0 00007ffdf80a4866 System.Threading.Tasks.Task.FinishContinuations() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3350]
  22. 000000ABD6CBF3E0 00007ffdf80a2e9f System.Threading.Tasks.Task.FinishStageThree() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2125]
  23. 000000ABD6CBF410 00007ffdf80a2d0b System.Threading.Tasks.Task.FinishStageTwo() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2095]
  24. 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]
  25. 000000ABD6CBF500 00007ffdf80a3293 System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2271]
  26. 000000ABD6CBF540 00007ffdf80a323a System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2262]
  27. 000000ABD6CBF570 00007ffdf80969df System.Threading.ThreadPoolWorkQueue.Dispatch()
  28. 000000ABD6CBF610 00007ffdf809e566 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @ 107]
  29. 000000ABD6CBF730 00007ffdf8082f0f System.Threading.Thread.StartCallback() [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 105]
  30. 000000ABD6CBF9C0 00007ffdf8ccbde3 [DebuggerU2MCatchHandlerFrame: 000000abd6cbf9c0]
复制代码
卦中真的是眼花缭乱,找瞎了眼也没找到调用链上的三个方法名,只有一个 Example_18_1_1.DAL.CustomerRepository+d__2 状态机类,经过 ILSpy反编译才能勉强的看到是 GetTop10CustomersAsync 方法,截图如下:
1.png

所以sos为了让调试者免去这个痛苦,新增了 !dumpasync 命令。
  1. 0:017> !dumpasync
  2. STACK 1
  3. 0000028b00029338 00007ffd993d1e00 (-1) Example_18_1_1.DAL.CustomerRepository+<GetTop10CustomersAsync>d__2 @ 7ffd991502a0
  4.   0000028b00029438 00007ffd993d3290 (0) Example_18_1_1.BLL.CustomerService+<GetCustomersForDisplayAsync>d__2 @ 7ffd9914d6c0
  5.     0000028b00029550 00007ffd993d3fe8 (0) Example_18_1_1.UI.Program+<GetCustomersAsync>d__1 @ 7ffd9914b8f0
复制代码
虽然能以 屏蔽外部代码 的方式显示出了异步调用栈,但这个sos 命令是 .netcore 独有的,所以作为高级调试者,我们必须具有手工绘制的能力。
2. 如何手工绘制

要想手工绘制,需要了解异步状态机的内部机制,即子函数和父函数是通过 m_continuationObject 字段串联的,去年我写过一篇关于异步方法串联的文章,可以参考下 (https://www.cnblogs.com/huangxincheng/p/18662162)[聊一聊 C#异步 任务延续的三种底层玩法],这里就不具体说了,用一张图来表示吧。
2.png

本质上来说就是 Box 之间形成了一个跨线程的由m_continuationObject串联出的单链表,有了思路之后,我们开始验证吧,使用 !dso 找到头节点 box。
  1. 0:017> !dso
  2. OS Thread Id: 0x3118 (17)
  3.           SP/REG           Object Name
  4.              rbx     028b00029338 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Collections.Generic.IEnumerable<Example_18_1_1.DAL.Customer>>+AsyncStateMachineBox<Example_18_1_1.DAL.CustomerRepository+<GetTop10CustomersAsync>d__2>
  5. ....
  6. 0:017> !dumpobj /d 28b00029338
  7. Name:        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+<GetTop10CustomersAsync>d__2, Example_18_1_1]]
  8. Fields:
  9.               MT    Field   Offset                 Type VT     Attr            Value Name
  10. ...
  11. 00007ffd99125690  4000db9       20        System.Object  0 instance 0000028b00029438 m_continuationObject
  12. ...
  13. 0:017> !DumpObj /d 0000028b00029438
  14. Name:        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+<GetCustomersForDisplayAsync>d__2, Example_18_1_1]]
  15. Fields:
  16.               MT    Field   Offset                 Type VT     Attr            Value Name
  17. ...
  18. 00007ffd99125690  4000db9       20        System.Object  0 instance 0000028b00029550 m_continuationObject
  19. ...
  20. 0:017> !DumpObj /d 0000028b00029550
  21. Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Example_18_1_1.UI.Program+<GetCustomersAsync>d__1, Example_18_1_1]]
  22. Fields:
  23.               MT    Field   Offset                 Type VT     Attr            Value Name
  24. ...
  25. 00007ffd99125690  4000db9       20        System.Object  0 instance 0000000000000000 m_continuationObject
  26. ...
  27. 00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e7f8 StateMachine
  28. ...
复制代码
上面三个 m_continuationObject 值即是 !dumpasync 输出的结果,最后一个 m_continuationObject=null  说明为异步执行流的最后一个节点,流程正在这里没出来,可以把这个异步状态机给解包出来,即卦中的 StateMachine 字段,输出如下:
  1. 0:017> !do 0000028b0000e7f8
  2. Name:        Example_18_1_1.BLL.CustomerService+<GetCustomersForDisplayAsync>d__2
  3. Fields:
  4.               MT    Field   Offset                 Type VT     Attr            Value Name
  5. 00007ffd991c94b0  4000018       30         System.Int32  1 instance                0 <>1__state
  6. 00007ffd9924fca0  4000019       38 ...Private.CoreLib]]  1 instance 0000028b0000e830 <>t__builder
  7. 00007ffd99247298  400001a        8 ...L.CustomerService  0 instance 0000028b0000e7c8 <>4__this
  8. 00007ffd992453b0  400001b       10 ... Example_18_1_1]]  0 instance 0000000000000000 <customers>5__1
  9. 00007ffd992453b0  400001c       18 ... Example_18_1_1]]  0 instance 0000000000000000 <>s__2
  10. 00007ffd99246d60  400001d       20 ... Example_18_1_1]]  0 instance 0000000000000000 <>s__3
  11. 00007ffd99245338  400001e       28 ..._1_1.DAL.Customer  0 instance 0000000000000000 <customer>5__4
  12. 00007ffd99245448  400001f       40 ...Private.CoreLib]]  1 instance 0000028b0000e838 <>u__1
复制代码
再配上 ILSpy 反编译出来的状态机代码,截图如下:
3.png

可以根据这里面的字段赋值情况来推测当前正执行哪一个阶段。
3. 父节点如何找到子节点

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

用状态机的话术就是下面的4个 u__xxxx。
5.png

可能有些朋友还是有点懵,没关系,我也绘制一张图。
6.png

最后通过 windbg 来验证一下。
  1. 0:017> !do 0000028b00029550
  2. Name:        System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Example_18_1_1.UI.Program+<GetCustomersAsync>d__1, Example_18_1_1]]
  3. Fields:
  4.               MT    Field   Offset                 Type VT     Attr            Value Name
  5. 00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000de10 StateMachine
  6. 0:017> !DumpObj /d 0000028b0000de10
  7. Name:        Example_18_1_1.UI.Program+<GetCustomersAsync>d__1
  8. Fields:
  9.               MT    Field   Offset                 Type VT     Attr            Value Name
  10. 00007ffd99245448  400002b       50 ...Private.CoreLib]]  1 instance 0000028b0000de60 <>u__1
  11. 0:017> !DumpVC /d 00007ffd99245448 0000028b0000de60
  12. Name:        System.Runtime.CompilerServices.TaskAwaiter`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib]]
  13. Fields:
  14.               MT    Field   Offset                 Type VT     Attr            Value Name
  15. 00007ffd99247db8  400139e        0 ...Private.CoreLib]]  0 instance 0000028b00029438 m_task
  16. 0:017> !DumpObj /d 0000028b00029438
  17. Name:        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+<GetCustomersForDisplayAsync>d__2, Example_18_1_1]]
  18. Fields:
  19.               MT    Field   Offset                 Type VT     Attr            Value Name
  20. 00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e7f8 StateMachine
  21. 0:017> !DumpObj /d 0000028b0000e7f8
  22. Name:        Example_18_1_1.BLL.CustomerService+<GetCustomersForDisplayAsync>d__2
  23. 00007ffd99245448  400001f       40 ...Private.CoreLib]]  1 instance 0000028b0000e838 <>u__1
  24. 0:017> !DumpVC /d 00007ffd99245448 0000028b0000e838
  25. Name:        System.Runtime.CompilerServices.TaskAwaiter`1[[System.Collections.Generic.IEnumerable`1[[Example_18_1_1.DAL.Customer, Example_18_1_1]], System.Private.CoreLib]]
  26. Fields:
  27.               MT    Field   Offset                 Type VT     Attr            Value Name
  28. 00007ffd99247db8  400139e        0 ...Private.CoreLib]]  0 instance 0000028b00029338 m_task
  29. 0:017> !DumpObj /d 0000028b00029338
  30. Name:        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+<GetTop10CustomersAsync>d__2, Example_18_1_1]]
  31. MethodTable: 00007ffd993d1e00
  32. EEClass:     00007ffd993c1810
  33. Tracked Type: false
  34. Size:        96(0x60) bytes
  35. File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.36\System.Private.CoreLib.dll
  36. Fields:
  37.               MT    Field   Offset                 Type VT     Attr            Value Name
  38. 00007ffd99125708  4001337       48       System.__Canon  0 instance 0000028b0000e870 StateMachine
  39. 0:017> !DumpObj /d 0000028b0000e870
  40. Name:        Example_18_1_1.DAL.CustomerRepository+<GetTop10CustomersAsync>d__2
  41. Fields:
  42.               MT    Field   Offset                 Type VT     Attr            Value Name
  43. ...
  44. 00007ffd992602f0  4000014       60 ...vices.TaskAwaiter  1 instance 0000028b0000e8d0 <>u__1
  45. 00007ffd99267a60  4000015       68 ....Data.SqlClient]]  1 instance 0000028b0000e8d8 <>u__2
  46. 00007ffd99260450  4000016       70 ...Private.CoreLib]]  1 instance 0000028b0000e8e0 <>u__3
  47. 00007ffd99260ae8  4000017       78 ....ValueTaskAwaiter  1 instance 0000028b0000e8e8 <>u__4
复制代码
4. 有没有更快捷的方式

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

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

三:总结

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


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册