Talk about SOC startup (4) ATF BL31 startup process

https://zhuanlan.zhihu.com/p/520052961

This article is based on the following hardware and software assumptions:

Architecture: AARCH64

Software: ATF V2.5

1 BL31 startup process

Different from bl1 and bl2, bl31 contains two functions. It is used as a part of the boot process at startup to perform software and hardware initialization and start bl32 and bl33 images. After the system startup is complete, it will continue to reside in the system and handle smc exceptions from other exception levels, as well as other interrupts that need to be routed to EL3 for processing. Therefore, the bl31 startup process mainly includes the following tasks:

(1) cpu initialization

(2) C runtime environment initialization

(3) Basic hardware initialization, such as gic, serial port, timer, etc.

(4) Page table creation and cache enablement

(5) Preparation for starting the post-level image and jumping to the new image

(6) If bl31 supports el3 interrupt, you need to initialize the interrupt processing framework

(7) SMC processing of different secure states at runtime, and initialization of exception level switching context

(8) Runtime service registration for processing smc commands

Gossip less, the usual picture above:

insert image description here

2 bl31 basic initialization

2.1 Parameter saving

mov	x20, x0
mov	x21, x1
mov	x22, x2
mov	x23, x3

Same as bl2, save the parameters passed in by bl2 from the caller register to the callee register

2.2 el3_entrypoint_common function

This function has been introduced in detail in bl1, but the calling method of bl31 is still different from bl1. Let's look at the call in bl31:

#if !RESET_TO_BL31
	el3_entrypoint_common					\
		_init_sctlr=0					\
		_warm_boot_mailbox=0				\
		_secondary_cold_boot=0				\
		_init_memory=0					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
#else
		el3_entrypoint_common					\
		_init_sctlr=1					\
		_warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS	\
		_secondary_cold_boot=!COLD_BOOT_SINGLE_CPU	\
		_init_memory=1					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
	mov	x20, 0
	mov	x21, 0
	mov	x22, 0
	mov	x23, 0
#endif

As can be seen from the above code, according to whether RESET_TO_BL31 is set or not, this function has two different sets of calling parameters. This is because atf supports two boot methods:

(1) The startup starts from bl1, which is the default startup method of ATF. At this time, because bl1 has executed the el3_entrypoint_common function, the basic configuration of the system has been set up. Therefore, the process of setting sctlr register, hot start jump processing, secondary cpu processing, and memory initialization process has been completed in bl1, and they can be skipped in bl31

(2) The basis for supporting starting from bl31 is that armv8 supports dynamically setting the restart address of the cpu. The armv8 architecture provides the RVBAR (reset vector base address register) register for setting the start position of the cpu during reset. There are three registers: RVBAR_EL1, RVBAR_EL2, and RVBAR_EL3, which one to use is determined according to the highest exception level implemented by the system. We know that armv8 restart is always executed from the highest exception level, so we only need to set the RVBAR register with the highest exception level. Since bl31 runs under el3, if we need to support starting from bl31, it can be realized by setting the address to the RVBAR_EL3 register.

If the startup starts from bl31, since it is the first-level startup image, el3_entrypoint_common needs to set the system state from the beginning, so the sctlr register, startup jump processing, secondary cpu processing, and memory initialization process in this function need to be executed.

Although el3_entrypoint_common needs to do a lot of work, this method directly skips the two-level startup process of bl1 and bl2. Compared with the first method, its startup speed is faster, which is also its biggest advantage.

The last way to clear the value of the parameter saving registers x20 – x23 is also very understandable, because at this time bl31 is the first-level image to start, and naturally there are no parameters passed by the previous-level image. At this time, clearing these values ​​​​can be done Avoid problems when parsing parameters later.

3 bl31 parameter setting

3.1 bl31_early_platform_setup2

This function first initializes the qemu console, then parses the image description linked list parameters passed in by bl2, and saves the parsed bl32 and bl33 image ep_info into the global variable. Its main process is as follows:

qemu_console_init();1bl_params_t *params_from_bl2 = (bl_params_t *)arg0;2
	bl_params_node_t *bl_params = params_from_bl2->head;3while (bl_params) {
    
    4if (bl_params->image_id == BL32_IMAGE_ID) {
    
    
			bl32_image_ep_info = *bl_params->ep_info;5}

		if (bl_params->image_id == BL33_IMAGE_ID){
    
    
			bl33_image_ep_info = *bl_params->ep_info;6}

		bl_params = bl_params->next_params_info;
	}
	if (!bl33_image_ep_info.pc)7panic();

(1) Console initialization

(2) Obtain the image description parameter pointer passed in by arg0

(3) Obtain the head node of the mirror list

(4) Traversing the mirror list

(5) If the linked list contains a bl32 image descriptor, save its ep_info to a global variable

(6) Many of the linked lists contain bl33 image descriptors, and also save their ep_info to global variables

(7) Verify the entry address of the bl33 image

3.2 bl31_plat_arch_setup

This function is used to create a page table for bl31-related memory, and enable MMU and dcache, the code is as follows:

void bl31_plat_arch_setup(void)

{
    
    
	qemu_configure_mmu_el3(BL31_BASE, (BL31_END - BL31_BASE),

	BL_CODE_BASE, BL_CODE_END,

	BL_RO_DATA_BASE, BL_RO_DATA_END,

	BL_COHERENT_RAM_BASE, BL_COHERENT_RAM_END);
}

4 bl31 main processing function

4.1 bl31_platform_setup

This function is platform-dependent, and the implementation of the qemu platform is as follows:

void bl31_platform_setup(void)
{
    
    
	plat_qemu_gic_init();1qemu_gpio_init();2}

(1) Initialize gic, including the initialization of gic's distributor, redistributor, cpu interface, etc. For the detailed process of bl31 gic and interrupt processing, please refer to the following blog post:
https://blog.csdn.net/lgjjeff/article/details/122402214?spm=1001.2014.3001.5502

(2) Initialize the gpio of the qemu platform, that is, set the gpio base address and operation-related callback functions for it

4.2 ehf initialization

ehf is used to initialize el3 interrupt processing related functions. Interrupts in gicv3 are divided into three groups: group0, secure group1 and non secure group 1, which can be routed to different exception levels according to the configuration of the irq and fiq bits of scr_el3. Ehf is used to handle group0 interrupts. This interrupt is always triggered in the form of fiq. By setting scr_el3 to route it to el3 for processing, this type of interrupt can be processed in bl31. For the principle of interrupt routing, please refer to:
https://blog.csdn.net/lgjjeff/article/details/110729661?spm=1001.2014.3001.5502

The ehf initialization process is mainly to set the routing mode of group 0 and set a general interrupt processing function for it. Its main process is as follows:

void __init ehf_init(void)
{
    
    
	unsigned int flags = 0;
	int ret __unused;
	set_interrupt_rm_flag(flags, NON_SECURE);
	set_interrupt_rm_flag(flags, SECURE);1

	ret = register_interrupt_type_handler(INTR_TYPE_EL3,
			ehf_el3_interrupt_handler, flags);2assert(ret == 0);
}

(1) Calculate the flag related to interrupt routing

(2) Set the interrupt routing mode of EL3 type (group 0) interrupt and the general interrupt processing function of bl31

The bl31 interrupt handling function ehf_el3_interrupt_handler will be called by the exception vector table processing flow, and it will continue to call the actual processing function corresponding to each priority level according to the interrupt priority level. The registration process of the processing function corresponding to the interrupt priority is divided into the following two steps. The following is an example of the interrupt registration process:

ehf_pri_desc_t plat_exceptions[] = {
    
    
#if RAS_EXTENSION
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_RAS_PRI),
#endif
#if SDEI_SUPPORT
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_CRITICAL_PRI),
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_NORMAL_PRI),
#endif
#if SPM_MM
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SP_PRI),
#endif
#ifdef PLAT_EHF_DESC
	PLAT_EHF_DESC,
#endif
};

EHF_REGISTER_PRIORITIES(plat_exceptions, ARRAY_SIZE(plat_exceptions), PLAT_PRI_BITS);

In the above example, interrupts such as RAS and SDEI are registered, and different priorities are assigned to them, but at this time, only a place is occupied for the interrupt processing function, and it is not actually defined. They are actually registered in the driver via ehf_register_priority_handler. For example, for sdei, its registration process is as follows:

void sdei_init(void)
{
    
    
	ehf_register_priority_handler(PLAT_SDEI_CRITICAL_PRI,
			sdei_intr_handler);
	ehf_register_priority_handler(PLAT_SDEI_NORMAL_PRI,
			sdei_intr_handler);
}

When ehf_register_priority_handler is registered, theoretically bl31 can receive and process el3 interrupts. But in fact, when bl31 is executing, the irq and fiq interrupt masks of PSTATE are all masked, that is, the el3 interrupt can only be triggered and processed when the cpu is running below the EL3 exception level.

4.3 Runtime Service Initialization

We mentioned earlier that bl31 needs to reside in the system after the system initialization is completed, and handle SMC exceptions from low exception levels, and its exception handling process is called runtime service. Arm defines a series of specifications for their usage scenarios, which are used to deal with different types of tasks, such as cpu power management specification PSCI, software event proxy specification SDEI for proxying non secure world to handle interrupts, and for trust os related calls SPD etc. Obviously, before these services are used, their service processing functions need to be registered in bl31, and the runtime service initialization process is used for this purpose.

Before analyzing the runtime service initialization process, let's look at its registration method. The following is the definition of its registration interface DECLARE_RT_SVC:

#define DECLARE_RT_SVC(_name, _start, _end, _type, _setup, _smch)	\
	static const rt_svc_desc_t __svc_desc_ ## _name			\                 (1
		__section("rt_svc_descs") __used = {
    
    			\                 (2.start_oen = (_start),				\
			.end_oen = (_end),				\
			.call_type = (_type),				\
			.name = #_name,					\
			.init = (_setup),				\
			.handle = (_smch)				\
		}

The interface defines a structure __svc_desc_ ## _name, and put it in a special segment rt_svc_descs. The definition of this section is located in the link script header file include/common/bl_common.ld.h, which is defined as follows:

#define RT_SVC_DESCS                                    \
        . = ALIGN(STRUCT_ALIGN);                        \
        __RT_SVC_DESCS_START__ = .;                     \
        KEEP(*(rt_svc_descs))                           \
        __RT_SVC_DESCS_END__ = .;

That is, these registered runtime service structures are saved in the rt_svc_descs segment starting with __RT_SVC_DESCS_START__ and ending with __RT_SVC_DESCS_END__, and its data can be expressed as the following structure:

insert image description here
Therefore, if you need to obtain these structure pointers, you only need to traverse this address. This is the case for the runtime service initialization function runtime_svc_init flow, which is defined as follows:

void __init runtime_svc_init(void)
{
    
    
	rt_svc_descs = (rt_svc_desc_t *) RT_SVC_DESCS_START;1for (index = 0U; index < RT_SVC_DECS_NUM; index++) {
    
    2rt_svc_desc_t *service = &rt_svc_descs[index];

			rc = validate_rt_svc_desc(service);3if (rc != 0) {
    
    
			ERROR("Invalid runtime service descriptor %p\n",
				(void *) service);
			panic();
		}

		if (service->init != NULL) {
    
                
			rc = service->init();4if (rc != 0) {
    
    
				ERROR("Error initializing runtime service %s\n",
						service->name);
				continue;
			}
		}
	}
}

(1) Get the starting address of the rt_svc_descs segment RT_SVC_DESCS_START

(2) Traverse the corresponding runtime services of all registered rt_svc_desc_t structures in this segment

(3) Verify the validity of the runtime service

(4) Call the initialization callback corresponding to the service, which is passed in through the parameter _setup in the DECLARE_RT_SVC registration macro

4.4 Start bl32

Bl32 is mainly used to run trust os, which is mainly used to protect sensitive data of users (such as passwords, fingerprints, faces, etc.), and related functional modules, such as encryption and decryption algorithms, ta loading and execution, secure storage, etc. The trust os implementations of various manufacturers are different, but the basic ideas are similar. When the following analysis involves specific trust os, we will take the open source framework optee as an example.

The bl32 running process in the startup process is as follows:

if (bl32_init != NULL) {
    
    
		INFO("BL31: Initializing BL32\n");

		int32_t rc = (*bl32_init)();

		if (rc == 0)
			WARN("BL31: BL32 initialization failed\n");
	}

It first judges whether bl32_init has been registered, and if so, executes the actual bl32 operation process by calling this function. Let's first look at the bl32_init registration process (services/spd/opteed) under the optee architecture:

DECLARE_RT_SVC(
	opteed_fast,

	OEN_TOS_START,
	OEN_TOS_END,
	SMC_TYPE_FAST,
	opteed_setup,1
	opteed_smc_handler
);

static int32_t opteed_setup(void)
{
    
    
	bl31_register_bl32_init(&opteed_init)2return 0;
}

void bl31_register_bl32_init(int32_t (*func)(void))
{
    
    
	bl32_init = func;3}

(1) Set optee's initialization callback opteed_setup through DECLARE_RT_SVC

(2) Register the opteed_init function as the startup function of bl32

(3) Actual callback registration

Therefore, optee's bl32 startup function is opteed_init, and its process is similar to our previous jump method of bl1 starting bl2. The flow chart is as follows:

insert image description here

It first obtains the previously saved secure image ep information (that is, the ep information of bl32), and then uses it to initialize the context of exception level switching, and sets the system registers of secure el1, spsr_el3 and elr_el3, etc. Then call the opteed_enter_sp function to jump to bl32. There is a problem here. In addition to starting bl32, bl31 needs to continue to start bl33. Therefore, after bl32 is started, it needs to jump back to bl31 and continue to execute the bl33 startup process. Since bl32 is executed in secure EL1, its synchronous access to bl31 can only use the smc method, so it needs to jump to the original breakpoint in the smc processing flow. The lr register of the c language in Armv8 is x30, so if we save x30 and the running context before the jump, and then restore these contexts in the smc processing flow, the execution at the breakpoint can be resumed. The following is the context saving process of the opteed_enter_sp function:

func opteed_enter_sp
	mov	x3, sp
	str	x3, [x0, #0]
	sub	sp, sp, #OPTEED_C_RT_CTX_SIZE

	stp	x19, x20, [sp, #OPTEED_C_RT_CTX_X19]
	stp	x21, x22, [sp, #OPTEED_C_RT_CTX_X21]
	stp	x23, x24, [sp, #OPTEED_C_RT_CTX_X23]
	stp	x25, x26, [sp, #OPTEED_C_RT_CTX_X25]
	stp	x27, x28, [sp, #OPTEED_C_RT_CTX_X27]
	stp	x29, x30, [sp, #OPTEED_C_RT_CTX_X29]

	b	el3_exit
endfunc opteed_enter_sp

In this function, the context will be saved to the global variable opteed_sp_context, and the process of returning to smc after optee initialization is completed is as follows (services/spd/opteed/opteed_main.c):

uintptr_t opteed_smc_handler()
{
    
    
optee_context_t *optee_ctx = &opteed_sp_context[linear_id];
	switch (smc_fid) {
    
    
	case TEESMC_OPTEED_RETURN_ENTRY_DONE:1assert(optee_vector_table == NULL);
		optee_vector_table = (optee_vectors_t *) x1;
		opteed_synchronous_sp_exit(optee_ctx, x1);2break;
	}
}

(1) Indicates that this smc call is returned after the bl32 startup is completed

(2) Call this function to restore the context saved before entering bl32, and return to the breakpoint to continue execution. The function is defined as follows:

func opteed_exit_sp
	mov	sp, x0                                                                  (1

	ldp	x19, x20, [x0, #(OPTEED_C_RT_CTX_X19 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x21, x22, [x0, #(OPTEED_C_RT_CTX_X21 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x23, x24, [x0, #(OPTEED_C_RT_CTX_X23 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x25, x26, [x0, #(OPTEED_C_RT_CTX_X25 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x27, x28, [x0, #(OPTEED_C_RT_CTX_X27 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x29, x30, [x0, #(OPTEED_C_RT_CTX_X29 - OPTEED_C_RT_CTX_SIZE)]2

	mov	x0, x1
	ret                                                                              (3
endfunc opteed_exit_sp

(1) Restore the stack saved in the context before entering bl32

(2) Restore the callee register saved before entering bl32

(3) Return to the breakpoint to continue execution, go around and go around, we finally returned to the bl31_main function

Finally, use a diagram to describe the entire process above:

insert image description here
4.4 Start bl33

The bl33 startup process is similar to the previous image startup process at all levels. It also sets the context, entry address and parameters of bl33 according to ep_info, and then jumps to the entry for execution. If you are interested, you can analyze it yourself based on the code, so I won’t repeat it here. Well, the atf startup process is finally over, and then we will jump to the world of bl33 (uboot), all preparations are for the beauty of the moment when uboot starts the kernel, enjoy!

Guess you like

Origin blog.csdn.net/qq_41483419/article/details/130975803