LLVM Bitcode格式介绍(一)
Bitcode是LLVM IR的二进制形式。关于LLVM的整体架构网上已经有很多很好的文章进行介绍,这里不再废话。本文将通过实例分析的方式介绍LLVM bitcode整体格式,后续文章会进一步介绍其他细节。
"Hello, World!"例子
由于是一篇入门文章,所以没有比"Hello, World!"更合适的例子了。下面是大家所熟悉的C语言版"Hello, World!"程序,保存在hw.c文件中:
#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}用最新版clang(11.0.0)编译上面的hw.c文件,加上-emit-llvm和-o选项,就可以得到bitcode文件:
$ clang -emit-llvm -c hw.c -o hw.bc
$ file hw.bc
hw.bc: LLVM bitcode, wrapper x86_64用xxd命令查看刚刚生成的hw.bc文件,输出看起来是下面这样:
$ xxd -u -g 1 hw.bc
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
00000040: 02 00 00 00 16 00 00 00 07 81 23 91 41 C8 04 49 ..........#.A..I
00000050: 06 10 32 39 92 01 84 0C 25 05 08 19 1E 04 8B 62 ..29....%......b
... 省略180行输出 ...LLVM提供了一个llvm-bcanalyzer工具,可以分析bitcode文件,并以类似XML的格式打印出主要内容,用法如下所示(为了节约篇幅,省略了大部分输出内容):
$ llvm-bcanalyzer -dump hw.bc
<BITCODE_WRAPPER_HEADER Magic=0x0b17c0de Version=0x00000000 Offset=0x00000014 Size=0x00000b88 CPUType=0x01000007/>
<IDENTIFICATION_BLOCK_ID NumWords=5 BlockCodeSize=5> ... </IDENTIFICATION_BLOCK_ID>
<MODULE_BLOCK NumWords=661 BlockCodeSize=3> ... </MODULE_BLOCK>
<SYMTAB_BLOCK NumWords=43 BlockCodeSize=3> ... </SYMTAB_BLOCK>
<STRTAB_BLOCK NumWords=20 BlockCodeSize=3> ... </STRTAB_BLOCK>接下来我们就一点一点的分析bitcode格式,请读者跟上文章的节奏,千万不要走神呀 :)
Wrapper
从前面file命令的输出也可以看到,hw.bc文件实际上是bitcode包装格式,其内容主要分为两部分:header和body。文件开头的20个字节是header,真正的bitcode数据则是body。Header的内容是5个32比特整数,依次表示魔数、版本号、bitcode偏移量(单位是字节)、bitcode字节数、CPU类型(下面的定义来自LLVM文档,圆扣号里是比特数,下同):
[Magic(32), Version(32), Offset(32), Size(32), CPUType(32)]
注意整个bitcode和包装格式都是采用小端字节序,每32个比特为一组,称为一个word。观察xxd输出可知,wrapper的魔数是0x0B17C0DE(看起来就好像是BITCODE),版本号是0,bitcode偏移量是0x14(20),字节数是0x0B88(2952),CPU类型是0x01000007:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
... ^^^^^^^^^^^以上信息和前面llvm-bcanalyzer输出的第一行是一致的。由于wrapper的header里记录了bitcode数据的offset和size,所以理论上来讲,wrapper里还可以包含其他信息。不过就hw.bc而言,header之后紧接着就是bitcode数据了。
Bitstream
我们都知道,Java的字节码(Bytecode)本质上就是一个字节数组,或者字节流。类似的,Bitcode本质上就是一个比特序列,或者叫做比特流。从字面上看就知道,bitcode信息密度要比bytecode大一些,也就是说更紧凑一些。例如说要表示布尔值,在字节码里就需要一个字节,有7个比特是浪费的;但是在bitcode里,只要一个比特即可。读完本文后,读者将能深入的体会到这一点。
Bitcode也有自己的魔数,然后就是真正的数据了。魔数长1个word(4字节),定义如下:
[‘B’(8) ‘C’(8), 0x0(4), 0xC(4), 0xE(4), 0xD(4)]
可以看到,这里又玩了一次文字游戏(字母B和C的ASCII码,以及C0DE)。继续观察xxd输出便可以看到bitcode的魔数:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
... ^^^^^^^^^^^Primitives
在基于字节流的二进制格式里(后文简称字节流格式),最小单位就是字节了,连续N个(N固定)字节可以表示更大的整数(或浮点数等)类型。这些数据类型通常叫做二进制格式的基本类型,这些基本类型是构成其他更复杂类型的基石。例如Java类文件格式就定义了u1、u2和u4(1、2和4字节无符号整数)等基本类型。
定长整数虽然解析起来很方便也很快,但是空间利用率比较低。二进制格式往往要记录很多小的整数(例如各种列表的长度等),如果用定长整数存储这些小整数就会非常浪费空间。为了提高空间利用率,一些字节流格式引入了变长整数类型。例如WebAssembly模块二进制格式就充分使用了LEB128整数编码格式,Lua 5.4也引入了类似的变长整数。关于LEB128的更多信息可以看这篇文章,此处就不再展开介绍了。
和字节流类似,LLVM比特流也定义了几种基础数据类型,包括定长整数、变长整数、6比特字符等。当然了,这里的变长或者定长,都是指比特数,而非字节数。定长整数比较好理解,固定占用N个比特而已,我们用fixedN来表示N比特定长整数类型。6比特字符实际就是fixed6类型,只是其实际含义是可打印字符,所以我们称之为char6类型。char6类型最多能表示64个字符,和ASCII码有一个简单的对应关系:
'a' .. 'z' --- 0 .. 25
'A' .. 'Z' --- 26 .. 51
'0' .. '9' --- 52 .. 61
'.' --- 62
'_' --- 63变长整数又叫做“Variable Bit-Rate”整数,简称VBR。如果大家已经理解LEB128整数编码的话,就很容易理解这种类型。LEB128是为字节流设计的,每个字节用低7位来表示有效数据,高1位(Most Significant Bit,简称MSB)是标志位,用来表示下一个字节是否属于当前编码的整数。VBR将这种编码方式进一步放宽,可以用任意数量的比特来表示有效数据。换言之,vbrN类型使用低N-1比特来表示有效数据,高1比特来做标志位。例如,vbr4用低3比特来表示有效数据,高1比特来做标志位。所以,可以认为LEB128是VBR编码的一种特殊形式,也就是vbr8。
关于fixedN和vbrN整数的文字介绍就到此为止,接下来我们通过实例分析来进一步理解这两种整数编码格式。我们接着上一小节,来看看比特流中的下一个word:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
... ^^^^^^^^^^^由于是小端字节序,所以我们可以把这个word写成0x00001435,然后再把它转换成二进制:
0000_0000_0000_0000_0001_0100_0011_0101我们假设比特流中的第一个整数是fixed2类型,那么该整数会用掉比特流的2个比特。这两个比特可以用二进制表示为0b01,也就是十进制的1:
0000_0000_0000_0000_0001_0100_0011_0101
^^再假设比特流中的下一个整数是vbr8类型。我们还不知道该整数总共占用多少比特,只能先读8个比特看看。读完8个比特后,可以知道MSB是0,因此不需要再往后读了。这个vbr8用二进制表示是0b00001101,也就是十进制的13(MSB的下面标了星号):
0000_0000_0000_0000_0001_0100_0011_0101
*^^^^^^^^^Abbreviation ID
为了理解Abbreviation ID,我们先来看看XML文件是如何被解析的。首先,任何数据都可以认为是比特序列,但是通常我们还是会以字节为单位思考问题,因此一个XML文件就是一个字节流。为了简化问题,我们假设XML文件只包含ASCII字符,因此一个XML文件也是一个ASCII字符流。了解编译原理的读者肯定知道,XML解析器也并不是直接处理一个个的ASCII字符,而是先把字符流分解为token流,然后再处理。XML文档是自描述的,XML解析器首先会遇到类似<Foo>这样的起始标签,然后解析标签属性和子标签结构,最后期待一个结束标签</Foo>,这样递归下去就可以解析完整个XML文档,最终构造出一个树状结构。
我们对比XML来看看bitcode格式(注意只是粗略的类比,目的是对bitcode格式有个大致了解)。XML是文本格式,会被解析器分解为token序列;bitcode则是比特流,会被解析器分解为基本类型(fixedN或者vbrN等)序列。XML解析器首先要找到开始标签,然后解析内容,最后找到结束标签。Bitcode则是要找到开始abbreviation ID,然后解析内容,最后找到结束abbreviation ID。Abbreviation ID是fixed类型整数,默认的长度是2比特,每个block(后面会介绍)都会重新定义该长度。Bitcode格式内置了4种abbreviation ID(0 ~ 3),更大的abbreviation ID由block自己定义。下面先给出内置abbreviation ID的定义,具体含义后面会解释:
- 0 - END_BLOCK — This abbrev ID marks the end of the current block.
- 1 - ENTER_SUBBLOCK — This abbrev ID marks the beginning of a new block.
- 2 - DEFINE_ABBREV — This defines a new abbreviation.
- 3 - UNABBREV_RECORD — This ID specifies the definition of an unabbreviated record.
Block
如果遇到的abbreviation ID是1(ENTER_SUBBLOCK),说明正在解析block。Block的格式为:
[ENTER_SUBBLOCK, blockid(vbr8), newabbrevlen(vbr4), <align32bits>, blocklen_32]
看到了把,block首先会给出自己的ID(类型是vbr8),然后会定义新的abbreviation ID的长度(比特数,类型是vbr4),然后会对齐word,然后会给出block内容的长度(word数,类型是fixed32)。Block内部可以有子block,这样就形成了类似XML那样的嵌套结构。如果解析器对于某个block不感兴趣(比如不认识block ID),那么就可以通过blocklen跳过这个block。
我们继续来分析hw.bc,下一个word是0x00001435。实际上之前给出的两个假设就是根据block的定义做出的,所以开始两个比特是0x01,刚是ENTER_SUBBLOCK。然后是一个vbr8,表示block ID,是十进制13(IDENTIFICATION_BLOCK_ID,本文不展开介绍)。然后是一个vbr4,二进制是0b0101,十进制是5,表示在这个块的内部,abbreviation ID的长度是5比特:
0000 0000 0000 0000 0001 0100 0011 0101
*^^^^然后要word对齐,所以跳过这个word里剩下的18个0。下一个word是0x00000005,正好是一个32比特整数,表示block的内容长5个word(20字节):
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
... ^^^^^^^^^^^这20个字节如下所示:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
^^^^^^^^^^^
...Record
除了前面介绍的block结构,bitcode还可以包含record结构。Record分为2种,第一种是Unabbreviated Record,这种record格式比较简单,可以直接解析,格式由UNABBREV_RECORD定义(稍后会有实例分析):
[UNABBREV_RECORD, code(vbr6), numops(vbr6), op0(vbr6), op1(vbr6), …]
第二种是Abbreviated Record,这种record类似于C语言里的结构体,需要预先给出定义,然后才可以解析。具体定义由DEFINE_ABBREV给出,格式是下面这样(稍后会有实例分析):
[DEFINE_ABBREV, numabbrevops(vbr5), abbrevop0, abbrevop1, …]
Record Def
先来看一下DEFINE_ABBREV。我们继续分析hw.bc文件,回到第一个block内容的第一个word,也就是0x24300C62:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
^^^^^^^^^^^
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
...现在已经在第一个block的内部了,该block定义的abbreviation ID的长度是5比特,所以我们先读取前5个比特。可以看到,这5个比特是二进制0b00010,十进制2,也就是DEFINE_ABBREV:
0010_0100_0011_0000_0000_1100_0110_0010
^^^^^^根据DEFINE_ABBREV的格式定义,我们知道接下来应该是一个vbr5,给出正在定义的record有几个operands(简称ops)。我们先读取5个比特,二进制是0b00011。由于MSB是0,所以读取完毕,得到了十进制3。由此可知,正在定义的record有3个ops:
0010_0100_0011_0000_0000_1100_0110_0010
*^^^^^接下来就是挨个解析operands了,对于每一个operand,共有3种可能的情况(本文就不展开解释了):
- Literal operands —
[1(1), litvalue(vbr8)]— Literal operands specify that the value in the result is always a single specific value. This specific value is emitted as a vbr8 after the bit indicating that it is a literal operand. - Encoding info without data —
[0(1), encoding(3)]— Operand encodings that do not have extra data are just emitted as their code. - Encoding info with data —
[0(1), encoding(3), value(vbr5)]— Operand encodings that do have extra data are emitted as their code, followed by the extra data.
我们先来分析op#0。无论哪种情况,先要读取一个比特:
0010_0100_0011_0000_0000_1100_0110_0010
^这个比特是1,所以是个literal operand。这种operand相当于给record定义了一个静态字段,字段的值在定义中通过一个vbr8给出。我们读取这个vbr8,二进制是0b00000001,也就是十进制1:
0010_0100_0011_0000_0000_1100_0110_0010
*^^^^^^^^^再来分析op#1,还是先读取一个比特:
0010_0100_0011_0000_0000_1100_0110_0010
^这次读取到的是0,所以只能是后两种情况,需要再读取一个fixed3才能进一步处理。下一个fixed3是二进制0b011,十进制3:
0010_0100_0011_0000_0000_1100_0110_0010
^^^也就是说字段的encoding是3,是一个数组。数组的元素类型由下一个operand给出。下面是encoding的定义:
- Fixed (code 1): The field should be emitted as a fixed-width value, whose width is specified by the operand’s extra data.
- VBR (code 2): The field should be emitted as a variable-width value, whose width is specified by the operand’s extra data.
- Array (code 3): This field is an array of values. The array operand has no extra data, but expects another operand to follow it, indicating the element type of the array. When reading an array in an abbreviated record, the first integer is a vbr6 that indicates the array length, followed by the encoded elements of the array. An array may only occur as the last operand of an abbreviation (except for the one final operand that gives the array’s type).
- Char6 (code 4): This field should be emitted as a char6-encoded value. This operand type takes no extra data. Char6 encoding is normally used as an array element type.
- Blob (code 5): This field is emitted as a vbr6, followed by padding to a 32-bit boundary (for alignment) and an array of 8-bit objects. The array of bytes is further followed by tail padding to ensure that its total length is a multiple of 4 bytes. This makes it very efficient for the reader to decode the data without having to make a copy of it: it can use a pointer to the data in the mapped in file and poke directly at it. A blob may only occur as the last operand of an abbreviation.
接下来该分析op#2了,还是先读取一个fixed1,结果是0:
0010_0100_0011_0000_0000_1100_0110_0010
^再读一个fixed3,结果是0b100,也就是十进制4:
0010_0100_0011_0000_0000_1100_0110_0010
^^^根据上面的定义可知,4代表char6类型。换句话说,op#1所定义的field的数组元素类型是char6。到这里,我们就把这个record定义分析完了。Bitcode格式规定,用户定义的abbreviation ID从4开始。这是当前block里的第一个record定义,所以它的abbreviation ID是4。该record有2个字段,第一个字段的值已经在定义中给出,第二个字段是个char6数组。我们可以用伪代码描述一下这个record:
AbbreviatedRecord {
abbreviationID: 4
field0 : 1
field1 : char6[]
}Abbreviated Record
我们继续分析hw.bc,读取下一个abbreviation ID(fixed5),二进制是0b00100,十进制是4,刚好是我们前面才分析的这个record定义:
0010_0100_0011_0000_0000_1100_0110_0010
^^^^^^我们知道这个record只有一个字段需要读取,类型是char6数组,长度由vbr6给出。于是我们再拿出一个word,用十六进制表示是0x66BE594A:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
^^^^^^^^^^^
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
...还是把这个word转换成二进制,然后拿出一个vbr6,二进制是0b001010,十进制是10:
0110_0110_1011_1110_0101_1001_0100_1010
*^^^^^^于是我们知道这个数组包含10个char6,其中第一个char6的二进制是0b100101,十进制是37,转换成ASCII码是字符'L'(可以参考前面给出的char6到ASCII字符的映射表):
0110_0110_1011_1110_0101_1001_0100_1010
^^^^^^^读者可以自行分析剩下的9个char6。把这10个char6都转换成ASCII码之后是字符串LLVM11.0.0,也就是编译这个例子所使用的LLVM/Clang的版本号。
Unabbreviated Record
最后我们来分析一个Unabbreviated Record例子。为了便于参考,这里再次给出这种record的格式定义:
[UNABBREV_RECORD, code(vbr6), numops(vbr6), op0(vbr6), op1(vbr6), …]
继续观察hw.bc文件,直接跳过第一个block,接下来的两个word是0x00000C21和0x00000295:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
^^^^^^^^^^^ ^^^^^^^^^^^
...先看第一个word。根据之前的介绍可知,Abbreviation ID(fixed2)是1,因此是一个block;block的ID(vbr8)是8,根据Bitcode文档可知,这是一个module block;新的abbreviation ID长度(vbr4)是3比特;然后是32比特对齐:
0000_0000_0000_0000_0000_1100_0010_0001 <- 0x00000C21
|| |||| |||| ||^^ AbbreviationID = 1
|| ||*^^^^^^^^^ BlockID = 8
*^^^^ NewAbbrevLen = 3下一个word记录了block的大小,共0x295(661)个word。接下来的word是0x0021020B:
00000000: DE C0 17 0B 00 00 00 00 14 00 00 00 88 0B 00 00 ................
00000010: 07 00 00 01 42 43 C0 DE 35 14 00 00 05 00 00 00 ....BC..5.......
00000020: 62 0C 30 24 4A 59 BE 66 5D FB B4 4F 0B 51 80 4C b.0$JY.f]..O.Q.L
00000030: 01 00 00 00 21 0C 00 00 95 02 00 00 0B 02 21 00 ....!.........!.
^^^^^^^^^^^
...我们已经知道新的abbreviation ID是3比特,于是读出一个fixed3,二进制是0b011,十进制是3,可知这是一个 UNABBREV_RECORD:
0010_0001_0000_0010_0000_1011
^^^接下来是一个vbr6,记录了该unabbreviated record的code,二进制是0b000001:
0010_0001_0000_0010_0000_1011
^^^^^^^^接下来还是一个vbr6,记录了该unabbreviated record的ops个数,二进制也是0b000001,只有一个operand:
0010_0001_0000_0010_0000_1011
^^^^^^^唯一的operand也是一个vbr6,二进制是0b000010,十进制是2:
0010_0001_0000_0010_0000_1011
^^^^^^^^根据bitcode文档可知,这是一个VERSION record。在llvm-bcanalyzer中也可以找到这个record:
<MODULE_BLOCK NumWords=661 BlockCodeSize=3>
<VERSION op0=2/>
... 其他信息省略
</MODULE_BLOCK>到此,bitcode整体格式就分析完毕了。如果你坚持读到了这里,那么我必须要为你点赞 。
总结
本文通过实例分析的方式,介绍了LLVM bitcode整体编码格式。读完本文,读者应该已经了解了bitcode文件的大致结构,包括wrapper、VBR编码、abbreviation ID、block、record等概念。在后续文章中,作者会进一步分析MODULE_BLOCK等重要的block。
也许bitcode的比特流格式在比特利用率上做到了极致,但是相比Java类文件和WebAssembly二进制模块等基于字节流的格式,分析起来(包括阅读解析器等相关源代码)是真的痛苦。如果大家对其他二进制格式感兴趣,并且也喜欢这种讲解风格,请关注我写的三本书。其中《自己动手写Java虚拟机》对Java类文件格式和Java字节码进行了详细介绍,《自己动手实现Lua》对Lua二进制块和指令编码格式进行了详细介绍,最新出版的《WebAssembly原理与核心技术》对Wasm二进制模块格式和字节码等进行了详细介绍。

&spm=1001.2101.3001.5002&articleId=112190318&d=1&t=3&u=602ca727118041dea92a337a622c50c0)
664

被折叠的 条评论
为什么被折叠?



