怎么查看 DLL 是哪个 .NET 版本:目标框架、版本号和依赖
本页目录
结论先行: .NET DLL 把它面向的框架记在程序集级别的 TargetFrameworkAttribute 里,值类似 .NETCoreApp,Version=v6.0、.NETFramework,Version=v4.5 或 .NETStandard,Version=v2.0。程序集版本在元数据的 Assembly 表里,它依赖的每个程序集各占一行 AssemblyRef。这三样东西都不需要 Visual Studio、不需要 .NET SDK,也不需要加载 DLL 就能读出来:拖进本站的 .NET 程序集元数据与 IL 查看器(文件在浏览器的 Web Worker 里解析,不上传也不执行),或者按第 6 步在命令行里跑。
有两种情况最容易让人犯迷糊:面向 .NET Framework 2.0 或 3.5 的 DLL 根本没有目标框架属性;你在 NuGet 上看到的版本号,通常不是程序集版本。
示例文件: nuget.org 上的 Newtonsoft.Json 13.0.3(MIT 许可证)。包里有同一个库的八份构建,每个目标框架一份,非常适合横向对比:
curl -sSL -o nj.nupkg https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
unzip -q nj.nupkg -d nj
ls nj/lib
# net20 net35 net40 net45 net6.0 netstandard1.0 netstandard1.3 netstandard2.0
第 1 步:是 .NET 程序集还是原生 DLL?
扩展名 .dll 说明不了里面是什么。.NET 程序集是一个 PE 文件,它的第 14 个数据目录指向 CLI 头,CLI 头再指向元数据。原生 DLL 没有 CLI 头。
.NET 查看器 第一步就检查这个。遇到原生 PE,它会直接提示“这是原生 PE,不是 .NET 程序集(没有 CLI 头)。“如果发现 NativeAOT 的特征(.managed 或 hydrated 段,或者文件里有 DotNetRuntimeDebugHeader 字符串),它会告诉你这是 NativeAOT 产物:里面只有机器码,没有 IL 元数据。
第 6 步的命令行方式会把同样的判断以数据形式给出。对本站测试构建器生成的一个原生 PE,REA 6.7.0 返回的分类如下(省略了部分字段):
{
"status": "not-managed",
"container": "pe",
"implementation": "not-managed",
"evidence": [{ "code": "cli-directory-absent",
"detail": "The PE has no CLI data directory; managed metadata and CIL are unavailable." }]
}
拿到这个结果,下面的步骤就都不适用了。原生 DLL 要用原生工具分析,见 Windows EXE 和 DLL 分析。
第 2 步:读目标框架
在查看器里打开 nj/lib/net6.0/Newtonsoft.Json.dll,程序集标识区会显示来自 TargetFrameworkAttribute 的目标框架。下面是查看器和 REA 6.7.0 对八份构建给出的结果:
| 目录 | 目标框架(属性) | 元数据版本 | REA runtime_family |
|---|---|---|---|
net20 |
(无) | v2.0.50727 |
unknown |
net35 |
(无) | v2.0.50727 |
unknown |
net40 |
.NETFramework,Version=v4.0 |
v4.0.30319 |
dotnet-framework |
net45 |
.NETFramework,Version=v4.5 |
v4.0.30319 |
dotnet-framework |
net6.0 |
.NETCoreApp,Version=v6.0 |
v4.0.30319 |
modern-dotnet |
netstandard1.0 |
.NETStandard,Version=v1.0 |
v4.0.30319 |
modern-dotnet |
netstandard1.3 |
.NETStandard,Version=v1.3 |
v4.0.30319 |
modern-dotnet |
netstandard2.0 |
.NETStandard,Version=v2.0 |
v4.0.30319 |
modern-dotnet |
查看器和 REA 每一项都一致。
属性缺失的时候
System.Runtime.Versioning.TargetFrameworkAttribute 是 .NET Framework 4.0 才引入的,面向 2.0 或 3.5 的编译器不会生成它。对这类构建,要看同一份报告里的其他证据:
- 元数据版本
v2.0.50727表示 CLR 2 运行时,.NET Framework 2.0、3.0、3.5 共用它。v4.0.30319则是 .NET Framework 4.0 及以后所有版本都会写的值,包括 .NET Core 和 .NET 5+,所以单看它分不出 .NET Framework 4.x 和 .NET 6。 - 引用可以补全答案。
net20构建引用的是mscorlib 2.0.0.0、System 2.0.0.0、System.Xml 2.0.0.0和System.Data 2.0.0.0;net35构建多了System.Core 3.5.0.0和System.Xml.Linq 3.5.0.0,这两个程序集是 3.5 才有的。
REA 在这里很克制:对这两份构建,它返回 runtime_family: "unknown" 和空的 target_frameworks,不做猜测。
为什么 .NET Standard 会显示成 modern-dotnet
REA 6.7.0 把 .NETCoreApp, 和 .NETStandard, 开头的属性都当作 modern-dotnet 的证据。但要记住,.NET Standard 是一份 API 约定,不是运行时:netstandard2.0 的库在 .NET Framework 4.6.1 及以后也能跑。target_frameworks 才是确切的事实,runtime_family 是在它之上做的归类。
第 3 步:程序集版本、文件版本和产品版本
13.0.3 包里的八个 DLL 标识完全相同:
Newtonsoft.Json 13.0.0.0, public key token 30ad4fe6b2a6aeed
13.0.0.0 是程序集版本,运行时绑定强名称引用时比较的就是它,binding redirect 写的也是它。NuGet 包版本(13.0.3)是另一个数字。在一个主版本内固定程序集版本,应用就不必为每个补丁版本新增 binding redirect(见程序集版本控制)。
文件版本和产品版本放在别处:Win32 版本资源里,同时由 AssemblyFileVersionAttribute 和 AssemblyInformationalVersionAttribute 记录。浏览器查看器不显示这两项;REA 6.7.0 会列出这些属性,但不解码它们的字符串值。
在 Windows 上,PowerShell 可以直接读版本资源:
(Get-Item .\Newtonsoft.Json.dll).VersionInfo | Format-List FileVersion, ProductVersion
[System.Reflection.AssemblyName]::GetAssemblyName("$PWD\Newtonsoft.Json.dll").Version
在 macOS 或 Linux 上,版本资源里的字符串是 UTF-16 编码,很好找。我核对样本时用的就是下面这个小脚本:
import re, sys
data = open(sys.argv[1], "rb").read()
for key in ("FileVersion", "ProductVersion", "Assembly Version"):
m = re.search(re.escape(key.encode("utf-16-le")) + rb"\x00\x00+((?:[^\x00]\x00)+)", data)
print(f"{key}: {m.group(1).decode('utf-16-le') if m else '(not found)'}")
FileVersion: 13.0.3.27908
ProductVersion: 13.0.3+0a2e291c0d9c0c7675d445703e51750363a549ef
Assembly Version: 13.0.0.0
三个数字分别是 13.0.0.0(程序集版本)、13.0.3.27908(文件版本)和 13.0.3 加提交哈希(产品版本)。别人问“这个 DLL 是什么版本”时,先问清楚指的是哪一个。
第 4 步:列出引用,排查版本冲突
查看器里的引用列表就是程序集的 AssemblyRef 表:名称、版本、区域性和公钥标记。对比同一个库的三份构建,能看出目标框架对依赖面的影响有多大:
net45:8 个引用,全是4.0.0.0(mscorlib、System、System.Core、System.Numerics、System.Xml、System.Xml.Linq、System.Runtime.Serialization、System.Data)。net6.0:22 个引用,全是6.0.0.0,开头是System.Runtime、System.Runtime.InteropServices和System.Runtime.Numerics。没有mscorlib,这本身就是 .NET Core 或 .NET 5+ 的强烈信号。netstandard2.0:只有一个引用,netstandard 2.0.0.0。
排查版本冲突时可以这样用:
- 记下每个被引用程序集的确切版本。引用就是在请求这个版本(在 .NET Framework 上配了 binding redirect 的话,是一个兼容版本)。
- 打开实际部署在应用旁边的那个 DLL,对比它的程序集版本。引用了
Foo 2.0.0.0,部署的却只有Foo 1.5.0.0,就是典型的FileLoadException。 - 依赖里引用的版本比你应用里的高,通常就是 .NET Framework 上需要加 binding redirect 的原因。
查看器还会列出 ModuleRef 和 P/Invoke 声明,也就是程序集调用的原生 DLL。要把它移植到别的操作系统时,这份清单很关键。
第 5 步:看某个方法的 IL(替代 ildasm)
类型树里每个方法都带 C# 风格的签名和元数据 token。在 net6.0 构建里,Newtonsoft.Json.JsonConvert 有一个 string SerializeObject(object value),token 是 0x06000055。点它就会反汇编方法体(这里整理成纯文本):
tiny header, maxStack 8, codeSize 9
IL_0000 ldarg.0
IL_0001 ldnull
IL_0002 ldnull
IL_0003 call string Newtonsoft.Json.JsonConvert::SerializeObject(object, System.Type, Newtonsoft.Json.JsonSerializerSettings)
IL_0008 ret
也就是单参数的重载把类型和设置都填成 null,转给三参数的重载,和公开源码一致。token 只在当前构建里有效:0x06000055 只在这个文件里指向这个方法,所以记录 token 时要同时记下文件的 SHA-256。
查看器展示的是元数据和 IL,不会反编译成 C#。需要 C# 的话,在本地对你有权检查的程序集使用 ILSpy 等反编译器。
第 6 步:用 REA 在命令行里做同样的事
写脚本或接 CI 时,可以用第三方开源命令行工具 REA,它同样不加载程序集就能读元数据。6.7.0 里这个命令只接受一个参数,即文件路径:
DLL=/absolute/path/nj/lib/net6.0/Newtonsoft.Json.dll
rea inspect-managed-artifact "$DLL" --json > artifact.json
jq '.normalized_result.classification | {status, runtime_family, implementation, managed_architecture}' artifact.json
jq '.normalized_result | {target_frameworks, name: .assembly.name, version: .assembly.version}' artifact.json
jq -r '.normalized_result.references[] | "\(.name) \(.version)"' artifact.json
对 net6.0 构建,6.7.0 的检查模块给出:status: "managed"、runtime_family: "modern-dotnet"、implementation: "cil"、managed_architecture: "anycpu"、target_frameworks: [".NETCoreApp,Version=v6.0"],程序集 Newtonsoft.Json 13.0.0.0,引用和查看器一样是 22 个。每项分类都附带证据代码和文件偏移,这里是 cli-header-observed、target-framework-observed 和 managed-architecture-derived。结果里的限制说明写得很直白:“Static inspection did not load the assembly, resolve dependencies through a CLR, decompile C#, or execute target code.”
注意 anycpu。查看器的 PE 头信息里,这个文件显示为 PE32、machine x86,因为 AnyCPU 程序集用的是 32 位 PE 头。REA 是根据 CLI 标志推出 anycpu 的:ILONLY 置位,32BITREQUIRED 没有置位。想知道程序集能不能以 64 位运行,看 CLI 标志,别看 PE 的 machine 字段。
要看整个程序集的成员、签名和 CIL 哈希,接着跑 rea inspect-managed-members "$DLL"。
和常用工具比一比
- PowerShell 的
AssemblyName.GetAssemblyName给出名称、版本和公钥标记,VersionInfo给出文件版本和产品版本。但它没法一次列出目标框架和引用。这些命令我只在 Windows 上用过。 - ildasm 随 Windows SDK 提供,能看清单(引用、属性)和 IL,只能在 Windows 上用。
- ILSpy 除了上面这些还能反编译出 C#,需要读到源码层面时用它。
- 浏览器查看器和 REA 什么都不用装(REA 只要 Node.js),任何系统都能用,从不加载程序集;REA 的 JSON 输出也方便做 diff 或交给 agent。
常见问题
能检查 NativeAOT 或单文件发布的应用吗? NativeAOT 产物没有 IL 元数据,查看器会直接说明。单文件包的开头是一个原生宿主程序,先把里面打包的程序集解出来,再逐个检查(见 这个 EXE 是不是 .NET 程序)。