赞
踩
与相互间共享了(除了UI和一些框架之外的)大部分底层代码的OS X和OS 不同,Android 中引入了大量的框架以及支持这些框架的运行时(Dalvik)。事实上,多数面对用户的特性和版本升级时所做的增强。都是以新增框架或添加 API的形式完成的,只有很少一部分涉及内核级的代码。
Android源码根目录 | 描 述 |
---|---|
art | 全新的ART运行环境 |
bionic | 系统C库 |
bootable | 启动引导相关代码 |
build | 存放系统编译规期及 generic 等基础开发包配置 |
cts | Android 兼容性测试件标准 |
dalvik | Dalvik 虚拟机 |
developers | 开发者目录 |
development | 与应用程序开发相关 |
device | 设备相关配置 |
docs | 参考文档目录 |
external | 开源模组相关文件 |
frameworks | 应用程序框架,Android 系统核心部分,由Java和C++编写 |
hardware | 主要是硬件抽象层的代码 |
libcore | 核心库相关文件 |
libnativehelper | 动态库,实现JI库的基础 |
out | 编译完成后代码在此目录输出 |
pdk | Plug Development Kit 的缩写,本地开发套件 |
platform testing | 平台测试 |
prebuilts | X86和ARM架构下预编译的一些资源 |
sdk | SDK和模拟器 |
packages | 应用程序包 |
system | 底层文件系统库、应用和组件 |
toolchain | 工具链文件 |
tools | 工具文件 |
makefile | 全局Makefile文件,用来定义编译规则 |
Android 运行时库的代码在art/目录中;硬件抽象层的代码在hardware/目录中,这是手机厂商改动最大的部分,根据手机终端所采用的硬件平台不同会有不同的实现。
getprop这个工具
由于所提供的东西正合厂商的心意,Android 获得了戏剧性的成功一一它并不是另一个Linux 发行版本,而是一个完整的“软件栈”(software stack)。术语“栈”意指多个层(layer)。
Android 提供的不光只是一个基本的内核和一堆能在 shell 中运行的二进制可执行文件,它还自带一个完整的GUI环境以及丰富多样的框架(外加便于使用的开发语言 Java),Android 向开发者提供了一个真正的快速应用开发(RAD,Rapid Application Development)环境,这样开发者就能直接调用框架中预先写好并经过良好测试的代码,只需寥寥数行代码就能使用诸如摄像头、运动传感器、GUIWidgets 等各种高级功能。
大致的估算 Android 和 Linux 在内核层上有95%的相似之处,而在用户模式下有约65%的相似性。
用户态这一级上,由于引入了两个全新的组件 Dalvik 虚拟机运行时和硬件抽象层(Hardware Abstraction Layer),再加上替换了 Bionic 的 glibc,以及提供了一个定制版本的 init(系统启动守护进程),Android 和 Linux 的分歧就大多了。
Android 取得成功的关键因素之一就是它丰富的框架集。没有这些框架,Android 可能会和其他一些嵌入式 Linux 发布版本一样混得很差,通过提供各种框架,Android 让应用可以很方便地创建进程,允许开发者使用高级的 Java 语言而不是底层的 C/C++语言进行编程。各种框架的不断增加也在进一步强化这一过程,因为有大量的用于进行图形、音频和硬件访问的API可供开发者使用。这一点与X-Windows和GNOME/KDE不同一-这些系统简洁得多,操作也是以更直接的方式进行的。
使用 Java 的包命名规则后,Android 的框架会根据它们各自不同的功能被分割在各自不同的命名空间(namespace)中。
包 | API | 作用 |
---|---|---|
android.app | 1 | 支持应用的各类常规操作 |
android.content | 1 | 提供使用Content的各种方法 |
android.database | 1 | 支持数据库一-主要是SOLite |
android.graphics | 1 | 图形支持 |
android.opengl | 1 | 支持OpenGL图形功能 |
android.hardware | 1 | 支持摄像头、输入以及传感器 |
android.location | 1 | 支持地理位置定位 |
android.media | 1 | 支持多媒体 |
android.net | 1 | 支持网络操作,iava.net中的各个API都是构建在其上的 |
android.os | 1 | 支持操作系统的核心(core)服务和IPC |
android.provider | 1 | Android内置的content-provider |
android.sax | 1 | SAXXML解析器 |
android.telephony | 1 | 支持电话的核心功能 |
android.text | 1 | 文字渲染 |
android.view | 1 | UI组件(类似于ios中的UIView) |
android.webkit | 1 | Webkit 浏览器控制组件 |
android.widget | 1 | 应用的窗口小部件(widget) |
android.speech | 3 | 提供语音识别和“语音”到“文字”功能的组件 |
android.accounts | 4 | 支持账号管理和身份认证 |
android.gesture | 4 | 支持定制的手势操作 |
android.accounts | 5 | 支持用户账户 |
android.bluetooth | 5 | 支持蓝牙 |
android.media.audiofx | 9(G) | 支持声音特效 |
android.net.sip | 9(G) | 使用SIP(会话初始协议,Session Initiation Protocol)(RFC3261)支持 VoIP |
android.os.storage | 9(G) | 支持Opaque Binary Blobs(OBB) |
android.nfc | 9(G) | 支持NFC(近场通信,Near Field Communication) |
android.animation | 11(H) | view和obiect的动画 |
android.drm | 数字版权管理(DRM)和版权保护 | |
android.renderscript | RenderScript (一种类似 OpenCL的计算机语言) | |
androidhardware.usb | 12 | 支持USB外围设备 |
android.mtp | 支持以MTP/PTP方式连接摄像头等 | |
android.net.rtp | 支持实时传输协议(Real-Time-Protocol)(RFC3501’) | |
androidmedia.effect | 14() | 支持图像和视频特效 |
android.net.wifi.p2p | 支持Wi-Fi直连(Wi-FiDirect)(点对点) | |
android.security | 支持keychain和keystore | |
android.net.nsd | 16) | 基于多播DNS(Multicast DNS)的邻接节点服务发现协议(Neighbor-Service-Discovery)(Bonjour) |
android.hardware.input | 输入设备监听器 | |
android.hardware.display | 17 | 支持外接或虚拟显示器 |
android.service.dreams | 支持Dream(屏保) | |
android.graphics.pdf | 19(K) | PDF Rendering |
android.print[.pdfl | 支持外接打印机 | |
android.app.job | 21(L) | 任务调度 |
android.bluetooth.le | 支持低功耗(LE,Low-Energy)蓝牙 | |
android.hardware.camera2 | 新的摄像头API | |
android.media.[browse/proiection/session/tv] | 支持多媒体浏览和电视 | |
android.service.voice | 支持语音解锁设备(比如说一句“OK Google”设备就解锁了) | |
android.system | uname()、poll(2) 和 fstatvfs | |
android.service.carrier | 22 | 支持SMS/MMS(CarrierMessaging 服务) |
android.hardware.fingerprint | 23(M) | 支持指纹采集器 |
android.security.keystore | 密码学意义上的密钥生成和存储 | |
android.service.chooser | 应用“深度链接”(deep linking) |
Android 使用的所有框架都是被打包在设备的/system/framework 目录下的数个Java格式的*.jar 文件中的,调用dexdump (或者dextra 工具)直接分析JAR 文件中的 classes.dex 文件。
从 Linux 的角度讲,所有的可执行文件都是 ELF 二进制可执行文件。Android 中的关键系统组件都是用 C/C++编写,并被编译成原生的二进制可执行文件的。而Dalvik虚拟机本身也是一个ELF格式的二进制可执行文件。
在Android中,二进制可执行文件通常都被放在/system/bin和/system/xbin这两个目录中(当然,还有一些重要的二进制可执行文件是放在/sbin 目录中的)。
shellehtc m8wl:/ $ ps | grep " /" -c1-22,55-
除了 Bionic 之外,Android 中还含有其他一些重要的库,这些库提供了对 Dalvik、框架和系统进程的支持。这些库被放置在源码树中各个不同的目录中,所以我们根据这些库 (在源码树中)的存放目录对它们进行分类。
这些库都位于system/core目录中,它们的主要作用是封装内核中一些Android特有的特性,或是在用户态中实现一些额外的功能。这些库包括:
Libcutils 一一这个库提供了一些便于用户使用的支持函数,以提供对内核导出数据(比如/proc/cpuinfo)的封装,支持 socket 以及 ASHMem 之类的Android 特有的一些特性。
liblog 一一这个库封装了 Android 的/dev/log 机制,提供了一个快速、有效、基于ring-bufer的日志机制。
libion一一这个库封装了在冰激凌三明治版中引入的ION Memory Allocator。0libnl2这个库提供了对 Linux NetLink socket 机制的封装。
libpixelfinger 一一这个库主要用于 SurfaceFlinger(Android 图形栈的核心部件,详见第2本)-名称中的Flinging指的是将两个或两个以上的输入合并在一起的操作。以图形画面为例,经过这一操作后,画面上的每个像素点上都是合并前各个输入的画面上对应点上的色彩混合的合成效果 (可能是 alpha-blend 混色的)。
libsuspend一一这个库中提供了电源管理中某些(特别是在与休眠和挂起操作系统相关的方面)封装好了的函数。其他次要些的库包括:
libdiskconfig一一这个库中提供了对磁盘(闪存的配置以及分区管理方面的封装好了的函数。
libcorkscrew一一这个库中的函数是供 debuggerd 分析栈使用情况以及应用(可以带符号链接的)“tombstone”(Android 应用崩溃或者被系统主动杀时留下的类似于“崩溃转储”的东西)的。
libmemtrack一一在硬件模块的帮助下(如果有的话),提供进程内存的trace 服务告
libmincrypt一一提供在数字签名过程中所需的基本的RSA和SHA-[1]256实现代码。
libnetutils一一提供简化的网卡配置功能,并且支持 DHCP。
libsync一一这个库中提供了内核中一些 Android 特有的特性的封装。。
libsysutils一一这个库中提供的函数主要是供 Framework[Client Listener Command]Netlink[Event Listener]、Socket[ClientListener]和 ServiceManager 等系统实用程序(utility)使用的。
libzipfile一一提供对 lib 的封装,以处理 zip 文件。Android 中 zip 文件的使用是非常广泛的一-甚至应用的安装包 (.apk 文件)也是一种特殊格式的 zip 文件。
位于 frameworks/目录中的库,为 Android 中的各种框架提供了原生代码级的支持服务。这些库又可以进一步细分。
frameworks/base/core/ini 目录中存放着非常重要的 libandroid runtime.so文件,这个库提供了对 Dalvik 虚拟机中的JNI的底层支持。在这个目录中还存放着拥有超过85个(Dalvik虚拟机级的)框架类的各种JNI组件。
firameworks/base/services/ini目录中存放着重要性毫不亚于上一个库的 libandroid_servers.so 文件,这个库提供了对各种 Android 服务的底层JNI 支持。
frameworks/base/native/android 目录中存放着 libandroidso,这个库提供了 assets、存储管理器(storage manager)等功能的原生代码接口。
base/libs 目录中存放的库中,包括名为 libandroidfw.so 和 libhwui.so 的库。前者提供了诸如 zip 文件解析、asset 管理等各种烦琐杂物的支持服务,后者(通过 OpenGL 和SKIA)提供了硬件加速的UI渲染功能。
还有一些位于 av 目录中的库,用来提供处理多媒体、音频和视频的函数。这些库包括
在 av/services 这个子目录中还存放着一些为这些服务提供进一步支持的库,它们是libcameraservice.so、libaudioflinger.so 和libmedialog.so。
native/libs目录中存放的库包括:
libbinder一一提供用来支持 Binder 的各种函数,详见第2本的深入讨论
libdiskusage一一这是一个很小的库,用来提供查询目录所占空间大小等功能的函数o libgui-一提供 surface 之类的各种 GUI抽象,工作在 libui.so 之上。
libinput一一这个库提供的函数基本上是仅供 Android 输入栈使用的,详见第2本的讨论。
libui一一提供与窗体和图形缓存相关的原生API,供surfaceflinger 使用。在native/子目录中还含有一个opengl/文件夹,其中存放着EGL和OpenGLES 的相关库详见第2本的讨论。
所有的库最终都是一起放在/system/lib目录中的
Android 会运行在许多不同种类的设备上 (平板电脑、手机、机顶盒、跑步机等),底层硬件在适用范围和支持方面都有极大的不同。
为了解决这个问题,Android 定义了一个硬件抽象层(HAL,Hardware Abstraction Layer),其目标是通过定义一个转换层,使对各种不同类型硬件的操作趋于标准化。硬件厂商可以在内核态中随心所欲地使用它们自己生产的设备的驱动程序,但是必须提供一个接口库 (shim),以 Android (特别是 Dalvik) 预期的方式与操作系统对接硬件抽象层上定义了从 Android 的角度看来,摄像头、GPS、传感器以及其他的硬件组件应该是个什么样子。这并不会妨碍厂商扩展或修改硬件的功能,只需要厂商把接口库放到system/lib/hw 目录里去就行了,然后HAL(硬件抽象层)(也就是 libhardwareso) 会自动加载它们。
Android 在内核中引入了一些它特有的特性 (Androidism)。其中的些被放在内核的 core 中,用条件编译语句#ifdef
加了一层保护,而剩下的则被放在了drivers/staging/android 目录中。(在3.10 或更高版本中)这些Android 特有的特性包括:
匿名共享内存(ASHMem一一这是一种允许进程间共享内存的机制。应用可以打开一0个字符设备节点 (/dev/ashmem)并创建一段内存空间,然后再把它映射 (map)到进程内存中去。这需要把工作限定在非全局可写(no world-writable)的目录中,并进行 SystemV 进程间通信。
Binder一一Binder是 Android 中所有进程间通信的关键,它源自于 BeOS。Binder 表现为一个所有进程都能够打开的字符设备节点 (/dev/binder)。Android 中的各种服务都注册在 Binder 这里,客户端可以在 servicemanager 的帮助下连上相应的服务。
Logger一一提供基于内核的 ring buffer,用以高速记录日志。Android 的日志并不是记录在文件中的,而是由一个字符设备节点(/dev/log)来承载的。Android Lollipop 版中专门为此新增了一个用户态守护进程一logd,我们将在第 5 章中讨论这个守护进程。
ION内存管理器一一ION 内存管理器是在冰激凌三明治版中被引入的,其功能是向内核驱动和用户态下的类似模块(通过/dev/ion)提供高效的内存分配服务。ION 替换掉了旧的Android特有的组件PMEM,ION 的设计标是为各种不同的SoC 架构中的内存管理提供一个统一的标准。
内存不足时的进程终止器一一这个组件改进自 Linux 原有的00M(Out-Of-Memory)进程终止器一它会在内存不足时,杀掉一些进程,以释放内存空间。不过 Android 中的这个组件是使用启发式的方法来寻找要被杀掉的进程的,而 Linux 原有的进程终止器是以一种更具确定性的方式控制杀进程的行为的,而且还允许定义内存压力等级。AndroidLollipop 版中为此专门增加了一个用户态守护进程 lmkd,详见第 5章的讨论。
RAMConsole一一这是一种保存内核崩溃时输出的诊断用数据(线程转储和最近的日志)的机制。不过较新版本的Android 系统已经弃用了这一特性,转而采用 Linux 内核中自有的一个功能了(详见第 2 章的相关讨论)。
Sync driver一一这是最新引入的 Android 特有的特性,引入它是为了快速同步基元(primitive),它主要是用在 Android 图形栈(特别是surfaceflinger)中的。
定时输出和GPIO一一使得用户态程序在用户态空间里就能访问 GPIO寄存器,并在间隔段时间之后自动设置GPIO寄存器的值。它的主要客户端是设备中的振动器(vibrator)功能一框架(通过硬件抽象层)可以把一个数值 (单位为毫秒)写入/sys/classtimedoutput/vibrator/enable 中,这样就启动了振动器,并让振动器振动了这个值规定的时间之后停下来。
wakelocks一一最初这是一个单独的用来控制电源管理并阻止内核进入休眠状态的Android 特有的特性。不过 wakelocks 现在已经逐步被并入到内核自己的 wakeup source机制中去了(在第 2本中将会详细讨论电源管理机制,在本书的官方网站上有一篇相关内容的预览章节)。
拿到Android 之后,删掉 Dalvik 及与之配套的框架,你就得到了一个既没有 GUI 支持,又不能用来构造/加入生态系统的操作系统。不过即使是这样一个操作系统,也仍然有其自身的价值。
Android 可能可以在这两个世界里(一个是需要丰富框架的世界,另一个是不需要 UI界面的世界)都混得风生水起。通过设置系统属性,即便没有 UI,系统也能完成特定的操作。
设备中的存储器被划分成一些互不相交的部分,每个部分都会被单独格式化,用于不同的目的。
Android 文件系统,如/system (安装操作系统的地方)、/data(存储用户数据的地方)等。
由于其记录的位置处于所有分区之外,所以在用户模式下一般是访问不到 GTP 表的,因此我们需要从二进制底层去访问存储设备。如果你的设备已经 root 了,那么你只要把开头的几个扇区复制出来,就能(在你的主机上)使用 file(1)命令去分析它并检查 GTP 表的内容了(见输出结果2-1)
On device: use chmod as root to allow adb to read drive sectors
root@htc m8wl:/# chmod 604 /dev/block/mncblk0
你可以通过查看/proc/partitions 文件中的内容,获知你 Android 设备的分区映射情况。这是一个标准内核/proc中的伪文件,其中记录了一张(系统中)所有块设备的列表。
在所有的设备中,这类分区都有一个共同的特性:它们是被硬编码到 Android 系统自身中会出现在源码树的各个不同的位置上。这些分区构成了操作系统的核心 (core)。的,
标准分区基本上都是可以 mount 的,仅有 boot 和 recovery 分区是例外,这两个分区是用Android 专用的 bootimg 文件系统(详见下一章)格式化的。表 2-1 中列出了这些分区。
名 称 | 文件系统 | 注释 |
---|---|---|
boot | bootimg | 内核+initramfs。含有内核和默认启动过程中所需的initramfs |
cache | Ext4 | Android的/cache分区:用来进行系统升级或recovery |
recovery | bootimg | 用于把系统启动到recovery模式下:内核+把系统启动至recovery模式的initramfs |
system | Ext4 | Android的/system分区-存放操作系统的二进制可执行文件和框架 |
userdata | Ext4/F2FS | Android的/data分区–存放用户数据和配置文件 |
Android 设备中有一张文件系统 mount 表,这张表位于/system/etc/vold.fstab 或者(在版本更高一些的Android系统中)/fstab.hardware目录下,在系统启动过程中会被vold(Volume Daemon.卷守护进程)加载。该表指出了哪些分区应该被自动 mount 上来(其作用类似于经典的 UNIX系统中的/ect/fstab)。
芯片组制造商经常需要一些分区用来存储支持芯片组工作的程序和数据。这里最著名的例子当属高通了。它的 MSM 芯片组 。
能在 Android 设备上找到的其他剩下的分区都是厂商专用的。
可以使用df或mount命令查看所有的mount点。
如果不考虑厂商造成的差异,Android 标准的分区还是能构成一个定义良好的文件系统层的。
注意,尽管文件系统是被存放在访问方式基本相同的存储器上的,但是你的 Android 设备上安装的各种硬件可以而且基本上都是五花八门的,所以很多厂商,包括谷歌自己,会额外为这些硬件提供一些专用的、被称为“proprietary blobs”的二进制可执行文件。每个设备所需的“proprietary blobs(需要被提取到AOSP的/system目录下的device/子目录中)都会在一个名为“proprietary-blobs.txt的文件中列出。
Android的root 文件系统(/)是 mount 自一个RAM Disk (initramfs)的。每次启动时,启动加载器 (fastboot)从 bot 分区中把这个文件系统的镜像加载到内存(RAM)中,并把它交给内核。除非知道设备被“刷机”(flashed),否则 root 文件系统(/)是不能被修改的。这是很重要的,因为root文件系统中含有系统最重要的组件/init,它能以 root 权限执行任何操作,并控制着系统的启动过程。
Linux在启动过程中,通常是用initramfs(以内核模块的形式)把驱动加载到内核中而且最终会在它被真正的文件系统取代后把它丢弃掉。不过在 Android 中却不是这样,Android的initramfs 还会驻留在内存中,并提供 root 文件系统(/)的功能。
目录或文件 | 注 释 |
---|---|
default.prop | 其中记录的是编译时/build/core/mainmk 中ADDITIONAL DEFAULT PROPERTIES变量中的值,init 将根据它(的内容)去加载其他系统范围内的属性文件(property)。加载只读的属性文件有利于强制执行安全保护 |
file contexts | Kitkat:记录SE-Linux 中的文件context。用于限制非授权用户访问系统文件和目录,将在第8章中讨论 |
/system/bin 目录中含有 Android 使用的各种原生可执行文件,此外,它也是存放各种调试工具的地方。具体来说,这些二进制可执行文件可以被分成 5 类
用来提供服务的二进制可执行文件:这类二进制可执行文件都是在系统运行过程中由/init 调用的,它们的调用路径会被写进/init 使用的rc 文件中。
调试工具:被归入这一类的是一些用于调试的原生二进制可执行文件。
UNIX 命令:为了让 shell 用户也能在 Android 上玩,UNIX 命令都被封装在一个单独的二进制可执行文件/system/bin/toolbox中。busybox是嵌入式系统中常见的多合一工具集,而 toolbox则是它在Android 中的定制版。toolbox 和 busvbox 并不是逐条提供各个UNIX。
调用 Dalvik 的脚本(upcall script):这些调用Dalvik 的脚本让用户通过 shell 与 Dalvik运行时框架交互,这多半是为了进行调试。
商定制的二进制可执行文件:从本质上讲,这类二进制可执行文件可以完全由厂商控制,但是这类二进制可执行文件通常都是些提供服务的程序或调试工具。
/data 分区是所有用户个人数据的存放地点。为此单独提供一个分区。
/data 与 Android 操作系统版本之间低合。系统升级和恢复时会擦除或者重写整个/system 分区,但却不会以任何方式影响用户数据。反之,通过格式化/data 分区,也可以快速重置设备,并擦除所有用户个人数据。实际上这就是在“恢复出厂设置”过程中所做的工作。
在用户需要时,/data 可以被加密起来。加密,不论怎么优化,由于在读写过程中分别要进行解密和加密操作,所以总会在一定程度上增加系统延迟。而在设计上,/system 分区中是没有敏感数据的,所以也就没有必要加密这个分区,因而也就规避了因加密这一分区而带来的延迟。
/data也可以被设为不可执行(即,用noexec 参数mount 该分区,或者强制执行SELinux)。
Android 的“安全存储”(Secure Storage) 特性,通常也被称为 asec,它提供了一种让应用能安全地安装到设备上的机制一一它确保在一个“合理的前提假设下”,用户没法把(安装在一台设备上的)应用拷贝到另一台设备上去-该过程通常被称为“预先锁定”(forward-locking),通过使用asec容器(container),应用可以被安装到任何地方。该特性从Android 2.2/2.2.1版(Froyo)起被添加进了 Android,Froyo 是第一个支持 SD卡之类的扩展存储的 Android 版本。实际上,asec 容器也可以被存放在 SD 卡里,但是没有密就没法使用。显然,密钥是需要被存放到其他什么地方的一Android 把它们放在系统密钥存储器(keystore,位于/data/system/misc/systemkeys)中。因此,这个“合理的前提假设”就是 root 用户是能够读取加密密钥的。
imgtool
各种Android设备都只能刷专门为相应型号的设备定制的镜像。厂商会提供一套系统镜像把它作为“出厂默认”的 Android 系统刷在设备上。一个完整的系统镜像是由以下几个文件组成的,刷机时它们会被写入各自对应的分区中。
Boot Loader:其中含有在启动阶段由应用处理器 (application processor)“执行的代码这些代码一般是用来寻找和加载 boot 镜像的,但同时也被用来进行固件升级和让系统启动到recovery 模式下。大多数 Boot Loader 还会实现一个小小的 USB栈(USB stack),通过它,用户在电脑上就可以控制启动和升级过程(通常是通过 fastboot)。Boot Loader会被刷到“aboot”分区里去,不过在有些手机上(比如HTC)这个分区会被称为“hboot’分区。
boot镜像:它一般是由内核和 RAMdisk 组成的,作用是加载系统。假设启动正常,RAMdisk 会被 Android 用作 root 文件系统(),其中的/init.rc 及相关文件规定了系统余下的其他部分该如何被加载。boot 镜像会被刷到“boot”分区里去。
recovery镜像:这个镜像同样是由内核和(另一个)RAMdisk组成的,一般用来在正常启动失败或者通过 OTA 升级时,把系统加载到“recovery 模式”。这个镜像会被刷到“recovery”分区上。
/system 分区镜像:这里存放的是一个完整的Android 系统,其中除了谷歌提供的二进制可执行文件和框架(framework)之外,还有厂商和/或运营商提供的类似的东西。
/data 分区镜像:这里存放的是“默认出厂设置”的数据文件,它们是/system 分区中程序正常运行所必需的文件。当手机需要重置到“出厂状态”时,也只需把这个分区镜像刷写到/data 分区,覆盖掉原来的数据即可。
尽管Android 设备的厂商可以随心所欲地去实现一个自己的启动加载器 (Boot Loader),但它们大多数还是选用了“LK”(Litle Kermel)启动加载器。LK启动加载器并不是 Android 源码树的一部分,但它还是可以从 CodeAuroraa和 googlesourcel3b)那里(至少部分地)下载到。
顾名思义,LK 只实现了启动功能中最基本的那一部分。是一个可启动的运行在 ARM 处理器上的二进制镜像。LK中只实现了启动加载器所应实现的最小的那部分功能,其中包括:
基本的硬件支持一这是在 LK 的 dev/(屏幕缓存,按键和使设备可以被用作 USB 目标设备的驱动等基本通用驱动)、platform/(SoC/芯片组驱动)和target/ (设备中特定硬件的驱动)等源码子树中提供的,没有这些,其他的一切全都免谈
找到并启动内核一一编写所有启动加载器的目的都是找到bootimage(下文会详细讨论),并把它的组成部分,也就是内核的镜像文件、ramdisk 和设备树(device tree)解析出来然后用给定的命令行,把系统的控制权交给内核。这一任务是由 app/aboot 来完成的。
基本的UI–如果用户中断了本该自动完成的启动过程(一般是通过执行adbreboot bootloader 命令,或者在设备刚一开始启动时马上按下特定的组合键来中断启动过程的),aboot 会提供一个很简单的文字界面,用户可以用设备上的物理键完成操作(用音量增大/减小键上下切换当前选中项,用电源键确认执行选中项对应的操作)。但是这会儿触摸屏还不能用。
支持 console-尽管在上市销售的手机/平板电脑中很少可以使用 console接口连接电脑但是开发母板还是支持通过串口(RS232/UART)提供 console 功能的。LK 在lib/console(它会被 app/shel1 调用)中提供了一个命令解释器(运行在一个单独的线程中),并且支持用户往里面添加新的命令。而在 lib/gfxconsole 中还提供了字体 (font)之类的基本图形功能。
使设备可被用作USB 目标设备-这使得 Boot Loader 可以通过一个名为 fastboot 的简洁的协议(本章将会在稍后讨论它) 与电脑进行通信。我们可以在 app/aboot/fastboot.c中找到这个协议的一个基本实现框架,不过厂商可以根据自己的需要,在这个协议中加入它们自己的 (OEM)扩展命令
支持闪存分区–由于在系统升级和重置手机(recovery) 的过程中,需要 Boot Loader能够擦除或重写某些分区,所以 LK 通过 lib/fs,也支持基本的文件系统。
支持数字签名一为了能够加载用SSL证书做过数字签名的镜像,LK 在 lib/openssl源码子树中,集成了 OpenSSL 项目的一部分代码。
Android 设备上的 Boot Loader 一般都会被加锁。也就是说,如果数字签名验证没有通过手机/平板电脑会拒绝刷机或用更新的镜像启动。厂商会在 ROM 中提供他们的公钥,该密钥被用来建立一条贯穿启动过程始终的信任链。这样,所有的启动组件(从rpm到sbl再到AndroidBoot Loader)就都可以被验证 (是否来自厂商或有无遭到修改)了。对这些组件的逆向分析表明,其中都有一个X.509v3 的证书,以及验证密钥时所必需的OpenSSL相关代码。
Android 的 Boot 镜像中存储的是操作系统的核心组件-内核和 RAM disk。
imgtool 这个工具除了能解压 boot.img,把其中的内核和 ramdisk 提取出来之外,也会自动地把内核镜像中的设备树文件提取出来(如果能找到它的话 )。
Boot或recovery镜像的另一个组件就是“initialRAM disk”,它通常也被简称为initrd。RAMdisk 提供了最初的文件系统,在操作系统启动时,它被用作根文件系统(/)。它会和内核一起被 BootLoader 预加载到 RAM (也就是内存,所以得名 RAM disk)中去,所以是可以直接访问而不需要经过任何特殊的驱动的。
使用imgtool 这个工具,你可以从 Boot 镜像或者recovery 镜像中提取出 RAM disk。
手机中的固件,就相当于 PC 机里的 BIOS(或者现在的 EFI),其主要部件是一个由硬件制造厂商提供的 bootROM。顾名思义,bootROM 是记录在只读 (read-only-memory)部件中的所以它一般都很小,只记录了一小段在手机刚一加电到加载 sbl之前必须要先执行的启动代码。它的作用是初始化其他的硬件部件,使它们处于至少是可以使用的状态。然后,bootROM 就会加载sbl (secondary boot lader,次级启动加载器)并把系统控制权交给 sbl。sbl是个软件,所以它不受硬件 ROM 容量的限制,可以记录更多的数据,所以也能去执行更复杂的初始化任务(比如,显示一个启动画面)。
Android 系统的 Boot Loader 大多都支持“fastboot”协议,谷歌把它作为Android 的一部分,放在官网上供下载”。fastboot 是一个简单的、基于文本的协议,这意味着它能使用在手机/平板电脑与电脑连接的 USB 信道上。
Android 内核的启动过程和 Linux的没什么太大区别。
按下物理键,会产生一个中断,然后该中断会被 Linux 内核捕获到,并被内核转换成一个输入事件,再作为一个EV KEY/KEY POWER/DOWN事件传给Android运行时(runtime)。和其他所有事件一样,该事件会依次交给Android 的InputReader 和InputDispatcher (这两个都是system server 线程),'后者会把该事件传递给 WindowPolicy 对象。如果按键的时间足够长(这个时长是定义在 ViewConfiguration的GLOBAL ACTIONS KEY TIMEOUT上的常量500毫秒)默认的 WindowPolicy 对象 (com.android.internalpolicyimpl.PhoneWindowManager)就会拦截到该事件,并(通过调用 GlobalActions.showDialog0方法) 在屏幕上弹出一个菜单。
输入“adb backup -all”命令会触发系统对所有应用做一个完全备份 (full backup)。
morpheus@Forge (~) adb backup -nocompress -al1
# UI is displayed on device.
Now unlock your device and confirm the backup operation.
morpheus@Forge (~)Is -l backup.ab
morpheus staff17158168 Jan 1 23:37 backup.ab
morpheus@Forge(~)% head -6 backup.ab
ANDROID BACKUP # MACIC
3 #Version(3=L)
0 #Compression (0 = False)
none # Encryption (none)
apps/android/manifest000600 0175001750000000036240010767 0ustar001
android
..
系统的重置(recovery)和升级的过程十分相似:系统都要用另一套代码去启动。用这套代码不会加载完整的操作系统用户界面,而是只加载一个最小的配置,用一个特定的二进制可执行文件/sbin/recovery 来执行我们目前正在讨论的这个启动过程。
不论是系统升级还是重置,一般都是在系统完全启动起来之后在图形界面里开始操作的。当然,也可以用adb reboot recovery 命令或者通过 fastboot 让手机进入 recovery 模式。
android.os.RecoverySystem 这个类也可以在recovery过程中传递参数。要传递的参数会被写入/cache/recovery/command 文件,然后这个类会去重启系统,并在重启过程中传一个参数给 BootLoader,让它从recovery 分区而不是从 boot 分区启动。记得在之前的讨论中我们讲过,recover分区和 boot分区中的内容差不多,都存有 RAM disk 镜像。只不过在从recovery 分区启动时,加载的是/sbin/recovery,而不是完整的Android 操作系统。
注意,作为使用定制 ROM 的先决条件,手机的 Boot Loader 必须已经解锁了,否则是没办法把自制的 ROM刷到手机里的。
风险:
了解了上述这些危害之后,我们就明白在接下来的操作中,适当地加点小心总是不会有错的。大多数的 Boot Loader 允许你在不修改现有的任何一个分区中数据的情况下,从另一个镜像启动(通过 fastboot)。这样做的目的就是为了提供一个安全的测试环境。接下来我们就来讲讲定制一个 ROM的方法。
morpheus@Forge(-/Android/Book/tmp)% adb push bootimg.img/data/local/tmp
morpheus@Forqe(~/Android/Book/tmp)% adb shell
shell@Android /$ grepboot /proc/partitions
start_kernel()
initcall 机制
kernel_init线程
rest_init()
do_basic_setup()
do_initcalls()
android_reboot()
reboot()
onBackup()
所有的 UNIX 系统都有一个特殊的进程,它是内核启动完毕后,用户态中启动的第一个进程,它负责启动系统,并作为系统中其他进程的祖先进程。传统上,这个进程被称为 init,Android也沿用了这个约定俗成的规矩。
不过Android 中的init和UNIX或Linux 中的init还是有很大不同的,其中最重要的不同之处在于:Android 中的 init 支持系统属性,并使用一些指定的r 文件(来规定它的行为)。在介绍完这两个特性之后,我们将拼凑出一幅 init 运行流程的全景图:它的初始化过程和在主循环(run-loop)中要做的操作。
另外,init 还扮演着其他的角色一它还要化身为 ueventd 和 watchdogd。这两个重要的核心服务也是由 init 这个二进制可执行文件来实现的一通过符号链接的方式加载。
和大多数 UNIX 内核一样,Linux 内核会去寻找一个路径和文件名预先规定好的二进制可执行文件,并把它作为第一个用户态进程执行。在桌面 Linux 系统中,这个可执行文件一般是/sbin/init,它会去读取/etc/initab 文件的内容,以获取所支持的“运行级”(run-evels)、运行时。
ee | Linux的/sbin/init | Android的/init |
---|---|---|
配置文件 | /etc/inittab | /init.rc以及它导入的任何文件[通常 是init.usb.rc ( 有时 是和 init.hardware.rcinit.hardware.usb.rc)] |
多种配置 | 支持:“运行级”(run-levels的概念(0:系统停机状态1; 1:单用户工作状态;2-3:多用户状态:·.·…·)。每个“运行级”都会从/etc/rcrunlevel.d2那里加载脚本 | 没有“运行级”的概念,但是通过触发器(trigger)和系统属性提供了配置选项 |
看门狗( Watchdog)功能 | 支持:用respawn关键字定义过的守护进程 会在退出时重启一一除非该进程反复崩溃, 在这种情况下,反复崩溃的进程会被挂起几分钟 | 支持:服务默认是应该保持活跃的,除非启 动脚本中启动它时使用了oneshot 参数。服 务启动时还可以使用critical参数, 这会使系统在该服务无法重启时被强制重启 |
收容孤儿进程 | 支持:/sbin/init会调用 wait40)系统调用去 获取(孤儿进程的)返回码,并避免出现僵尸进程 | 支持:/init注册了 一个SIGCHLD信号的处理模块 SIGCHLD信号是内核在子进程退出时 自动发送的大多数 进程会默默地调用 waitNULL)清理掉已退出的 子进程,而不去管它的退出码是什么 |
系统属性 | 不支持:Linux的/sbin/init 不支持“系统属性”这个概念 | 支持:/init 通过共享一块内存区域 的方式,让系统中的所有进程都能读取 系统属性(通过 getprop),并通 过一个名为“property_service” 的socket让有权限的进程能 够(通过setprop)写相关的属性 |
分配 Socket | 不支持:Linux的init 不会向子进程提供socket,这个功能是交给 inetd 的 | 支持:/init会绑定一个 UNIXdomain socket(从I版开始,是 seqpacket socket)提供给子进程,子进程可以通过 android get control socket 函数获取到它 |
触发操作 | 不支持:Linux只支持非常特殊的触 发控作,比如ctrl-alt-del和UPS 电源 事件,但是不允许任意的触发操作 | 支持:/init 可以在任何 一个系统属性被修改时,执行记录在 trigger 语 句块中的,由任何一个(或某些)用户预先写好的指令 |
处理uevent 事件 | 不支持:Linux依靠的是hotplug守护进程(通常为 udevd) | /init也会化身为ueventd, 用专门的配置文件来指导其行为 |
/init 是个静态链接的二进制可执行文件。也就是说在编译时,它的所有依赖库都已经被合并到这个二进制可执行文件里去了。这样做是为了防止仅仅因为缺少某个库或者某个库被破坏而造成系统无法正常启动的情况发生。在/init 刚被执行时,只有和内核被一起打包放在 boot 分区上的RAM disk(也就是root 文件系统)被 mount了上来,换句话说,系统中只有/和/sbin。
Android 的系统属性提供了一个可全局访问的配置设置仓库。它们在形式和功能上都和用各种MIB 数组作为参数调用的 sysctl(2)有些类似,只不过是在用户态中,由init 实现罢了。
因为 imit 是系统中所有进程的祖先,所以只有它才天生适合实现系统属性的初始化。在它刚开始初始化的时候,init 中的代码会调用 property_init0去安装系统属性。这个函数(最终)会调用map prop area0函数,并打开PROP FILENAME(这个宏定义指的是/dev/ properties ),然后在关闭这个文件的描述符之前,用 mmap(2)系统调用以“读/写”权限把这个文件的内容map 到内存里。之后,init 又会再次打开这个文件,不过这次用的是 O READONLY,然后再unlink 掉它。
system property area 内存区域进行写操作的权力,则被init 独揽了。
watchprops 工具是用来实时监视系统属性变化情况的。尽管我们可以在设备启动过程中尽可能早地启动这个工具(在电脑端上输入 adb wait-for-device 或 adb shell watchprops 命令)。
init 的主要操作是加载它的配置文件,并执行配置文件中的相关指令。
.rc 文件是由 trigger 语句块和 service 语句块构成的。trigger 语句块中的命令,会在触发条件被满足时执行;而 service 语句块中定义的是各个守护进程,init 会根据命令启动相关守护进程,也可以根据相关参数(“OPTION”类关键字)修改服务的状态。service 语句块由关键字 service开头,后面跟着服务名及命令行。
trigger 语句块由关键字 on 定义,后面跟着一个参数一这个参数既可以是预先规定的各个启动阶段 (boot stage) 的名称,也可以是一个 property 关键字,后面跟冒号加“属性名称=属性值”这样的表达式(在这种情况下,如果触发条件被满足,相应属性的属性值会改成指定的值)。在执行指定的动作 (action)或命令(command)时,init 会分别把属性initaction或init.command的值设为当前正在执行的动作的名称或当前正在执行命令的名称。
init_parser.c 中的代码在解析 rc 文件时,能够识别出两类关键字:
COMMAND[也就是在trigger 语句块或各个启动阶段中要执行的动作 (action),这类关键字只在“on”关键字开头的语句块中有效]
OPTION (修改相关服务的状态,这类关键字只在“service”关键字开头的语句块中有效)。
尽管 Android 有一个专门的卷管理守护进程 (vold),init 仍需亲自执行一些 mount 操作。我们回忆一下,当init 进程刚启动时,只有root 文件系统被 mount了上来,此时既没有/system也没有/data,所以init 就面临这样一个窘境: 它至少把/system 给 mount 上来吧,只有这样(/system 上的)各个守护进程(也包括 vold 在内)才能启动。显然,这是个很关键的操作:如果无法把文件系统mount 上来,/init 会把系统重启到recovery 模式下。
作为大多数守护进程的样板,init 代码的指令流程完全遵循建立服务的经典模式:初始化然后陷入一个循环中,而且永远不想从中退出来。
init 进程的初始化工作由以下步骤组成:
检查自身这个二进制可执行文件是不是被当成 ueventd 或者 (KitKat 及之后版本中的)watchdogd 调用的。如果是的话,余下的执行流程就会转到相应的守护进程的主循环那里去,我们将在本章稍后讨论这一部分内容。
创建/dev、/proc 和/sys 等目录,并且mount 它们。
添加文件/dev/.booting(通过打开这个文件,然后再关闭它的方式),在启动完毕之后这个文件会被 (check_startup) 清空(见图4-2)。
调用 open_devnull_stdio0)函数完成“守护进程化”操作(把/stdin/stdout/stderr 链接到/dev/null 上去)。
调用 klog_init0)函数创建/dev/__kmsg__ (Major1,Minor 11)
,然后立即删除它。
调用property_init0)函数,在内存中创建__system_property_area 区域,相关讨论见本章之前的“系统属性”一节。
调用get_hardware_name()函数,读取/proc/cpuinfo 伪文件中的内容,并提取出“Hardware一行的内容作为硬件名 (hardware name)。以这种方式获取硬件名,法子虽然糙了点但确实有效 (至少在使用ARM 体系结构处理器的设备上是能够正常工作的)。
调用 process_kernel_cmdline0)函数,读取 /proc/cmdline伪文件中的内容,并把所有androidboot.XXX的属性都复制一份出来,变成ro.boot.XXX
。
在 JellyBean 及以后的版本中,这里要初始化 SELinux。在 JellyBean 版本中,如果没有用#ifdef定义HAVE SELINUX还不会启用SELinux,而到了KitKat版,SELinux 就是默认启用的了。SELinux的context 是放在/dev 和/sys 里的。
接着还要专门检查一下设备是否处于“充电模式”(根据一个名为“androidboot”的内核参数进行判断)。如果设备处于“充电模式”的话,会使 init 跳过大部分初始化阶段,并且只加载各个服务中的“charger”类(当前也只有“charger”守护进程有这个类)。如果设备并没有处于“充电模式”,那么init 将会去加载/default.prop,正常执行启动过程用
init_parse_config_file()函数去解析/initrc 脚本文件。
init 会把init.rc 文件中各个on 语块里规定的action (用action for each trigger0函数)以及内置的 action (用queue_builtin_action0)函数)添加到一个名为 action queue 的队列里去。
查看/proc伪文件系统,来了解init 进程打开了哪些文件描述符:
/proc/1 # ls -l fd
Android 设备经常会根据用户的选择,修改自己通过USB 线连上电脑后,该以哪种设备的面目出现:到底是作为一个大容量存储设备,还是模拟一台数码相机;是不是要启用/关闭ADB调试功能,等等。Android 设备中没有一个专门的守护进程来处理这些问题,所以这一重任就落在了init的肩上一由它来和内核中的USB组件通信。
Android 设备用USB连上电脑后表现为哪种设备是由系统属性sysusb.config 决定的。
property_init()
man_prop_area()
__system_property_area
system_properties.c
toolbox工具:
Android 在后台运行着好几个守护进程 (daemon)用来完成各种杂七杂八的事务性和操作性工作。这些服务的定义大多数散落在/init.rc 的各个 service 语句块中,它们在/initrc 里出现的顺序并不重要,唯一影响它们启动顺序的是它们所属的分类。被分在“core”类中的服务会首先启动,接着启动的是被分在“main”类中的服务。rc 文件中也可以定义一个名为“late start”的服务类,供那些依赖于/data 分区中的数据的服务使用,不过默认的服务里没有一个是属于这一类的。在这一节中,我们将使用这一服务分类方法一-只是,因为大多数服务都属于“main’大类,所以我们还将把这些服务按照其功能进一步细分成各个子类。
紧接着上一章中我们对init 进程的讨论,将讨论“core”类的服务adbd、servicemanager,healthd,Imkd,logd。
其他所有的服务通常都被归在“main”大类下:
所以接下来,我们又会把它们细分为网络类服务(Network Services,这类服务包括netd、mdnsd、mtpd和rid)
图形及媒体类服务(Graphicsand Media Services,这类服务包括 surfaceflinger、bootanimation、mediaserver 和 drmserver),
剩下的服务很难归类,所以我们只好把它们全都放在“其他服务”这一分类下,这些服务包括installd、keystore、debuggerd、sdcard,以及最后一个服务Zygote。
在用户模式启动过程中,首先运行的是被归在“core”类中的服务,这些服务都不需要访问/data 分区,所以无论这个分区有没有被 mount上来,都不会影响这些服务的运行。
如果你是从头一路读到这儿的,我想 ADB 就不用我多介绍了吧:电脑和移动设备就是通过这一媒介 (也就是众所周知的 Android 调试桥) 进通信的。我们既可以通过 adb 命令直接使用Android 调试桥,也可以通过 ddms 命令间接地使用Android 调试桥。
在Android的默认配置下,adbd 作为提供ADB 服务功能的设备守护进程,定义在/initrc中。当系统属性 sys.usb.config 的值中含有字符串“adb时,可以由/init.usb.rc 中的相关指令启动这一服务。
注意,在默认情况下,adbd 是以 uid root 的权限启动的。不过它确实还会主动把自己降到uid shell:shell,以及另几个组 (group) 的权限。
当然,我们也可以让 adbd保留 root 权限(在电脑端执行“adb root”命令,这会把系统属性 service.adb.root 设为 1),不过 adb 的源码中也对此功能做了限制。
adbd 通常使用的是有init进程安装的UNIX Domain socket-/dev/socket/adb,但是当设备通过USB线与电脑相连时它会使用/dev/android adb(或者,从L版开始,改成functionfs 伪文件系统中的文件/dev/usb-fs/adb/ep##)。后者是一个由USB Gadget 驱动创建的设备节点。(在通过网络进行 adb 连接时)adbd 也会去监听系统属性service.adb.tcp.port 中指定的 TCP 端口,如果这个系统属性不存在的话,该端口也可以由系统属性 persistadb.tcp.port 指定。
输入 dumpsys usb 命令,你就能看到当前的USB 调试状态(USB Debugging State)以及当前使用的adb keys。
servicemanager是Android中IPC机制的关键组件。这个二进制可执行文件尽管看上去很小但却是非常重要的程序,没有它,系统内部进程间的通信就会受到很大的影响。
让 servicemanager 变得如此关键,有如此众多的服务有赖于它的原因是:它具有服务映射器 (service mapper)的功能。事实上任何一种IPC 机制都需要有这样一个映射器,好让客户端进程能够找到并连上服务端进程,UNIX 有它的 portmapper (供 sunrpc 使用),Windows 有它的DCE endpointer mapper,而 servicemanager则是Android中实现这一功能的组件。
了解了这一点,/initrc中对servicemanager 的定义就好理解多了;因为一旦servicemanager 挂了的话,客户端进程就没法找到这些服务了。如果 servicemanager重启了一下,它就会被刷回到一张白纸的状态一一这就要求各个相关的服务重新去注册一下,以便能被客户端进程找到。
“健康度守护进程”(health daemon)的意思是:该服务会周期性地执行检测综合性的“设备健康值”的任务。该守护进程也会把自己注册为 BatteryPropertiesRegistrar 服务(在 L版中,则是 batterypropreg 或者 batteryproperties)。完成了这个注册之后,healthd 提供了几个框架服务(例如 BatteryStatsService),用它从sysfs 那里得到的数据,更新电池电量信息。
注意:sysfs 中的伪文件(/sys/class/power supply/*)的名称都是标准的一-事实上,它们都是指向特定的平台设备节点的符号文件,而这些设备节点的名称,在不同的设备上可能是各不相同的。
lmkd 提供了内核中的LMK (Lw Memory Killer)机制的一个接口,这是个Android 特有的东西(也就是说,它是个只存在于 Android 内核中、Linux 中没有的特性)。lmkd 允许 Android高级用户能够(部分)控制 Linux的Out-Of-Memory(00M)机制[即,在系统物理内存不足时通过杀掉(kill)部分进程的方式,缓解内存压力的机制]。
Android L版中新增了一个需求呼声很高的日志机制,它是由 logd 守护进程来实现的。相对于旧版 Android 使用/dev/log/中的各个文件(这些文件是由内核中的各个对应的 ring buffers实现的)记录日志的方式,这个守护进程提供了集中式的用户态日志记录服务。这不光解决了ring buffer 的主要缺点(它们太小而且需要常驻内存)而且还让logd 能和SELinux 的审计操作结合在一起一通过将自身注册为auditd,就能从内核中(通过netlink)接收到SELinux 消息并把它们记录到系统日志里去。
Android 的 log 机制是由 liblog 支持的,因此应用可以名正言顺地无视 liblog 之下的 log机制的具体实现。
客户端程序可以连到/dev/socket/logd 上,用一组协议命令来控制 logd。通常这样做的客户端程序是 logcat 命令。
strace logcat
service list
Android中的vold是卷管理 (volume-management)守护进程。这个概念[使用一个用户态中的守护进程,在内核检测到文件系统(卷)时,自动mount它们] 最初是源自 Solaris 操作系统的(尽管现在在 Solaris 操作系统上已经没有 vod 了)。
要成为一个能真正完成mount 卷任务的守护进程,vold 需要一个配置文件一一在其中列出所有已知的文件系统以及它们的 mount 点。这个文件也被称为 file system table(文件系统表),或简称为“fstab”。
vold 内部可以分为三个组件:
VolumeManager:负责维护卷的状态,并处理各种卷操作,这个(单独的)类提供了所有面向框架(framework-facing)的功能。
NetLinkManager:负责监听 block 子系统使用NetlinkHander 传递给 VolumeManager的内核NetLink 事件
CommandListener:负责监听/dev/socket/vold 这个 socket,它是用来接收框架(framework发出的命令,并传递这些命令的执行结果,或其他从 VolumeManager 那里接收到的事件。
vdc monitor
从Honeycomb版开始,Android 引入了对磁盘加密的支持。通过扩展 Linux 中dm-crypt 机制(它同时还是 asec 机制的基础),Android 能够加密整个用户数据分区。系统分区仍是不加密的,毕竟系统总是要以某种方式启动。
Android 可以在/data 还没有被 mount 上来之前,通过用户解锁屏幕的操作获取解锁口令或解锁图形(如果用户已经启用了屏幕保护锁),并使用comandroid.settings.CryptKeeper 这个activity,提示用户输入解锁设备所需的口令。
在系统启动的过程中,init 调用 fs mgr 去 mount 文件系统,如果没有文件系统是加密的,它就会把系统属性ro.crypto.state 设为“unencrypted”,然后去做“nonencrypted”这个 trigger语句块中规定的动作一-通常是启动被归在 late start 类中的服务。
反之,如果有某个文件系统被加密了,那么显然在获得口令之前,加密文件系统是 mount不上来的。这时,fs mgr 会用一个 tmpfs 代替这个文件系统,mount 在规定的地方,然后向 init返回1。于是 init 就会把系统属性 ro.crypto.state 设为“encrypted”,并通知 vold 需要解密/data分区(具体方法是:把系统属性 vold.decrypto 设为1)。
mount /data 分区首先要加载一个UI框架,而这又会需要向/data 分区写一些文件······解决这个死结的方法是先在/data 上 mount 一个tmpfs (临时文件系统),而且由于这是个临时文件系统,所以现在写入的任何数据都不会被保留下来。在系统属性 vold.decrypto被设为1时,SystemServer 将只会运行属于“core”类的应用和服务。
加密文件系统的操作与加载加密的文件系统的操作十分类似。再重复一遍,UI 是由CryptKeeper 生成的,并由 MountService 提供 Dalvik 虚拟机级到 vod 的桥接。UI提示用户输入加密口令,并确认设备正在充电状态 (这是为了防止加密操作进行到一半手机没电了·…)。然后MountManager 中的encryptStorage0方法会被调用,该方法会 vold 发送cryptfs enablecrypto命令,这个命令的第一个参数可以是 wipe(在加密之前格式化/data 分区),也可以是 inplace,第二个参数是加密口令。
在收到该命令并验证它可以执行后,vold 会把系统属性 vold.decrypt 的值改成trigger shutdown framework。这会导致init停止所有服务(“core”类中的服务除外),这实际上就是让系统回到了系统刚刚启动、用户还没有输入用户口令的状态。然后,/data 分区会被安全地unmount 下来。vold 随后会把 vold.encrpyt_progress 设为初始值0,并把系统属性 vold.decrypt的值设为 trigger restart min framework,让init 重新启动 main 类中的服务。
再说一遍,CryptKeeper 是作为一个全屏应用被加载的。它把读取到的 vold.encrpyt_progress的值,显示在 UI 的进度状态条中。如果所有的操作都顺利完成了,进度条也就到 100%了。如果有操作没能成功完成,vold.encrpyt progress 的值就会被设为一个error 字符串。在L版中提供了“可恢复加密”(resumable encryption)这一功能,可是如果恢复失败了,用户就别无选择,只能把设备重置为出厂状态了。
Android 使用一个专门的守护进程来控制各个网络接口并管理它们的配置。如果你使用过Android 中的 Tethering、防火墙或者 Wi-Fi 热点等特性,甚至只要用过最基本的DNS 查询功能你都能把自己视为netd 的至尊用户。
Android 中的图形和多媒体服务是系统尽可能提供最佳用户体验的不可分割的一部分。这一节中只能大致介绍一下这些服务,更详细的讨论要留到第 2 本深入讨论声音和图形内部实现机制时进行。
surfaceflinger 居于 Android 图形栈的中心位置上。服务名中的“flinger[有时也被称为compositor”(排字工)] 这个词是指将一个或多个层(Layer)的输入整合在一个层中予以统一输出的角色。在 surfaceflinger 中,被整合的是图形“界面”(surface)(android.view.Surface 的各个实例),它既可以是框架提供的各种用户层输出的 view,也可以是开发者提供的 raw 或GLSurface。在与 surfaceflinger 通信之前,框架会通过 servicemanager 寻找 surfaceflinger 服务,所以surfaceflinger 并不需要使用 socket,这也使它在/initrc 中的定义非常简单。
bootanimation 这个服务是/system/bin 目录下一个很小的二进制可执行文件,它专门被surfaceflinger 用作在它[和其他多媒体 (media)框架] 被加载时的占位符。
这个二进制可执行文件实质上是个很简单的程序一一它启动之后,只是去寻找 3 个 zip 包。
/system/media/bootanimation-encrypted.zip: 当系统属性 vold.decrypt 被设为1(这表示文件系统已经被加密了)时,需要使用这个压缩包。
/data/local/ bootanimation.zip:(高级)用户可以把他们自己的开机动画用adb push 命令上0传到这个位置上,如果这个文件存在的话,它就会替代系统自带的启动动画。
/system/media/bootanimation.zip:系统默认的开机动画,通常是由厂商提供的。
bootanimation 会依次寻找这三个文件,如果三个文件都没有找到,bootanimation 默认会交替显示两张图片 android-logo-mask.png 和 android-logo-shine.png,这两张图片都藏在/system/framework/framework-res.apk 的/assets/images 目录里。
bootanimation 最有意思的一点是:它的 raw(即不需要使用框架)图形权能。因为它是最先被启动的几个服务之一,那个时候框架还没有初始化呢! 所以 bootanimation 只能自己动手丰衣足食,以直接调用底层的 OpenGL 和 SKIA 的相关函数的方式自给自足。正是因为需要直接访问设备的帧缓存(/dev/graphics/fb0),所以它才需要以 graphics 这个uid[(/dev/graphics/fb0这个设备节点的所有者 (owner)] 的身份来运行。在第 2本中,我们还将更进一步地讨论与底层图形相关的函数。
morpheus@Forge(~)$ adb pull /system/media/bootanimation.zip
“main”大类中剩下的这些服务都很难被归为一类-因为它们分别体现了系统支持的各个不同的方面,但这并不会在任何程度上减弱它们重要性。
installd 这个守护进程是负责安装或卸载App 包(package)的。不论你的App 包是如何安装的[不论是直接从谷歌市场(Google Play) 下载安装的,还是用adb install 安装的],最终都还是要调用 installd 的。不过这个守护进程本身,还是被动式的:它会去监听一个由init 安装的socket,Android 框架产生的命令将由这个 socket 传递给它。在这个守护进程的/initrc 中定义了这个 socket。
并非所有的Android 设备都必须支持 SD卡,不过在Android 系统中还是有一个sdcard 守护进程,提供用户态中对 SD卡的支持,其中包括在不支持权限管理的 FAT 文件系统上强制使用权限管理。
Zygote 服务以提供了一台初始化完毕的空 Dalvik 虚拟机的形式(就差加载 main 类了),对所有 Android 架运行时服务都提供了核心支持。
Zygote“真正的名字”是 app_process。不过不管怎么说,“Zygote”被接受得更为广泛,某种程度上已经成了这个进程的“绰号”了。就像 Zygote 在生物学意义上的含义一样,这个进程也有着无限多种发展的可能性,它可以加载任何给定的 Dalvik 类,并且可以将进程的拥有者切换为任意用户,不过这类演化是个不可逆的单向过程。
代码清单5-23 Zygote在/initrc 中的定义
service zygote /system/bin/app process -Xzygote /system/bin
-zygote--start-system-server
Classmain
socket zygote stream 660 root system
onrestart write /sys/android power/request state wake
write /sys/power/state ononrestart
restart mediaonrestart
onrestart restart netd
命令行中余下的部分是传进来的参数,在这些参数中,除前面缀有“–”的那两个以外都是直接传给 Dalvik 虚拟机的。而最后那两个参数则是传递给 app_process 进行处理的,它们的作用是令虚拟机加载 Zygote 类,并 fork0出一个被用作system server的新进程来。
system server 进程会继续加所有的Android 运行时框架,而Zygote 将会去绑定(bind)它的 socket (/dev/socket/zygote),以监听外部发来的请求。当有新的请求发过来时,这个请求中应该包含一个要求加载的类的类名,于是 Zygote 只需 fork0)出一个新进程,并在新进程中加载指定的类就行了。这样就完成了一个新 App 的启动过程,个新的“生命”就又诞生了。
读到这里你可能不禁要问:不就是启动一个新的 App 嘛,有必要搞得这么复杂吗?还真有必要!因为这样做有不少好处,还不止是在一个方面,这些好处体现在两个方面。
应用启动所需的时间会被大大缩短:不论是哪个 App,所有的虚拟机都必须按照某种一样的而且是确定的方式初始化。加载 App 的类只不过是这个操作的最后一步罢了,但是这一操作产生的实际开销却主要是花在加载大量构成了 Android 各种丰富的框架的运行时类上的。如果你把App 的加载想象成一场长跑赛,那么使用 Zygote 让Android 可以在临近终点时才突然插进比赛,只用跑最后一个冲刺即可一这样就省去了前面加载各种库的时间,耗时当然会相对短很多。
优化共享内存:因为所有的虚拟机都是从 Zygote 这里 fork()出来的,所以它们无疑能够享受到由内核实现的内存共享的优势。特别是,尽管每个app_process 的实例都有它自己的虚拟内存,但这些内存中的大部分是只读的(因为其中存放的是类的实现代码),在物理内存中只需保留一份,让各个进程共享就够了。
这一章中讨论了 Android 的各个原生 (native) 服务,这些服务都是根据/initrc 中的各个service 语句块从init 进程那里 fork 出来的守护进程。这些原生进程负责完成各种杂七杂八的事务性操作,并提供对系统框架的底层支持
不过框架服务(framework service)就完全是另一回事了一由于这些服务的巨大数量以及需要详细讨论相关细节,我们把相关讨论留在第 2 本中进行。
cryptfs restart命令
/system/bin/sgdsk
这两个单词很像:column, volume
ls -l /proc/pid/exe
所有的App都一定是Zygote的子进程,唯一的例外是那些直接通过命令行直接调用app_process的情况,比如upcall脚本。
上一章一部分Adnroid运行时服务有一个共同特点:它们都是原生级(native-level)的服务,这些服务都是用c++实现的,而且在Java层上也没有直接的可用的编程接口。它们应该归类为支持操作系统自身的服务。
不过 App 使用的却是完全另一个服务集 (set of services)中的服务,这些服务是由 Dalvik 虚拟机级的框架提供的,带有特定接口。这些服务都有一个 Java 语言的接口,而且其中的大多数是运行在同一个进程(system_server)的上下文环境中的,并且可以在 servicemanager 的帮助下被找到。
句柄就是数组下标
回顾一下上一章,你就会发现,这个叫 servicemanager 的服务被 init 归类在“core”类中,其他一些关键服务都依赖于这个服务,如果它崩溃了,这些服务都必须要重启。更夸张的是,servicemanager 是由critical关键字修饰的,也就是说,如果这个服务重启后又崩溃了,init 会不断地重启它。要是这样还是不能解决这个问题的话,系统就会重启或者进入 recovery 模式。
使 servicemanager 如此极端地重要的原因在于它的功能:它是作为其他操作系统服务的定位器 (locator)或称索引目录(directory)而存在的。任何一个应用或系统组件想要使用其他服务(完成某个操作)时,都必须先到 servicemanager 这儿来查询,获得一个句柄 (handle),然后才能继续执行操作。类似地,服务也没法直接告诉客户端自己在哪儿,它也必须先到servicemanager 这儿注册一下,然后才能被需要使用它的客户端找到。
端点映射器
因此servicemanager也只是一个很小的二进制可执行文件,所完成的操作也很简单:它首先调用binder open获取/dev/binder的文件描述符,然后再调用binder becomecontext manager注册 ServiceManager服务,该服务的handle索引值为0。再接下来,servicemanager就会进入一个名为binder loop的无限循环中。在这个无限循环里,servicemanager会让自己进入阻塞状态,直到/dev/binder中产生一个transaction(也就是某个客户端发来了请求),随后进程就会被唤醒,并调用它的svcmgr_handler 回调函数处理这个transaction
服务的查找在某种程度上有点类似于自助式服务–换言之,servicemanager必须是全局可访问的。只有这样,各种服务才能上它这儿来注册,客户端也才能来查询服务。在使用C/C++代码时,服务和客户端同样可以调用defaultServiceManager()获得一个访问service manager 的句柄(从技术上说,这只是它的接口,是个sp类型的对象),这个定义在 IServiceManager.h 中的接口,导出了几个transaction 请求码。表6-1中列出了这些请求码,及实现这些请求码的可供C/C++代码调用的API。值得注意的是,在这张表中并没有用来删除服务的API,这是因为在对应的进程死掉之后,服务会被自动删除。因为Binder能够检测到这种情况,并在检查到这种情况时,向servicemanager发送一个BR_DEAD_BINDER(进程死亡通知)。
表6-1调用servicemanager各项功能的请求码及对应的API方法
请求码 | API | 注释 |
---|---|---|
SVC_MGR_ADD_SERVICE | addService(name, service, allowlsolated) | 供各类服务把自己注册到servicemanager里。服务可以 (通过allowlsolated参数)决定是否允许被隔离的进程(也就是运行在沙箱中的进程)连上自己 |
SVC_MGR_GET_SERVICE SVC_MGR_CHECK_SERVICE | checkService(name) | 获得指定名称的服务的句柄 |
SVC_MGR_LIST_SERVICE | listServices() | 返回一个记有所有服务的向量(列表),这个API并不是给普通框架用的,而是供service list命令使用的 |
addService被认为是个非常敏感的功能,只有UID为0或1000(AID SYSTEM)的服务才能自由地注册服务,其他的系统服务则会受到限制。到了KitKat及以后的版本中,这一限制注册的任务又改由源码中一个事先写好的allowed数组来完成,该数组中的内容如表6-2所示。
命令行工具:service
Android的框架服务都是实现在system_server的各个线程中的。因此应用调用它们时,必须使用进程间通信(IPC, Inter Process Communication)的方式。这就是Binder发挥作用的地方。
应用需要先在自己这个进程中调用 Binder,获取一个端点描述符,然后才能与远程服务建立连接。服务中提供的各种方法是通过IPC 消息进行调用的,这一模式,也被称为远程过程调用 (RPC,Remote Procedure Call)。
术语 IPC 和 RPC 经常会被混用–尽管通常是不正确的。由于这两个概念是接下来讨论Android 服务的基础,所以有必要在这里把二者的区别讲清楚
进程间通信(IPC,Inter Process Communication):这个概念泛指进程之间任何形式的通信行为,是个可以拿来到处套的术语。它不仅仅包括各种形式的消息传递,还可以指共享资源(最明显的就是共享内存),以及同步对象[utex 或者其他类似的东西,即确保安全地并发访问共享资源(也就是防止两个或两个以上的对象同时对同一个数据成员进行修改,从而导致的数据被破坏,或者竞争条件下同时读/写数据而导致错误的情况发生)的东西]。
远程过程调用(RPC,Remote Procedure Call): 这个术语特指一种隐藏了过程(方法)e调用时实际通信细节的 IPC 方法。(在使用 RPC 时)客户端将调用一个本地方法,而这个本地方法则是负责透明地与远程服务端(这个远程服务端甚至可以在不同的时间段里是不同的机器) 进行过程间通信。这个本地方法会将相关参数顺序打包到一个消息中!然后把这个消息发送给服务端提供的方法,服务端的方法会从消息中解出序列化deserialize)发来的参数,然后执行,最后仍以这一方式(当然这时发送方和接收方换了个位置)将方法的返回值 (如果有的话)发送给客户端。
所以,任何 RPC 机制都一定是 IPC 机制(因为前者只是后者的一种特殊形式 ),但反过来却不一定是这样。正如我们在这一节中将要讨论和深究的那样,Android 中服务的调用模式是用RPC 方式实现的。表6-3 中对比了现代操作系统中使用的 RPC机制。
Android 应用的开发者可以幸福地忽略掉服务调用的底层实现方式。大多数Android 应用的开发者所熟悉的调用服务的方法是:他们只需调用 Context 对象的 getSystemService()
方法,这个方法只需接收某个 Android 系统服务的服务名作为输入参数,就能返回一个具体格式/含义不详的对象,通过这个对象就能得到指定的服务对象,并通过它调用服务的方法。
根据其设计本意,Android 的 Binder 将其作用范围限制在了本地,但是我们很方便就能安装一个本地代理(proxy)进程,进一步对通过TCP或UDP socket传输的数据进行序列化或解序列化。这样就扩展了Binder 的作用范围,对于远程访问工具 (RAT,Remote Access Tool)来说,这是一个极其有用的功能。一原注
在调用模式的设计的术语中,getSystemService()方法返回的对象只是一个“代理”(Proxy)。在这个对象内部记录着一个通过调用 binder 获得的指向实际服务的引用(reference),而该对象导出的各个方法,在大多数情况下实际上也只是一些 stub 容器而已,这些容器也被称为“Parcel”,其中存放的是被顺序打包(序列化)到 Binder 消息里去的,需要传递给远程方法的各个参数。远程调用的各种方法及其参数就是以这一方式,使用 AIDL 序列化的。
实际上 AIDL 本身并不是一种真正的语言,它实质上只是一种能被 aidl SDK 程序(在 build 过程中,如遇到aidl 文件时就会调用它)读懂的Java 衍生语言而已。aidl能够自动生成把相关参数序列化打包到 Binder 消息中去,并从返回的 Binder 消息中提取出远程方法的返回值所需的 Java 源码。这些代码被称为“样板文件”(boilerplate),即它可以根据定义文件自动生成,并保证编译得干净利落。.aidl 文件的样例如代码清单6-2 所示。
aidl工具完成了一项几乎不可思议的任务:向开发者隐藏了Android IPC机制实现的细节。
上一个实验中,我们只是把 service 用作 servicemanager 进程的一个命令行接口,演示了它的基本用法。不过 service 真正强大的部分在于:它能直接调用各个 service 中的方法。
使用service call调用一个方法其实也很方便:只需指定服务名要调用的方法的序号(这个序号其实就是按各个方法在服务的.aidl文件中的出现顺序,分配到的一个流水顺序号)即可。此外,根据被调用方法的具体定义,可能还需要输入一些被调用的方法的参数。
用这种方法调用一个方法,方法将会把返回结果放在一个 Parcel( Binder 中用来称呼消息的术语)中返回来。每个Parcel 中至少含有一个32 位的返回值[用 0x00000000 表示方法执行成功,其他的一些值表示不同的出错码。
前文中已经提及 Binder 好几次了,但我们一直仅仅是从高层(而非底层)对它进行讨论的。事实上,从高层角度讲,我们只要知道 Binder 就是一个特殊的文件描述符就够了 (尽管它实际上是个连向服务的专用内核驱动)。这实际上也是 Linux 层的视角下的 binder----进程通过/proc/pid/fd 目录看到的 binder。事实上,系统中的几乎所有进程(除了少数几个原生进程外)都会去打开一个指向/dev/binder 的句柄。
Linux 中的 Binder是开源的(网址:http://openbinder.org,虽然这个网站好像已经挂了,但网上还有一些该网站的镜像站点)。
Binder 是一种远程过程调用 (Remote Procedure Call)机制。它允许应用间能够以程序调用的方式进行通信,而无须关心消息到底是如何发送和接收的。
从应用程序(无论是客户端还是服务器)的角度来看,程序要做的无非就是调用一个方法(客户端)或者提供一个方法(服务端)而已。当客户端调用某个方法时,服务端程序中的对应方法就会被“神奇地”调用,而所有的“细节”显然都是由 Binder 来处理的。这些“细枝末节”的工作包括:
找到服务端进程——在大多数情况下,客户端和服务端分别是两个不同的进程(除非是system_server中的各个服务相互调用)。Binder 需要为客户端找出服务端进程,然后才能向它投递消息。
如前文所述,这个“找到服务端”也就是众所周知的“端点映射”(endpointmapping)]的工作,从技术上讲是由 servicemanager 来完成的。但是servicemanager 只负责维护一个服务目录,把一个接口名(interface name)映射成一个 Binder 句柄 (handle)而已。
而这个“句柄”却是 Binder 交给 servicemanager 的,一个谁也看不懂得标识符(identifier),只有 Binder 才知道它的“真正”含义,其中记录了要找的服务端进程的 PID。
传递消息——如前文所述,生成获取被调用方法的参数,并将其序列化 (serialize)(即把它们顺序打包到内存里的一个结构体中去)或是解序列化 (deserialize)(即把结构体中各个参数一一还原出来) 的代码的任务是由 AIDL 来完成的。但是从一个进程向另一个进程传递序列化了的结构体的工作,则是由 Binder 亲自完成的。客户端进程会用BINDER WRITE READ 参数调用ioctl2)。这将会通过 Binder 发送消息,并且阻塞掉客户端进程,直到服务端进程返回结果为止(因此,代码是先写,后读)。
传递对象——Binder 也可以用来传递对象。如前文所述,service 处理的是一种类型的对象,这些对象也包括“文件描述符”(比如UNIX Domain socket)。传递文件描述符是一个非常重要的特性。因为这使得可信进程(比如system_server)可以用原生(native)代码为一个不可信进程(比如用户安装的 App)打开某个设备或 socket。当然,这里我们假设这个不可信进程是拥有相应权限的(即在 App的manifest 文件中声明了该种权限)。
支持安全认证——进程间通信的安全性,自然是极为重要的,消息的接受者应该能够验证消息的发送者的身份,以免落入圈套,进而殃及整个系统的安全性。Binder 可以获取到它的使用者的安全证书(PID 和 UID)并把它们安全地嵌入到消息中去。这样,服务端进程就能按合理的安全级的要求做出相应的安全认证操作。
Binder 在所有的应用中都有使用,无论开发者自己有没有意识到这一点。在进行 Binder 操作时,所涉及的代码无非分为三个层次:Java代码,原生代码,内核层。
/dev/binder 这个文件描述符连接着任意数量的服务端。这也就意味着进程在打开这个文件描述符时,并不会去管它连接的是一个服务还是多个服务。事实上,进程甚至可以在它根本就(还)没连上任何服务时就打开它。
那么问题来了:好像也没有什么简单的方法能够让我们准确地知道指定的某个句柄所表示的服务正在被谁使用着,如果 Bider 的调试功能被启用了,我们就能用(本书官网上提供的)bindump 工具,根据从Linux的 debugfs 伪文件系统(sys/kernel/debug/binder)那里获取的信息,分析出谁正连着什么服务。
每个使用 binder 的进程(在这个目录中)都有一个(对应的)伪文件。
Android 设备中有几十个服务,要是再加上厂商和用户安装的 App,服务的数量或许就上百了。所幸,大部分框架服务十分的简单,并不需要一个专门的进程,只需要以线程的形式承载就足够了。不过不管怎么说,这些线程还是需要运行在一个宿主进程中的,而 system_server 就是这个宿主进程的提供者。
system_server与 Windows 系统中的svchost.exe 类似,二者提供的都只不过是一个空壳,也就是个容器进程罢了。二者的主要区别在于: svchost.exe 加载的是动态链接库 (DLL),而system_server 加载的则是 Java类。
不过,在Android 中这样做甚至带来了一个更为重要的好处:尽管 Dalvik 虚拟机已经为共享内存做了大量的优化,但是在由同一个虚拟机可执行文件启动的另一个进程中运行各个服务,更有利于保护各类资源。但这一做法也并非没有任何风险一一其中任何一个服务中出现错误,都会波及(运行在 system_server 中的)其他服务。不过在大多数情况下,这个问题并不算是太大。因为能运行在 system_server 中的只有Android 系统的服务厂商提供的或者其他什么服务根本就是进不来的。
system_server 并不是一个原生应用一一它是用 Java 编写的,只有在必须调用原生代码时才使用了一些JNI函数。而它加载的那些服务也同样是用Java 编写的一一尽管许多服务也会使用JNI跳出虚拟机直接与硬件组件进行交互。
作为系统中如此重要的一个支点,system_server 的执行流程却是相当简单的,在从 Zygote那里被 fork()出来之后,这个子进程会把自己的权限降低到前文已述的权限上。接着,它会去加载com.android.server.SystemServer
类,在启动各个框架服务之前,这个类的 main()方法会去完成基本的初始化工作(值得注意的是,它会提升它的虚拟机限制,并加载libandroid_servers.so
执行JNI组件的初始化)。
在所有的服务都创建完毕(及其对应的线程启动)之后,除了进入一个循环之外,main 线程就不需要再做什么事了。(我们希望)这个循环是个无限循环(否则系统就得停工),高层视角中的执行流程如图 6-4 所示。
不过,由于要启动大量的系统服务,system_server 还是需要一个一个地实例化它们。AndroidL版中对这一步的流程做了大幅改进。尽管仍有大量的工作要做,但通过归类类型相似的服务,执行的流程相比之前的版本却是大大地简化了。当前,所有的服务都可以被归入以下三个大“类中。
引导服务(Bootstrap services): 这类服务包括 Installer、ActivityManagerService、PowerManagerService、DisplayManagerService、 PackageManagerService 和 UserManagerService另外还会检测一下设备的/data 分区是不是已经被加密了,或是正在被加密的过程中一因为这会对那些被指定为“core App”的App 的启动造成一定的影响。
核心服务(Core services):这类服务包括 LightsService、BatteryService、UsageStatsService和WebViewUpdateService。最后这个服务是L版中新增的服务,它会定期检查浏览器件是否有更新。
其他服务:基本上,这个大类里涵盖的就是剩下的所有服务。在这个类中有数十个服务(就像源码的注释中承认的那样:这是一个容纳所有有待重新归类和组织的服务的超级大口袋)。
并非所有的服务都能被应用看见。有些服务,比如 installer 是内部服务,因此它们就不会出现在 service list 命令的执行结果中,App 当然也看不见它们。
在启动了所有的服务之后,SystemServer 的主线程就没事可干了。这时,这个线程会进入一个我们希望永远不停的循环中,否则就会抛出一个运行时异常出来。在进程内部,这个循环中的代码会不断地轮询它的文件描述符(特别是它的 Binder 句柄)以获取输入的消息。当有消息到达时,这些消息就会被分发给它们各自对应的目标服务予以处理。
我们可以通过设置一些系统属性的值来修改system server 的执行流程以及它启动的服务类型。
个重要的参数是系统属性 ro.factorytest 的值,用它可以设置设备是不是要被配置为“工厂测试”(factory test)模式。
另一个重要的参数是 ro.headless 系统属性。如果这个系统属性的值被设为了 1,那么WallPaper (壁纸)服务和System UI服务都会被禁用。
另外还有一组 config 系统属性,可以用来选择性地禁用一些子系统。
config 属性名 | 禁用的服务 |
---|---|
disable_storage | MountService |
disable_media | AudioService,WiredAccessoryManager,CommonTimeManagementService |
disable_bluetooth | BluetoothManagerService |
disable_telephony | 未使用 |
disable_location | LocationManagerService, CountryDetectorService |
disable_systemui | StatusBarManagerService |
disable_noncore | UpdateLockService, LockSettingsService, TextServicesManager, SearchManagerService,WallpaperManagerService, DockObserver, UsbService |
disable_network | NetworkStatsService,NetworkPolicyManagerService,WifiP2pService, WifiService.ConnectivityService, NsdServiceNetworkTimeUpdateService.CertBlacklistel |
感谢 Linux的/proc伪文件系统,你可以通过它来检视system server 及其中的许多线程。同理,查看进程使用的 socket 和 pipe(管道)也没什么意思。不过把这个进程中的所有线程都一一枚举出来却是非常有用的。
在创建 Dalvik 的线程对象时,我们可以给它命名。给线程命名需要调用底层的 prctl(2)系统调用。在命名之后,线程的名称就可以通过查看/proc 伪文件系统中该线程对应的目录里的 status 文件看到了。
一般而言,线程id(TID)是不可预测的,但是由于 system server 中大部分线程是紧邻着启动的,所以它们的 TID 也大致是顺序增长的,所以通过观察这些 TID 的具体数值,也能让你对系统中框架服务的启动顺序有个大致的印象。
本章讨论了Android 框架服务的架构,解释了Android 中通过远程过程调用(RPC,RemoteProcedure Call)进行进程间通信(IPC,Inter Process Communication)的底层机制,重点讲述了servicemanager 和 service 工具。接下来,本章重点介绍了 system server 进程,它是作为 Android中大量框架的宿主进程而存在的,完全使用 Java 编写。
Android开发人员习惯于用我们上一章中讨论的Android 软件运行生命周期中的相关概念去看待他们的软件。不过从Linux 角度看,Android 应用也是 Linux 进程,它们和系统中的其他进程本就没多大的区别。
对于/proc伪文件系统,我们在第 2 章里已经接触过了。不过那时只不过是蜻蜓点水式的一瞥,远远没有揭示/proc 伪文件系统的极端重要性。事实上,每个进程(和线程)在/proc 中都有对应的目录,其中记录了大量关于程序内部运行状况的实时状态信息。
输出结果 7-1 中显示的是当前正在使用的 Android shell 自身在/proc 中对应目录里的子目录和文件(请注意,我们是用符号“SS”而不是/proc/self 来表示当前 shell 的进程ID 的一因为如果用的是/proc/self 的话,显示的将是 s 进程,而不是当前 shell 在proc 中的对应目录)。
这些文件并不存在,只有在你请求访问它们时,它们才会临时出现一下。这也就意味着每次你看到的文件内容都可能是不同的。在使用 Is 命令查看这些目录中的内容时,内核去查询文件的大小。
维护/proc 中的文件和目录是没有任何开销的。内核只需在正常操作的过程中记录相关信息即可,只有当用户请求访问它们时,内核才会实时地收集相关信息,并以伪文件系统的方式向用户提供这些信息。这使得/proc 文件系统成了一种 trace 系统或程序的异常强大的机制,而使用它的方式通常是轮询(polling)。
这三个符号链接指向的都是真实存在的文件或目录。
cwd:它指向的是进程当前的工作目录。用cd命令,切换到任意一个目录上,然后再执行 ls -l /proc/SS/cwd。无论你什么时候去查询 cwd 符号链接,内核总是会把它指向你查询时的当前工作目录。
exe: 它指向的是用来产生这个进程的可执行文件(也就是由系统调用execve(2)加载的那个可执行文件)的全路径。这个东西很有用,因为许多进程会修改自己的进程名,让 ps显示出修改后的进程名,但它们改不了这个符号链接。
root: 它指向的是根 (root)目录。
fuser 工具的作用是找出系统中所有打开了指定目录中文件或子目录的进程: lsof 工具的作用是按进程ID逐个列出各个进程打开的文件。这两个工具里就会用到 cwd 和root 这两个符号链接。
进程得通过文件描述符 (file descriptor) 才能完成I/0 操作。不论是用哪种语言一一Java、C 还是其他什么语言,被打开的文件、管道(pipe)、套接字 (socket) 统统都会映射为数字形式表示的文件描述符。进程创建时会默认打开三个文件描述符,它们是标准输入(stdin)、标准输出 (stdout)和标准错误信息 (stderr)一一它们各自对应的文件描述符分别是 0、1和2。
进程看到的这个数字(文件描述符),事实上只是指向内核中某个对象的一个句柄(handle)而已一在内核中,每个进程都有一个数组,其中记录着所有该进程打开的文件对象(包括套接字、管道等),而这个句柄就是对应文件在该数组中的索引号。
lsof (list of file),它会把每个进程打开的所有文件以及其他一些信息起列出来。
只要在给定 PID的 fd/目录上执行 1s -1命令,就能看到该进程正在使用那些文件。
在文件系统中没有用来表示 socket 的文件。
尽管只要用 lsof(1)这类工具(不过 lsof(1)不是 toolbox 工具箱中的工具)就能自动为你找出包括 socket 在内的所有描述符的含义。但只要再知道一些关于/proc 中存储的数据的知识,你自
己也能很快得出答案。Linux 和 Android 中的 socket 通常是下面三种类型之一。
UNIX domain socket:这类 socket 仅用于本地通信,其中的一些是 named的,也就是说它们在文件系统中是有对应的文件的。不过实际上,这些并不是真正的文件,dommainsocket 只是内存中的内核数据结构,文件名的作用只不过是为了确保 socket 在整个系统中的唯一性。在Android 系统中,这些 socket 和另一种 socket 一起(这种 socket 是以字符“@”开头的,由于“@”是隐藏文件的符号,所以这些文件是不会在文件系统中显示出来的)存放在/dev/socket 目录中,除此之外,其他类型的 socket 都是unnamed 的。domainsocket 的相关实时使用情况信息,则被内核存放在/proc/net/unix 文件中
基于IP的socket: Linux(和Android)可以同时使用IPv4和 IPv6 这两个不同的地址族(address family),然后我们还可以把它们再进一步细分为 udp 和 tcp 这两个不同的协议类型。因此,毫不奇怪,存放相关实时使用情况信息的文件一共有四个: tcp6、udp4、tcp和udpo
Netlink socket: 这种 socket 被用作一种高效的内核用户空间通知机制。它只在Linux 中被使用,而且因为它所具有的多播功能(也就是说,它能让一组用户共享同一个 socket,向这些用户同时发送通知)而受到青睐。这类 socket 的实时使用情况信息存放在proc/net/netlink文件中。
toolbox工具
wchan
syscall
有个并不广为人知的事实:你可以用 cd 命令直接切换到指定线程的目录中去,尽管在 ls/proc 目录时,只会显示出各个主线程(和内核线程),但是你还是可以用 cd 再加上一个有效的TID 的方式,直接切换到该线程的目录中去。这样做得到的结果和切换到/proc/tgid/task/tid 里去得到的结果是完全一样的。因为当你对/proc 执行ls 操作时,procfs 这个伪文件系统会吹毛求疵地把所有子线程(的目录)全都过滤掉。但是当你用 cd 命今要求直接切换到某个目录中去时procfs 可就没这么细心了,只要你指定了一个有效的线程 ID,管它是子线程、主线程还是内核线程,你总能切换到那里去。
为了弥补因缺乏swap 机制而带来的问题,Android 系统在优化利用可用的内存上下足了功夫。Dalvik 虚拟机的设计就非常注重共享尽可能多的虚拟内存。事实也确实如此,如果系统中有多个传统的Java 虚拟机(比如Sun 公司的J2ME)的实例,每个实例至少都需要 100MB以上的内存。但是如果换成 Dalvik 虚拟机,由于几乎所有的东西都是可共享的,结果每个 App 所需的内存就相当的少。
每个进程在/proc 伪文件系统中的对应目录里都有一个 maps 文件,其中记录了所有虚拟内存的 map 情况,因为设备代码和 iode 号以及文件的完整路径都会被明白无误地列在其中,所以named 页可以很方便地被认出来。而那些匿名页的这些项上则会被填零或者为空,当然一些“特殊的”匿名页除外,比如说被用作栈和堆的页也会被明确地标注出来。
cat /proc/$$/maps
如果还想看更详细的信息,你还可以去看该进程对应目录中的 smaps 文件。该文件中除提供了 maps 文件中的所有信息之外,还有各个内存段的详细属性信息一-对于这一点,在下个实验中将会予以简要地演示。
如果要看系统级的内存实时使用情况,你可以去查阅/proc/meminfo文件。
VmSize = VmRSS + VmFileMapped + VmSwap + VmLazy
每个进程在/proc 伪文件系统中的 smaps 文件和 maps 文件一样,都是以内存段的形式显示内存的实时使用情况的,它们的区别在于: smmaps 文件中还提供了每个内存段更详细的信息。
AOSP 提供了两个能查看内存实时使用情况的很有用的工具 procrank 和 librank,不过在大多数手机/平板电脑里,这两个工具都会被厂商删掉。不过,把这两个程序连同它们的依赖库/system/lib/libpagemap.so 一起从模拟器镜像里拷出来,然后再复制到手机/平板电脑上去,也不是什么难事。整个过程如输出结果 7-12 所示。
每个App 的私有页所占的物理内存空间 (USS)平均都不超过几MB!平均每个 App 里有大约 85%~90%的 RSS 是共享的,这极大地减小了相关进程的 PSS 的大小。这些共享部分大多是 Zygote 和 Dalvik 虚拟机(包括 ART)的组成部分这一做法最大化地利用了共享内存的优势,把 Java 虚拟机远远地甩在了身后(但可能有些人还是会跳出来争辩说,这还是不如 iOS有效 )。
按 procrank -h 中给出的解释,cached page 就是 storage backed page,non-cached page 就是 ram/swapbacked page,分别指已经备份到闪存里的文件中的页,和正在内存中及已被页交换到 swap 中的页。乍一看很奇怪,已被页交换到 swap 中的页怎么会和正在内存中的页搅在一起呢?原因是因为闪存读写次数的限制,不适宜用作 swap,所以Android 中的swap 是内存中单独专门划出的一段空间充作 swap 的,被交换到这个swap 中的内存页中的数据会先被压缩,然后再存放到这个 swap 中,这样这个swap 就能存储比正常情况更多的数据。由于不论是正在内存中的页,还是被交换到 swap 中的页,它们都是在物理内存中的,所以就被称为 non-cached page。-译者注
OOM 并不是一个线程,它被实现为发生内存不足的情况时,要被执行的一系列代码。这些代码逐一遍历所有的进程,试图从中找出一个最合适的“受害者”一一杀掉这个进程能在最大程度上缓解系统内存不足之苦。所有的进程都作为候选对象,在这个“死亡队列”中,以oom score值(这是一个启发式的评分测试分值,具体评分测试的算法一直随着内核版本的更新而不断地调整)排序。这个分值也可以在/proc/pid/oom score 这个只读的伪文件中读取到。
你可以通过查看/proc 中进程对应目录中相关文件的内容,实时观察 oom score 值的变化情况。在这个实验中,我们需要在打开一个App 的同时,再打开一个 adb shell。例如,如果你选用的样例App是Chrome Web 浏览器,你打开adb shell 之后,可以做如输出结果 7-16(a)所示的操作。
事实上,一个用户模式下的线程要想完成任何“有意义”的操作,都必须要请求系统以某种方式介入。无论是处理文件,打开 socket 还是对(除了它之前已经分配到的虚拟内存之外的)资源做任何类型的处理,用户态线程总是需要请求来自内核的服务一一也就是系统调用(systemcall)。
使用系统调用首先需要用户模式的进程进入内核模式,具体的进入方式随处理器的体系结构的不同而各不相同,不过这总是会涉及某条特殊的机器指令一-在 ARM 处理器中它是 SVC指令(又名SWI指令):在Intel处理器中它是 SYSENTER 指(或者SYSCALL指)。这些指令会把处理器的模式改为特权(privileged 或 supervisor)模式,并把系统控制权转移到系统启动时就定义好了的内核入口点 (system call)上。因此,所有的系统调用会先被交到一个函数手上,这个函数再用(通过ARM中的r12寄存器,或者Intel中的EAX寄存器传来的)系统调用号(system call number)查询一张内部的(系统调用地址)表,把执行流跳转到提供对应服务的系统调用函数那里。
了解了上述这些知识之后,我们就能明白,为什么在调试(debug)或跟踪 (trace)进程时系统调用是这么的重要。
toolbox ps 工具的输出结果中有两列非常有价值(尽管比较简略)的关于系统调用的信息WCHAN和PC。
前者是“Wait Channel”的缩写,它表示该进程(或内核线程)当前在内核中(执行到)的地址,如果不能确定的话,则记为-1 (0xfm)(回忆一下,除非使用的是 ps -t命令,ps 输出结果中的每一行表示一个内核线程或某个进程的主线程)。
后者则是(用户态里的)返回地址,也就是系统调用执行完毕返回之后,进程该从哪里开始继续执行。解析 WCHAN中记录的内核地址是属于哪个内核函数的,需要手工操作一番,如下面这个实验所示。
不光简略,还有 bug。直到Android M,在使用64 位处理器的手机/平板电脑中,ps 并不能正确地显示PC 的值一一它还是认为 PC 应该是个32 位的值。一一原注
参数 | 用法 |
---|---|
-i | 输出系统调用的入口指针 |
-t [t[t)]] | 以不同的时间格式和精度,在输出中的每一行前加上时间信息 |
-f | 在目标使用clone0系统调用时,自动附加子进程/线程 |
-o file | 把输出结果保存到fle指定的文件中去 |
-v[v] | 输出所有系统调用参数的详细模式 |
本章重点讨论了/proc 伪文件系统的使用方法(特别是与每个进程对应的/proc/pid子目录和与每个线程对应的/proc/pid/task/tid 子目录),其中提供了大量信息,以及让你能执行强大的原生级的进程调试和跟踪的功能。本章中演示的各个方法一样可以应用到主流版本的 Linux 系统上.因为 procfs 就是完整的 Linux内核的一个组成部分。
[黑话里甚至把它们称为“呆鹅”(sitting duck)],而个人电脑上网则是断断续续的一一就算它连在网上的时候,用的也是一只超慢的“猫”(modem)。
以前黑客追求的是能够远程完全控制被攻击目标(黑客的行话把这叫“pwning”)。
从 JellyBean 版开始,通过引入 SELinux,Android 对应用的限制又上了一个新台阶–使用一个强制实施的访问控制框架,有效地把所有的进程置于沙箱里,将其束缚在可信区域内。到了Android Lollipop 版中,这些框架又被进一步扩展,已经能支持对包 (package) 进行限制了。
Android 在 Linux的基底上构建出了一个富框架(rich framework),但在它最核心的层面上,还是要靠 Linux来完成所有操作的。Android 是构建在 Linux之上的这一事实,也使得它沿用了Linux 提供的这些安全特性-权限 (permission)、权能(capability)、SELinux 和其他一些底层安全保护措施。
Linux 使用的安全模型就是标准 UNIX 的安全模型。这个模型从40 多年前发布之后,一直没有做过任何重大的修改。
Android 也继承了这一经典模型(这是底层的 Liux 系统免费提供的),而且也很自然地使用了它,只不过(和 Linux 中的用法相比)在用法上有一个小小的、多少有点新颖的区别:在Android 中,“用户”是分配给各个应用,而不是分配给使用计算机的人的。突然之间,原本用来区分共享同一个UNIX 服务器的人类用户的方法,也被用在了应用身上,并且也用相同的隔离措施限定了它们可以使用的权限。一个用户不能访问另一个用户的文件,目录或是进程一这种严密的隔离,使我们可以同时并发地运行多个应用,并且使这些应用不会相互影响。Android的这一做法确实是独一无二的–iS 中是在同一个 uid (mobile 或501)下运行所有进程,并依赖于内核强制使用的沙箱来实现进程间的相互隔离的。
android_filesystem_config.h
之前有一些Android 系统的root 工具就是利用了root 所拥有的进程中的漏洞(特别是 vold,这个进程常被用作这类跳板)来root 手机/平板电脑的,所以谷歌希望通过减少 root 所拥有的进程的数目的方式,缩小 Android 系统的受攻击面。installd 就是这样一个例子:这个进程以前是拥有 root 权限的,不过从JellyBean 开始,它的root权限就被去掉了。
不过把全部 root 所拥有的进程的 root 权限都去掉好像也是不可能的:至少,init 进程必须拥有root 权限,同样 Zygote 进程也要有 root 权限(Zygote 进程要 fork()出uid 不一样的子进程,这一点只有 uid0才能做到)。你可以用下面这条命令来列出你的手机中所有 root 所拥有的进程:
ps|grep ^rootlgrep -v"2
其中,“grep-v”命令的作用是忽略掉所有 PPID为2的内核线程。
权能就像是一个沙箱,允许应用执行设计所需的操作一一同时又防止它为所欲为,危害系统安全。
SELinux,即安全加固的 Linux(Security Enhanced Linux),对相关操作进行审查限制的强制访问控制(MAC,Mandatory Access Control)框架。和权能一样SELinux 也实现了最小权限原则:只是粒度更细罢了。通过严格地防止(进程的) 操作超出预定义的操作边界。如果进程行为不端(在大多数情况下,这可能是因为进程本身就是恶意的,也可能是因为进程被注入了恶意代码而在正常进程中出现恶意操作),SELinux 就会阻断所有这些越界操作。
SELinux 所使用的基本方法是“打标签”(labeling)(事实上它也是大多数 MAC 框架所使用的基本方法)。所谓“标签”(label) 就是为资源[客体 (object)] 分配一个类型 (type),或为进程[主体(subject)]分配一个安全域 (security domain)。
SELinux 可以据此强制让只有在同一个域中(即,打了同样的标签)的进程才能访问对应的资源 [有些 MAC 框架走得更远即会使资源对于打了不同的标签的进程是不可见的,这有点像 Linux 中命名空间(namespace)的概念]。
因为尽管我们可以在源码中修改这个“property_perms”数组的内容,但是一旦编译完成之后,这个数组中的内容就被写死在/imnit 这个二进制可执行文件中,不能修改了。如果一定要修改它,只有去修改源码,然后重新编译才行。而不像/property contexts 文件中的内容,能在无需重新编译的前提下,修改其中的内容,故有此说。一-译者注
init | toolbox | 用作 |
---|---|---|
无 | getenforce | 查看当前SELinux是否被启用了 |
setcon SEcontext | 无 | 设置(修改)SELinux的context./init的上下文为u:r:init:s0 |
restorecon path | restorecon[-nrRv]pathname… | 从参数path 指定的文件中恢复SELinux上下文设置 |
setenforce [0\1] | setenforce[Enforceing permissivel1/0] | 启用/禁用强制执行SELinux |
setseboolname value | setsebool name value | 设置SELinux中的布尔值(0对应false/off ;1对应true/on) |
在电源管理模块唤醒屏幕并通知 WindowPolicyManager 实现代码的第一时间里,屏幕解锁代码就会被调用。这会调用 KeyguardServiceDelegate 的 onScreenTurnedOn0方法,接着这个方法又会去调用 keyGuard,并等待它执行完毕并返回。从这里开始,控制权就交给了 keyGuard,它会去确定用户选择的是哪种屏幕解锁方案,并画出相应的屏幕解锁界面(通过相应的 activity)。当系统策略要求强制自动锁屏时,锁屏代码也可以被 DevicePolicyManager 中的 lokNow0方法调用。
实际的解锁操作是由 LockPatternUtils 来完成的,它会请求使用 LockSettingsService 服务(这个服务是 system server 中的一个线程),该服务将会验证用户的输入与 LOCK PATTERN FILE(这个宏指的是 gesture.key 文件,用户使用图形锁时,图形锁的 Hash 存放在该文件中)或LOCK PASSWORD FILE (这个宏指的是 password.key 文件,用户使用 PIN 码或口令解锁时相应的 Hash 存放在该文件中)文件中的记录是否一致,以判定用户的输入是否正确。不论是在哪种情况下,文件中都不会记录图形锁或口令的明文,而是只记录它们的 Hash。另外LockSettingsService 服务还需使用 locksettingsdb 文件,该文件是一个 SOLite 数据库文件,其中记录了与屏幕解锁相关的各种设置
注意:文件系统加密和OBB与ASEC 有个显著的区别:OBB 与ASEC 的解密密是以明文的形式藏匿在系统的某个角落里的一-只不过是只有 root 才有权读取它们罢了。而/data 分区的加密密钥却不能放在手机里,而是需要在系统启动时,通过与用户进行交互,请用户输入密码(或者更准确地说,是根据用户的屏幕解锁密码推导出/data 分区的加密密钥来的)。这就需要对 Android 系统的启动流程以及 nit 和 vld 之间的交操作进行修改一这些我们在第4章中都已经讨论过了。
Android 是把应用当贼防的,而is却是把用户当贼防的。
一般需要你在设备启动时,按住某个指定的实体键的组合键[通常是按住某个音量键或者同时按住音量增加/降低键,再按下 home 键(如果设备上有 hme 键的话)]或者用USB 线把移动设备和电脑相连,并在电脑上运行 fastboot 命令。一旦启动流程被转移之后,就可以引导 Boot Loader 去加载另一个可选的 bot 镜像一这个镜像既可以是闪存上的recovery 镜像,也可以是 SD 卡中的升级包,或者是 (通过USB 线)由 fastboot 传来的镜像。
如果一台设备的 Boot Loader 可以被解锁 (详见第3章),那么这台设备就可以被 root。这很简单,前文已述,解锁 Boot Loader 时,/data 分区将会被格式化一以防用户的敏感数据因此而落入不该拿到它们的人的手里。另外,有些 Boot Loader 还会设置一个永久性的标志位,表示Boot Loader 已经被修改过了-哪怕后来 Boot Lader 又被重新加锁了之后,这个标志位也不会被改回去或撤销。必须要注意的是:一旦 Boot Loader 不再强制查要被刷入移动设备的镜像的数字签名,那么它基本上也就没法对系统的安全负责了。
rootkit
本章试图引领你概览 Android 中大量的安全特性(无论它是继承自 Linux 的还是 Android特有日大多数是实现在 Dalvik 虚拟机层上的)。我们特别提到了 Android 现在已经开始使用的SELinux–尽管现在在使用SELinux时,尚有所保留,但是三星在KNOX中也采用了SELinux,而且可以预计在最近将会发布的新版Android 中,SELinux 将会扮演一个更为重要的角色。
尽管我们尽可能详细地讨论相关技术,但这并不意味着我们的讨论是十分全面的。有兴趣的读者应该进一步去阅读一些Android 安全方面的专著,比如Nikolay Elenkov 编写的AndroidSecuritvInternals[S这本书中所有的章节都被用来讨论这一章各个小节中讨论的内容。
谷歌的电子支付应用。一一译者注黑体
宋体
新宋体
仿宋
楷体
仿宋_GB2312
楷体_GB2312
微软雅黑
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。