Jimmy Chen

A Programmer

(转载)高通(Qualcomm)LK源码深度分析(一)

在CIA VAULT 7 , NSA Shadow Broker 泄漏事件之后,信息安全也逐渐受到越来越多的关注,作为信息安全行业从业人员,除了参与这一场集体躁动之外,还需回归技术本身,对泄漏的资料和工具进行分析研究,以做到知己知彼。

Sec·Ret 团队在对所有泄漏资料进行分析后,形成了一系列技术研究文章并会陆续进行发表,希望在增强自身技术储备的同时,也能够和同行多多交流,共同成长。

在Bootloader系列文章中,我们对主流bootloader的前身高通 little kernel 进行了深入的源码分析,并对CIA 泄漏的cadmium.pdf文档中的三星bootloader漏洞进行了分析和复现,在此之外,对主流手机厂商如华为,三星,小米系列手机产品的bootloader进行了标准流程化安全审计,这些工作最后都会以文章的形式呈现在bootloader研究系列中。由于自身技术有限,若文章中有表述不准确的地方,忘同行指正。

什么是 bootloader

bootloader 就是在 操作系统内核 启动之间运行的一段程序,它的作用是初始化一些硬件设备,将内核加载从 ROM 加载 RAM 中,并做好映射关系。bootloader 的目标就是为 操作系统 提供合适的运行环境。通俗的说 bootloader 就是将 操作系统 拉起来的程序。

什么是 lk(little kernel)

littlekernel 是一个为嵌入式设备开发的微内核,google 正在研发的 Fuchsia 操作系统的微内核 magenta 也是基于 lk 开发。而我们重点研究的 lk 是高通基于 {aboot} pass partition table through atags · littlekernel/lk@42168f2 commit 后 fork 修改而来。

高通修改后的版本和原始是 lk 已经有了很多的差别,高通主要是将 lk 作为一个 android bootloader 来适配自家的各种不同的芯片,并提供 fastboot 接口来处理 fastboot 命令。

此文章中以后的 lk 均指代高通的 lk,如果需要对 github 的 lk 做描述,则用 origin lk 表示。

lk 下载及编译

  1. kernel/lk 处 clone 源码到本地。
  2. 切换到主分支。
  3. 配置编译环境。1
  4. 开始编译。编译的时候需要选择对应的目标平台编译,对应目标平台可以在 target 目录下找到。2

    当前我们编译 msm8916 平台的 lk。

    编译时可能遇到 make[1]: *** No rule to make target build-msm8916/lib/openssl/crypto/bn/asm/armv4-mont.o, needed by build-msm8916/lk. Stop. 的问题。解决的方案是将 lib/openssl/crypto/bn/asm 中的 armv4-mont.s 文件重命名为 armv4-mont.S 即可。

lk 启动流程

  1. 进行各种早期的初始化工作,包括 cpu, emmc, ddr, clocks, thread etc。
  2. 判断进入 recovery 或 fastboot 的条件是否被触发。
  3. 从 emmc 中获取 boot.img 并加载到指定内存区域 (scratch region)。
  4. 从 scratch region 加载 kernel 到 KERNEL_ADDRESS
  5. 从 scratch region 加载 ramdisk 到 RAMDISK_ADDRESS
  6. 加载设备树到 TAGS_ADDRESS
  7. 关闭 cache, interrupts, 跳转到 kernel。

lk 代码流程

我们目前的研究是基于 msm8916 的代码流程来进行,在 msm8916 中不会使用到的代码暂时不分析。lk 是使用 arm 汇编c 语言联合编译而成的,其中偏向硬件和底层的代码使用 arm 汇编 编写,而偏上层提供功能的代码则使用 c 编写。

入口点

lk 代码的入口点是在 arch/arm 目录下的以 .ld 为后缀的 link 脚本文件中指定。一共有以下 4 个 link 脚本文件,指定的入口点均为位于 arch/arm/crt0.S 文件中的 _start 函数:

  1. system-onesegment.ld
  2. system-twosegment.ld
  3. trustzone-system-onesegment.ld
  4. trustzone-test-system-onesegment.ld

具体使用哪一文件在 platform/msm8916/rules.mk 中指定。

_start 最主要的作用是设置一些 cpu 的特性,然后初始化各种 c 程序运行需要的栈环境,完成后直接跳转到 kmian 函数进入 c 语言环境。

kmain

kmain 函数位于 kernel/main.c 文件中, kmain 所做的工作可以用下面的流程图表示:

《(转载)高通(Qualcomm)LK源码深度分析(一)》

thread_init_early

thread_init_early 的实现位于 kernel/thread.c 文件中,其代码结构如下:

thread_init_early 所完成的主要工作就是为 lk 完成早期的线程初始化工作,包括 运行队列 的初始化, 线程链表 的初始化, 第一个线程3 的创建等工作。在这个过程中一共有以下 4 个重要的数据结构:

  1. 运行队列 run_queue。

    run_queue 是作为多线程的调度中心存在,数组不同的下标对应不同的 运行优先级 ,具体的应用会在后面涉及到。
  2. 线程链表 thread_list。

    全局的线程链表,保存了所有创建的线程信息。
  3. bootstrap 线程。

    每个线程的上下文信息都通过 thread_t 结构体保存,而 bootstrap 是作为 lk 的第一个线程存在, 拥有 最高优先级(HIGHEST_PROORITY)
  4. current_thread

    current_thread 是一个全局变量,保存了当前运行的线程的信息。

这些数据结构都进行了初始化后,lk 就具有了创建和管理线程的结构及能力,在 msm8916 中后面环境初始化部分中的 thread_init 只是一个空接口,没有实现。

arch_early_init

arch_early_init 的代码位于 arch/arm/arch.c 文件中, arch_early_init 和所使用的 CPU 架构有很强的耦合性,这个函数的代码也主要是针对 CPU 的一些特性做设置。

由于这方面的初始化比较偏向硬件,设置方法都是按住 CPU 手册来进行,所以不详细分析,只需要知道开启了以下几个功能即可:

  1. 设置异常向量基地址为 0x8F600000。4
  2. 开启 cpu mmu(memory manager unit) 内存管理单元。
  3. 开启一些协处理器特性,具体的设置项可以查询 DDI0344K_cortex_a8_r3p2_trm.pdf

platform_early_init

platform_early_init 函数位于 platform/msm8916/platform.c 文件中,主要的工作是对 msm8916 平台的硬件设备进行早期初始化,其流程如下:

《(转载)高通(Qualcomm)LK源码深度分析(一)》

board_init

board_init 函数位于 platform/msm_shared/board.c 文件中,主要工作是获取主板的相关信息并填充到相关结构中:

board_init 的功能就是获取主板的信息并填充到 board 这个全局变量中,获取信息的具体来源涉及到 sbl1 等其他模块,暂时不分析,可以参考 Android 系统典型 bootloader 分析 这篇文章。

platform_clock_init

platform_clock_init 函数位于 platform/msm8916/platform.c 文件中,作用简单明了,就是初始化 msm8916 平台的一系列时钟,其中最重要的数据结构解释 msm_clocks_8916

qgic_init

qgic_init 函数位于 platform/msm_shared/qgic_common.c 文件中,主要的作用就是初始化 QGIC(Qualcomm GenericInterrupt Controller),分为以下两个部分的初始化。5

  1. qgic 分配器的初始化。
  2. qgic cpu 控制器的初始化。

由于具体的初始化工作涉及到具体的 CPU 架构,这里暂不分析。

qtimer_init

qtimer_init 函数位于 platform/msm_shared/qtimer.c 文件中,在 msm8916 平台下,这个频率固定为 19200000, 作用暂时未知,后续补充。

scm_init

scm_init 函数位于 platform/msm_shared/scm.c 文件中, scm 的全称是 Secure Channel Manager, 负责 Normal World(普通世界) 和 Secure World(安全世界) 之间的通信。Secure World 就是 TrustZonescm_init 的作用则是检查 scm 是否能够使用。

在上面的实现中 scm_call2 就相当于是 TrustZone 开放给普通世界的 API 接口,在这个接口中有两个重要的数据结构。

  1. scmcall_arg

    这个结构想当于一个数据包,负责携带需要传递给 TrustZone 的参数信息。
  2. scmcall_ret

    这个结构则存储着 TrustZone 返回的数据信息,但是只有数据小于 12 字节才用这个结构返回,其他的数据应该在参数中放入一个返回用的 buffer 和长度。

整个 platform_early_init 的作用就是初始化 msm8916 平台的相关硬件设备,包括主板,时钟,中断控制器,scm 等,为 lk 的启动和运行提供硬件环境。

target_early_init

target_early_init 函数位于 target/msm8916/init.c 文件中,这个函数的主要作用是为 msm8916 平台开启调试 uart 调试接口:

WITH_DEBUG_UART 宏定义在 project/msm8916.mk 中, 只要定义了 WITH_DEBUG_UART 宏,就可以开启板子的 uart 调试接口。

environment initialize

之所以将 bs_set_timestamp/call_constructors/heap_init/thread_init/dpc_init/timer_init 等分为一类,是因为前面几项初始化工作更偏向硬件的初始化,是后续运行的基础需求,而现在的几项初始化则是高级运行所需要的环境,是一种软件环境,和具体硬件信息并没有很强的关联性,所以分为一类。

bs_set_timestamp

bs_set_timestamp 函数位于 /platform/msm_shared/boot_stats.c 文件中,这个函数的功能非常简单,就是读取当前的 TimeTick 然后保存到 bootstate 中。在这个过程中比较重要的是以下两个内存地址:

  1. TimeTick 地址在 msm8916 平台下,TimeTick 的地址由以下宏定义:

    这个宏的定义在 platform/msm8916/include/platform/iomap.h 文件中。
  2. bootstate 地址在 msm8916 平台下,bootstate 即 bs 的地址由以下宏定义:

    这几个宏同样定义在 platform/msm8916/include/platform/iomap.h 文件中。bootstate 的地址其实就是一个 uint32 的数据,每个成员存储了对应的时间信息,具体的成员对应的含义有以下定义:

    当前设置的就是 BS_BL_START 的时间,代表了 bootloader 的启动时间。

call_constructors

这个函数是 lk 和 c++ 联合编译使用的特性,主要是为了调用 c++ 代码的构造函数,单纯的 lk 中这个函数并没有作用。

heap_init

heap_init 函数位于 lib/heap/heap.c 文件中,顾名思义,这个函数的作用就是初始化堆空间,其代码如下:

其中涉及到的最重要的全局变量就是 theheap, 这个变量保存了 lk 所使用的堆空间的信息,其结构如下:

theheap.basetheheap.len 对应的宏定义如下:

_end 和 _end_of_ram 这两个符号都是在链接的时候由链接器来确定的,其符号定义在 arch/arm/system-onesegment.ld 文件中。_end 表示程序代码尾地址, _end_of_ram 表示 lk 内存尾地址,也就是说 lk 堆空间就是程序代码尾部到内存尾部所有空间。theheap.free_list 维护着一个堆链表,其中保存着堆中所有空闲的堆块,现在的初始化阶段,只有一块完整的堆空间。

thread_init

thread_init 函数位于 kernel/thread.c 文件中,关于线程的初始化在 thread_init_early 中已经完成, thread_init 只是一个空接口。

dpc_init

dpc 函数位于 kernel/dpc.c 文件中,它的代码如下:

代码很简单,就是创建并启动一个名为 dpc 的线程, dpc 的全称是 deferred procedure call, 就是延期程序调用的意思,它的作用是可以在其中注册函数,然后在触发 event 时调用函数,比如在 thread_exit 中就通过 dpc 来清理线程栈环境。虽然 dpc 系统内容不多,但是这里涉及到了一个有意思的操作,线程的创建和启动, thread_init_early 对线程初始化后,这里是第一次对线程进行使用, 值得分析。 thread_create 函数位于 kernel/thread.c 文件中,代码如下:

thread_create 的逻辑比较简单,总体上来说只有 3 个步骤:

  1. 申请并填充 thread_t 结构体
  2. 初始化线程栈空间
  3. 添加线程到 thread_list 头部

这三个步骤除了第 2 步的栈空间结构,其他结构在 thread_init_early 中已经介绍过,不做赘述,主要介绍下 lk 的线程栈初始化。线程栈的初始化由 arch_thread_initialize 完成,这个函数位于 arch/arm/thread.c 文件中,其代码如下:

初始化线程栈空间其实就是在栈顶使用一块空间用于后续保存寄存器信息以方便线程切换。需要保存的寄存器信息定义在 context_switch_frame 结构体中,其中 lr 用于保存线程函数入口。 当这块内存空间设置好以后,线程栈的初始化工作基本就完成了,剩下的就是通过 thread_resume 来启动线程。

thread_resume 函数位于 kernel/thread.c 文件中,其代码如下:

代码逻辑简单明了,只有以下两个步骤:

  1. 修改 thread 的状态为 THREAD_READY, 然后添加到 run_queue 中。insert_in_run_queue_head 函数位于同一文件中,其代码如下:

    run_queuethread_init_early 中已经有过了解,就是一个大小为 32 的全局链表数组,这个数组每一项对应一个线程优先级,下标越大的数组项优先级越大。比较有趣的是使用了一个 uint32 类型的全局变量 run_queue_bitmap 来作为优先级的索引。这个变量的 每一位 对应数组的 每一项, 通过检查对应位的值是 0 或 1 就可以知道优先级的使用情况,两者的关系可以用下图表示。

    《(转载)高通(Qualcomm)LK源码深度分析(一)》

  2. 调用 thread_yield 来获取 cpu 执行。thread_yield 函数位于同一文件中,主要是修改 current_thread 的状态的 THREAD_READY 并插入对应优先级项链表的尾部,然后调用 thread_resched 函数来切换线程。thread_resched 函数位于同一文件中,其代码删除了一些无关代码后大体如下:

    整体流程如下:

    1. 通过 run_queue_bitmap 获取优先级最高的线程。
    2. 设置线程状态为 THREAD_RUNNING, 如果新线程不等于老线程则调用 arch_context_switch 切换线程。arch_context_switch 函数位于 arch/arm/thread.c 文件中,只是作为转接 arm_context_switch 的媒介。arm_context_switch 函数位于 arch/arm/asm.S 文件中,是汇编代码,其代码如下:

      函数的功能很简单,保存 old_thread 的寄存器环境到内存,从内存加载 new_thread 的寄存器环境,跳转到线程入口。

到这里新的线程就会执行起来,但是这里有一个问题,如果存在一个优先级很高的线程,并且是死循环,按照上面的逻辑优先级高的线程一定会先执行,那么这个优先级高的线程就会一直占有 cpu, 这种情况 lk 是如何处理的,还是对代码的理解有问题?

timer_init

timer_init 函数位于 kernel/timer.c 文件中,主要的作用创建 lk 中的定时器链表和定时器处理函数。每个定时器都存储在 struct timer_t 类型的结构体中:

其中全局链表 timer_queue 的作用就是存储定时器,而 timer_tick 函数的作用则是遍历 timer_queue 来处理其中注册的定时器回调函数。

参考资料

  1. androidLK 启动过程 – 简书
  2. 深入 MTK 平台 bootloader 启动之【 lk -> kernel】分析笔记其它综合_INFOCOOL.NET
  3. Android 启动过程深入解析 – 文章 – 伯乐在线
  4. Cortex-A8 处理器 – ARM
  5. Cortex-A9 处理器 – ARM
  6. ARM Cortex-A – Wikipedia
  7. DDI0344K_cortex_a8_r3p2_trm.pdf
  8. Android 系统典型 bootloader 分析 – 电脑系统安全 – 红黑联盟
  9. MSM8909+Android5.1.1 启动流程(3)—kmain() – 程序园
  10. SylixOS 中 GIC 通用中断控制器(一)——GIC 简介 – 11168899 – 51CTO 技术博客
  11. Secure Channel Manager

Footnotes

1 这里的编译环境需要 android NDK 的交叉编译器,可以从 Android Developers 下载 NDK。
2 测试时发现 qemu-arm 无法编译,是因为 project/tools 中的 rules.mk 文件并不存在。
3 这里的第一个线程 bootstrap 并不是被创建然后运行的,而是将本身就在运行的信息加入到线程链表中,作用第一线程存在。
4 MEMBASE 的值在 target/msm8916/rules.mk 中指定。
5 GIC(通用中断控制器) 是各外设中断和 CPU 之间的桥梁,也是各 CPU 中断之间互相的通道,主要的作用就是检测,管理和分发中断。


转载自: http://www.freebuf.com/news/135084.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注