一.dex文件的生成

我们可以通过java文件来生成一个简单的dex文件

编译过程:


首先编写java代码如下:


(1) 编译成 java class 文件
执行命令 : javac Hello.java

编译完成后 ,目录下生成 Hello.class 文件

(2) 编译成 dex 文件
dx --dex --output=Hello.dex Hello.class

编译正常会生成 Hello.dex 文件


00 34指明jdk版本,这个原因是jdk版本过高了,我们得使用1.6版本的JDK来进行编译


这个时候我们可以指明编译指定版本的来进行编译class文件
javac -source 1.6 -target 1.6 Hello.java

(3). 使用 ADB 运行测试
测试命令和输出结果如下 :
adb push Hello.dex /mnt/sdcard/
adb shell dalvikvm -cp /mnt/sdcard/Hello.dex Hello


第一次运行会在data/dalvik-cache目录生成一个odex的文件

有些加固没有考虑这个文件,dex的加固直接就废掉了

直接把这个文件拖到ida是可以分析的,拖到jeb就显示未知的文件格式
我们使用winhex查看,发现前面多了一些内容


把这些内容剪切掉,在用jeb分析,就成功解析出来了



二.dalvik文件文档
文档目录
android2.3.7/dalvik/docs/dex-format.html

相关源码目录:
android2.3.7\dalvik\libdex
android2.3.7\dalvik\libdex\DexFile.c   dexFileParse 做主要的解析工作
需要注意的是每个Android源码版本的路径可能不一样,dex文件的格式也可能会有细微的变化,请参考相关平台的源码

用source insight来进行源码分析


分析可以使用010Editor脚本进行分析



三.dex文件格式
整体格式概要



Android dex文件格式样例


文件格式相互之间的联系
二、dex文件的解析
一. dex文件头


(1) magic value
在DexFile.c   dexFileParse函数中 会先检查magic opt
啥是magic opt呢? 我们刚刚从cache目录拷贝出来的那个

前面的dey 036就是magic opt

在源码中会先解析magic opt,然后重设dexfile指针


重设magic opt指针后开始解析magic value
这 8 个 字节一般是常量。数组的值可以转换为一个字符串如下 :
{ 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00 } = "dex\n035\0"


(2) checksum
文件校验码 ,使用alder32 算法校验文件
先用dexheader先校验,校验失败在使用opt header去校验


其校验算法如下除去maigc,checksum 外余下的所有文件区域 ,用于检查文件错误


(3) signature
signature , 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件

由此可见我们在修改了dex文件之后,得先修正signature然后在修正checksum

(4) file_size
Dex 文件的大小 ,源码中会拿该字段和传入的长度值进行比较


(5) header_size
header 区域的大小 ,单位 Byte ,一般固定为 0x70 常量
在DexSwapVerify.c  dexSwapAndVerify

高版本不知道是不是这样校验的 大于居然没有置为okay

(6) endian_tag
大小端标签 ,标准 .dex 文件格式为小端 ,此项一般固定为 0x12345678常量 

CmdUtils.c  程序调用主线从
dexOpenAndMap->dexSwapAndVerifyIfNecessary->dexSwapAndVerify->swapDexHeader

这里逻辑有点绕,他默认就会转换一次, 如果是小尾,转换之后就是大尾,那么校验就不会通过
如果是大尾方式, 就转换成小尾, 校验通过,继续后面的转换流程

还一个校验是如果是odex格式,那么已经是优化之后的,则不需要转换


其转换算法如下:


(6) link_size和link_off
这个两个字段是表示链接数据的大小和偏移值

CHECK_OFFSET_RANGE 只是检查是否超出文件指针范围


(7) map_off
map item 的偏移地址 ,该 item 属于 data 区里的内容 ,值要大于等于 data_off 的大小 。


其结构体指向:


MapItem结构体


对应的枚举值


010Editor中呈现


(8) string_ids_size和string_ids_off
这两个字段表示dex中用到的所有的字符串内容的大小和偏移值,我们需要解析完这部分,然后用一个字符串池存起来,后面有其他的数据结构会用索引值来访问字符串,这个池子也是非常重要的。后面会详细介绍string_ids的数据结构

(9) type_ids_size和type_ids_off
这两个字段表示dex中的类型数据结构的大小和偏移值,比如类类型,基本类型等信息,后面会详细介绍type_ids的数据结构

(10) proto_ids_size和type_ids_off
这两个字段表示dex中的元数据信息数据结构的大小和偏移值,描述方法的元数据信息,比如方法的返回类型,参数类型等信息,后面会详细介绍proto_ids的数据结构

(11) field_ids_size和field_ids_off
这两个字段表示dex中的字段信息数据结构的大小和偏移值,后面会详细介绍field_ids的数据结构

(12) method_ids_size和method_ids_off
这两个字段表示dex中的方法信息数据结构的大小和偏移值,后面会详细介绍method_ids的数据结构

(13) class_defs_size和class_defs_off
这两个字段表示dex中的类信息数据结构的大小和偏移值,这个数据结构是整个dex中最复杂的数据结构,
他内部层次很深,包含了很多其他的数据结构,所以解析起来也很麻烦,所以后面会着重讲解这个数据结构

没有类的话,dex校验会失败

(14) data_size和data_off
这两个字段表示dex中数据区域的结构信息的大小和偏移值,这个结构中存放的是数据区域,比如我们定义的常量值等信息。
到这里我们就看完了dex的头部信息,头部包含的信息还是很多的,主要就两个个部分:
1) 魔数+签名+文件大小等信息
2) 后面的各个数据结构的大小和偏移值,都是成对出现的

先来看看整体的结构,结构体定义在DexFile.h里面

 
在dexFileSetupBasicPointers中设置各个子结构体,当然是在解析DexHeader之后
源码在DexFile.c文件中

 
在解析每个子结构体之前我们先了解下leb128格式,
源码leb128.c中解析这种格式

 LEB128 ( little endian base 128 ) 格式 ,是基于 1 个 Byte 的一种不定长度的编码方式 。若第一个 Byte 的最高位为 1 ,则表示还需要下一个 Byte 来描述 ,直至最后一个 Byte 的最高位为 0 。每个 Byte 的其余 Bit
用来表示数据,这个数据类型的出现其实就是为了解决一个问题,那就是减少内存的浪费,他就是表示int类型的数值,但是int类型四个字节有时候在使用的时候有点浪费,所以就应运而生了

一. string_ids数据结构
string_ids 区索引了 .dex 文件所有的字符串 

如何定位:
先在DexHeader中拿到偏移和数量

 
在文件偏移112(10进制)的地方有11713项string_ids也就是一个DexStringId数组
DexStringId只有一个结构体成员,他保存一个string_data_item的偏移值

这个数组看起来是像下面这个样子的

 
 
DexStringId指向的是一个leb128的字符串(文件偏移)
在源码中简单的拿到偏移直接读取leb128即可拿到字符串

010Editor脚本解析如下:

 
 
其他一些子结构,如 type-ids , method_ids 也会引用到这些字符串

二. type_ids数据结构
这个数据结构中存放的数据主要是描述dex中所有的类型,比如类类型,基本类型等信息。type_ids 区索引了 dex 文件里的所有数据类型 ,包括 class 类型 ,数组类型(array types)和基本类型(primitive types) 。 本区域里的元素格式为 type_ids_item , 
type_ids_item 里面 descriptor_idx 的值的意思 ,是 string_ids 里的 index 序号 ,是用来描述此type 的字符串

 
我们来手工找一找
第一项保存的值为695

 
我们定位到字符串表第695项,成功找到B

 

源码中调用dexStringByTypeIdx拿到指定type字符串
同JNI一样
L 表示 class 的详细描述 ,一般以分号表示 class 描述结束 ;
V 表示 void 返回类型 ,只有在返回值的时候有效 ;
[ 表示数组 ,[Ljava/lang/String; 可以对应到 java 语言里的 java.lang.String[] 类型 。

后面的其他数据结构也会使用到type_ids类型,所以我们这里解析完type_ids也是需要用一个池子来存放的,后面直接用索引index来访问即可

三. proto_ids数据结构
proto的意思是 method prototype 代表 java 语言里的一个 method 的原型

其保存的是这样一个结构体
shorty_idx :跟 type_ids 一样 ,它的值是一个 string_ids 的 index 号 ,最终是一个简短的字符串描述 ,用来说明该 method 原型 
return_type_idx :它的值是一个 type_ids 的 index 号 ,表示该 method 原型的返回值类型
parameters_off :后缀 off 是 offset , 指向 method 原型的参数列表 type_list ; 若 method 没有参数 ,值为0 。

 
参数列表的格式是 type_list ,结构从逻辑上如下描述 。
size 表示参数的个数 ;
type_idx 是对应参数的类型 ,它的值是一个 type_ids 的 index 号 ,跟 return_type_idx 是同一个品种的东西 

其描述Method原型算法如下:(DexProto.c)

四、field_ids数据结构
filed_ids 区里面存放的是dex 文件引用的所有的 field 。本区的元素格式是 field_id_item
class_idx :表示本 field 所属的 class 类型 , class_idx 的值是 type_ids 的一个 index , 并且必须指向一个class 类型 
type_idx :表示本 field 的类型 ,它的值也是 type_ids 的一个 index 
name_idx : 表示本 field 的名称 ,它的值是 string_ids 的一个 index 

 
注意:这里的字段都是索引值,一定要区分是哪个池子的索引值,还有就是,这个数据结构我们后面也要使用到,所以需要用一个池子来存储

五、 method_ids数据结构
method_ids 是索引区的最后一个条目 ,它索引了 dex 文件里的所有的 method.

 
method_ids 的元素格式是 method_id_item , 结构跟 fields_ids 很相似:
class_idx :表示本 method 所属的 class 类型 , class_idx 的值是 type_ids 的一个 index , 并且必须指向一个 class 类型 
name_idx :表示本 method 的名称 ,它的值是 string_ids 的一个 index 
proto_idx :描述该 method 的原型 ,指向 proto_ids 的一个 index 
注意:这里的字段都是索引值,一定要区分是哪个池子的索引值,还有就是,这个数据结构我们后面也要使用到,所以需要用一个池子来存储。

六、class_defs数据结构
1、class_def_item
从字面意思解释 ,class_defs 区域里存放着 class definitions , class 的定义 。它的结构较 dex 区都要复杂些 ,因为有些数据都直接指向了data 区里面 。
class_defs 的数据格式为 class_def_item 
(1) class_idx:描述具体的 class 类型 ,值是 type_ids 的一个 index 。值必须是一个 class 类型 ,不能是数组类型或者基本类型 。
(2) access_flags: 描述 class 的访问类型 ,诸如 public , final , static 等 。在 dex-format.html 里 “access_flagsDefinitions” 有具体的描述 。
(3) superclass_idx:描述 supperclass 的类型 ,值的形式跟 class_idx 一样 。
(4) interfaces_off:值为偏移地址 ,指向 class 的 interfaces , 被指向的数据结构为 type_list 。class 若没有interfaces ,值为 0。
(5) source_file_idx:表示源代码文件的信息 ,值是 string_ids 的一个 index 。若此项信息缺失 ,此项值赋值为NO_INDEX=0xffff ffff 
(6) annotions_off:值是一个偏移地址 ,指向的内容是该 class 的注释 ,位置在 data 区,格式为annotations_direcotry_item 。若没有此项内容 ,值为 0 。
(7) class_data_off:值是一个偏移地址 ,指向的内容是该 class 的使用到的数据 ,位置在 data 区,格式为class_data_item 。若没有此项内容 ,值为 0 。该结构里有很多内容 ,详细描述该 class 的 field ,method, method 里的执行代码等信息 ,后面有一个比较大的篇幅来讲述
class_data_item 。
(8) static_value_off:值是一个偏移地址 ,指向 data 区里的一个列表 ( list ) ,格式为 encoded_array_item。若没有此项内容 ,值为 0 。

header 里 class_defs_size = 0x01 , class_defs_off = 0x 0110 。则此段二进制描述为 :


其实最初被编译的源码只有几行 ,和 class_def_item 的表格对照下 ,一目了然 

 
source file : Hello.java
public class Hello
{
element value associated strinigs
class_idx 0x00 LHello;
access_flags 0x01 ACC_PUBLIC
superclass_idx 0x02 Ljava/lang/Object;
interface_off 0x00
source_file_idx 0x02 Hello.java
annotations_off 0x00
class_data_off 0x0234
static_value_off 0x00
public static void main(String[] argc)
{
System.out.println("Hello, Android!\n");
}
}

2、 class_def_item => class_data_item
class_data_off 指向 data 区里的 class_data_item 结构 ,class_data_item 里存放着本 class 使用到的各种数据
在DexClass.c中dexReadAndVerifyClassData来读取DexClassData
相关结构体定义如下:

 
3.对于DexMethod有
(1) method_idx_diff:前缀 methd_idx 表示它的值是 method_ids 的一个 index ,后缀 _diff 表示它是于另外一个 method_idx 的一个差值 ,就是相对于 encodeed_method [] 数组里上一个元素的 method_idx 的差值 。其实 encoded_filed
- > field_idx_diff 表示的也是相同的意思 ,只是编译出来的 Hello.dex 文件里没有使用到class filed 所以没有仔细讲 ,详细的参考 dex_format.html 的官网文档 
(2) access_flags:访问权限 , 比如 public、private、static、final 等 。
(3) code_off:一个指向 data 区的偏移地址 ,目标是本 method 的代码实现 。被指向的结构是
code_item ,有近 10 项元素 ,后面再详细解释 

 

4、class_def_item => class_data_item => code_item
到这里 ,逻辑的描述有点深入了 。先理一下是怎么走到这一步的 ,code_item在 dex 里处于一个什么位置
遍历过程如下:

 
(1) dex_header拿到class_def_item_list偏移,遍历解析class_def_item
(2) 对于指定的class_def_item,每一项都有class_data_off,通过该offset定位到dex_class_data
(3) 解析dex_class_data其中在method_item中有一个code_off,即可定位到code_data在文件中的偏移


(1) registers_size:本段代码使用到的寄存器数目。
(2) ins_size:method传入参数的数目 。
(3) outs_size: 本段代码调用其它method 时需要的参数个数 。
(4) tries_size: try_item 结构的个数 。
(5) debug_off:偏移地址 ,指向本段代码的 debug 信息存放位置 ,是一个 debug_info_item 结构。
(6) insns_size:指令列表的大小 ,以 16-bit 为单位 。 insns 是 instructions 的缩写 (这里就该对着dalvik去做指令解析)
(7) padding:值为 0 ,用于对齐字节 。
(8) tries 和 handlers:用于处理 java 中的 exception , 常见的语法有 try catch 。


4、 分析 main method 的执行代码并与 smali 反编译的结果比较
在 8.2 节里有 2 个 method , 因为 main 里的执行代码是自己写的 ,分析它会熟悉很多 。偏移地址是
directive_method [1] -> code_off = 0x0148 ,二进制描述如下 :

insns 数组里的 8 个二进制原始数据 , 对这些数据的解析 ,
需要对照官网的文档 《Dalvik VM InstructionFormat》和《Bytecode for Dalvik VM》。
分析思路整理如下
(1) 《Dalvik VM Instruction Format》 里操作符 op 都是位于首个 16bit 数据的低 8 bit ,起始的是 op =0x62。
(2) 在 《Bytecode for Dalvik VM》 里找到对应的 Syntax 和 format 。
syntax = sget_object
format = 0x21c 。
(3) 在《Dalvik VM Instruction Format》里查找 21c , 得知 op = 0x62 的指令占据 2 个 16 bit 数据 ,格式是 AA|op BBBB ,解释为 op vAA, type@BBBB 。因此这 8 组 16 bit 数据里 ,前 2 个是一组 。对比数据得 AA=0x00,
BBBB = 0x0000。
(4)返回《Bytecode for Dalvik VM》里查阅对 sget_object 的解释, AA 的值表示 Value Register ,即0 号寄存器; BBBB 表示 static field 的 index ,就是之前分析的field_ids 区里 Index = 0 指向的那个东西 ,当时的 fields_ids
的分析结果如下 :

对 field 常用的表述是
包含 field 的类型 -> field 名称 :field 类型 。
此次指向的就是 Ljava/lang/System; -> out:Ljava/io/printStream;
(5) 综上 ,前 2 个 16 bit 数据 0x 0062 0000 , 解释为
sget_object v0, Ljava/lang/System; -> out:Ljava/io/printStream;
其余的 6 个 16 bit 数据分析思路跟这个一样 ,依次整理如下 :
0x011a 0x0001: const-string v1, “Hello, Android!”
0x206e 0x0002 0x0010:
invoke-virtual {v0, v1}, Ljava/io/PrintStream; -> println(Ljava/lang/String;)V
0x000e: return-void
(6) 最后再整理下 main method , 用容易理解的方式表示出来就是 。
ACC_PUBLIC ACC_STATIC LHello;->main([Ljava/lang/String;)V
{
sget_object v0, Ljava/lang/System; -> out:Ljava/io/printStream;
const-string v1,Hello, Android!
invoke-virtual {v0, v1}, Ljava/io/PrintStream; -> println(Ljava/lang/String;)V
return-void
}
看起来很像 smali 格式语言 ,不妨使用 smali 反编译下 Hello.dex , 看看 smali 生成的代码跟方才推导出
来的有什么差异 。
.method public static main([Ljava/lang/String;)V
.registers 3
.prologue
.line 5
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello, Android!\n"
index 0
class_idx 0x04
type_idx 0x01
name_idx 0x0c
class string Ljava/lang/System;
type string Ljava/io/PrintStream;
name string out
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 6
return-void
从内容上看 ,二者形式上有些差异 ,但表述的是同一个 method 。这说明刚才的分析走的路子是没有跑偏
的 。另外一个 method 是 <init> , 若是分析的话 ,思路和流程跟 main 一样 。走到这里,心里很踏实了

七、总结
到这里我们就解析完了dex文件的所有东东,讲解的内容有点多,在这里就来总结一下:
学习到的技术
1、我们学习到了如何不是用任何的IDE工具,就可以构造一个dex文件出来,主要借助于java和dx命令。
同时,我们也学会了一个可以执行dex文件的命令:dalvikvm;不过这个命令需要root权限。
2、我们了解到了Android中的DVM指令,如何翻译指令代码
3、学习了一个数据类型:uleb128,如何将uleb128类型和int类型进行转化


我们解析dex的目的是啥?
我们开始的时候,并没有介绍说解析dex干啥?那么现在可以说,解析完dex之后我们有很多事都可以做了。
1、我们可以检测一个apk中是否包含了指定系统的api(当然这些api没有被混淆),同样也可以检测这个apk是否包含了广告,以前我们可以通过解析AndroidManifest.xml文件中的service,activity,receiver,meta等信息来判断,因为现在的广告sdk都需要添加这些东西,如果我们可以解析dex的话,那么我们可以得到他的所有字符串内容,就是string_ids池,这样就可以判断调用了哪些api。那么就可以判断这个apk的一些行为了,当然这里还有一个问题,假如dex加密了我们就蛋疼了。好吧,那就牵涉出第二件事了。
2、我们在之前说过如何对apk进行加固,其实就是加密apk/dex文件内容,那么这时候我们必须要了解dex的文件结构信息,因为我们先加密dex,然后在动态加载dex进行解密即可
3、我们可以更好的逆向工作,其实说到这里,我们看看apktool源码也知道,他内部的反编译原理就是这些,只是他会将指令翻译成smail代码,这个网上是有相对应的jar包api的,所以我们知道了dex的数据结构,那么原理肯定就知道了,同样还有一个dex2jar工具原理也是类似的

作者关于dex文件的学习和总结,写的不错,支持。

感谢链接

dex文件格式学习的更多相关文章

  1. Android Dex文件格式(二)

    第三块: 数据区         索引区中的最终数据偏移以及文件头中描述的map_off偏移都指向数据区, 还包括了即将要解析的class_def_item, 这个结构非常重要,下面就开始解析   c ...

  2. Android Dex文件格式(一)

    dex是Android平台上(Dalvik虚拟机)的可执行文件, 相当于Windows平台中的exe文件, 每个Apk安装包中都有dex文件, 里面包含了该app的所有源码, 通过反编译工具可以获取到 ...

  3. Android逆向之旅---解析编译之后的Dex文件格式

    一.前言 新的一年又开始了,大家是否还记得去年年末的时候,我们还有一件事没有做,那就是解析Android中编译之后的classes.dex文件格式,我们在去年的时候已经介绍了: 如何解析编译之后的xm ...

  4. dex文件格式一

    一.生成dex文件 我们可以通过java文件来生成一个简单的dex文件 编译过程: 首先编写java代码如下: (1) 编译成 java class 文件 执行命令 : javac Hello.jav ...

  5. dex文件格式二

    一. dex文件头 (1) magic value 在DexFile.c   dexFileParse函数中 会先检查magic opt 啥是magic opt呢? 我们刚刚从cache目录拷贝出来的 ...

  6. [Android Security] DEX文件格式分析

    copy from : https://segmentfault.com/a/1190000007652937 0x00 前言 分析 dex 文件格式最好的方式是找个介绍文档,自己再写一个简单的 de ...

  7. ELF文件格式学习总结

    ELF文件格式学习总结 ELF文件格式学习总结1. 概述2. 目标文件结构3. ELF文件头3.1 魔数3.2 文件类型3.3 机器类型4. ELF文件内容4.1段表4.2字符串表(.**strtab ...

  8. dex文件格式三

    先来看看整体的结构,结构体定义在DexFile.h里面   在dexFileSetupBasicPointers中设置各个子结构体,当然是在解析DexHeader之后 源码在DexFile.c文件中 ...

  9. C语言的ELF文件格式学习

    最近的lab里面有ELF文件相关的,所以成这个几乎,学点ELF的东西. ELF,是一种文件格式.暂时,只看可执行文件的ELF文件格式. 首先,给出文件的格式的布局图: 光看这个很难理解,所以写一个小的 ...

随机推荐

  1. Arrays.Sort()中的那些排序算法

    本文基于JDK 1.8.0_211撰写,基于java.util.Arrays.sort()方法浅谈目前Java所用到的排序算法,仅个人见解和笔记,若有问题欢迎指证,着重介绍其中的TimSort排序,其 ...

  2. Java练习——String类练习

    需求: 给定一个字符串String str=" Hello World",返回长度,返回o第一次出现的索引,返回最后一个o的索引,把所有的l都替换为m,并把字符串str按空格分割为 ...

  3. 剑指 Offer 56 - II. 数组中数字出现的次数 II + 位运算

    剑指 Offer 56 - II. 数组中数字出现的次数 II Offer_56_2 题目详情 解题思路 java代码 package com.walegarrett.offer; /** * @Au ...

  4. vue 页面生成图片保存

    需求:将页面中的元素转成图片,支持保存或下载.要求下载的图片包含页面背景,头像,用户名,文本为"我的邀请码"和个人二维码. 实现:将页面绘制到canvas中,生成base64图片链 ...

  5. 3.DataFrame的增删改查

    以此为例 一.DataFrame的初步认知 在pandas中完成数据读取后数据以DataFrame保存.在操作时要以DataFrame函数进行了解 函数 含义 示例 values 元素 index 索 ...

  6. Java 语言基础 (初识Java语言, 变量和数据类型, 运算符, 流程控制语句, 数组)

    初始 Java 语言 Java SE -- Java Platform, Standard Edition 是 Java 平台的基础 Java SE 以前称为 J2SE, 可以编写桌面应用和基于 we ...

  7. 从零学脚手架(八)---webpack-dev-server源码分析

    上一篇中介绍了webpack-dev-server属性配置 这一篇就简单的梳理下webpack-dev-server内部实现. 由于涉及到源码解析,所以会涉及到一些比较难啃的知识,我会尽量进行简单化描 ...

  8. 攻防世界 resver catch-me

    catch-me asis-ctf-quals-2016 附件给了个压缩包文件,重命名,解压,获取到elf文件 程序有两处关键比较 第一处: 这里进行动态调试,得到v3=0xB11924E1, byt ...

  9. 使用docker搭建sonarqube

    sonarqube是一款代码质量检查工具,使用sonar扫描我们写过的代码,可以有助于检查出代码的bug.规范性和健壮性,有助于提高我们的代码质量. 一.安装docker 安装完成之后,命令行输入 d ...

  10. [源码解析] 分布式任务队列 Celery 之启动 Consumer

    [源码解析] 分布式任务队列 Celery 之启动 Consumer 目录 [源码解析] 分布式任务队列 Celery 之启动 Consumer 0x00 摘要 0x01 综述 1.1 kombu.c ...