ARM64架构启动流程

Linux和android在烧写kernel前需要烧写以下文件:

 

Bootparam_sa0*: 需要传递给linux kernel 的param

 

究竟是如何完成boot param 的传递呢:

 

都知道linux kernel C语言的入口函数是start_kernel()

 

在start_kernel()函数里面会有setup_arch(&command_line) ,

 

之后调用setup_processor(), setup_machine_fdt(__atags_pointer), setup_machine_tags(machine_arch_type).

 

这些调用流程如果有Jtag 的话,可以清晰的看到调用流程,同时需要注意的是linux kernel 编译的时候会进行GNU 编译优化, 如果一个函数是static 类型,而且只被调用过一次,那么这个函数就会被优化掉,不会产生栈帧,类似有内联函数的处理。

 

接着说setup_machine_tags()

 

这个函数中会有如下代码:

 

if(__atags_pointer)

    tags = phys_to_virt(__atags_poniter);

 

这里解释一下,__atags_pointer 是在head-common.S 中进行的赋值操作。

 

也就是说在start_kernel 打上断点的话就能看到__atags_pointer 的值,在OMAP4430开发板上就是0x80000100,这是物理地址。

 

因此,需要使用phy_to_virt() 函数把物理地址转换成虚拟地址。转换后一般就是0xc0000100,这样在这个地址上就能看到boot param 了!

 

 

 

Bl2*:BL2 image将会为后续image的加载执行相关的初始化操作。主要是内存,MMU,串口以及EL3软件运行环境的设置,并且加载bl3x的image到RAM中

 

1:通过查看bl2.ld.S文件就可以发现,bl2 image的入口函数是bl2_entrypoint。该函数定义在bl2/aarch64/bl2_entrypoint.S文件中。

bl2_entrypoint:该函数最终会出发smc操作,从bl1中将CPU的控制权转交给bl31

2:bl2_main

该函数主要实现将bl3x的image加载RAM中,并通过smc调用执行bl1中指定的smc handle将CPU的全向交给bl31。

3: bl2_load_images

该函数用来加载bl3x的image到RAM中,返回一个具有image入口信息的变量。smc handle根据该变量跳转到bl31进行执行

4: REGISTER_BL_IMAGE_DESCS(bl2_mem_params_descs)

该宏的执行将会初始化组成bl2加载bl3x image的列表使用到的重要全局变量, 其中bl2_mem_params_descs变量的定义如下:

static bl_mem_params_node_t bl2_mem_params_descs[] = {

#ifdef SCP_BL2_BASE

         /* Fill SCP_BL2 related information if it exists */

    {

             .image_id = SCP_BL2_IMAGE_ID,

 

             SET_STATIC_PARAM_HEAD(ep_info, PARAM_IMAGE_BINARY,

                       VERSION_2, entry_point_info_t, SECURE | NON_EXECUTABLE),

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_IMAGE_BINARY,

                       VERSION_2, image_info_t, 0),

             .image_info.image_base = SCP_BL2_BASE,

             .image_info.image_max_size = PLAT_CSS_MAX_SCP_BL2_SIZE,

 

             .next_handoff_image_id = INVALID_IMAGE_ID,

    },

#endif /* SCP_BL2_BASE */

 

#ifdef EL3_PAYLOAD_BASE

         /* Fill EL3 payload related information (BL31 is EL3 payload)*/

    {

             .image_id = BL31_IMAGE_ID,

 

             SET_STATIC_PARAM_HEAD(ep_info, PARAM_EP,

                       VERSION_2, entry_point_info_t,

                       SECURE | EXECUTABLE | EP_FIRST_EXE),

             .ep_info.pc = EL3_PAYLOAD_BASE,

             .ep_info.spsr = SPSR_64(MODE_EL3, MODE_SP_ELX,

                       DISABLE_ALL_EXCEPTIONS),

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,

                       VERSION_2, image_info_t,

                       IMAGE_ATTRIB_PLAT_SETUP | IMAGE_ATTRIB_SKIP_LOADING),

 

             .next_handoff_image_id = INVALID_IMAGE_ID,

    },

 

#else /* EL3_PAYLOAD_BASE */

 

         /* Fill BL31 related information */

    {

             .image_id = BL31_IMAGE_ID,

 

             SET_STATIC_PARAM_HEAD(ep_info, PARAM_EP,

                       VERSION_2, entry_point_info_t,

                       SECURE | EXECUTABLE | EP_FIRST_EXE),

             .ep_info.pc = BL31_BASE,

             .ep_info.spsr = SPSR_64(MODE_EL3, MODE_SP_ELX,

                       DISABLE_ALL_EXCEPTIONS),

#if DEBUG

             .ep_info.args.arg1 = ARM_BL31_PLAT_PARAM_VAL,

#endif

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,

                       VERSION_2, image_info_t, IMAGE_ATTRIB_PLAT_SETUP),

             .image_info.image_base = BL31_BASE,

             .image_info.image_max_size = BL31_LIMIT - BL31_BASE,

 

# ifdef BL32_BASE

             .next_handoff_image_id = BL32_IMAGE_ID,

# else

             .next_handoff_image_id = BL33_IMAGE_ID,

# endif

    },

 

# ifdef BL32_BASE

         /* Fill BL32 related information */

    {

             .image_id = BL32_IMAGE_ID,

 

             SET_STATIC_PARAM_HEAD(ep_info, PARAM_EP,

                       VERSION_2, entry_point_info_t, SECURE | EXECUTABLE),

             .ep_info.pc = BL32_BASE,

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,

                       VERSION_2, image_info_t, 0),

             .image_info.image_base = BL32_BASE,

             .image_info.image_max_size = BL32_LIMIT - BL32_BASE,

 

             .next_handoff_image_id = BL33_IMAGE_ID,

    },

# endif /* BL32_BASE */

 

         /* Fill BL33 related information */

    {

             .image_id = BL33_IMAGE_ID,

             SET_STATIC_PARAM_HEAD(ep_info, PARAM_EP,

                       VERSION_2, entry_point_info_t, NON_SECURE | EXECUTABLE),

# ifdef PRELOADED_BL33_BASE

             .ep_info.pc = PRELOADED_BL33_BASE,

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,

                       VERSION_2, image_info_t, IMAGE_ATTRIB_SKIP_LOADING),

# else

             .ep_info.pc = PLAT_ARM_NS_IMAGE_OFFSET,

 

             SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,

                       VERSION_2, image_info_t, 0),

             .image_info.image_base = PLAT_ARM_NS_IMAGE_OFFSET,

             .image_info.image_max_size = ARM_DRAM1_SIZE,

# endif /* PRELOADED_BL33_BASE */

 

             .next_handoff_image_id = INVALID_IMAGE_ID,

    }

#endif /* EL3_PAYLOAD_BASE */

};

 

在该变量中规定了SCP_BL2, EL3_payload, bl32, bl33 image的相关信息,例如:

 

 

 

image的入口地址信息:ep_info

image在RAM中的基地址:image_base

image的基本信息:image_info

image的ID值:image_id

 

 

3:cert_header:加载认证程序

 

 

 

 

 

 

bl31*arm trusted firmware

在bl2中通过调用smc指令后会跳转到bl31中进行执行,bl31最终主要的作用是建立EL3 runtime software,在该阶段会建立各种类型的smc调用注册并完成对应的cortex状态切换。该阶段主要执行在monitor中。

1:bl31_entrypoint

通过bl31.ld.S文件可知, bl31的入口函数是:bl31_entrypoint函数

执行完bl31_entrypoint函数后,将跳转到bl33中执行,即执行bootloader

2:bl31_main

该函数主要完成必要初始化操作,配置EL3中的各种smc操作,以便在后续顺利响应在CA和TA中产生的smc操作

3: runtime_svc_init

该函数主要用来建立smc索引表并执行EL3中提供的service的初始化操作

4:DECLARE_RT_SVC

该宏用来在编译的时候将EL3中的service编译进rt_svc_descs段中,该宏定义如下:

 

#define DECLARE_RT_SVC(_name, _start, _end, _type, _setup, _smch) \

       static const rt_svc_desc_t __svc_desc_ ## _name \

              __section("rt_svc_descs") __used = { \

                     .start_oen = _start, \

                     .end_oen = _end, \

                     .call_type = _type, \

                     .name = #_name, \

                     .init = _setup, \

                     .handle = _smch }

start_oen:该service的起始内部number

end.oen: 该service的末尾number

call_type: 调用的smc的类型

name: 该service的名字

init: 该service在执行之前需要被执行的初始化操作

handle: 当触发了call type的调用时调用的handle该请求的函数

 

5: 以OP-TEE为例从bl31跳转到OP-TEE

实现从bl31到OP-TEE的跳转是通过执行opteed_setup函数来实现的,该函数在执行runtime_svc_int中对各service做service->init()函数来实现,而OPTEE这个service就是通过DECALARE_RT_SVC被注册到tr_svc_descs段中,代码存在service/spd/opteed/opteed_main.c文件中,内容如下:

 

 

 

 

 

Tee*

 

驱动与secure world之间的数据交互是通过共享内存来完成的,在OP-TEE启动的时候会将作为共享内存的物理内存块reserve出来,具体的可以查看OP-TEE启动代码中的core_init_mmu_map函数。在OP-TEE驱动初始化阶段会将reserve出来作为共享内存的物理内存配置成驱动的内存池,并且通知OP-TEE OS执行相同的动作。配置完成之后,secure world就能从共享内存中获取到来自于REE端的数据。

1:配置驱动与OP-TEE之间的共享内存

在驱动做probe操作时,会调用到optee_config_shm_memremap函数来完成OP-TEE驱动和OP-TEE之间共享内存的配置。该函数定义在linux/drivers/tee/optee/core.c文件中。

 

在secure world中reserve出来的内存块作为驱动与sercure world之间的共享内存使用。在驱动端将会建立一个内存池,以便驱动在需要的使用通过调用驱动的alloc函数来完成共享内存的分配。而共享内存池的建立是通过调用tee_shm_pool_alloc_res_mem来实现的。

 

2:分配和设置OP-TEE驱动作为被libteec和tee_supplicant使用的设备信息结构体

在OP-TEE驱动做probe操作时会分配和设置两个tee_device结构体变量,分别用来表示被libteec和tee_supplicant使用的设备。分别通过调用tee_device_alloc(&optee_desc, NULL, pool, optee)和tee_device_alloc(&optee_supp_desc, NULL, pool, optee)来实现,主要是设置驱动作为被libteec和tee_supplicant使用的设备的具体操作和表示该设备对应的名称等信息,

  当libteec调用文件操作函数执行打开,关闭等操作/dev/tee0设备文件的时,其最终将调用到optee_desc中具体的函数来实现对应操作。

  当tee_supplicant调用文件操作函数执行打开,关闭等操作/dev/teepriv0设备文件的时,其最终将调用到optee_supp_desc中具体的函数来实现对应操作。

  上述配置操作都是通过调用tee_device_all函数来实现的

 

3:设备注册

完成版本检查,共享内存池配置,不同设备的配置之后就需要将这些配置好的设备注册到系统中去。对于被liteec和tee_supplicant使用的设备分别通过调用tee_device_register(optee->teedev)和tee_device_register(optee->supp_teedev)来实现。其中optee->teedev和optee->supp_teedev就是在上一章中被配置好的分别被libteec和tee_supplicant使用的设备结构体。调用tee_device_register函数来实现将设备注册到系统的目的,

 

4:两个队列的建立

在OP-TEE驱动提供两个设备,分别被libteec使用的/dev/tee0和被tee_supplicant使用的/dev/teepriv0。为确保normal world与secure world之间数据交互便利和能在normal world端进行异步处理。OP-TEE驱动在挂载的时候会建立两个类似于消息队列的队列,用于保存normal world的请求数据以及secure world请求。optee_wait_queue_init用于初始化/dev/tee0设备使用的队列,optee_supp_init用于初始化/dev/teepriv0设备使用的队列

 

5:通知op-tee是能以下共享内存的cache

当一切做完之后,最终就剩下通知OP-TEE使能共享内存的cache了,在驱动挂载过程中调用optee_enable_shm_cache函数来实现

 

6:fast smc与std smc

在OP-TEE驱动的挂载过程中会使用fast smc的方式从OP-TEE中获取到相关数据,例如从secure world中获取reserve的共享内存信息时就是通过调用如下函数来实现的:

invoke_fn(OPTEE_SMC_GET_SHM_CONFIG, 0, 0, 0, 0, 0, 0, 0, &res.smccc);

  在支持smc操作的32位系统中该函数等价于

arm_smccc_smc(OPTEE_SMC_GET_SHM_CONFIG, 0, 0, 0, 0, 0, 0, 0, &res.smccc);

  而OPTEE_SMC_ENABLE_SHM_CACHE的定义如下:

#define OPTEE_SMC_FUNCID_GET_SHM_CONFIG 7

#define OPTEE_SMC_GET_SHM_CONFIG OPTEE_SMC_FAST_CALL_VAL(OPTEE_SMC_FUNCID_GET_SHM_CONFIG)

 

  完全展开之后OPTEE_SMC_GET_SHM_CONFIG 宏的值的各个bit中的数值如下如所示:

 

在执行smc操作时,cortex会解读第一个参数中的相关位来决定进入到monitor模式后的相关操作,在ARM官方定义了第一个参数(a0)的定义如下

 

当bit31为1时,则表示进入monitor模式后会执行fast smc的中断处理函数,而在不带ATF的ARCH32中,monitor的中断向量表如下:

 

 

 

FUNC thread_vector_table , :

UNWIND(     .fnstart)

UNWIND(     .cantunwind)

       b     vector_std_smc_entry

       b     vector_fast_smc_entry

       b     vector_cpu_on_entry

       b     vector_cpu_off_entry

       b     vector_cpu_resume_entry

       b     vector_cpu_suspend_entry

       b     vector_fiq_entry

       b     vector_system_off_entry

       b     vector_system_reset_entry

UNWIND(     .fnend)

END_FUNC thread_vector_table

由此可以见,驱动在挂载的时候请求共享内存配置数据的请求将会被vector_fast_smc_entry处理。而当请求来自于CA的请求时,驱动会将第一个参数的bi31设置成0,也就是CA的请求会被vector_std_smc_entry进行处理

 

总结:

OP-TEE的驱动的挂载过程来看,OP-TEE驱动会分别针对libteec和tee_supplicant建立不同的设备/dev/tee0和/dev/teepriv0。并且为两个设备中的des执行各自独有的operation,并建立类似消息队列来存放normal world与secure world之间的请求,这样libteec和tee_supplicant使用OP-TEE驱动的时候就能做到相对的独立。secure world与OP-TEE驱动之间使用共享内存进行数据交互。用于作为共享内存的物理内存块在OP-TEE启动的时做MMU初始化时需要被reserve出来,在OP-TEE挂载过程中需要将该内存块映射到系统内存中。

 

 

 

U-boot目标功能是从flash中读出内核,放到内存中,启动内核

1:第一阶段

 

硬件设备初始化;
为加载 Bootloader 的第二阶段代码准备 RAM 空间;
复制 Bootloader 的第二阶段代码到 RAM 空间中;
设置好栈;
跳转到第二阶段代码的 C 入口点(start_armboot);
备注:在第一阶段进行的硬件初始化一般包括:关闭 WATCHDOG、关中断、设置 CPU的速度和时钟频率、 RAM 初始化等。这些并不都是必须的,比如 S3C2410/S3C2440的开发板所使用的 U-Boot 中,就将 CPU的速度和时钟频率放在第二阶段进行设置。

 

2:第二阶段

 

初始化本阶段要使用到的硬件设备;
检测系统内存映射( Memory map );
将内核映像和根文件系统映像从 Flash上读到 RAM 空间中;
为内核设置启动参数;
调用内核;
备注:为了方便开发,初始化一个串口以便程序员与 Bootloader 进行交互。

 

 

 

 

common/cmd_nand.c文件提供了操作NAND Flash的各种命令,这些命令调用drivers/nand/nand_base.c中的擦除、读写函数来实现;而这些函数是针对NAND Flash的共性做的封装,与平台/开发板相关的代码用宏或外部函数代替;平台相关则在cpu/xxx,开发板相关则在board/xxx。
以增加yaffs文件系统映像功能为例,先在common下的cmd_nand.c增加命令,比如nand write.yaffs,这个命令调用/drivers/nand/nand_util.c中的函数,而这些函数依赖于drivers/nand/nand_base.c、cpu/arm920t/s3c24x0/nand_flash.c文件中的相关函数

 

所谓检测内存映射,就是确定板上使用了多少内存、他们的地址空间是什么。由于嵌入式开发中的 Bootloader 多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。
Flash上的内核映像有可能是经过压缩的,在读到 RAM 之后,还需要进行解压。当然,对于有自解压功能的内核,不需要 Bootloader 来解压。
将根文件系统映像复制到RAM中并不是必须的,这取决于是什么类型的根文件系统,以及内核访问它的方法。
将内核放在适当的位置后,在跳入执行内核之前,需要根据内核启动的需求,配置相应的启动参数。

posted @ 2020-04-17 12:02  蒲城小农  阅读(3345)  评论(0编辑  收藏  举报