.NET/CLI元数据中使用的压缩整数

本文地址:http://www.cnblogs.com/AndersLiu/archive/2010/02/09/compressed-integer-in-metadata.html

作者:Anders Liu

摘要:.NET/CLI的PE文件中广泛采用了一种整数压缩算法,这种算法可以将一个32位整数根据其大小的不同放置在1、2或4个字节中。当整数的值比较小时,这种算法能够有效地减少PE文件的大小。本文介绍了这种压缩算法,并给出了压缩/解压缩的参考实现。

参考文献

  • 《ECMA-335——Common Language Infrastructure (CLI) 4th Edition》,June 2006
  • 《Expert .NET 2.0 IL Assembler》,Serge Lidin,Apress,2006
  • 《.NET探秘:MSIL权威指南》(《Expert .NET 2.0 IL Assembler》中文版),Serge Lidin著,包建强 译,人民邮电出版社,2009

简介

简单来说,整数压缩算法就是将一个32位整数(通常占用4个字节)放置到尽可能少的存储空间中(1、2或4个字节)的方法。

整数压缩算法广泛地应用在.NET/CLI PE文件中,如各种元数据签名、#Blob和#US流等。在这些地方,需要使用整数值来记录条目的数量或是数据块的大小等。如果单纯地采用32位整数,由于绝大多数数量值或大小值都不大,会造成大量字节都被置为无意义的0值。在这些场景中使用压缩算法,可以有效地节省PE文件占用的磁盘空间或网络带宽。

以下是PE文件中一些使用到压缩整数的场景:

  • Blob堆(#Blob流和#US流所采用的存储格式)中的每个条目开始处,使用压缩的无符号整数表示条目的大小;
  • 方法的元数据签名中,使用压缩的无符号整数存储参数的数量;
  • 元数据签名中的数组下标,采用压缩的带符号整数进行存储。

注意,本文所介绍的压缩与解压算法,都是针对32位整数的。此外,在本文的介绍中,如果没有特殊提及,则所出现的整数都按照大尾数法表示(最高权重字节放在左侧或上方)。

无符号整数的压缩与解压

无符号整数的压缩算法

无符号整数的压缩是比较简单的,即将无符号整数的整个取值范围划分为几个区段,而整数值根据其所在的区段不同,放置在1、2或4个字节中。表1列出了无符号整数的区段划分和压缩方式。

表1 - 无符号整数的区段划分
区段 字节数 掩码 二进制形式
[00000000h, 0000007Fh] 1 80h 0BBBBBBBB
[00000080h, 00003FFFh] 2 C0h 10BBBBBB BBBBBBBB
[00004000h, 1FFFFFFFh] 4 E0h 110BBBBB BBBBBBBB BBBBBBBB BBBBBBBB

在表1中:

  • “区段”列出了每个区段的最小值(含)和最大值(含)。
  • “字节数”列出了压缩后的值占用的字节数。
  • “掩码”列出了在压缩后的值上施加的掩码,
    • 如果压缩后的整数值占用1字节,则与掩码80h进行&(按位与)操作后的结果为0h,
    • 如果压缩后的整数值占用2字节,则其首字节与掩码C0h进行&操作后的结果是80h,
    • 如果压缩后的整数值占用4字节,则其首字节与掩码E0h进行&操作后的结果是C0h。
  • “二进制形式”列出了压缩结果的二进制形式,其中的“1”和“0”都是固定值,而“B”则表示实际整数值的有效位。

从表1可以清晰地看出,无符号整数压缩算法的适用范围是[0h, 1FFFFFFFh]([0, 536870911])之内的无符号整数,大于1FFFFFFFh的无符号整数不能用这种方式进行压缩。

代码1给出了无符号整数压缩算法的参考实现。

代码1 - 无符号整数压缩算法的参考实现

public static byte[] CompressUInt(uint data)
{if (data <= 0x7F){var bytes = new byte[1];bytes[0] = (byte)data;return bytes;}else if (data <= 0x3FFF){var bytes = new byte[2];bytes[0] = (byte)(((data & 0xFF00) >> 8) | 0x80);bytes[1] = (byte)(data & 0x00FF);return bytes;}else if (data <= 0x1FFFFFFF){var bytes = new byte[4];bytes[0] = (byte)(((data & 0xFF000000) >> 24) | 0xC0);bytes[1] = (byte)((data & 0x00FF0000) >> 16);bytes[2] = (byte)((data & 0x0000FF00) >> 8);bytes[3] = (byte)(data & 0x000000FF);return bytes;}elsethrow new NotSupportedException();
}

无符号整数的解压缩算法

无符号整数的解压缩算法也非常简单,如下所示:

  • 如果首字节的二进制形式型如0bbbbbbb(与80h进行按位与运算,结果为0h),则采用1个字节存放整数值(字节值为b0),原整数值=b0。
  • 如果首字节的二进制形式型如10bbbbbb(与C0h进行按位与运算,结果为80h),则采用2个字节存放整数值(字节值依次为b0,b1),原整数值=(b0 & 0x3F) << 8 | b1。
  •  如果首字节的二进制形式型如110bbbbb(与E0h进行按位与运算,结果为C0h),则采用4个字节存放整数值(字节值依次为b0,b1,b2,b3),原整数值=(b0 & 0x1F) << 24 | b1 << 16 | b2 << 8 | b3。.

代码2给出了无符号整数解压缩算法的参考实现。

代码2 – 无符号整数解压缩算法的参考实现

public static uint DecompressUInt(byte[] data)
{if (data == null)throw new ArgumentNullException("data");if ((data[0] & 0x80) == 0&& data.Length == 1){return (uint)data[0];}else if ((data[0] & 0xC0) == 0x80&& data.Length == 2){return (uint)((data[0] & 0x3F) << 8 | data[1]);}else if ((data[0] & 0xE0) == 0xC0&& data.Length == 4){return (uint)((data[0] & 0x1F) << 24| data[1] << 16 | data[2] << 8 | data[3]);}elsethrow new NotSupportedException();
}

带符号整数的压缩与解压

带符号整数的压缩算法

带符号整数的压缩与解压略微复杂一些,因为需要处理符号位。简单来说,需要在确定好所需的存储字节数之后,将原整数整体向左移1位,然后将符号位放置在最低位上(0表示正数,1表示负数),最后按照同无符号整数一样的方式为首字节设置掩码。

在为带符号整数确定需要用多少个字节来存放压缩值时,需要首先取得原整数的“准绝对值”,即对负数进行按位取反(而不是数学求负),然后将这个“准绝对值”左移1位(为符号位空出最低位),再按照表1列出的区段取得最终占用的字节数。

或者,可以省略左移1位的操作,而是按照表2中列出的区段进行查找。

表2 - 带符号整数“准绝对值”的区段划分
区段 字节数 有效位掩码
[00000000h, 0000003Fh] 1 0000003Fh
[00000040h, 00001FFFh] 2 00001FFFh
[00002000h, 0FFFFFFFh] 4 0FFFFFFFh

在表2中:

  • “区段”列出的是根据原整数“准绝对值”划分出的每个区段的最小值(含)和最大值(含)。
  • “字节数”列出了压缩后的值占用的字节数。
  • “有效位掩码”列出的掩码在与原整数进行&操作之后,可以取得原整数中真正有意义的位数。这建立在这样一个事实上——对于正整数来说,其最左侧的一些位都是0,是没有意义的,可以省略;而对于负整数来说,其最左侧的一些位都是1,也是没有意义的,可以省略。

在与有效位掩码进行&操作取得有效位之后,需要将这些有效位整体左移1位。接下来,如果原整数是负数,则需要将最低位(符号位)置1。

最后,为压缩值的首字节设置掩码,规则与无符号整数一样。

带符号整数压缩算法的适用范围为——对于正数为[0h, 0FFFFFFFh]([0, 268435455]),对于负数为[F0000000h, FFFFFFFFh]([-268435456, -1]),在此范围之外的整数不能用这种方式进行压缩。

代码3给出了带符号整数压缩算法的参考实现。

代码3 -带符号整数压缩算法的参考实现

public static byte[] CompressInt(int data)
{var u = data >= 0 ? (uint)data : ~(uint)data;if (u <= 0x3F){var uv = ((uint)data & 0x0000003F) << 1;if (data < 0)uv |= 0x01;var bytes = new byte[1];bytes[0] = (byte)uv;return bytes;}else if (u <= 0x1FFF){var uv = ((uint)data & 0x00001FFF) << 1;if (data < 0)uv |= 0x01;var bytes = new byte[2];bytes[0] = (byte)(((uv & 0xFF00) >> 8) | 0x80);bytes[1] = (byte)(uv & 0x00FF);return bytes;}else if (u <= 0x0FFFFFFF){var uv = ((uint)data & 0x0FFFFFFF) << 1;if (data < 0)uv |= 0x01;var bytes = new byte[4];bytes[0] = (byte)(((uv & 0xFF000000) >> 24) | 0xC0);bytes[1] = (byte)((uv & 0x00FF0000) >> 16);bytes[2] = (byte)((uv & 0x0000FF00) >> 8);bytes[3] = (byte)(uv & 0x000000FF);return bytes;}elsethrow new NotSupportedException();
}

注意,只有在确定压缩值占用的字节数时用到了原整数的“准绝对值”,一旦字节数确定之后,实际进行压缩时,使用的还是原整数,只不过将其当做无符号整数对待。

带符号整数的解压缩算法

由于带符号整数的压缩值与无符号整数的压缩值具有相同的结构,所以带符号整数的解压缩算法可以建立在无符号整数的解压缩算法基础之上。

首先,按照无符号整数的解压缩算法对压缩值进行解压缩,得到一个32位无符号整数,根据最低位(符号位)确定原整数的符号。

如果原整数为正数(最低位,即符号位为0),则将解压得到的无符号整数右移1位,再强制转换为带符号整数,即可得到原整数值。

如果原整数为负数(最低位,即符号位为1),则需要将解压得到的无符号整数右移1位,再将负数最左侧那些没有意义的“1”位恢复回来:

  • 如果压缩值占用了1字节,则与FFFFFFC0h进行|(按位或)操作;
  • 如果压缩值占用了2字节,则与FFFFE000h进行|操作;
  • 如果压缩值占用了4字节,则与F0000000h进行|操作。

最后,将这个无符号整数强制转换为带符号整数,即可得到原整数值。

代码4给出了带符号整数解压缩算法的参考实现。

代码4 - 带符号整数解压缩算法的参考实现

public static int DecompressInt(byte[] data)
{var u = DecompressUInt(data);if ((u & 0x00000001) == 0)return (int)(u >> 1);var nb = GetCompressedIntSize(data[0]);uint sm;switch (nb){case 1: sm = 0xFFFFFFC0; break;case 2: sm = 0xFFFFE000; break;case 4: sm = 0xF0000000; break;default: throw new NotSupportedException();}return (int)((u >> 1) | sm);
}

这里调用了一个工具方法GetCompressedIntSize,用于根据压缩值的第一个字节判断采用几个字节存放该压缩值。该方法非常简单,如代码5所示。

代码5 – 根据压缩值的第一个字节判断所需字节数

public static uint GetCompressedIntSize(byte firstByte)
{if ((firstByte & 0x80) == 0)return 1;else if ((firstByte & 0xC0) == 0x80)return 2;else if ((firstByte & 0xE0) == 0xC0)return 4;elsethrow new NotSupportedException();
}

各种实现中的问题

压缩的带符号整数在.NET/CLI元数据中的使用场景非常少——据我所知,只有元数据签名中的数组下标值使用了压缩的带符号整数(这意味着原理上.NET/CLI的底层是支持下标为负数的数组的)。而在这方面,几乎所有现有的CLI实现都或多或少的出现了一些问题,同时,我所参考的文献中,关于带符号整数压缩算法的描述也都是含糊不清的。幸运的是,几乎所有高级语言都不允许开发者声明下标为负数的数组,CLS规范也要求数组的下标必须从0开始,所以这些问题并不会对实际项目造成重大影响。

下面列举几个我所研究过的实现中的问题,下一节将列出参考文献中的问题。

ILASM/ILDASM

很显然,微软自己对带符号整数的压缩算法也不是很清晰。ILASM是我所接触过的编译器中唯一能接受负数下标数组的,也是我在研究这个课题时使用最多的编译器。对于正数数组下标,ILASM完全没有问题;但对于负数下标,当下标值在-8192(含)到-8129(含)之间时,得到的压缩值是错误的。

另外,ILASM使用的带符号整数压缩算法实现,很明显与本文介绍的不同,因此并不能涵盖所有理论上支持的整数([-268435456, 268435455]),当下标值小于或等于-268427265时,得到的压缩值也是错误的。

由于ILASM存在错误,所以对ILDASM无法进行完全准确的测验。不过,即便是对ILASM产生的错误值进行解压缩,ILDASM得到的结果和本文中介绍的带符号整数解压缩算法得到的结果都是一致的,所有有理由相信ILDASM在解压缩算法上应该是正确的。但是,错误的压缩值会随机造成ILDASM的崩溃。

以上问题存在于ILASM的2.0、3.0和3.5版本中,但在4.0 Beta版中已经得到改正,.NET Framework SDK 4.0 Beta携带的ILASM能够对所有理论上可接受的负数数组下标进行正确的压缩,而ILDASM也能对其进行正确的解压缩。

Mono Cecil

通过对Mono Cecil源代码的研究发现,Mono Cecil的实现非常忠诚于ECMA-335标准,而ECMA-335对数组下标的描述恰恰是错误的(参见后面“参考文献之修正”一节)——称数组下标值是压缩的无符号整数(而不是带符号整数)。

因此,Mono Cecil只提供了针对无符号整数的压缩和解压缩实现(参见Mono.Cecil.dll中的Mono.Cecil.Metadata.Utilities.WriteCompressedInteger(BinaryWriter, Int32) : Int32方法和Mono.Cecil.Metadata.Utilities.ReadCompressedInteger(Byte[], Int32, Int32&) : Int32方法)。而在写入和读取元数据签名时,也是将数组下标作为无符号整数处理的(参见Mono.Cecil.Signatures.SignatureWriter.Write(SigType) : Void方法和Mono.Cecil.Signatures.SignatureReader.ReadType(Byte[], Int32, Int32&) : SigType方法)。

在使用Mono Cecil库进行反射时,如果数组的下标为正数,则得到的结果是实际下标的2倍(因为缺少了解压缩带符号整数时的右移操作);而如果数组的下表是负数,则得到的结果就是完全错误的了。

我只对Mono Cecil 0.6版本的源代码做了调查,其他版本不详,读者可自行检查、分析。

CCI Metadata

CCI Metadata则确实将数组下标当作带符号整数对待了,但是它使用的压缩算法非常简单——将原整数的绝对值左移1位,再将符号位放置在最低位(参见Microsoft.Cci.PeWriter.dll中的Microsoft.Cci.BinaryWriter.WriteCompressedInt(Int32) : Void方法),然后按照无符号整数进行压缩;而解压缩算法是对应的——先按照无符号整数的解压算法得到一个无符号整数,然后根据最低位确定结果的符号,最后将整个无符号数右移1位,再根据符号位设置正负号(参见Microsoft.Cci.PeReader.dll中的Microsoft.Cci.UtilityDataStructures.MemoryReader.ReadCompressedInt32() : Int32方法)。

CCI Metadata所采用的算法与《Expert .NET 2.0 IL Assembler》一书中提到的算法描述相符,但该书中的描述也是有误的(参见后面“参考文献之修正”一节)。

我所调研的CCI Metadata版本是2.0.49.23471。

其他尚未研究的实现

还有一些.NET/CLI的实现尚未研究,例如:

  • System.Reflection/System.Reflection.Emit
  • Shared Source CLI (Rotor)

参考文献之修正

《Expert .NET 2.0 IL Assembler》

本书在第8章表8-4之后的一个自然段(P150第一段)描述了带符号整数的压缩算法,此处的描述有误,正确的描述请参见本文中“带符号整数的压缩算法”一节。

不幸的是,本书的中文版《.NET探秘:MSIL权威指南》并没有对这个问题进行修正(同样是第8章表8-4之后的一个自然段,P132)。当初包建强在翻译这本书的时候,我也向他提到过这里的问题,不过那时候我还没有完全准确地推断出正确的压缩算法,因此他只好直译。

《ECMA-335——Common Language Infrastructure (CLI) 4th Edition》

在ECMA-335标准中,完全没有区分“压缩的无符号整数”和“压缩的带符号整数”这两个术语,统称之为“compressed integer”。

ECMA-335 Partition II: Metadata Definition and Semantics中的23.2 Blobs and signatures一节中给出了“compressed integer”的压缩算法(P153),这实际上是无符号整数的压缩算法,该算法是正确的。

ECMA-335 Partition II: Metadata Definition and Semantics中的23.2.13 ArrayShape一节中给出了元数据签名中的数组表示方法(P161),其中称Size和LoBound都是“compressed integer”,这是不准确的。

修正方法是,引入术语“compressed unsigned integer”,用于描述其他地方的“compressed integer”;引入术语“compressed signed integer”,用于描述数组下标值(LoBound)。并按照本文“带符号整数的压缩算法”一节的描述,提供带符号整数的压缩算法。

(完)

转载于:https://www.cnblogs.com/AndersLiu/archive/2010/02/09/compressed-integer-in-metadata.html

.NET/CLI元数据中使用的压缩整数相关推荐

  1. js实现随机选取[10,100)中的10个整数,存入一个数组,并排序。 另考虑(10,100]和[10,100]两种情况。...

    1.js实现随机选取[10,100)中的10个整数,存入一个数组,并排序. 1 <!DOCTYPE html> 2 <html lang="en"> 3 & ...

  2. 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数

    给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数.            1.在文件中至少存在这样一个数?            2.如果有足够的内存,如何处理? ...

  3. python整数池_【Python】Python中神奇的小整数对象池和大整数对象池

    小整数对象池 整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间. Python 对小整数的定义是 [-5, 256] 这些整数对象是提前建立 ...

  4. 可以获取python整数类型帮助的是什么-下列选项中可以获取Python整数类型帮助的是...

    [单选题]关于 Python 语言的特点,以下选项中描述错误的是 [单选题]下面代码的输出结果是: s1 = "The python language is a scripting lang ...

  5. 可以获取python整数类型帮助的是什么-下列选项中可以获取Python整数类型帮助的是()。...

    [填空题]smooth finish [简答题]请完成考试系统<Word操作>中试卷8938,交卷成功时截全屏图(包括分数.姓名.右下角时间)并在本题答案区上传该 屏图 . [填空题]立柱 ...

  6. 关于bat中使用rar压缩命令

    数据库备份,导出的dmp 文件比较大,需要压缩,压缩后大小能变为原来十分之一左右吧. 写的是批处理的语句,每天调用,自动导出dmp 文件,压缩删除原文件. 首先写下路径 先将压缩软件的路径写入系统的环 ...

  7. 【Android 内存优化】Android 工程中使用 libjpeg-turbo 压缩图片 ( 初始化压缩对象 | 打开文件 | 设置压缩参数 | 写入压缩图像数据 | 完成压缩 | 释放资源 )

    文章目录 一.使用 libjpeg-turbo 压缩图片流程 二.初始化 JPEG 压缩对象 三.打开文件 四.设置压缩参数 五.开始压缩 六.循环写入压缩数据 七.完成图片压缩及收尾 八.libjp ...

  8. QEMU-KVM中的多线程压缩迁移技术

    导读 目前的迁移技术,都是通过向QEMUFILE中直接写入裸内存数据来达到传送虚拟机的目的端,这种情况下,发送的数据量大,从而会导致更高的迁移时间(total time)和黑宕时间(downtime) ...

  9. 四篇NeurIPS 2019论文,快手特效中的模型压缩了解一下

    在即将过去的 2019 年中,快手西雅图实验室在 ICLR.CVPR.AISTATS.ICML 和 NeurIPS 等顶会上发表了十多篇论文. 除了这些研究方面的成果,针对实际业务,西雅图实验室和快手 ...

  10. PHP网站开启gzip压缩,php中开启gzip压缩的2种方法代码

    Gzip网页压缩可以大幅度提升网站访问速度,对于网站在国外的站来说,这是必不可少的一步,提升网页打开速度非常明显,现在我们就系统的来认识一下这个Gzip的庐山真面目. 一.何为GZIP GZIP概念 ...

最新文章

  1. 云从科技IPO上市,AI四小龙同路不同归
  2. 数据持久化 技术比较
  3. UVA11549计算器谜题
  4. 已解决:PC插上串口工具后PC端口com那里有个黄色叹号,无法使用串口工具
  5. ssm 异常捕获 统一处理_统一异常处理介绍及实战
  6. 程序猿的双十一最佳攻略
  7. oracle system用户创建job 其他用户,oracle创建表空间、用户和表以及sys和system的区别...
  8. mysql innodb monitor_MySQL innodb_table_monitor 解析
  9. C++:编译原理实验之词法分析器
  10. IntelliJ IDEA使用技巧(三)——Debug 篇
  11. 新鲜出炉,Amazon SDE 面经(电面+Onsite)
  12. 树莓派+传感器+公网服务器 组件自己的物联网平台(四)制作一个智能鱼缸
  13. 交换机access接口
  14. 从屡遭拒稿到90后助理教授,罗格斯大学王灏:好奇心驱使我不断探索
  15. 1、RPC框架解析:开篇-什么是RPC?
  16. 360 || 2021校园招聘的一道笔试题思路分享
  17. SparkSQL内置函数
  18. 推荐一款国内首个开源线上全链路压测平台
  19. 【干货】直播聊天室详细分解,让你一眼学会快速搭建!
  20. CEO、COO、CFO、CTO、CIO、CBO、CDO……日常必知

热门文章

  1. TFS 2017 持续集成速记
  2. 趣达学院学习有奖活动!
  3. 资源过于硬核,8h删!这波福利....请笑纳~
  4. 施一公:论文和科技实力是两回事,大家千万要分开
  5. 【面经】来啦!百度凤巢算法面经
  6. 【面试经验】关于BERT,面试官们都怎么问
  7. numpy——flat与flatten
  8. TensorFlow函数使用总结
  9. 深度学习花书-2.9 伪逆矩阵
  10. Netty的并发编程实践1:正确使用锁