安全路透社
当前位置:安全路透社 > 网络转载 > 正文

高通(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 源码到本地。

    git clone git://codeaurora.org/kernel/lk.git
    
  2. 切换到主分支。

    git checkout -b mylk remotes/origin/master
    
  3. 配置编译环境。1

    export PATH=$PATH:/path/to/arm-linux-androideabi-/bin
    export TOOLCHAIN_PREFIX=arm-linux-androideabi-
    
  4. 开始编译。编译的时候需要选择对应的目标平台编译,对应目标平台可以在 target 目录下找到。2

    ➔ ll -F target | grep '/$'
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 apq8084/
    drwxr-xr-x   4 takboo  staff   136B May  4 15:05 armemu/
    drwxr-xr-x   4 takboo  staff   136B May  4 15:03 beagle/
    drwxr-xr-x   6 takboo  staff   204B May  4 15:05 fsm9010/
    drwxr-xr-x   6 takboo  staff   204B May  4 15:05 fsm9900/
    drwxr-xr-x   7 takboo  staff   238B May  4 15:05 mdm9607/
    drwxr-xr-x   7 takboo  staff   238B May  4 15:05 mdm9615/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 mdm9625/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 mdm9635/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 mdm9640/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 msm7627_surf/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm7627a/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 msm7630_surf/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm8226/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm8610/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 msm8660_surf/
    drwxr-xr-x  10 takboo  staff   340B May  4 15:05 msm8909/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm8916/
    drwxr-xr-x  11 takboo  staff   374B May  4 15:05 msm8952/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm8960/
    drwxr-xr-x   9 takboo  staff   306B May  4 15:05 msm8974/
    drwxr-xr-x  11 takboo  staff   374B May  4 15:05 msm8994/
    drwxr-xr-x  11 takboo  staff   374B May  4 15:05 msm8996/
    drwxr-xr-x   6 takboo  staff   204B May  4 15:05 msmtitanium/
    drwxr-xr-x   4 takboo  staff   136B May  4 15:03 osk5912/
    drwxr-xr-x   3 takboo  staff   102B May  4 15:05 pc-x86/
    drwxr-xr-x   3 takboo  staff   102B May  4 15:03 qemu-arm/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 qsd8250_ffa/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 qsd8250_surf/
    drwxr-xr-x   8 takboo  staff   272B May  4 15:05 qsd8650a_st1x/
    drwxr-xr-x   6 takboo  staff   204B May  4 15:03 sam7ex256/
    drwxr-xr-x   7 takboo  staff   238B May  4 15:05 surf-msm7k/
    drwxr-xr-x   6 takboo  staff   204B May  4 15:05 surf-qsd8k/
    

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

    make msm8916 EMMC_BOOT=1
    

    编译时可能遇到 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 所做的工作可以用下面的流程图表示:

kmain.png

thread_init_early

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

/**
 * @brief  Initialize threading system
 *
 * This function is called once, from kmain()
 */
void thread_init_early(void)
{
  int i;

  /* initialize the run queues */
  for (i=0; i < NUM_PRIORITIES; i++)
    list_initialize(&run_queue[i]);

  /* initialize the thread list */
  list_initialize(&thread_list);

  /* create a thread to cover the current running state */
  thread_t *t = &bootstrap_thread;
  init_thread_struct(t, "bootstrap");

  /* half construct this thread, since we're already running */
  t->priority = HIGHEST_PRIORITY;
  t->state = THREAD_RUNNING;
  t->saved_critical_section_count = 1;
  list_add_head(&thread_list, &t->thread_list_node);
  current_thread = t;
}

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

  1. 运行队列 run_queue。

    #define NUM_PRIORITIES 32
    
    struct list_node {
      struct list_node *prev;
      struct list_node *next;
    };
    
    static struct list_node run_queue[NUM_PRIORITIES];
    

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

  2. 线程链表 thread_list。

    static struct list_node thread_list;
    

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

  3. bootstrap 线程。

    #define THREAD_MAGIC 'thrd'
    
    typedef struct thread {
      int magic;
      struct list_node thread_list_node;
    
      /* active bits */
      struct list_node queue_node;
      int priority;
      enum thread_state state;
      int saved_critical_section_count;
      int remaining_quantum;
    
      /* if blocked, a pointer to the wait queue */
      struct wait_queue *blocking_wait_queue;
      status_t wait_queue_block_ret;
    
      /* architecture stuff */
      struct arch_thread arch;
    
      /* stack stuff */
      void *stack;
      size_t stack_size;
    
      /* entry point */
      thread_start_routine entry;
      void *arg;
    
      /* return code */
      int retcode;
    
      /* thread local storage */
      uint32_t tls[MAX_TLS_ENTRY];
    
      char name[32];
    } thread_t;
    
    static thread_t bootstrap_thread;
    
    static void init_thread_struct(thread_t *t, const char *name)
    {
      memset(t, 0, sizeof(thread_t));
      t->magic = THREAD_MAGIC;
      strlcpy(t->name, name, sizeof(t->name));
    }
    
    init_thread_struct(t, "bootstrap");
    
    /* half construct this thread, since we're already running */
    t->priority = HIGHEST_PRIORITY;
    t->state = THREAD_RUNNING;
    t->saved_critical_section_count = 1;
    list_add_head(&thread_list, &t->thread_list_node);
    current_thread = t;
    

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

  4. current_thread

    /* the current thread */
    thread_t *current_thread;
    

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

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

arch_early_init

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

void arch_early_init(void)
{
  /* turn off the cache */
  arch_disable_cache(UCACHE);

  /* set the vector base to our exception vectors so we dont need to double map at 0 */
#if ARM_CPU_CORTEX_A8
  set_vector_base(MEMBASE);
#endif

#if ARM_WITH_MMU
  arm_mmu_init();
#endif

  /* turn the cache back on */
  arch_enable_cache(UCACHE);

#if ARM_WITH_NEON
  /* enable cp10 and cp11 */
  uint32_t val;
  __asm__ volatile("mrc    p15, 0, %0, c1, c0, 2" : "=r" (val));
  val |= (3<<22)|(3<<20);
  __asm__ volatile("mcr    p15, 0, %0, c1, c0, 2" :: "r" (val));

  isb();

  /* set enable bit in fpexc */
  __asm__ volatile("mrc  p10, 7, %0, c8, c0, 0" : "=r" (val));
  val |= (1<<30);
  __asm__ volatile("mcr  p10, 7, %0, c8, c0, 0" :: "r" (val));
#endif

#if ARM_CPU_CORTEX_A8
  /* enable the cycle count register */
  uint32_t en;
  __asm__ volatile("mrc    p15, 0, %0, c9, c12, 0" : "=r" (en));
  en &= ~(1<<3); /* cycle count every cycle */
  en |= 1; /* enable all performance counters */
  __asm__ volatile("mcr    p15, 0, %0, c9, c12, 0" :: "r" (en));

  /* enable cycle counter */
  en = (1<<31);
  __asm__ volatile("mcr    p15, 0, %0, c9, c12, 1" :: "r" (en));
#endif
}

由于这方面的初始化比较偏向硬件,设置方法都是按住 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 平台的硬件设备进行早期初始化,其流程如下:

platform_early_init.png

board_init

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

#define SMEM_V11_SMEM_MAX_PMIC_DEVICES  4 // this is the max that device tree currently supports
#define SMEM_MAX_PMIC_DEVICES           SMEM_V11_SMEM_MAX_PMIC_DEVICES
#define MAX_PMIC_DEVICES       SMEM_MAX_PMIC_DEVICES
#define LINUX_MACHTYPE_UNKNOWN 0

enum platform {
  HW_PLATFORM_UNKNOWN = 0,
  HW_PLATFORM_SURF = 1,
  HW_PLATFORM_FFA = 2,
  HW_PLATFORM_FLUID = 3,
  HW_PLATFORM_SVLTE = 4,
  HW_PLATFORM_QT = 6,
  HW_PLATFORM_MTP_MDM = 7,
  HW_PLATFORM_MTP = 8,
  HW_PLATFORM_LIQUID = 9,
  HW_PLATFORM_DRAGON = 10,
  HW_PLATFORM_QRD = 11,
  HW_PLATFORM_HRD = 13,
  HW_PLATFORM_DTV = 14,
  HW_PLATFORM_RUMI   = 15,
  HW_PLATFORM_VIRTIO = 16,
  HW_PLATFORM_BTS = 19,
  HW_PLATFORM_RCM = 21,
  HW_PLATFORM_DMA = 22,
  HW_PLATFORM_STP = 23,
  HW_PLATFORM_SBC = 24,
  HW_PLATFORM_32BITS = 0x7FFFFFFF,
};

enum platform_subtype {
  HW_PLATFORM_SUBTYPE_UNKNOWN = 0,
  HW_PLATFORM_SUBTYPE_MDM = 1,
  HW_PLATFORM_SUBTYPE_8974PRO_PM8084 = 1,
  HW_PLATFORM_SUBTYPE_CSFB = 1,
  HW_PLATFORM_SUBTYPE_SVLTE1 = 2,
  HW_PLATFORM_SUBTYPE_SVLTE2A = 3,
  HW_PLATFORM_SUBTYPE_SGLTE = 6,
  HW_PLATFORM_SUBTYPE_DSDA = 7,
  HW_PLATFORM_SUBTYPE_DSDA2 = 8,
  HW_PLATFORM_SUBTYPE_SGLTE2 = 9,
  HW_PLATFORM_SUBTYPE_POLARIS = 64,
  HW_PLATFORM_SUBTYPE_32BITS = 0x7FFFFFFF
};

struct board_pmic_data {
  uint32_t pmic_type;
  uint32_t pmic_version;
  uint32_t pmic_target;
};

enum baseband {
  BASEBAND_MSM = 0,
  BASEBAND_APQ = 1,
  BASEBAND_CSFB = 2,
  BASEBAND_SVLTE1 = 3,
  BASEBAND_SVLTE2A = 4,
  BASEBAND_MDM = 5,
  BASEBAND_SGLTE = 6,
  BASEBAND_DSDA = 7,
  BASEBAND_DSDA2 = 8,
  BASEBAND_SGLTE2 = 9,
  BASEBAND_MDM2 = 10,
  BASEBAND_32BITS = 0x7FFFFFFF
};

typedef enum
  {
    PMIC_IS_PM6610,
    PMIC_IS_PM6620,
    PMIC_IS_PM6640,
    PMIC_IS_PM6650,
    PMIC_IS_PM7500,
    PMIC_IS_PANORAMIX,
    PMIC_IS_PM6652,
    PMIC_IS_PM6653,
    PMIC_IS_PM6658,
    PMIC_IS_EPIC,
    PMIC_IS_HAN,
    PMIC_IS_KIP,
    PMIC_IS_WOOKIE,
    PMIC_IS_PM8058,
    PMIC_IS_PM8028,
    PMIC_IS_PM8901,
    PMIC_IS_PM8027 ,
    PMIC_IS_ISL_9519,
    PMIC_IS_PM8921,
    PMIC_IS_PM8018,
    PMIC_IS_PM8015,
    PMIC_IS_PM8014,
    PMIC_IS_PM8821,
    PMIC_IS_PM8038,
    PMIC_IS_PM8922,
    PMIC_IS_PM8917,
    PMIC_IS_INVALID = 0x7fffffff,
  } pm_model_type_afly;

struct board_data {
  uint32_t platform;
  uint32_t foundry_id;
  uint32_t chip_serial;
  uint32_t platform_version;
  uint32_t platform_hw;
  uint32_t platform_subtype;
  uint32_t target;
  uint32_t baseband;
  struct board_pmic_data pmic_info[MAX_PMIC_DEVICES];
  uint32_t platform_hlos_subtype;
  uint32_t num_pmics;
  uint32_t pmic_array_offset;
  struct board_pmic_data *pmic_info_array;
};

static struct board_data board = {UNKNOWN,
                                  0,
                                  0,
                                  0,
                                  HW_PLATFORM_UNKNOWN,
                                  HW_PLATFORM_SUBTYPE_UNKNOWN,
                                  LINUX_MACHTYPE_UNKNOWN,
                                  BASEBAND_MSM,
                                  {{PMIC_IS_INVALID, 0, 0}, {PMIC_IS_INVALID, 0, 0},
                                   {PMIC_IS_INVALID, 0, 0}},
                                  0,
                                  0,
                                  0,
                                  NULL,
};

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

platform_clock_init

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

#define CLK_LOOKUP(con, c) { .con_id = con, .clk = &c }

struct clk_ops {
  int (*enable)(struct clk *clk);
  void (*disable)(struct clk *clk);
  void (*auto_off)(struct clk *clk);
  int (*reset)(struct clk *clk, enum clk_reset_action action);
  int (*set_rate)(struct clk *clk, unsigned rate);
  int (*set_min_rate)(struct clk *clk, unsigned rate);
  int (*set_max_rate)(struct clk *clk, unsigned rate);
  int (*set_flags)(struct clk *clk, unsigned flags);
  unsigned (*get_rate)(struct clk *clk);
  int (*list_rate)(struct clk *clk, unsigned n);
  int (*is_enabled)(struct clk *clk);
  long (*round_rate)(struct clk *clk, unsigned rate);
  int (*set_parent)(struct clk *clk, struct clk *parent);
  struct clk *(*get_parent)(struct clk *clk);
  bool (*is_local)(struct clk *clk);
};

struct clk {
  uint32_t flags;
  uint32_t rate;
  struct clk_ops *ops;
  const char *dbg_name;
  unsigned count;
};

struct clk_lookup {
  const char        *con_id;
  struct clk        *clk;
};

static struct clk_lookup msm_clocks_8916[] =
{
  CLK_LOOKUP("sdc1_iface_clk", gcc_sdcc1_ahb_clk.c),
  CLK_LOOKUP("sdc1_core_clk",  gcc_sdcc1_apps_clk.c),

  CLK_LOOKUP("sdc2_iface_clk", gcc_sdcc2_ahb_clk.c),
  CLK_LOOKUP("sdc2_core_clk",  gcc_sdcc2_apps_clk.c),

  CLK_LOOKUP("uart2_iface_clk", gcc_blsp1_ahb_clk.c),
  CLK_LOOKUP("uart2_core_clk",  gcc_blsp1_uart2_apps_clk.c),

  CLK_LOOKUP("usb_iface_clk",  gcc_usb_hs_ahb_clk.c),
  CLK_LOOKUP("usb_core_clk",   gcc_usb_hs_system_clk.c),

  CLK_LOOKUP("mdp_ahb_clk",          mdp_ahb_clk.c),
  CLK_LOOKUP("mdss_esc0_clk",        mdss_esc0_clk.c),
  CLK_LOOKUP("mdss_esc1_clk",        mdss_esc1_clk.c),
  CLK_LOOKUP("mdss_axi_clk",         mdss_axi_clk.c),
  CLK_LOOKUP("mdss_vsync_clk",       mdss_vsync_clk.c),
  CLK_LOOKUP("mdss_mdp_clk_src",     mdss_mdp_clk_src.c),
  CLK_LOOKUP("mdss_mdp_clk",         mdss_mdp_clk.c),

  CLK_LOOKUP("ce1_ahb_clk",  gcc_ce1_ahb_clk.c),
  CLK_LOOKUP("ce1_axi_clk",  gcc_ce1_axi_clk.c),
  CLK_LOOKUP("ce1_core_clk", gcc_ce1_clk.c),
  CLK_LOOKUP("ce1_src_clk",  ce1_clk_src.c),

  CLK_LOOKUP("blsp1_qup2_ahb_iface_clk", gcc_blsp1_ahb_clk.c),
  CLK_LOOKUP("gcc_blsp1_qup2_i2c_apps_clk_src", gcc_blsp1_qup2_i2c_apps_clk_src.c),
  CLK_LOOKUP("gcc_blsp1_qup2_i2c_apps_clk", gcc_blsp1_qup2_i2c_apps_clk.c),

  CLK_LOOKUP("blsp1_qup4_ahb_iface_clk", gcc_blsp1_ahb_clk.c),
  CLK_LOOKUP("gcc_blsp1_qup4_i2c_apps_clk_src", gcc_blsp1_qup4_i2c_apps_clk_src.c),
  CLK_LOOKUP("gcc_blsp1_qup4_i2c_apps_clk", gcc_blsp1_qup4_i2c_apps_clk.c),
};

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 是否能够使用。

int is_scm_call_available(uint32_t svc_id, uint32_t cmd_id)
{
  int ret;
  scmcall_arg scm_arg = {0};
  scmcall_ret scm_ret = {0};

  scm_arg.x0 = MAKE_SIP_SCM_CMD(SCM_SVC_INFO, IS_CALL_AVAIL_CMD);
  scm_arg.x1 = MAKE_SCM_ARGS(0x1);
  scm_arg.x2 = MAKE_SIP_SCM_CMD(svc_id, cmd_id);

  ret = scm_call2(&scm_arg, &scm_ret);

  if (!ret)
    return scm_ret.x1;

  return ret;
}

static int scm_arm_support_available(uint32_t svc_id, uint32_t cmd_id)
{
  int ret;

  ret = is_scm_call_available(SCM_SVC_INFO, IS_CALL_AVAIL_CMD);

  if (ret > 0)
    scm_arm_support = true;

  return ret;
}

void scm_init()
{
  int ret;

  if (scm_initialized)
    return;

  ret = scm_arm_support_available(SCM_SVC_INFO, IS_CALL_AVAIL_CMD);

  if (ret < 0)
    dprintf(CRITICAL, "Failed to initialize SCM\n");
}

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

  1. scmcall_arg

    typedef struct {
      uint32_t x0;/* command ID details as per ARMv8 spec :
                     0:7 command, 8:15 service id
                     0x02000000: SIP calls
                     30: SMC32 or SMC64
                     31: Standard or fast calls*/
      uint32_t x1; /* # of args and attributes for buffers
                    * 0-3: arg #
                    * 4-5: type of arg1
                    * 6-7: type of arg2
                    * :
                    * :
                    * 20-21: type of arg8
                    * 22-23: type of arg9
                    */
      uint32_t x2; /* Param1 */
      uint32_t x3; /* Param2 */
      uint32_t x4; /* Param3 */
      uint32_t x5[10]; /* Indirect parameter list */
      uint32_t atomic; /* To indicate if its standard or fast call */
    } scmcall_arg;
    

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

  2. scmcall_ret

    /* Return value for the SCM call:
     * SCM call returns values in register if its less than
     * 12 bytes, anything greater need to be input buffer + input len
     * arguments
     */
    typedef struct
    {
      uint32_t x1;
      uint32_t x2;
      uint32_t x3;
    } scmcall_ret;
    

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

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

target_early_init

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

/* Defining functions that's exposed to outside world and in coformance to
 * existing uart implemention. These functions are being called to initialize
 * UART and print debug messages in bootloader.
 */
void uart_dm_init(uint8_t id, uint32_t gsbi_base, uint32_t uart_dm_base)
{
  static uint8_t port = 0;
  char *data = "Android Bootloader - UART_DM Initialized!!!\n";

  /* Configure the uart clock */
  clock_config_uart_dm(id);
  dsb();

  /* Configure GPIO to provide connectivity between UART block
     product ports and chip pads */
  gpio_config_uart_dm(id);
  dsb();

  /* Configure GSBI for UART_DM protocol.
   * I2C on 2 ports, UART (without HS flow control) on the other 2.
   * This is only on chips that have GSBI block
   */
   if(gsbi_base)
    writel(GSBI_PROTOCOL_CODE_I2C_UART <<
      GSBI_CTRL_REG_PROTOCOL_CODE_S,
      GSBI_CTRL_REG(gsbi_base));
  dsb();

  /* Configure clock selection register for tx and rx rates.
   * Selecting 115.2k for both RX and TX.
   */
  writel(UART_DM_CLK_RX_TX_BIT_RATE, MSM_BOOT_UART_DM_CSR(uart_dm_base));
  dsb();

  /* Intialize UART_DM */
  msm_boot_uart_dm_init(uart_dm_base);

  msm_boot_uart_dm_write(uart_dm_base, data, 44);

  ASSERT(port < ARRAY_SIZE(port_lookup));
  port_lookup[port++] = uart_dm_base;

  /* Set UART init flag */
  uart_init_flag = 1;
}

void target_early_init(void)
{
#if WITH_DEBUG_UART
  uart_dm_init(2, 0, BLSP1_UART1_BASE);
#endif
}

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 的地址由以下宏定义:

    #define MPM2_MPM_SLEEP_TIMETICK_COUNT_VAL  0x004A3000
    

    这个宏的定义在 platform/msm8916/include/platform/iomap.h 文件中。

  2. bootstate 地址在 msm8916 平台下,bootstate 即 bs 的地址由以下宏定义:

    #define MSM_SHARED_IMEM_BASE        0x08600000
    #define BS_INFO_OFFSET              (0x6B0)
    #define BS_INFO_ADDR                (MSM_SHARED_IMEM_BASE + BS_INFO_OFFSET)
    

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

    /* The order of the entries in this enum does not correspond to bootup order.
     * It is mandated by the expected order of the entries in imem when the values
     * are read in the kernel.
     */
    enum bs_entry {
      BS_BL_START = 0,
      BS_KERNEL_ENTRY,
      BS_SPLASH_SCREEN_DISPLAY,
      BS_KERNEL_LOAD_TIME,
      BS_KERNEL_LOAD_START,
      BS_KERNEL_LOAD_DONE,
      BS_MAX,
    };
    

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

call_constructors

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

heap_init

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

void heap_init(void)
{
  LTRACE_ENTRY;

  // set the heap range
  theheap.base = (void *)HEAP_START;
  theheap.len = HEAP_LEN;

  LTRACEF("base %p size %zd bytes\n", theheap.base, theheap.len);

  // initialize the free list
  list_initialize(&theheap.free_list);

  // create an initial free chunk
  heap_insert_free_chunk(heap_create_free_chunk(theheap.base, theheap.len));

  // dump heap info
  //    heap_dump();

  //    dprintf(INFO, "running heap tests\n");
  //    heap_test();
}

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

struct list_node {
  struct list_node *prev;
  struct list_node *next;
};

struct heap {
  void *base;
  size_t len;
  struct list_node free_list;
};

// heap static vars
static struct heap theheap;

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

// end of the binary
extern int _end;

// end of memory
extern int _end_of_ram;

#define HEAP_START ((unsigned long)&_end)
#define HEAP_LEN ((size_t)&_end_of_ram - (size_t)&_end)

_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 文件中,它的代码如下:

void dpc_init(void)
{
  event_init(&dpc_event, false, 0);

  thread_resume(thread_create("dpc", &dpc_thread_routine, NULL, DPC_PRIORITY, DEFAULT_STACK_SIZE));
}

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

thread_t *thread_create(const char *name, thread_start_routine entry, void *arg, int priority, size_t stack_size)
{
  thread_t *t;

  t = malloc(sizeof(thread_t));
  if (!t)
    return NULL;

  init_thread_struct(t, name);

  t->entry = entry;
  t->arg = arg;
  t->priority = priority;
  t->saved_critical_section_count = 1; /* we always start inside a critical section */
  t->state = THREAD_SUSPENDED;
  t->blocking_wait_queue = NULL;
  t->wait_queue_block_ret = NO_ERROR;

  /* create the stack */
  t->stack = malloc(stack_size);
  if (!t->stack) {
    free(t);
    return NULL;
  }

  t->stack_size = stack_size;

  /* inheirit thread local storage from the parent */
  int i;
  for (i=0; i < MAX_TLS_ENTRY; i++)
    t->tls[i] = current_thread->tls[i];

  /* set up the initial stack frame */
  arch_thread_initialize(t);

  /* add it to the global thread list */
  enter_critical_section();
  list_add_head(&thread_list, &t->thread_list_node);
  exit_critical_section();

  return t;
}

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

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

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

struct context_switch_frame {
  vaddr_t r4;
  vaddr_t r5;
  vaddr_t r6;
  vaddr_t r7;
  vaddr_t r8;
  vaddr_t r9;
  vaddr_t r10;
  vaddr_t r11;
  vaddr_t lr;
  vaddr_t usp;
  vaddr_t ulr;
};

void arch_thread_initialize(thread_t *t)
{
  // create a default stack frame on the stack
  vaddr_t stack_top = (vaddr_t)t->stack + t->stack_size;

  // make sure the top of the stack is 8 byte aligned for EABI compliance
  stack_top = ROUNDDOWN(stack_top, 8);

  struct context_switch_frame *frame = (struct context_switch_frame *)(stack_top);
  frame--;

  // fill it in
  memset(frame, 0, sizeof(*frame));
  frame->lr = (vaddr_t)&initial_thread_func;

  // set the stack pointer
  t->arch.sp = (vaddr_t)frame;
}

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

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

status_t thread_resume(thread_t *t)
{
#if THREAD_CHECKS
  ASSERT(t->magic == THREAD_MAGIC);
  ASSERT(t->state != THREAD_DEATH);
#endif

  if (t->state == THREAD_READY || t->state == THREAD_RUNNING)
    return ERR_NOT_SUSPENDED;

  enter_critical_section();
  t->state = THREAD_READY;
  insert_in_run_queue_head(t);
  thread_yield();
  exit_critical_section();

  return NO_ERROR;
}

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

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

    static void insert_in_run_queue_head(thread_t *t)
    {
    #if THREAD_CHECKS
      ASSERT(t->magic == THREAD_MAGIC);
      ASSERT(t->state == THREAD_READY);
      ASSERT(!list_in_list(&t->queue_node));
      ASSERT(in_critical_section());
    #endif
    
      list_add_head(&run_queue[t->priority], &t->queue_node);
      run_queue_bitmap |= (1<<t->priority);
    }
    

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

    run_queue_bitmap.png

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

    void thread_resched(void)
    {
      thread_t *oldthread;
      thread_t *newthread;
    
      oldthread = current_thread;
    
      // at the moment, can't deal with more than 32 priority levels
      ASSERT(NUM_PRIORITIES <= 32);
    
      // should at least find the idle thread
    #if THREAD_CHECKS
      ASSERT(run_queue_bitmap != 0);
    #endif
    
      int next_queue = HIGHEST_PRIORITY - __builtin_clz(run_queue_bitmap) - (32 - NUM_PRIORITIES);
      //dprintf(SPEW, "bitmap 0x%x, next %d\n", run_queue_bitmap, next_queue);
    
      newthread = list_remove_head_type(&run_queue[next_queue], thread_t, queue_node);
    
      if (list_is_empty(&run_queue[next_queue]))
        run_queue_bitmap &= ~(1<<next_queue);
    
      newthread->state = THREAD_RUNNING;
    
      if (newthread == oldthread)
        return;
    
      /* set up quantum for the new thread if it was consumed */
      if (newthread->remaining_quantum <= 0) {
        newthread->remaining_quantum = 5; // XXX make this smarter
      }
    
      /* do the switch */
      oldthread->saved_critical_section_count = critical_section_count;
      current_thread = newthread;
      critical_section_count = newthread->saved_critical_section_count;
      arch_context_switch(oldthread, newthread);
    }
    

    整体流程如下:

    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 文件中,是汇编代码,其代码如下:

      /* arm_context_switch(addr_t *old_sp, addr_t new_sp) */
      FUNCTION(arm_context_switch)
      /* save all the usual registers + user regs */
      /* the spsr is saved and restored in the iframe by exceptions.S */
        sub        r3, sp, #(11*4)        /* can't use sp in user mode stm */
        mov        r12, lr
        stmia    r3, { r4-r11, r12, r13, r14 }^
      
      /* save old sp */
        str        r3, [r0]
      
        /* clear any exlusive locks that the old thread holds */
      #if ARM_ISA_ARMV7
        clrex
      #elif ARM_ISA_ARMV6
        /* have to do a fake strex to clear it */
        ldr        r0, =strex_spot
        strex    r3, r2, [r0]
      #endif
      
        /* load new regs */
        ldmia    r1, { r4-r11, r12, r13, r14 }^
        mov        lr, r12                /* restore lr */
        add        sp, r1, #(11*4)     /* restore sp */
        bx        lr
      

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

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

timer_init

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

typedef struct timer {
  int magic;
  struct list_node node;

  time_t scheduled_time;
  time_t periodic_time;

  timer_callback callback;
  void *arg;
} 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 中断之间互相的通道,主要的作用就是检测,管理和分发中断。


请期待后续系列文章,如果有任何疑问和交流意向可以和我们联系 SecRet201611@gmail.com

未经允许不得转载:安全路透社 » 高通(Qualcomm)LK源码深度分析

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册