序言
在大前年,为了说服框架组采用Nuget包的形式分发框架类库,我费了老鼻子的劲也没有取得成功,其中最致命的一个问题是,nuget包不能获得源码调试级的支持,在分发和包的管理形式上其比其他方案都优秀。最后折中的选择是采用源码直接引用项目的方式,这种方案对框架类库的新分支的开发不是很有利,在源码的保护上更是完全没有了保障,不过在当时场景下,也算是可以接受的方案之一了。而经过这几年的发展,微软在这些方面都有了长足的发展,那跟着我,来看看能否解决各位心中的疑惑?
1. 历史悠久的PDB
当看到一个人光鲜亮丽,光彩照人的时候,我们都有着一股探究其老底的冲动。是啊,凭啥他就能行,而我们就不行呢?我们起起底,探究下他的小秘密,说不定从他身上能发掘出不为人知的一面,为我们的崛起提供一些参考的方向,这不香吗?
扯得有点远了,我们回归正题。
1.1 PDB 和符号文件
PDB全称:Program Database,由微软开发的一种调试符号文件存储格式,在windows系统中,为了调试dll或者exe文件,需要有一个符号文件(Symbols file)来支撑调试。符号文件保存多个数据,这些数据在运行二进制文件时实际上并不需要,但在调试过程中可能非常有用。通常,符号文件可能包含:
全局变量
局部变量
函数名称和其入口点的地址
帧指针省略 (FPO) 记录
源行号
调试时,必须确保调试器能够访问与正在调试的目标关联的符号文件。实时调试和调试崩溃转储文件都需要符号。你必须获取要调试的代码的正确符号,并将这些符号加载到调试器中。
而PDB文件就是windows保留符号的文件格式。熟悉C++的朋友,应该对这个文件非常熟悉,它伴随着Visual Studio 和WinDbg而产生,可谓是历史悠久的文件格式之一。
什么,想看看PDB文件保存的是啥玩意?好吧,本尊就满足你的好奇心,在windows的调试工具有个dbh小工具,可以查看pdb的文件内容。
mysymbols [1000000]: symopt -2Symbol Options: 0x14c13
Symbol Options: 0x14c11mysymbols [1000000]: addr 102cb4e_MyFunction1@4name : _InterlockedIncrement@4addr : 102cb4esize : 0flags : 0type : 0
modbase : 1000000value : 0reg : 0scope : SymTagNull (0)tag : SymTagPublicSymbol (a)index : 2ab
当然了,符号文件本身也有很多格式,例如大名鼎鼎的COFF
是Unix下常用的调试符号文件格式。
1.2 PDB的变种-便携式PDB
为了能适应跨平台的需求,微软提出了便携式PDB(Portable PDB )标准,以便和长期使用的Windows PDB相区别。其主要支持.NET中的托管代码。从历史上看,Windows PDB用于存储本机代码和托管代码的调试信息,其用于读取和写入这些PDB的工具仅在Windows平台上得到支持。便携式PDB旨在以平台无关的格式有效地存储托管的调试信息,其在多个平台上的有着丰富的支持工具,其以可移植格式存储托管的调试信息,并会生成更小的PDB,这在考虑分发大小时也是重要的优势。
2 使用符号调试
2.1 想要调试三方库或.net 框架
让我们来看一个例子。有时您想进入框架以查看发生了什么,特别是如果发生了意外的事情。假设您如同下面所示设置了断点。那么在按F11想进入框架内部的时候,代码直接往下面执行了,这就是您所看到的。
默认情况下,Visual Studio在调试应用程序时仅逐步执行代码。这是一个非常有用的功能,因为您通常希望理解和研究自己所编写代码的逻辑。
关注自己,是人生在牙牙学语阶段就开始逐渐不断增长的意识,因此始于人性,才是最好的Feature!启用这种体验的功能被恰当地称为仅我的代码(“ Just my Code”)。
在某天,您想调试第三方组件或平台本身的逻辑,在这之前,进行调试非常困难。主要是两个方面的困难:
缺少第三方组件或平台二进制文件的符号;
缺少第三方组件或平台二进制文件匹配的符号相关联的源文件。
相比而言,JavaScript具有与.NET几乎相反的问题。JavaScript社区(包括浏览器和node.js变体)都使用SourceMap,其提供了调试第三方精简代码的良好体验。但是,JavaScript编辑器无法提供“仅我的代码”的体验。
对于.NET Core 开发人员,我们希望能够轻松自然地在默认的“ 仅我的代码”体验以及带有第三方组件和平台源调试之间进行自由切换,这一切,并不是梦!
2.2 调试.net core 平台代码
那我们怎么能够调试三方库或.net 框架呢?
Visual Studio 2017版中已经支持了符号调试,在VS 的 工具菜单下,选择选项/调试/常规页签,配置如下参数:
禁用 “启用仅我的代码”
启用源链接
选择选项/调试/符号页签,配置如下参数:
选择 Microsoft符号服务器;
选择 NuGet.org 符号服务器(如果调试的是nuget库);
在缓存符号目录中设定一个目录,避免多次下载符号库。
如果使用的是VS Code,可以为每个项目配置调试器设置:launch.json
"justMyCode": false,
"symbolOptions": {"searchMicrosoftSymbolServer": true,"searchNuGetOrgSymbolServer": true
},
"suppressJITOptimizations": true,
"env": {"COMPlus_ZapDisable": "1","COMPlus_ReadyToRun": "0"
}
注意:
并非nuget.org上的每个库都会为其.pdb文件建立索引。如果发现调试器找不到正在使用的开源库的PDB文件,请鼓励作者上载其PDB;
只有Microsoft提供的库才会在Microsoft符号服务器上拥有其.pdb文件,因此,如果您只对三方库感兴趣,可以禁用该选项。
启动调试,发现VS开始下载符号文件,下载完毕后,进入断点。当我们按F11后,弹出如下界面:
3. SourceLink
Source Link是开发人员的一项生产力功能,它允许在编译过程中将有关程序集原始源代码的唯一信息嵌入到PDB中的一组软件包和规范, 通过SourceLink,添加到PDB文件中的元数据,和本地源代码文件、仓库内的代码文件建立了一个映射关系。
因此Visual Studio调试时可以在需要时下载文件, 并为用户提供源代码调试, Microsoft库(例如.NET Core和Roslyn)都已启用Source Link。
3.1 为什么使用SourceLink呢?
大多数调试是针对开发人员计算机上本地构建的源代码完成的。在这种情况下,将二进制文件与源代码匹配并不困难。
但是,在许多调试方案中,原始源代码没法立即就可用。这方面的两个很好的例子是调试崩溃转储或第三方库。在这些情况下,对于开发人员来说,要获取为生成正在调试的二进制文件而构建的确切源代码可能非常困难(可能是特定的版本)。Source Link通过在PDB中嵌入有关源代码的唯一信息(例如git commit hash)来解决此问题。诊断工具(例如调试器)可以使用此独特信息从托管服务(例如GitHub)中检索原始源代码。
sourcelink 最初的版本是 @ctaggart 实现的,目前已归档, 现在已经加入了 .Net 团队,微软人员和ctaggart 一起做了现在的版本。
官网地址: https://github.com/dotnet/sourcelink
3.2 SourceLink的文件规范
SourceLink 是一个Json配置的文件,其内容格式如下:
{"$schema": "http://json-schema.org/draft-04/schema#","title": "SourceLink","description": "A mapping of source file paths to URLs","type": "object","properties": {"documents": {"type": "object","minProperties": 1,"additionalProperties": {"type": "string"},"description": "Each document is defined by a file path and a URL. Original source file paths are compared case-insensitively to documents and the resulting URL is used to download source. The document may contain an asterisk to represent a wildcard in order to match anything in the asterisk's location. The rules for the asterisk are as follows:1. The only acceptable wildcard is one and only one '*', which if present will be replaced by a relative path.2. If the file path does not contain a *, the URL cannot contain a * and if the file path contains a * the URL must contain a *.3. If the file path contains a *, it must be the final character.4. If the URL contains a *, it may be anywhere in the URL."}},"required": ["documents"]
}
为了减轻生成该json的工作量,微软提供了一系列的软件包,自动生成Source Link 文件。
3.3 自动生成SourceLink
在.net core项目内,在.csproj文件内增加如下配置
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netcoreapp2.1</TargetFramework><!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) --><PublishRepositoryUrl>true</PublishRepositoryUrl><!-- Optional: Embed source files that are not tracked by the source control manager in the PDB --><EmbedUntrackedSources>true</EmbedUntrackedSources><!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link --><IncludeSymbols>true</IncludeSymbols><SymbolPackageFormat>snupkg</SymbolPackageFormat></PropertyGroup><ItemGroup><!-- Add PackageReference specific for your source control provider (see below) --> </ItemGroup>
</Project>
按照需要引用下面的软件包,注意,这里设置为 PrivateAssets
,以避免发布为nuget包后,引用该包的项目下载sourcelink 包。
github.com and GitHub Enterprise
<ItemGroup><PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
Azure Repos (former Visual Studio Team Services)
<ItemGroup><PackageReference Include="Microsoft.SourceLink.AzureRepos.Git" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
Azure DevOps Server (former Team Foundation Server)
<ItemGroup><PackageReference Include="Microsoft.SourceLink.AzureDevOpsServer.Git" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
如果您的服务器配置有非空的IIS虚拟目录,请在SourceLinkAzureDevOpsServerGitHost项目中指定此目录,如下所示
<ItemGroup><SourceLinkAzureDevOpsServerGitHost Include="server-name" VirtualDirectory="tfs"/>
</ItemGroup>
该Include属性指定域以及服务器的端口(例如server-name或server-name:8080)。
GitLab
<ItemGroup><PackageReference Include="Microsoft.SourceLink.GitLab" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
Bitbucket
<ItemGroup><PackageReference Include="Microsoft.SourceLink.Bitbucket.Git" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
如果您的项目是由版本4.7之前的Bitbucket Server或Bitbucket Data Center托管的SourceLinkBitbucketGitHost,则除了软件包参考之外,还必须指定项目组:
<ItemGroup><SourceLinkBitbucketGitHost Include="bitbucket.yourdomain.com" Version="4.5"/>
</ItemGroup>
项目组SourceLinkBitbucketGitHost指定Bitbucket主机的域和Bitbucket的版本。该版本非常重要,因为用于访问文件的URL格式随版本4.7更改。默认情况下,源链接采用新格式(4.7+版)。
gitweb (pre-release)
<ItemGroup><PackageReference Include="Microsoft.SourceLink.GitWeb" Version="1.1.0-beta-20204-02" PrivateAssets="All"/>
</ItemGroup>
开发人员必须选择生成源链接文件。这些文件中包含的URL可能指向私有源存储库,这些存储库可能不打算公开给有权访问符号文件的任何人,因此,开发人员应做出明智的选择。
开源开发人员通常选择不参与源链接文件生成,因为他们通常不存在公开问题。
公司开发人员还应选择采用以下方式选择源链接文件生成:
所有应用程序资产(二进制文件,符号和源代码)都在公司防火墙内使用,因此只有有权访问这些资产的用户才能看到它们。
二进制资产是从外部运送的,但符号和源仅在公司防火墙内使用过,因此只有有权访问符号和源资产的用户才可以查看它们。
公司开发人员还有另一个选择(可以说是反模式),如下所示:
二进制和符号资产在外部共享。符号资产包含源链接文件(以及可能生成的文件资产)。
源链接文件指向需要身份验证的符号源,例如VSTS。
授权用户(数量可能很少)将可以访问源。
未经授权的用户(数量可能更多)会从他们不理解的端点接收拒绝访问的消息。
3.4 源嵌入
在某些情况下,将源代码嵌入符号中是有益的,这样您就可以方便地部署源代码进行调试。但是,这是在便利性和PDB大小之间进行权衡的。尽管将源压缩存储在包含许多源文件的PDB中,但可能会大大增加PDB的大小。
以下嵌入选项(适用于Windows和便携式PDB)均可用:
仅嵌入源代码管理未跟踪的源文件(例如,在生成期间生成的文件)。其余文件由源链接映射。
嵌入手动选择的源文件子集。
嵌入所有源文件
EmbedAllSources
具有布尔值的Project属性表示所有传递给编译器的源都应嵌入到PDB中。
自动嵌入未跟踪的源文件
源链接使调试器和其他工具可以查找源控件跟踪的文件的源内容。但是,并非所有参与构建的文件都被跟踪。例如,在构建期间生成的文件通常不检入存储库。尽管可以手动识别此类文件并标记它们以将其嵌入到PDB中,但是这种过程繁琐且容易出错。
该SourceLink.Embed项目已经支持自动识别,而不是由源代码控制跟踪文件的嵌入。这些API将确定源代码控制未跟踪的文件(例如,对于git存储库,匹配.gitignore文件中条目的文件)和设置EmbedUntrackedSources,然后将指示编译器嵌入未跟踪的源代码。
下面是嵌入源代码的例子,在.csproj项目文件内增加如下配置
<PropertyGroup> <EmbedAllSources>True</EmbedAllSources><!--<EmbedUntrackedSources>true</EmbedUntrackedSources>-->
</PropertyGroup>
4.发布符号文件
默认情况下,符号被构建为单独的文件,以最小化二进制文件的大小。这些文件需要由构建它们的系统(例如CI服务器)发布,并由需要它们的系统(例如调试器)发现和检索。
4.1将符号发布到符号服务器
如今,符号服务器主要用于在企业环境中托管符号文件。符号服务器工具可用于此类环境,并且最近也已集成到VSTS中并作为服务公开。
对于在NuGet.org上发布其库的开发人员而言,可公开使用的符号服务器的选项受到限制,并且发布和使用符号的过程比应有的要复杂得多。因此,在NuGet.org上发布的易于调试的软件包数量很少。
符号包(snupkg)
今天,符号包用于分发符号和源。良好的调试体验依赖于调试符号的存在,因为它们提供了一些关键信息,例如已编译的代码与源代码之间的关联、局部变量的名称、堆栈跟踪等。你可以使用符号包 (.snupkg) 来分发这些符号,并改善 NuGet 包的调试体验。
如果使用 dotnet CLI 或 MSBuild,则除 .nupkg 文件外,还需要设置 IncludeSymbols 和 SymbolPackageFormat 属性以创建 .snupkg 文件。
创建 .snupkg 文件有多种方式实现该需求。
将以下属性添加到 .csproj 文件:
<PropertyGroup><IncludeSymbols>true</IncludeSymbols><SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
在命令行上指定这些属性:
dotnet pack MyPackage.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
或者使用msbuild
msbuild MyPackage.csproj /t:pack /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg
使用nuget
nuget pack MyPackage.nuspec -Symbols -SymbolPackageFormat snupkg
nuget pack MyPackage.csproj -Symbols -SymbolPackageFormat snupkg
发布服务包到nuget
# 为方便起见,首先使用 NuGet 保存 API 密钥
nuget SetApiKey Your-API-Key
# 将主包发布到 nuget.org 后,按如下方式推送符号包。
nuget push MyPackage.snupkg
# 还可以使用以下命令同时推送主包和符号包。当前文件夹中必须同时有 .nupkg 和 .snupkg 文件。
nuget push MyPackage.nupkg
4.2 不使用服务器,直接嵌入发布符号包和源文件
嵌入符号文件和源代码更简单,它不需要网络或互联网连接,也不需要任何配置即可指定源代码存储库和符号服务器的位置。因为配置私有的符号包服务,我暂时并没有找到合适的平台,因此我高度推荐你采用这种方式,非常的便捷,您只需要在csproj内增加如下代码,即可完成嵌入的便携式PDB,以及源文件的发布。
<PropertyGroup><DebugSymbols>True</DebugSymbols><DebugType>Embedded</DebugType><EmbedAllSources>True</EmbedAllSources>
</PropertyGroup>
对于.NET应用程序来说,嵌入PDB文件不会影响编译器的优化,所以也完全不会影响应用的性能。
4.3 使用私有gitlab保护代码
结合第3小节的介绍,我们可以很方便的结合gitlab sourceLink,来制作嵌入符号包,保护源码的发布方式:
<PropertyGroup><TargetFramework>netcoreapp3.1</TargetFramework> <!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) --><PublishRepositoryUrl>true</PublishRepositoryUrl><!-- Optional: Embed source files that are not tracked by the source control manager in the PDB --><EmbedUntrackedSources>true</EmbedUntrackedSources><!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link --><DebugType>Embedded</DebugType><!--<IncludeSymbols>false</IncludeSymbols><SymbolPackageFormat>snupkg</SymbolPackageFormat>--></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.SourceLink.GitLab" Version="1.0.0" PrivateAssets="All" /></ItemGroup>
如果能早点提供该方式,相信几年前的方案,我应该能胜出,是吧?
5.使用PDB(符号)和SourceLink
在Visual Studio或其他工具中使用符号应该很方便。它需要适用于所有.NET实现,包括.NET Core,.NET Framework,Xamarin,Unity和UWP。
使用符号进行调试的主要场景有以下三种:
在开发阶段调试应用程序。在这种情况下,需要在构建系统外部检索第三方库符号,同时将应用程序符号生成为构建的一部分。
在部署状态下调试应用程序。在这种情况下,需要检索应用程序和库符号。该方案将附加到正在运行的应用程序。
调试应用程序的故障转储。在这种情况下,需要应用程序和库符号。
5.1符号和二进制文件一起分发
此方案适用于开发阶段。如果符号已经嵌入二进制文件中,则不适用。NuGet是二进制分发的一种常见情况,它允许将符号和二进制文件一起部署。
关键特征是符号文件将直接存在于磁盘上已加载的代码文件旁边,从而使调试器可以轻松地找到给定二进制文件的匹配符号文件。根据本文档中的指导,二进制文件和符号通常会以与以下示例类似的结构并置在NuGet包中。
/
/lib
/netstandard2.0
foo.dll
foo.pdb
.NET Core开发是以NuGet为中心的,这有助于解决此问题。在开发过程中(例如使用dotnet run),. NET Core运行时默认情况下从NuGet缓存加载库,从而使调试器可以在同一位置查找匹配的符号文件。
.NET Framework开发使用NuGet,但形式上使用较少。构建项目时,NuGet库将被复制到应用程序bin目录,而不是符号。结果,代码二进制和符号之间的链接丢失了。
构建系统应使用以下逻辑,以更好地启用带符号的调试
如果将代码二进制文件复制到某个位置,则还要将符号文件复制到同一位置。在大多数情况下,这将是应用程序bin目录。
5.2 符号和二进制文件分开分发
此方案适用于调试已部署的应用程序,崩溃转储以及调试NuGet软件包中未附带符号的第三方库。在这些情况下,您需要从符号服务器获取符号。
如上所述,我们建议为上传到NuGet.org的符号提供公共符号服务。您也可以根据需要使用其他符号服务器。
5.3 获取和消耗源文件
获取和消耗源文件主要是首先具有获取符号的功能。有了符号后,调试器将发现以下一种或多种情况是正确的:
符号文件包含调试器正在寻找的源文件的嵌入式源,此时调试器将使用该源。
该符号文件是Windows PDB,包含嵌入式源服务器信息。可以使用诸如pdbstr或GitLink之类的构建工具,使用源服务器信息来修改现有的Windows PDB。
该符号文件包含一个嵌入式源链接文件,此时调试器将解释并执行源链接文件中的声明。
否则,将无法通过本文档中讨论的机制获得源
注意:如果调试器无法在符号文件中或通过源链接找到源文件,它仍可以尝试使用**简单符号查询协议**在符号服务器上查找它。这将允许在二进制文件和符号构建完成之后稍后使源可用的情况。
6 小结
弄懂SourceLink和pdb的关系,竟然花费了我一个元旦假期,本来计划在假期内出篇文章简单介绍下,发现在自己都没有搞清楚的情况,写这些是对大家和自己的不负责任,因此,还是静下心来,仔细理清楚各个环节,希望能对大家有所帮助。