找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4072

积分

0

好友

526

主题
发表于 3 小时前 | 查看: 3| 回复: 0

在深入钻研 PE 文件解析库之后,我对 .NET 二进制结构有了非常深的理解。其中有一类元数据让我们既爱又恨——自定义属性。它们在 .NET 生态中几乎无处不在,但底层存储机制却让无数开发者和工具作者头疼。

本文将详细剖析自定义属性的工作原理、存储机制,以及为什么它们在底层如此复杂,让解析工作充满挑战。

示例源码:https://pan.baidu.com/s/1O0EMpm9y8PTqC0E534YkBQ?pwd=7uya

一、自定义属性是什么?

自定义属性是附加在类、方法、字段、参数等对象上的元数据。它们本质上是一种行为描述,告诉编译器或运行时在特定场景下需要执行额外逻辑。

你可以使用 CFF Explorer 等工具在 .NET 二进制文件中查看这些元数据表的原始内容:

CFF Explorer 查看 System.Private.CoreLib.dll 的元数据表

单个自定义属性行的展示:

CustomAttribute 表的详细数据

举个经典例子:

[Obsolete]
public class OldClass { }

使用 OldClass 时,编译器会提示:

CS0612: 'OldClass' is obsolete

这里,Attribute 并不执行任何逻辑,它只是提供了额外信息。

自定义属性不仅限于编译器控制,还广泛用于:

  • 框架注解:如 ASP.NET Core 路由 [Route("api/users")]
  • ORM 映射:如 Entity Framework 的 [Column("user_name")]
  • JSON 序列化:如 [JsonPropertyName("user_name")]
  • 依赖注入:如 [Singleton]
  • 单元测试框架:如 [Test]
  • 源代码生成:如 [AutoNotify] 自动生成 INotifyPropertyChanged 代码

可以说,现代 .NET 开发几乎离不开自定义属性。

二、自定义属性的底层存储

.NET 的文件格式采用元数据表(Metadata Tables)管理所有类型、方法、字段、参数等对象。每个对象都可以通过一个 metadata token(表 + 行号) 高效索引。

自定义属性也存储在专门的 CustomAttribute 表中,每行包括:

  1. 所附加对象的引用(Member)
  2. 属性构造函数的引用
  3. 参数数据在 Blob Stream 中的偏移量

Blob Stream 中的参数序列按照构造函数参数类型序列化。例如:

public class MyAttribute(int x, string y) : Attribute { }

[MyAttribute(0x1337, "Hello, world!")]
public class MyClass { }

Blob 中存储顺序为:

4 字节整数 + 长度前缀字符串

三、枚举参数的解析复杂性

大多数自定义属性使用 枚举参数。枚举值在存储时使用其底层类型(int、short 等)。

示例:

public enum MyIntEnum { Value1=1, Value2=2 }
public enum MyShortEnum : short { Value1=1, Value2=2 }

public class FooAttribute(MyIntEnum a, MyShortEnum b) : Attribute { }

[Foo(MyIntEnum.Value2, MyShortEnum.Value2)]
public class SomeClass { }

序列化后:

MyIntEnum.Value2 -> 4 字节
MyShortEnum.Value2 -> 2 字节

问题是:Blob 并不存储枚举底层类型

解析时必须:

  1. 解析枚举类型所在的 Assembly
  2. 遍历 TypeDef 表找到枚举定义
  3. 查找隐藏字段 value__
  4. 确定底层类型长度(1、2、4 或 8 字节)

对于嵌套枚举或类型转发(Type Forwarder),解析逻辑可能涉及多个 DLL 和复杂的递归查找。即便加了缓存,仍然远比读取原始字节复杂得多。

四、类型参数(Type)解析的困境

自定义属性还可以使用 System.Type 作为参数:

[Foo(typeof(int))]

.NET 编译器并不使用 metadata token,而是存储完全限定名(FQN)字符串

"System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

问题在于:

  1. 空间效率低:单个整数变成 89 字符,泛型类型甚至可能超过 300 字符
  2. 解析复杂:需要拆解 FQN,解析类型名、程序集、版本、文化信息、PublicKeyToken
  3. 不一致:没有提供程序集信息时,解析行为依赖运行时猜测,导致某些类型无法解析

相比于元数据表索引机制,这种做法既慢又占用空间。

五、为什么会有这种设计?

我的猜测是历史原因和向 Java 借鉴的习惯:

  • Attribute 在 .NET 中可跨 Assembly 使用,早期设计需要灵活序列化类型信息
  • 字符串方式容易序列化和反序列化,也方便版本兼容
  • 枚举和 Type 参数在大多数场景下是小众需求,兼顾通用性而牺牲了性能

无论如何,这为 PE 文件解析器 增加了大量复杂逻辑,也导致 AsmResolver 维护中出现了许多 bug。

六、总结

自定义属性是 .NET 元编程的核心工具,它极大丰富了语言和框架能力,但底层存储设计非常复杂:

  • 枚举参数必须解析底层类型
  • Type 参数使用 FQN,空间浪费且解析复杂
  • 多层类型转发和 Assembly Resolution 增加了逻辑复杂度

作为开发者,我们可以:

  • 理解它的底层原理,有助于调试、反射和工具开发
  • 使用时关注性能和序列化成本
  • 对 PE/IL 工具作者来说,这是“噩梦级”的挑战

归根结底,.NET 自定义属性是一个典型的高层设计优雅、底层实现复杂的案例。

即便如此,它依然是现代 .NET 框架不可或缺的基石。本文由云栈社区技术编辑发布。

引用链接

[1] https://blog.washi.dev/posts/custom-attributes-and-why-they-suck/




上一篇:archify:Claude开源技能,自然语言生成可交互架构/时序/数据流图
下一篇:DeepSeek创始人梁文锋分享:开源、克制与AGI,为何不走寻常路?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-24 09:38 , Processed in 0.672826 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表