说明:未经作者同意,不得转载本站文章
前言:
2012年9月,我有幸参加了IT168在北京举办的系统架构师大会,并在该会议上做了一篇关于《软件管理平台设计》的演讲,PPT下载在这里:
PPT下载地址在这里
在大会结束后,为了不使得这个讨论话题只成为一次演讲,而能将其延伸下去,我便又在 chinaunix 发表了三篇关于软件管理的文章,本文就是其中的第一篇。
本文最早发布在 chinaunix 社区的“架构设计”板块,后来经过系统化的整理后作为基础部分纳入了《linux软件管理平台设计与实现》这本书中。对本书感兴趣的读者可以搜索相关文章或者购买本书阅读。闲话少说,转入正文。
从文章的标题和PPT的内容,可以知道,本文要说的对象是 rpm 文件和 yum 服务,本人不太喜欢那种教材方式的介绍,而是喜欢从个人的最直观理解去阐述,叙说一个事情或者一门技术,那么,本文采用的就是这种书写方式。
RPM文件
首先说说 RPM 文件,百度搜索下 “rpm”,可以看到他的英文名字是: RedHat Package Manager, 你可以认为 rpm 是一种软件包管理方式,或者是一个软件包管理工具。提到软件包管理,就赘述几句,什么是软件包呢?
我对“软件包”的理解是:最原始的包是一堆需要运行的程序的集合,可能还需要加上一些配置文件,动态库之类,就构成了软件包。比如,
你把自己写好的脚本或者 C 代码编译生成的二进制文件,加上依赖的某些 so 文件和 conf 文件,存储在一个目录中,叫做 execute, 那么,这时就可以称呼 execute 为
一个软件包了。
有人可能说:这怎么能叫作软件包呢? 朋友,让我们仔细想想,它(execute)确实应该是一个软件包,它包含了要运行的应用需要的基本东西:执行程序,配置,依赖库等,这时,他不是软件包还能是什么? 只不过这个软件包看起来有些原始或者简单而已。
有了基本的软件包-一个目录中存放文件的集合,我们就会想着更高级格式的软件包,比如对 execute 进行压缩后,也是一个软件包,通过 tar 或者 gzip 得到 .tar.gz, .rar 或者 .zip 格式的文件,你就获得了一个较为高级的软件包了,为什么称其为“高级”软件包呢?因为它把程序和配置变成了一个单一的文件,这样就方便拷贝 了,另外压缩文件格式的软件包,也节省了磁盘空间占用和网络数据传输的量。在我之前从事3年的单位,我们所在项目组的程序基本都是通过这种方式发布的,每次发布程序,都是把动态库,二进制程序和配置文件压缩成一个 tgz 文件,拷贝到需要运行的 2000 多台机器上,解压到指定目录下,然后直接运行即可。可以看到,这种软件包组织方式使用起来已经比较方便了。
有了 tgz/rar/zip 格式的软件包就够了吗?可能对于某些小型应用确实够了,但是,日常工作中我们经常还会碰到下面这些问题:
(1): 想要查某个软件包的信息,比如谁制作的,什么时候制作的,以及描述信息?
(2): 给软件包带上一些特殊功能,除了文件拷贝功能外,还要有配置文件生成,安装服务,执行命令等操作,压缩文件怎么用?
(3): 软件包版本升级时,通过压缩文件怎么做?
除了以上这3点,应该还有其他方面的问题,你应该都会遇到或者思考到。再思考下。。。压缩文件格式的软件包确实存在这些功能上的不足,有没有一种格式的软件包,除了压缩存储之外,还能实现更多的功能呢?
答案是肯定的。
rpm 就是具有上面提到的诸多功能的一种包组织方式。当然还有其他方式的高级软件包格式,不过本文和后续的文章中只对 rpm 这种格式的软件包进行讨论。
先来看看这种高级软件包的特点(或者功能)吧:
(1): 压缩数据存储。
(2): 文件安装到系统中指定位置。
(3): 配置文件生成。
(4): 系统服务注册。
(5): 软件依赖检查。
除了(1)和(2),后面这三个功能,大概是压缩格式软件包都不具有的功能吧。
压缩存储
这是所有软件包的基本功能,比如,我这里有个目录是5.2M字节, 用它制作好的 rpm文件 只有2.4M大小。
文件安装
这个功能同样是软件包的基本功能,运行命令:
rpm -qpl ./cmeguard-1.1.2-34.i386.rpm
能够看到如下输出:
/etc/init.d/cmeguard
/usr/local/cmeguard/bin/auto_update
/usr/local/cmeguard/bin/cmeguard
/usr/local/cmeguard/bin/cmesync
/usr/local/cmeguard/bin/daemon
/usr/local/cmeguard/bin/genfinger
/usr/local/cmeguard/bin/run
/usr/local/cmeguard/bin/sync_plug
/usr/local/cmeguard/bin/sync_plug_back
/usr/local/cmeguard/conf/cmeguard.conf
/usr/local/cmeguard/conf/cmeproxy.conf
/usr/local/cmeguard/conf/cmesync.conf
/usr/local/cmeguard/conf/error.html
/usr/local/cmeguard/conf/mime.types
/usr/local/cmeguard/data
/usr/local/cmeguard/db
/usr/local/cmeguard/finger
/usr/local/cmeguard/lib
/usr/local/cmeguard/log
/usr/local/cmeguard
上面看到的文件列表就是将来会安装的文件列表,如果你用默认安装命令:
rpm -ivh cmeguard-1.1.2-34.i386.rpm
安装这个包的话,就会在你的系统上找到上面列表中对应的所有文件,而且最少是这些文件(因为安装时可能会产生新的文件,这个后面会说)。
配置文件产生
配置文件既可以通过安装列表中的文件来生成,又可以通过安装过程中的脚本来生成。
服务注册
如果你用rpm安装过 apache, mysql-server 等常见的软件,应该会知道,安装完成后,会对应在 /etc/init.d/ 目录下创建一个 httpd 或者 mysqld 的可执行文件,这个文件按照标准的自启动脚本格式书写的(参考chkconfig),当系统以对应的模式启动时,被安装导系统中的服务程序就会运行起来。
比如,运行命令:
rpm –nosignature -qpl ./vsftpd-2.0.5-10.el5.i386.rpm | grep init
输出如下:
/etc/rc.d/init.d/vsftpd
能够看到:
/etc/rc.d/init.d/vsftpd
这个文件被安装到了系统中,这时,你就可以通过命令:
service vsftpd start/stop/restart/status
来控制 ftp 服务的启动和停止了。
软件依赖检查
我们开发的程序很少是单独运行的,大多数都会依赖其它软件,比如你开发的数据库处理程序可能需要 libmysql,网络报文处理程序需要libpcap这个包的支持,这时,为了保证你的软件安装后能正常运行,而且在安装时就能够检查环境是否就绪,就可以通过软件依赖的方式来实现(当然,有人可能会问怎么实现这些呢,这个在后面文章的spec 语法中会详细说明)。
还是举个例子,比如有个 rpm 叫作:
test_rpm-1.1.1-21.i386.rpm
首先我们看下它 require了 哪些咚咚,
运行命令:
rpm -qp test_rpm-1.1.1-21.i386.rpm –requires
输出如下:
test__require_pkg
ruby-libs
/bin/sh
/bin/sh
rpmlib(PayloadFilesHavePrefix) >= 4.0-1
rpmlib(CompressedFileNames) >= 3.0.4-1
能够看到它依赖了这些组件或者包。
然后尝试安装下这个软件包,运行命令:
rpm -ivh test_rpm-1.1.1-21.i386.rpm
提示如下:
error: Failed dependencies:
test__require_pkg is needed by test_rpm-1.1.1-21.i386
能够看到,系统缺少 “test__requre_pkg” 这个包,因此 test_rpm-1.1.1-21.i386.rpm
这个包是不能安装成功的,这样的逻辑是合理的。虽然我们通过如下命令查看系统安装的包
rpm -qa ruby-libs
能够看到依赖之一的 “ruby-libs” 已经安装好了,但是在每一个rpm的依赖列表中,他们彼此是缺一不可的,或者说他们是”与”的关系。
好了,rpm 的五个特性,暂时就说到这里。
再来少许赘言对上面起初提的问题进行回答: rpm 是怎样在压缩存储之外又能做到其它功能的呢,比如安装服务,执行某些命令,打印信息,发邮件,检查依赖包,还有升级时做版本检查。这些都怎么实现的?
在此只进行简单的解释,细节留待 spec 文件那节再说。
rpm 其中一个功能就是对脚本( scripts) 的支持,除了文件压缩存储,它支持在安装软件或者卸载软件的过程中(确切点说,是这个过程的开始,进行和结束后这些不同时间点),执行一些命令,常用的脚本有:
post install
pre install
post uninstall
pre uninstall
从字面意思就能看到其作用,也就是说rpm能够在安装前,安装完,卸载前,卸载完执行某些脚本,这就为扩展软件包的功能提供了极大的空间。
我们看一个例子,运行命令:
pm -qp test_rpm-1.1.1-21.i386.rpm –scripts
输出如下:
preinstall scriptlet (using /bin/sh):
echo “pre install scripts by duanjigang”
postinstall scriptlet (using /bin/sh):
echo “post install scripts by duanjigang”
preuninstall scriptlet (using /bin/sh):
#!/bin/bash
echo “pre uninstall by duanjigang”
postuninstall scriptlet (using /bin/sh):
#!/bin/bash
echo “post uninstall by duanjigang”
能够看到这个软件包有 5 个 scripts 在 rpm 中附带着,分别在安装前后,卸载前后,build 这五个点执行,这样,你就可以通过这些 scripts 来实现想要的功能了。
关于 scripts 在那些不同点执行,有什么效果,后面会详述。
还有就是 rpm 怎么做到版本的控制和更新的?
通过运行命令:
rpm -qpi test_rpm-1.1.1-21.i386.rpm
能够看到输出:
Name : test_rpm
Version : 1.1.1
Release : 21
可以得知 该软件的版本为1.1.1, Release 号是21,因此它的包名默认就是“test_rpm-1.1.1-21”。
当 version 进行升级,或者 Release 号进行升级后,新版本的 RPM 就可以在老的版本上做升级了,而且 rpm 安装命令会检查版本号,然后确定是否能够升级。
有时候,在制作rpm包时,有的人会采用 spec中的 “Epoch” 这个字段来控制版本,不过这个字段却是一个神秘数字,他会加在版本的前面,用冒号隔开,比
如 “99:1.1.1-21” 这样的一个版本字符串,和 “1.2.3-xx”
来比较,前者始终比后者版本高,后者版本的包就没法升级前者了,当然这么做并不全是为了控制版本升级,主要目的还是做特殊标识的。
rpm 的基础介绍部分先到这里。
rpm 文件格式
做过网络程序开发或者熟悉 wireshark (或者 tcpdump)的同学肯定都对网络报文文件比较熟悉,我们都知道,网络报文文件都有固定的格式,或者我觉得叫“协议”更容易理解。
这些协议是人为定下来的,告知大家:我们这个文件前面多少比特写什么内容,中间哪个数据结构表示什么意思,后面那个字段又代表什么。。总之,通过这么一种 通告的方式向众生告知,如果你要参与到这种格式的数据(或者文件)中来,就必须按照我说的格式(协议)去读写,只有这样,才能够读取到正确的数据,或者 写成一个正确格式(能被别人识别)的文件。
报文文件为大家熟知,我们就从报文文件的例子开始。下面是用 wireshark 抓取的一个数据包,用 wireshark 打开并查看该文件,能够清晰看到按照报文文件协议解析后的数据包文件的各个字段:
链路层
IP层
TCP层
FTP层
每层的各个字段都能看到。如下图所示:
TCP/IP协议层说明图
之所以 wireshark 能够展示出来报文的每个字段,因为它是按照网络报文的协议格式对该文件进行了解析,对于 rpm 文件而言,存储原理也是一样的,知晓了文件的数据格式,然后逐个解析,就能拿到你想要的信息,rpm 命令中的大部分功能,都是这样实现的。
下面,我们看看 rpm 的文件格式。
就像报文文件由 mac 层,ip 层,tcp/udp/tcp层,http/snmp 层这些信息块组成一样,一个 rpm 文件由一下几种数据块组成:
lead
signature
header
archive
在 一个 rpm 文件中,上面这四种数据元,会被包含一个或者多个,其中每个数据元中又有自己的数据格式,这样,一层层的存储协议,就构成了一个 rpm 文件。
首先看 Lead 信息。
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! */
通过字面意思大概能看到其中的意思,这个结构体中包含了一个软件包的名字,适合安装的操作系统类型, 适合的平台信息,包类型(二进制包还是源码包)。
前四个字节 magic,作为标识符, 表示这个文件是否是rpm文件,file 命令和 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
这个文件类型的识别方法,就是从 lead 中获取magic 的值来判断的。
===============================================================================================
目前,rpm文件中这个magic 数组的值是 “edab eedb”, 可以通过 ultraedit 打开一个RPM 文件 查看前四个字节,如图示:
接下来两个字节 major 和 minor 用来标识 rpm 文件的版本(是rpm格式的版本,而不是rpm包的版本),这个和 TCP/IP 协议中的 version 字段作用类似。
在 rpm 文件中能看到的大多数version的值都是
major = 3
minor = 0
也就是 3.0 版本的 RPM 文件,在上图中可以看到这一点。
接下来是 rpm 文件的类型字段 type,0 表明是二进制 rpm 文件,1 表明是源码文件。
下来的 archnum 标识软件包将要安装的平台架构信息,1 标识 i368, 在最新的 rpm version 3.0 中看到的 这个字段在 x86_64, noarch 和 i386 中都是0,可能已经不用这个字段了,而是采用header来存储。
66个字符的 name 用来存储软件包的名称。
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
signature_type 字段标识了下一个 数据块 signature 的类型,在 rpm version 3.0 中,这个变量的值是 5.
头结构体 header structure
从 上面 讲到的 lead 结构体可以知道,lead 这个结构体从编程角度来来讲,很好用,要读取成员,你只需要用
pointer->name
即可获取到 rpm 的名字,但是,我们很容易就会发现,name 这个数组的长度只能容纳 66 个字符,如果包的名字长度超过66个字符,怎么办呢?
有的开发人员可能会说:我把 name 长度改成 100,256, 这样是能处理大多数包,但是会带来两个问题:
(1): name 长度修改了,重新生成的 rpm 命令,会去按照新的格式读取rpm文件,这样,新版的rpm命令就不能读取老的格式的 rpm 文件了。
(2): 老版本的rpm 命令,不能正确读取新版本的rpm文件。
因此,要解决此类问题,就需要将数据的存储和读取规范化,或者说,需要把跟业务相关的数据在协议中弱化,在协议中只见协议,不见业务相关的数据。
我想,这也是软件设计之道吧,工具和业务分开,才能灵活扩展,特别是通讯程序之类的应用,尤为重视这一点。
rpm 文件中为了解决数据统一读取和存储的需求,引入了 头结构体 (header structure),在一个文件中,可以有一个或者多个 header structure, 而在 rpm 文件中
只有两个header structure,一个是 signature 数据块,一个是header 数据块。
每一个 header structure 包含三部分内容:
第一部分叫做: header structure 的头(header), 用来标识一个header structrure 的起始位置,header structure 的大小,以及它包含的数据条目数。
第二部分紧跟 header structure header,叫索引 (index) ,index 包含了多个index条目,其中每个 index 条目都是对一块数据的描述,每个index告诉你它指向的数据是什么样子的,在哪里存储着,根据 index 就能获取到这个index对应的数据。
第三部分是存储字段,叫作 store, 存储了 index 描述的数据。
请注意,这里之所以引入 对 header structure 的介绍, 是因为在新版本的 rpm 中,header structure 已经在使用了,为了保持文章逻辑的自然逻辑性(从lead发展到header structure),我们专门花一些篇幅来介绍header structure。
继续回到 rpm 文件后面数据块的介绍部分:signature 和 header,看看这两个 header structure 是怎样的。
在一个 rpm 文件中,每个 header structure 开始都是用三个字节的神秘数字开始 “8E AD E8”,
而且前文,我已经介绍过了
,每个 rpm 有两个 header structure– signature 和 header, 因此,不妨打开一个rpm文件看看,以下面的 rpm 为例, 如图:
能够看到这两个神秘数字,也就是说,我们的两个 header structure 都被找到了。
接着往下分析。
header structure 之 header
在 三字节的神秘数字之后,是 1 个字节的版本号(为1),然后是4个字节的保留字,在保留字之后,是4个字节的整数, 表示在该header structure 中有多少个索引项,也就是说有多少个index, 接下来的4个字节整数,标识在该header structure 中有多少字节的数据。 这些,在上图都能看到。
header structure 已经很清楚了,但是还有两个概念没清楚,一个是 index, 一个是 store,接下来,我们来剖析这两个咚咚。
header structure 之 index
每个 index 有 16个 字节的存储空间,前四个字节整数是一个 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
这个很容易让人误解为是,这个类型是 整形,字符串,或者数组,其实不是,这里的 “TYPE” 其实就是我们通常说的变量名称, 包括了通过命令
rpm -qpi xxoo.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_INT64_TYPE = 5, —- These aren’t supported (yet) */
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 的集合。
接下来的 4字节的整数,是该 index 对应的数据在 store 段的偏移。
最后12-15这4字节的整数,表明了该index指向的数据,有多个数据据条目,主要用于 STRING 和 STRING_ARRY 类型,STRING 的话,是1,STRING_ARRY就是STRING的个数。
header structure 之store
store 就是 该 header structure 中数据存储的地方,有几个点要注意:
(1): 每个 STRING 类型,都以 空字符结尾。
(2): 整数存储都是按照它的自然边界存储的。 也就是说,64位的用8个字节存储,16位的用2个字节存储,32位的用4个字节存储等等。
(3): 所有数据都是网络字节序存储的。
接着来看由 header structure 组成的 rpm 文件中的两个要素:
signature 和 Header
signature 和 header 的本质都是 header structure,所以将其放在一起来介绍。
rpm 中的第一个header structure是signature。在signature中存储了 rpm 包的校验信息,如 md5sum、sha1值等,这个header structure的头信息中有5个index, 如下所示:
00006-00000096: 8E AD E8 01 00 00 00 00 00 00 00 05 00 00 00 54 ;
00007-00000112: 00 00 00 3E 00 00 00 07 00 00 00 44 00 00 00 10 ;
00008-00000128: 00 00 01 0D 00 00 00 06 00 00 00 00 00 00 00 01 ;
00009-00000144: 00 00 03 E8 00 00 00 04 00 00 00 2C 00 00 00 01 ;
00010-00000160: 00 00 03 EC 00 00 00 07 00 00 00 30 00 00 00 10 ;
00011-00000176: 00 00 03 EF 00 00 00 04 00 00 00 40 00 00 00 01 ;
===========================================================================
第一行是 标准头,从第二行开始是 5 个 index, 每行一个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. */
};
从上面的五个index中可以看到,例子中打开的rpm文件的signature中的index的TAG取值分别如下(第7行到11行)。
第一个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。
事实上,大多数rpm 文件的这5个index 都是相同的(为甚还需要研究,目前看到现象是这样的)。
signature这个header structure 存储了软件包的校验信息等相关数据,而name,version等字段都不在这个数据块中存储。因此,我们在此只通过实际数据来验证
rpm 软件包的组成是否与协议描述一致即可,而不必关心每个TAG的作用。对这块感兴趣的读者可以参考相关文档。
===============================================================
图6和图7所示是两个 rpm 的signature段截图。
图6 rpm 文件格式(一)
图7 rpm 文件格式(二)
图6所示 rpm 的signature段有5个index,数据长度为0x54。我们可以在图中按照协议去计算,向后跳过5个index (5个16字节),然后再数0x54个字节,store段结束。因为是8字节对齐,所以header段的header structure从4字节之后开始。
图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内容,如图 8 所示。
图8 rpm文件格式(三)
从图8中可以看到,header中包含了0x31个,也就是49个index,而且它的store段占据空间为0x02F6字节,也就是758个字节。
跳过type为0x3F和0x64这两个index查看后面的index,如图9所示。
图9 rpm 文件格式(四)
在偏移00000320(本书凡涉及偏移地址的地方,都默认用十进制表示)处:0x03 E8 对应的TAG为十进制1000(RPMTAG_NAME),对应于RPM的软件包名称。它的类型为0X06(RPM_STRING_TYPE),偏移地址为相对store起始位置为0x02的地方,只有一个串,并且是空字符结尾。我们来计算一下:
1) 从偏移00000296处,index开始存储,有0x31个index,那么index的结束位置应该为00000296+0x31*16=00001080的前一个位置,也就是说偏移地址00001080为store的开始位置,那么偏移为0x2时,00001082处偏移就是RPM名称的存储位置,如图1-10所示。事实证明,实际数据与我们的计算的结果是一致的。
图 10 rpm文件格式(五)
同理,0x03E9对应的index的TAG为十进制1001(RPMTAG_VERSION),类型为字符串,偏移为0xB,起始位置为偏移00001091处,个数为0x1,它在 rpm 中的存储位置如图11所示。
图11 rpm 文件格式(六)
关于header中 rpm 信息的存储结构,这里就不再多举例子了,感兴趣的读者可以自己分析。
至此,signature和header的结构就介绍完成了。
rpm 之archive
header 之后是archive 字段,archive中存储了组成 RPM包的所有文件的内容,可以通过标志位“1F 8B”来找到它的起始位置。archive是通过 gzip算法压缩存储的。
为了保持内容的连续性,接着上一节head er的内容继续分析,最后一个index的内容如图12所示。
图12 rpm文件格式(七)
它对应的TAG为十进制1147(RPMTAG_FILECONTEXTS),类型为0x8,对应于字符串数组
(RPM_STRING_ARRAY_TYPE),偏移为0x2B0。因此,实际的存储位置为00001080 + 0x2B0 = 00001768,字符串个数为0x2个,所以,我们只需要去00001768偏移处跳过两个字符串,接着到达了00001821偏移处,如图13所示。
图13 rpm文件格式
从图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命令的源代码。
================================================
RPM解析例程
笔者基于rpm-devel 开发了一个C程序,用来读取RPM文件的基本信息并且打印到标准输出。如果你对前面章节介绍的知识已经理解并且掌握,下面这段代码就比较容易阅读了。
核心代码片段如下:
char * readHeaderString (Header header, int_32 tag_id)
{
int_32 type;
void *pointer;
int_32 data_size;
//获取header中的一个string
int header_status = headerGetEntry (header,
tag_id, &type, &pointer, &data_size);
if (header_status)
{
if (type == RPM_STRING_TYPE)
{
return pointer;
}
}
return NULL;
}
int samplerpm (const char *szrpm)
{
char g_szname[1024] = {0};
FD_t fd = Fopen (szrpm, “r”);
memset (g_szname, 0, 1024);
sprintf (g_szname, “%s”, szrpm);
fflush (stdin);
fflush (stdout);
if (!fd)
{
printf (“open file ‘%s’ failed\n”, szrpm);
return 0;
}
struct rpmlead plead;
int lead = readLead (fd, &plead);//读取lead结构体
if (lead)
{
printf (“readLead of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//sigType sig_type = plead.signature_type;
Header header;
//读取第一个header structure–signature
rpmRC ret = rpmReadSignature (fd, &header, plead.signature_type);
if (ret != RPMRC_OK)
{
printf (“rpmReadSignature of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//读取第一个header structure–header
Header newheader =
headerRead (fd, (plead.major >= 3) ? HEADER_MAGIC_YES : HEADER_MAGIC_NO);
if (!newheader)
{
printf (“headerRead of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//读取各个TAG
const char *name = readHeaderString (newheader, RPMTAG_NAME);
const char *version = readHeaderString (newheader, RPMTAG_VERSION);
const char *release = readHeaderString (newheader, RPMTAG_RELEASE);
const char *group = readHeaderString (newheader, RPMTAG_GROUP);
const char *packager = readHeaderString (newheader, RPMTAG_PACKAGER);
if (!group) group = “NONE_GROUP”;
if (!packager) packager = “NONE_PACKAGER”;
printf (“name:%s\nversion:%s\nrelease:%s\ngroup:%s\npackager:%s\n\n”,
name, version, release, group, packager);
Fclose (fd);
return 1;
}
可执行程序的编译方法如下:
gcc test.c -I/usr/include/rpm -lrpm -lrpmdb -lrpmio -lpopt -o test_rpm
可以通过以下方法进行测试:
./test_rpm ./mysql-server-5.0.77-4.el5_6.6.x86_64.rpm
输出如下:
name:mysql-server
version:5.0.77
release:4.el5_6.6
group:Applications/Databases
packager:Red Hat, Inc.
可以看到,test_rpm这个程序解析并且打印了指定 rpm 文件的基本信息,读者可以参考文档对该程序进行功能细化。
由于笔者并没阅读rpm命令或者rpmbuild 的源码,因此对 rpm 格式的剖析只能到此深度,不过rpmbuild的实现应该也是类似的,感兴趣的读者可以参考 rpm 的文档和源码进一步学习。
补充
rpm 结构总图
补充下后来整理的 rpm 结构总图,如下:
rpm 结构总图
rpm 文件十六进制结构图展示
下面的三幅图是打开的一个rpm文件的十六进制内容,读者可以结合本文的内容进行查看,以加深对rpm格式的理解。
例子代码
附件是例子代码:
点击此处下载例子代码
2014-01-24 修改完结。