1.3 RPM格式剖析
在对RPM格式的软件包有了基本的认识之后,本节将以开发人员的身份,从协议的角度来剖析RPM的组成结构。
1.3.1 从协议说起
如果你做过TCP/IP应用相关程序开发或者熟悉wireshark(或者tcpdump)等工具,应该对网络报文文件比较熟悉。我们知道,网络报文文件都有固定的格式(称其为协议可能更通俗些),所有这些协议都是人为定下来的,每个协议的创造者通过RFC告知大家:某个文件应该是怎么样的,前面多少字节存储了什么内容,中间某个数据结构代表示什么,最后面那个字段又代表什么,等等。总之,通过这么一种通告或者约定的方式向众人表达这样一层意思:如果你要使用这种格式的数据(或者文件),就必须按照我们规定的格式(协议)去读写此类文件,只有这样才能够读取到正确的数据,或者生成一个正确格式的文件。
本节就从报文协议开始,逐步引入RPM协议。首先,用wireshark抓取一个数据包,存成cap文件,然后打开查看其结构,如图1-1所示。
图1-1 网络报文协议格式
通过图1-1能够清晰地看到按照报文协议解析后的数据包中的各个字段。在专业的TCP/IP教程中,协议栈中各个层的排列顺序跟表1-1中的顺序相反,底层是链路层,最上面是应用层,因为wirshark对协议层的显示是从上往下的。为了保持与图1-1中顺序一致,我们也将链路层放在列表的最顶部(在实际中,一定要明确,链路层处在协议栈的最底端)应用层在下面,如表1-1所示。
我们知道,wireshark之所以能够查看报文的每个组成元素,是因为它按照TCP/IP协议的格式对报文文件进行了解析。对于RPM格式文件的解析,rpm命令也是按照同样的原理进行的,一旦掌握了RPM文件的存储协议,就可以按照协议格式逐个解析文件中的数据,获取想要的信息。rpm命令的信息显示部分的大部分功能都是通过这样的方式来实现的。
表1-1 网络报文协议层列表
1.3.2 RPM格式总览
正如1.3.1节中提到过的,网络报文协议由MAC层、IP层、TCP/UDP/ICMP层和FTP层等信息块组成和网络报文文件一样,一个RPM文件通常由以下几部分组成:
- lead
- signature
- header
- archive
上面这4种元素,在每一个RPM文件中都会包含一个或者多个,其中每个元素中又有特定的数据格式。这样,多个数据元层层封装,就构成了RPM文件。
RPM的数据元素构成示意图如图1-2所示。
图1-2 RPM数据元素构成示意图
由图1-2可知,一个RPM由lead、signature、header和archive这4种元素组成,其中signature和header的本质都是一个header structure。header structure又分为header(这个header不是RPM中的header)、index列表和store三部分。header structure之间以8B为一个单位进行对齐,而head structure内部是紧密存储的,没有存储对齐。下面,我们来看一下这4种数据元素的作用以及它们在RPM文件中是怎样组织的。
1.3.3 RPM之lead
RPM文件中的第一个数据块是lead,在RPM的开发库中,lead数据块对应于头文件中定义的rpmlead结构体。
Linux的发行版中自带了一个名为rpm-devel的软件包,这个软件包中包含了一个文件,它的安装路径如下:
/usr/include/rpm/rpmlib.h
在该文件中,对rpmlead结构体的定义如下:
unsigned char magic[4]; unsigned char major; unsigned char minor; short type; short archnum; char name[66]; short osnum; short signature_type; /*!< Signature header type (RPMSIG_HEADERSIG)*/ /*@unused@*/char reserved[16];/*!< Pad to 96 bytes-- 8 byte aligned! */
我们来解释一下上述代码中每个字段的意义和作用。
1)占据前4个字节的整数magic:用于标识这个文件是否是RPM文件,file和rpm命令对RPM文件的识别都是通过这个字段来实现的。
例如,运行如下命令来查看一个文件的信息:
file test-rpm-1.1.1-15.x86_64.rpm
输出如下:
test-rpm-1.1.1-15.x86_64.rpm:RPM v3 bin i386 test-rpm-1.1.1-15
从输出情况可以看到,file命令识别出这是一个RPM文件,而做出这个识别的依据就是文件中的magic字段的值。
目前,这个magic数组的值是edab eedb,可以通过UltraEdit等工具打开一个RPM文件查看前4个字节的取值,如图1-3所示。
图1-3 RPM文件格式
对于通过magic number来判断文件类型的做法,由于代码可读性差和不容易修改,因此一般不建议使用,但是现实中很多文件类型的检测却又都是这么做的。比如,Win32可执行程序的开头一般都是[MZ];rar压缩文档的开头一般是3个字符Rar;zip文件的开头为[PK]。
在file命令的源码中,能看到对magic number(不止针对RPM文件)的定义。比如在file的源码文件magic/magic.mime中可以看到如下内容。
# RAR archiver (Greg Roelofs,newt@uchicago.edu) 0 string Rar! application/x-rar
还有如下内容:
# RPM:file(1)magic for Red Hat Packages Erik Troan (ewt@redhat.com) # 0 beshort 0xedab >2 beshort 0xeedb application/x-rpm
其中edab和eedb正是RPM文件开头的magic number,而Rar则是rar文件开头的magic number。
2)字节major和minor:用来标识RPM文件的版本,这两个字段的作用和TCP/IP协议的协议版本字段version类似。
在RPM文件中版本信息多被定义为:
major = 3 minor = 0
也就是说3.0版本的RPM是目前的主流,在图1-3中也可以看到这一点。
3)RPM文件的类型type:该字段标识了RPM中存的是二进制程序还是源代码。取值0代表二进制RPM包;取值1代表源码RPM包。
4)archnum:标识了RPM包应该安装到机器的架构信息。不过在最新的rpm v3.0中看到的这个字段对于x86_64、noarch和i386类型的包来说,取值都是0。因为在新版本RPM中,这个字段已经被存储进header数据块中了。
5)66字节的name字段:用来存储RPM的名称。
6)osnum:标识操作系统的类型,1代表Linux,2代表IRIX。这些常量被定义在文件/usr/lib/rpm/rpmrc中,代码如下:
os_canon: Linux: Linux 1 os_canon: IRIX: Irix 2 # This is wrong os_canon: SunOS5: solaris 3 os_canon: SunOS4: SunOS 4 os_canon: AmigaOS: AmigaOS 5 os_canon: AIX: AIX 5 os_canon: HP-UX: hpux10 6 os_canon: OSF1: osf1 7 os_canon: osf4.0: osf1 7 os_canon: osf3.2: osf1 7 os_canon: FreeBSD: FreeBSD 8 os_canon: SCO_SV: SCO_SV3.2v5.0.2 9 os_canon: IRIX64: Irix64 10 os_canon: NEXTSTEP: NextStep 11 os_canon: BSD_OS: bsdi 12 os_canon: machten: machten 13 os_canon: CYGWIN32_NT: cygwin32 14 os_canon: CYGWIN32_95: cygwin32 15 os_canon: UNIX_SV: MP_RAS: 16 os_canon: MiNT: FreeMiNT 17 os_canon: OS/390: OS/390 18 os_canon: VM/ESA: VM/ESA 19
7)signature_type:标识了下一个数据块(也就是signature)的类型,在RPM v3.0中,这个变量的取值一般为5。
1.3.4 header structure
从上一节讲到的lead部分的内容可以知道,lead数据块的rpmlead结构体从编程角度来讲,是很方便的。要读取结构体的成员,先获取rpmlead结构体的地址(指针),然后直接调用如下结构体成员获取操作就可以获取到RPM的名字。
pointer->name
然而,我们会发现,name数组只能容纳66字节的数据,如果包的名字长度超过66字节,又该怎样处理呢?
有的读者的第一反应可能是:把name的长度改成100或者256。是的,这样修改能处理大部分RPM包(因为现实中很少有人会把自己开发的RPM包名称定义为上百个字符),但是,我们仍然需要考虑以下几个问题:
- name长度修改了,重新编译rpm命令的源码生成的rpm命令,会按照新的格式读取RPM文件。这样,新版的rpm命令就不能正确读取老版本的RPM文件了,因为结构体rpmlead的大小已经发生了改变。
- 老版本的rpm命令不能正确读取新版本的RPM文件。
- 从编程角度来讲,256字节或者更大的数组空间在存储短名称RPM软件包时会造成空间浪费。
要解决以上3个问题,就需要对RPM数据的存储和读取方式进行调整,其中的一个出发点就是:尽量使可变长度数据不要存储在固定大小的结构体中。简单说,就是只见协议,不见业务。这应该也是大多数优秀软件设计的原则吧,实现协议和业务分开,才能保障在业务增加时,对已有功能代码的修改量减到最小,甚至不修改。
在RPM文件中,为了解决数据统一读取和存储的问题,引入了header structure这个格式的数据元。所谓某种“数据元”,指的是按照一定顺序组织起来的数据块。通过本节对header structure的介绍,我们将认识一个数据元到底是怎样的。
在一个文件中,可以有一个或者多个header structure;在RPM类型的文件中,只有两个header structure,一个是signature数据块,一个是header数据块。
每一个header structure包含以下3部分内容:
- header structure的头(header)。用来标识一个header structure的起始位置、大小,以及它包含的数据条目的个数。
- 索引(index)列表。index列表包含了多个index条目,其中每个index条目都是对一块数据的描述。每个index描述它指向的数据是什么样的,存储在哪里。根据index就能获取这个index对应的实际数据了。
- 存储字段(store)。存储了index描述的数据。
注意 之所以在这里要对header structure结构做介绍,是因为它已经在新版RPM中被使用了,为了保持文章的连续和合理性,我们特意在这里安排一些篇幅来讲述这个结构。
在RPM文件中,每个header structure都是采用以下3字节的magic number来标识其开始的:
"8E AD E8"
而且在前文中,我们已经介绍过,每个RPM文件有两个header structure—signature和header。打开一个RPM文件进行查看,如图1-4所示。
图1-4 RPM文件中的header structure
在图1-4中,用方框标注了这两个magic number,它们分别是signature和header这两个header structure的开始标识符。
下面来看一下组成header structure的header、index和store的细节。
1.header
3字节的magic number是header structure的header的开始:3字节之后,是1字节的版本号(为1);然后是4字节的保留字;在保留字之后是4字节的整数,表示在该header structure中有多少个索引项,也就是说有多少个index;接下来的4字节的整数标识在该header structure中有多少字节的数据。这些字段在图1-4中都能够看到。
2.index
每个index占据16字节的存储空间,前4字节组成一个整数变量Tag,表明该index指向的数据是什么类型的。关于这段描述,引用原文如下:
The first four bytes contain a tag—a numeric value that identifies what type of data is pointed to by the entry.The tag values change according to the header structure's position in the RPM file.
这个字段很容易让人误以为它是用来标识这个index指向的数据是整型、字符串或者数组类型的,其实不然。这里的type实际上就是我们通常所说的变量名称(TAG ID),也就是运行以下命令的输出中,描述各个字段的TAG对应的整数值。
rpm-qpi test.rpm
部分TAG值的定义列表如下:
#define RPMTAG_NAME 1000 #define RPMTAG_VERSION 1001 #define RPMTAG_RELEASE 1002 #define RPMTAG_SERIAL 1003 #define RPMTAG_SUMMARY 1004 #define RPMTAG_DESCRIPTION 1005 #define RPMTAG_BUILDTIME 1006 #define RPMTAG_BUILDHOST 1007 #define RPMTAG_INSTALLTIME 1008 #define RPMTAG_SIZE 1009
接下来的4字节整数(4~7)才是前4字节代表的变量的类型,即整数、字符串等。具体定义在文件/usr/include/rpm/header.h中。
typedef enum rpmTagType_e { #define RPM_MIN_TYPE 0 RPM_NULL_TYPE = 0, RPM_CHAR_TYPE = 1, RPM_INT8_TYPE = 2, RPM_INT16_TYPE = 3, RPM_INT32_TYPE = 4, RPM_STRING_TYPE = 6, RPM_BIN_TYPE = 7, RPM_STRING_ARRAY_TYPE = 8, RPM_I18NSTRING_TYPE = 9 #define RPM_MAX_TYPE 9 } rpmTagType;
其中,STRNG_TYPE是以空字符结束的字符串;STRING_ARRAY_TYPE是由多个STRING_TYPE变量组成的字符串数组。
接下来的8~11这4字节的整数,是该index对应的数据存储在store段的偏移量。
最后12~15这4字节的整数,表明了该index指向的数据有多少个元素,主要用于STRING和STRING_ARRAY类型变量:对于STRING变量,取值是1;对于STRING_ARRAY变量,取值是STRING的个数。
3.store
store是header structure中标注的数据实际存储的地方,关于这个元素,在分析时有以下几点要注意:
- 每个STRING类型变量都是以空字符结尾的。
- 对于以整数类型存储的变量,都是按照它的自然边界对齐存储的,也就是说,16位整数用2字节存储,32位整数用4字节存储,64位整数用8字节存储,以此类推。
- 所有数据都是以网络字节序存储的。
注意 在上一小节分析index的时候,store的内容就被分析出来了,为避免重复,本小节未给出具体的分析。index类似于指针,而store则是指针指向的值。
1.3.5 RPM之signature和header
上边我们曾介绍过,signature和header本质都是一个header structure,所以将它们放在一起来分析。
1.signature
RPM中的第一个header structure是signature。在signature中存储了RPM包的校验信息,如md5sum、sha1值等。这个header structure的头信息中有5个index,因为可以看到表示index个数的字段的值如下:
00 00 00 05
然后该header structure存储的数据为84字节:00 00 00 54,如图1-5所示。
图1-5 signature存储信息
接下来是index,每个index由4个32位整数组成,具体如下:
TAG (0-3) TYPE (4-7) OFFSET (8-11) COUNT(12-15)
我们来看看这5个index。
signature中的TAG名称和整数对应表可以在文件/usr/include/rpm/rpmlib.h中找到。
/** \ingroup signature * Tags found in signature header from package. */ enum rpmtagSignature { RPMSIGTAG_SIZE = 1000,/*!< internal Header+Payload size in bytes.*/ RPMSIGTAG_LEMD5_1 = 1001,/*!< internal Broken MD5,take 1 @deprecated legacy.*/ RPMSIGTAG_PGP = 1002,/*!< internal PGP 2.6.3 signature.*/ RPMSIGTAG_LEMD5_2 = 1003,/*!< internal Broken MD5,take 2 @deprecated legacy.*/ RPMSIGTAG_MD5 = 1004,/*!< internal MD5 signature.*/ RPMSIGTAG_GPG = 1005,/*!< internal GnuPG signature.*/ RPMSIGTAG_PGP5 = 1006,/*!< internal PGP5 signature @deprecated legacy.*/ RPMSIGTAG_PAYLOADSIZE = 1007,/*!< internal uncompressed payload size in bytes.*/ RPMSIGTAG_BADSHA1_1 = RPMTAG_BADSHA1_1,/*!< internal Broken SHA1,take 1.*/ RPMSIGTAG_BADSHA1_2 = RPMTAG_BADSHA1_2,/*!< internal Broken SHA1,take 2.*/ RPMSIGTAG_SHA1 = RPMTAG_SHA1HEADER,/*!< internal sha1 header digest.*/ RPMSIGTAG_DSA = RPMTAG_DSAHEADER,/*!< internal DSA header signature.*/ RPMSIGTAG_RSA = RPMTAG_RSAHEADER /*!< internal RSA header signature.*/ };
从图1-5可以看到,例子中打开的RPM文件的signature中的index的TAG取值分别如下:
- 第一个index的TAG是62(十六进制00 3E),对应的TAG为HEADER_SIGNATURES。
- 第二个index的TAG是269(十六进制01 0D),对应的TAG为RPMTAG_SHA1HEADER。
- 第三个index的TAG是1000(十六进制03 E8),对应的TAG为RPMSIGTAG_SIZE。
- 第四个index的TAG是1004(十六进制03 EC),对应的TAG为RPMSIGTAG_MD5。
- 第五个index的TAG是1007(十六进制03 EF),对应TAG为RPMSIGTAG_PAYLOADSIZE。
signature这个header structure存储了软件包的校验信息等相关数据,而name,version等字段都不在这个数据块中存储。因此,我们在此只通过实际数据来验证RPM软件包的组成是否与协议描述一致即可,而不必关心每个TAG的作用。对这块感兴趣的读者可以参考相关文档。
图1-6和图1-7所示是两个RPM的signature段截图。
图1-6 signature段截图一
图1-7 signature段截图二
图1-6所示的RPM的signature段有5个index,数据长度为0x54。我们可以在图1-6中按照协议去计算,向后跳过5个index(5个16字节),然后再数0x54个字节,store段结束。因为是8字节对齐,所以header段的header structure从4字节之后开始。
图1-7的情况也是如此,它的signature有7个index,因此,先跳过7个index到达store段,store段的数据为0xD8个字节,所以再向后数0xD8个字节,到内容“00 10”处结束。紧接着就是它的header段了。
2.header
header和signature一样,都是由header structure组成的,header中存储了RPM包的所有描述信息(不包括数据)。还以上面的RPM为例,我们接着分析它的header段。
首先看下header段的header(是header structure中的header,而不是RPM的header)和index的内容,如图1-8所示。
图1-8 header段的header
从图1-8中可以看到,header中包含了0x31个(也就是49个)index,而且它的store段占据空间为0x02F6字节,也就是758字节。
跳过type为0x3F和0x64这两个index查看后面的index,如图1-9所示。
图1-9 后面的index
在偏移00000320(本书凡涉及偏移地址的地方,都默认用十进制表示)处:0x03E8对应的TAG为十进制的1000(RPMTAG_NAME),对应于RPM的软件包名称。它的类型为0x06(RPM_STRING_TYPE),偏移地址为相对store起始位置为0x02的地方,只有一个串,并且是空字符结尾。我们来计算一下。
从偏移00000296处,index开始存储,有0x31个index,那么index的结束位置应该为00000296+0x31×16=00001080的前一个位置,也就是说偏移地址00001080为store的开始位置。那么偏移为0x2时,00001082处偏移就是RPM名称的存储位置,如图1-10所示。事实证明,实际数据与我们的计算的结果是一致的。
图1-10 RPM名称的存储位置
同理,0x03E9对应的index的TAG为十进制的1001(RPMTAG_VERSION),类型为字符串,偏移为0xB,起始位置为偏移00001091处,个数为0x1,它在RPM中的存储位置如图1-11所示。
图1-11 0x03E9对应的index在RPM中的存储位置
关于header中RPM信息的存储结构,这里就不再多举例子了,感兴趣的读者可以自己分析。
至此,signature和header的结构就介绍完了。
1.3.6 RPM之archive
header之后是archive字段,archive中存储了组成RPM包的所有文件的内容,可以通过标志位1F 8B来找到它的起始位置。archive是通过gzip算法压缩存储的。
为了保持内容的连续性,接着上一节header的内容继续分析,最后一个index的内容如图1-12所示。
图1-12 最后一个index的内容
它对应的TAG为十进制1147(RPMTAG_FILECONTEXTS),类型为0x8,对应于字符串数组(RPM_STRING_ARRAY_TYPE),偏移为0x2B0。因此,实际的存储位置为00001080+0x2B0=00001768,字符串个数为0x2个,所以,我们只需要去00001768偏移处跳过两个字符串,接着到达了00001821偏移处,如图1-13所示。
图1-13 实际存储位置
从图1-13和上面的计算可以知道,header的store结束于00001821偏移处,而根据RPM的header段的头(header structure中的header)信息可以知道:RPM的header中的store段的大小是0x02F6,为758字节,而store的开始位置为00001080偏移处,所以,store的结束位置应该为00001080+0x02F6=00001838偏移处的前一个位置,也就是00001837偏移处,所以,store实际存储的数据比index告诉我们的数据少了16字节。那么,这16字节的数据是怎么来的呢?经过对多个RPM文件的分析发现,在RPM文件的header块的store结束处,通常都填补了16字节的数据(这16字节的数据看上去并没有什么作用,也不是常量值),也就是从00001822到00001837这16字节的内容。
紧接着,从00001838偏移处开始,是archive的数据,关键字1F 8B标识了archive的开始,紧挨着的08表明数据是用gzip的deflation方法压缩存储的。
如果读者想获取更多关于RPM文件数据存储的细节,可以参考rpmbuild或者rpm命令的源代码。