使用 Visual Studio Code 调试 .NET runtime 库:从 launch.json 到 Mono 远程附加的完整指南
2026/9/19 17:42:53 网站建设 项目流程
  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

本文聚焦于在 .NET runtime 仓库(runtime)中使用 Visual Studio Code 调试库(Libraries)测试的完整流程,覆盖两条核心路径:基于 CoreCLR 的本地启动调试(.NET Core Launch (console))与基于 Mono 运行时的远程附加调试(Attach to Mono)。读完本文,你将能够精准构造launch.json,把断点打在System.Net.Sockets等库的测试代码中,并理解测试运行命令exec ... xunit.console.dll ...背后的参数生成机制(见 xunit.console.targets)。

调试前的环境准备

开始之前,请确保本机满足以下条件:

  1. 安装 Visual Studio Code;
  2. 安装C# 扩展ms-dotnettools.csharp),它是 C# 语言服务与调试器的基础;
  3. 可选安装C# Dev Kitms-dotnettools.csdevkit),它提供更完整的项目管理与测试集成体验,非调试必需;
  4. 本地已经能够构建 runtime 仓库并产出测试产物(artifacts/bin目录下存在 testhost 与测试项目输出)。

调试的核心思路是:绕过常规的dotnet test启动方式,直接用调试器启动 testhost 中的dotnet可执行文件,把测试程序集作为参数传入。因此你不需要预先把断点绑定到 VS Code 的测试面板,只需要在测试代码里打断点并启动调试配置即可。

在 VS Code 中创建并配置调试会话

打开目标库源码目录

打开包含你想调试源码的文件夹。例如,如果你要调查System.Net.Sockets的测试失败,就打开:

runtime/src/libraries/System.Net.Sockets

只打开与问题相关的库目录,能让调试器的符号加载与断点命中范围更聚焦,也便于使用该目录下的工作区配置。

生成 launch.json

  1. Ctrl+Shift+D(macOS 为Cmd+Shift+D)打开调试窗口,或点击左侧活动栏的“运行和调试”按钮;
  2. 点击create a launch.json file,在下拉菜单里选择包含.NET Core的模板;
  3. VS Code 会生成一个.vscode/launch.json,里面默认包含.NET Core Launch (console)配置。

修改 .NET Core Launch (console) 配置

默认模板假设你要启动一个普通控制台应用,必须针对 runtime 的测试体系做三处关键修改:

1. 删除preLaunchTask属性

模板默认会先执行一个构建任务(preLaunchTask),这会让调试器每次启动前重新编译。在 runtime 仓库里,你应该先手动构建测试项目,然后纯粹用调试器启动已经构建好的产物,因此直接删除该属性,避免调试会话被构建任务干扰。

2. 设置program为 testhost 中的 dotnet 可执行文件

program指向真正承载测试运行的主机进程:

{full path to your dotnet/runtime directory}/artifacts/bin/testhost/net{Version}-{OS}-{Configuration}-{Architecture}/dotnet

例如,Linux x64 的 Debug 构建对应路径大致为:

/path/to/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet

这个dotnet是仓库构建脚本产出、专用于测试的运行时主机,它内部带上了刚构建的 CoreCLR 与库程序集,直接使用它才能调试到仓库里的最新代码。

3. 设置cwd为测试项目的 bin 目录

cwd必须指向测试程序集所在的输出目录,这样测试依赖的程序集、runtimeconfig.json才能被正确解析。以System.Net.Sockets为例:

{full path to your dotnet/runtime directory}/artifacts/bin/System.Net.Sockets.Tests/Debug/net{Version}-{OS}

不同库的目录命名可能略有差异(例如多目标框架会带上 TFM 后缀),以实际构建输出为准。如果拿不准,直接去artifacts/bin下找到对应测试项目的输出目录即可。

4. 设置args为测试运行参数

args是最核心、也最容易出错的部分。它的形态是:

[ "exec", "--runtimeconfig", "{TestProjectName}.runtimeconfig.json", "xunit.console.dll", "{TestProjectName}.dll", "-notrait", "category=failing" ]

其中{TestProjectName}是测试项目名,比如System.Net.Sockets.Tests

如何拿到这组参数(推荐做法):先在终端里跑一次你关心的测试命令,然后看输出中exec开头的片段,把它原样复制并改写成 JSON 数组填入args

例如,在runtime/src/libraries/System.Net.Sockets/tests/FunctionalTests目录下执行:

dotnet build /t:Test

终端输出中会出现类似:

exec --runtimeconfig System.Net.Sockets.Tests.runtimeconfig.json ... xunit.console.dll System.Net.Sockets.Tests.dll -notrait category=failing

将其改写成:

[ "exec", "--runtimeconfig", "System.Net.Sockets.Tests.runtimeconfig.json", "xunit.console.dll", "System.Net.Sockets.Tests.dll", "-notrait", "category=failing" ]
运行单个测试方法

如果只想调试某个具体测试,追加-method参数,格式为{类全名}.{方法名}

[ "exec", "--runtimeconfig", "System.Net.Sockets.Tests.runtimeconfig.json", "xunit.console.dll", "System.Net.Sockets.Tests.dll", "-method", "System.Net.Sockets.Tests.{ClassName}.{TestMethodName}", "-notrait", "category=failing" ]

同样,你可以先用 MSBuild 属性拿到精确参数再回填:

dotnet build /t:Test /p:xUnitMethodName=System.Net.Sockets.Tests.{ClassName}.{TestMethodName}

参数生成机制的源码印证

args里出现的这些参数并不是魔法字符串,它们由仓库的 MSBuild 测试基础设施组装而成。在 xunit.console.targets 中可以看到:

<RunScriptCommand Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">"$(RunScriptHost)" exec --runtimeconfig $(AssemblyName).runtimeconfig.json $(_depsFileArgument) $(XunitConsolePath)</RunScriptCommand> ... <RunScriptCommand Condition="'$(XUnitMethodName)' != ''">$(RunScriptCommand) -method $(XUnitMethodName)</RunScriptCommand> <RunScriptCommand Condition="'$(XUnitClassName)' != ''">$(RunScriptCommand) -class $(XUnitClassName)</RunScriptCommand> <RunScriptCommand>$(RunScriptCommand)$(_withCategories.Replace(';', ' -trait category='))</RunScriptCommand> <RunScriptCommand>$(RunScriptCommand)$(_withoutCategories.Replace(';', ' -notrait category='))</RunScriptCommand>

这解释了为什么-notrait category=failing会出现在每个测试命令行里:_withoutCategories把分号分隔的类别列表展开成多条-notrait category=...参数,用于默认排除标记为failing的用例。同时,/p:xUnitMethodName=...这个 MSBuild 属性最终通过-method落到运行命令上,这正是上面“先跑命令再抄参数”方案的底层依据。

其他可用的测试运行开关(同样来自 xunit.console.targets):

开关来源属性作用
-xml <file>$(TestResultsName)输出测试结果 XML(默认testResults.xml
-nologo固定追加关闭 xunit console 的启动横幅
-method <FQN>$(XUnitMethodName)只运行指定方法
-class <FQN>$(XUnitClassName)只运行指定类
-trait category=<x>$(WithCategories)仅运行带某 category trait 的用例
-notrait category=<x>$(WithoutCategories)排除带某 category trait 的用例
-maxthreads 1$(TestDisableParallelization)关闭并行,便于串行调试
-verbose$(XUnitShowProgress)打印运行进度

测试项目的命名与框架也有一套约定:tests.props 中TestProjectName默认为$(MSBuildProjectName)(即项目文件名),TestFramework默认为xunit;xunit.props 则统一引入了xunit.corexunit.assertxunit.analyzersMicrosoft.DotNet.XUnitExtensions等包,保证所有库测试项目使用一致的测试栈。

断点调试与配置固化

完成以上修改后:

  1. 在测试代码中设置断点;
  2. .NET Core Launch (console)配置启动调试;
  3. 断点命中后,即可正常查看局部变量、监视表达式、调用堆栈,单步执行。

可选优化:把配置保存到工作区文件(workspace)。VS Code 的.code-workspace工作区文件可以放在仓库根目录之外,这样git clean -dfx之类的清理操作不会误删你的调试配置,也不受.vscode目录位置限制。对于经常在同一台机器上切换调试多个库的开发者,这是保持配置长期可用的推荐做法。

在 Mono 运行时上调试库(远程附加模式)

如果你需要在桌面平台(Linux / macOS / Windows)上,让库代码跑在Mono 运行时(而非默认的 CoreCLR)上进行调试,则使用“远程附加”模式。WebAssembly 与 Android/iOS 的 Mono 调试属于另一套工具链,分别见 Android 调试 与 WebAssembly 调试。

1. 安装 Mono Debugger 扩展

安装 VS Code 扩展Mono Debuggerms-vscode.mono-debug),它提供mono类型的调试配置。

2. 编写 type=mono 的附加配置

launch.json中新增一个type: "mono"的 attach 配置:

{ "version": "0.2.0", "configurations": [ { "name": "Attach to Mono", "type": "mono", "request": "attach", "address": "localhost", "port": 1235 } ] }

该配置让调试器通过dt_socket传输协议,附加到本地1235端口上等待连接的 Mono 进程。

3. 用 MONO_ENV_OPTIONS 启动测试

在命令行启动测试,并通过MONO_ENV_OPTIONS环境变量把调试代理参数注入 Mono 运行时:

DOTNET_REMOTEEXECUTOR_SUPPORTED=0 MONO_ENV_OPTIONS="--debug --debugger-agent=transport=dt_socket,address=127.0.0.1:1235,server=y,suspend=y" ./dotnet.sh build /t:Test /p:RuntimeFlavor=Mono src/libraries/System.Buffers/tests

拆解这条命令:

  • ./dotnet.sh:仓库根目录下的构建脚本(见 dotnet.sh),负责调用仓库内置的 SDK;
  • build /t:Test:构建并运行测试目标;
  • /p:RuntimeFlavor=Mono:显式指定使用 Mono 运行时,这与构建脚本里PrimaryRuntimeFlavor=Mono的切换逻辑一致(见 build.sh);
  • MONO_ENV_OPTIONS="--debug --debugger-agent=transport=dt_socket,address=127.0.0.1:1235,server=y,suspend=y":启动 Mono 调试代理并挂起,等待调试器附加;
  • DOTNET_REMOTEEXECUTOR_SUPPORTED=0必须设置。否则测试的 RemoteExecutor 会启动多个运行时实例,多个进程会同时尝试监听1235端口导致冲突。这一点在测试基础设施中也有体现:MONO_ENV_OPTIONS会被注入到测试运行脚本里(见 tests.props),而DOTNET_REMOTEEXECUTOR_SUPPORTED=0正是为了抑制这种多实例抢占端口的场景。

Windows 注意:在 Windows 上,不要在MONO_ENV_OPTIONS里传--debug,只需保留调试代理参数。

4. 附加调试器

保持命令行中的测试进程等待(suspend=y),在 VS Code 测试代码里设置断点,然后以Attach to Mono配置启动调试。调试器附加成功后,断点即可命中,变量与调用栈的查看方式和本地调试一致。

Mono 调试的两个已知限制

  • Mono 不会在“首机会异常”(first chance exception)上暂停
  • xunit 会捕获所有异常

因此,如果测试里抛出异常,调试器不会自动停在未捕获异常处。排查时应在可能抛异常的代码位置显式设置断点,而不是依赖“异常中断”行为。

调试流程速查与常见问题

典型流程回顾

  1. 打开目标库目录(如src/libraries/System.Net.Sockets);
  2. /t:Test构建一次测试,从终端输出捕获exec ...参数;
  3. 生成launch.json(CoreCLR:删preLaunchTask、填program/cwd/args;Mono:写type: monoattach 配置);
  4. 在测试源码打断点,启动对应调试配置;
  5. /p:xUnitMethodName=...-method缩小到单个用例,加快定位。

常见问题

现象原因与处理
断点显示为“未绑定”program指向的 dotnet 版本与当前构建不匹配,或cwd不在测试输出目录;确认路径与artifacts/bin实际产物一致
启动报“找不到 runtimeconfig”args--runtimeconfig文件名与测试项目名不一致(注意是TestProjectName而非目录名)
调试器未命中但测试能跑args与真实测试命令不一致,尤其是-notrait/-method部分;重新跑命令核对exec输出
Mono 附加时端口被占用未设置DOTNET_REMOTEEXECUTOR_SUPPORTED=0,多个运行时实例抢占1235
想临时打印日志调试库代码时避免使用System.Console.WriteLine,可参考 CoreLib 调试指南 中的专用日志通道

延伸阅读

  • 在 Unix 上用 lldb 调试 core .NET 库:面向崩溃转储与 SOS 的底层调试路径;
  • 调试 System.Private.CoreLib:CoreLib 内部调试时日志输出的特殊约定;
  • Android Mono 调试 与 WebAssembly 调试:跨平台 Mono 场景的配套方案;
  • xunit 测试基础设施:查看测试运行命令的完整生成规则,可据此构造任意自定义调试参数。
  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询