Windows 下如何调试 PowerShell

背景

最近在用 PowerShell 的时候,发现一些地方特别有意思。于是就萌生了看源代码的想法,单看肯定不过瘾,调试起来才有意思。于是就有了这个,记录下。

调试 PowerShell 主要分为两种方式:通过 VS 直接编译运行源代码和通过 WinDbg 来调试。

由于 PowerShell 跨平台的特性,由于我目前只有 Windows 的诉求,所以以下内容将围绕 Windows 来进行。其他平台可以参照官方文档,可以在这里看到:

PowerShell/docs/building at master · PowerShell/PowerShell

准备工作

拉代码

第一步自然就是去 github 把代码 clone 下来了。地址是:

//github.com/PowerShell/PowerShell

截至到2022年7月16日,建议不要直接直接用 master 分支,根据文档介绍,master 分支为最新版本,并非稳定版本。也就是我们平常看到的 preview 的版本。而且,这里强烈建议不用是因为目前 preview 的版本已经切换到 .net 7 了,而 .net 7 的 sos 我没找到(如果知道怎么找到的小伙伴可以告诉我,感谢~)。所以我这里是采用最新的稳定版,也就是 7.2.5 版本的源代码进行。

准备环境

第二步就是准备好编译环境。因为 PowerShell 全部是 C# 实现的,自然就是依赖 dotnet 环境的了。

官方给了我们一个非常好的脚本,非常便利,在源代码的根目录下执行:

Import-Module ./build.psm1
Start-PSBootstrap

或者直接:

Install-Dotnet

就可以完成安装了。

如果只是这样,就结束了,是不是过于顺利了,就没有这篇文章的必要了。

由于我安装了 VS2022 preview 的版本,它顺带帮我安装了 .net 6.0.400-preview.22330.6 的版本,而且还不让卸载。因此我最新的版本始终是 6.0.400-preview,后续的编译,它就死命安装不了要的版本。也就是 6.0.203。

于是就只能从 dot.net 手动下载安装了。

方式一:通过 WinDbg 调试

这种方式适用于不想安装 VS,但需要详细调试的同学。(不要问我为啥不用 VSC,看代码 VSC 确实不错,但调试,还得 WinDbg 来)

编译代码

这里我们直接采用官方推荐的方式:

Import-Module ./build.psm1
Start-PSBuild

然后就出现下面的错误:

VERBOSE: In Find-DotNet
WARNING: The 'dotnet' in the current path can't find SDK version 6.0.203, prepending C:\Users\frend\AppData\Local\Microsoft\dotnet to PATH.
WARNING: The currently installed .NET Command Line Tools is not the required version.

Installed version: 6.0.400-preview.22330
Required version: 6.0.203

Fix steps:

1. Remove the installed version from:
    - on windows '$env:LOCALAPPDATA\Microsoft\dotnet'
    - on macOS and linux '$env:HOME/.dotnet'
2. Run Start-PSBootstrap or Install-Dotnet
3. Start-PSBuild -Clean

可是我明明安装了的呀:

- dotnet --list-sdks
6.0.203 [C:\Program Files\dotnet\sdk]
6.0.302 [C:\Program Files\dotnet\sdk]
6.0.400-preview.22330.6 [C:\Program Files\dotnet\sdk]

于是,就去找了下对应的脚本。在 [ps code root]/build.psm1 文件中的 Start-PSBuild → Find-Dotnet → Get-LatestInstalledSDK

function Get-LatestInstalledSDK {
    Start-NativeExecution -sb {
        dotnet --list-sdks | Select-String -Pattern '\d*.\d*.\d*(-\w*\.\d*)?' | ForEach-Object { [System.Management.Automation.SemanticVersion]::new($_.matches.value) } | Sort-Object -Descending | Select-Object -First 1
    } -IgnoreExitcode 2> $null
}

好家伙,直接就是拿最新的作为我安装的版本,所以导致匹配不上,无法开始编译。由于我这里安装了 6.0.203 的,所以我就修改了下。根据我的安装情况,改成让他返回我想要让他返回的版本了(如下加粗版本)。这里需要根据自己实际情况修改!!!

dotnet --list-sdks | Select-String -Pattern '\d*.\d*.\d*(-\w*\.\d*)?' | ForEach-Object { [System.Management.Automation.SemanticVersion]::new($_.matches.value) } | Sort-Object -Descending | Select-Object -First **3 | Sort-Object | Select-Object -First 1**

于是我们重来一遍导入,编译。就能成功啦!

然后就能找到啦 ./src/powershell-win-core/bin/Debug/net6.0/win7-x64/publish/pwsh.exe

调试代码

打开 WinDbg preview → Launch executable(advanced)

Executable 中填入上一步编译出来的地址,我的是这样的:C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\pwsh.exe

Arguments,填入你想调试的命令就好啦。我的是:ex bypass -nop -Command Invoke-webRequest www.baidu.com

其他就可以不管了。来,开始调试~

由于我们要调试的是托管代码,在启动点我们没办法打断点,得先等 sos 和 clr 加载好了之后才行。

这里我先打了一个clr上的断点:

bp coreclr!CallDescrWorkerInternal

待断点断到了,再加上 PowerShell 启动代码的断点。注意,托管代码需要用 sos 的 !bpmd 来打断点:

0:000> !bpmd pwsh Microsoft.PowerShell.ManagedPSEntry.Main
Adding pending breakpoints...
0:000> bd 0
0:000> g
ModLoad: 00007ff8`d7460000 00007ff8`d7478000   C:\WINDOWS\SYSTEM32\kernel.appcore.dll
ModLoad: 00000288`fa860000 00000288`fa884000   C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\pwsh.dll
ModLoad: 00000288`fa860000 00000288`fa884000   C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\pwsh.dll
(45e4.6834): CLR notification exception - code e0444143 (first chance)
ModLoad: 00000288`fa8a0000 00000288`fa8ae000   C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\System.Runtime.dll
(45e4.6834): CLR notification exception - code e0444143 (first chance)
ModLoad: 00000288`fa8b0000 00000288`fa90e000   C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\Microsoft.PowerShell.ConsoleHost.dll
(45e4.6834): CLR notification exception - code e0444143 (first chance)
(45e4.6834): CLR notification exception - code e0444143 (first chance)
JITTED pwsh!Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])
Setting breakpoint: bp 00007FF7E6F82530 [Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])]
Breakpoint 1 hit
*** WARNING: Unable to verify checksum for C:\Users\frend\source\repos\dotnet\PowerShell\src\powershell-win-core\bin\Debug\net6.0\win7-x64\pwsh.dll
pwsh!Microsoft.PowerShell.ManagedPSEntry.Main:
00007ff7`e6f82530 55              push    rbp

然后,就可以命中断点了。

这里可能不会自动打开并且跳转到源代码中,再 t 几下,就会自动打开的。最终就会变成这样。

于是,我们就可以开始顺利的调试了。

方式二:通过 VS 调试

这种方式相对就比较简单了,这里用 VS2022 来做演示。

记得一定要安装对应的 dotnet sdk。具体的版本可以在源代码根目录的 global.json 中查看。

直接在源代码中打开 PowerShell.sln 文件即可打开 PowerShell 项目。然后 build 一下,记得,Windows 下启动目录应该是:powershell-win-core

然后就可以配置调试的参数了:

在项目 powershell-win-core 上鼠标右击,选择属性 → 调试 → Open debug launch profiles UI

然后就是跟 WinDbg 调试类似的参数了。如下图:

然后,再你想观察的代码处打上断点,点击运行就好啦。

总结

其实官方文档已经比较完善了。基本都比较简单。特别是通过 VS,简直方便的不要不要的。

对于基于 dotnet 的 PowerShell,肯定还会遇到 dotnet 上的问题,那怎么调试 dotnet 呢?

看这里:

//github.com/dotnet/runtime