查看 .NET DLL 的目标框架、版本号和依赖(不用 Visual Studio)
本页目录
结论先行: .NET DLL 把它面向的框架记在程序集级别的 TargetFrameworkAttribute 里,值类似 .NETCoreApp,Version=v6.0、.NETFramework,Version=v4.5 或 .NETStandard,Version=v2.0。程序集版本在元数据的 Assembly 表里,它依赖的每个程序集各占一行 AssemblyRef。这三样东西都不需要 Visual Studio、不需要 .NET SDK,也不需要加载 DLL 就能读出来:拖进本站的 .NET 程序集元数据与 IL 查看器,或者用 REA(morluto 的开源项目)跑一下 rea inspect-managed-artifact。
有两种情况最容易让人犯迷糊,本文都用真实输出讲清楚:面向 .NET Framework 2.0 或 3.5 的 DLL 根本没有目标框架属性;你在 NuGet 上看到的版本号,通常不是程序集版本。
适合谁读: 正在排查 FileLoadException 或 binding redirect 的开发者;准备从 .NET Framework 迁移的团队;想在引入依赖前看清 NuGet 包里到底装了什么的人。
读完你能回答:
- 这个文件到底是不是 .NET 程序集,还是原生 DLL?
- 它面向哪个框架?属性缺失时怎么判断?
- 它的程序集版本、文件版本、产品版本分别是多少?
- 它引用了哪些程序集,各是什么版本?
- 某个方法的 IL 长什么样(替代 ildasm)?
示例文件: 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
同样的步骤可以用在你自己的构建产物或依赖包上。如果之后要运行或调试别人的应用,先确认你有授权,并阅读 REA 的 DISCLAIMER.md。
第 1 步:是 .NET 程序集还是原生 DLL?
扩展名 .dll 说明不了里面是什么。.NET 程序集是一个 PE 文件,它的第 14 个数据目录指向 CLI 头,CLI 头再指向元数据。原生 DLL 没有 CLI 头。
.NET 查看器 第一步就检查这个。遇到原生 PE,它会直接提示“这是原生 PE,不是 .NET 程序集(没有 CLI 头)。“如果发现 NativeAOT 的特征(.managed 或 hydrated 段,或者文件里有 DotNetRuntimeDebugHeader 字符串),它会告诉你这是 NativeAOT 产物:里面只有机器码,没有 IL 元数据。
REA 把同样的判断以数据形式给出。我用 6.4.0 的检查模块(dist/dotnet/ManagedArtifactInspector.js)跑了本站测试构建器生成的一个原生 PE,得到的分类结果如下(省略了部分字段):
{
"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 要用原生工具分析,见 用 REA 分析 Windows EXE 和 DLL。
第 2 步:读目标框架
在查看器里打开 nj/lib/net6.0/Newtonsoft.Json.dll,程序集标识区会显示来自 TargetFrameworkAttribute 的目标框架。下面是查看器和 REA 6.4.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 每一项都一致。这在意料之中,因为查看器解码属性的逻辑就是从 REA 6.4.0 移植的(src/tools/dotnet/analyze.ts 注明了出处 ManagedMetadataInventoryRows.ts)。
属性缺失的时候
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
6.4.0 里 REA 的 runtimeFamilyFor 把 .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)是另一个数字。
文件版本和产品版本放在别处:Win32 版本资源里,同时由 AssemblyFileVersionAttribute 和 AssemblyInformationalVersionAttribute 记录。浏览器查看器不显示这两项;REA 6.4.0 会列出这些属性,但只解码 TargetFrameworkAttribute 的字符串参数,其他属性的 decoded_fixed_string 一律是 null,只给原始数据的 value_sha256(见 ManagedMetadataInventoryRows.js)。
在 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 或者交给 AI agent,就用 REA 6.4.0。这个命令只接受一个参数,即文件路径(dist/cli/managedCommands.js):
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.4.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",详见 用 REA 静态分析 .NET 程序集。在 6.4.0 里这个命令同样只接受路径参数。
和常用工具比一比
- PowerShell 的
AssemblyName.GetAssemblyName给出名称、版本和公钥标记,VersionInfo给出文件版本和产品版本。但它没法一次列出目标框架和引用。这些命令我只在 Windows 上用过。 - ildasm 随 Windows SDK 提供,能看清单(引用、属性)和 IL,只能在 Windows 上用。
- ILSpy 除了上面这些还能反编译出 C#,需要读到源码层面时用它。
- 浏览器查看器和 REA 什么都不用装(REA 只要 Node.js),任何系统都能用,从不加载程序集;REA 的 JSON 输出也方便做 diff 或交给 agent。
常见问题
把 DLL 拖进查看器安全吗? 文件在你浏览器的 Web Worker 里解析,不会上传,也不会执行。Worker 的内容安全策略禁止任何网络请求。
查看器没显示目标框架,文件坏了吗?
多半没有。面向 .NET Framework 2.0 到 3.5 的构建本来就没有 TargetFrameworkAttribute,按第 2 步看元数据版本和引用。
为什么 Newtonsoft.Json 的 DLL 都显示 13.0.0.0? 我只核对了 13.0.3:八份构建都是 13.0.0.0,而文件版本是 13.0.3.27908。在一个主版本内固定程序集版本,应用就不必为每个补丁版本新增 binding redirect。
能检查 NativeAOT 或单文件发布的应用吗? NativeAOT 产物没有 IL 元数据,查看器会直接说明。单文件包的开头是一个原生宿主程序,先把里面打包的程序集解出来,再逐个检查。
下一步
- 检查你自己的程序集:.NET 程序集元数据与 IL 查看器
- 读成员、CIL,并对比不同版本:用 REA 静态分析 .NET 程序集
- 对比两次构建的产物:Evidence、快照与版本对比
- 参考资料:TargetFrameworkAttribute、程序集版本控制、ECMA-335