Linux软件管理平台设计与实现
上QQ阅读APP看书,第一时间看更新

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命令的源代码。