> 文章列表 > Zephyr 启动流程

Zephyr 启动流程

Zephyr 启动流程

文章目录

  • 程序入口
    • Cortex-M 中断向量表机制
    • zImage 和 uImage
  • 系统初始化
  • 初始化C语言运行环境
  • 初始化内核开启任务调度
  • main 线程
    • 启动第一个线程
    • 运行线程

程序入口

  • 使用gcc进行交叉编译时,可以通过链接脚本中添加 ENTRY 指令用于指定程序入口,通过该指令指定的标签即为程序入口,下面是 zephyr/include/zephyr/arch/arm/aarch32/cortex_m/scripts/linker.ld:85 中指定的程序入口:
ENTRY(CONFIG_KERNEL_ENTRY)
  • 此处的程序入口可通过 KConfig 修改,下面是其默认值:
config KERNEL_ENTRYstring "Kernel entry symbol"default "__start"helpCode entry symbol, to be set at linking phase.
  • 从上面的代码中可以得出如下结论,arm32 默认情况下程序的入口即为__start。
  • 通过 ENTRY 指定程序入口,并不是意味着 __start 对应的程序在链接后位于程序的最前面,而是在程序加载时从符号表查找预定义的入口地址。
  • 其他架构的CPU也可以通过类似的方式查找到入口函数

Cortex-M 中断向量表机制

  • 虽然通过 ENTRY 指定了入口函数,但是Cortex-M系列处理器并不会查找该符号的位置,使用 ENTRY 只是为了在动态加载程序时可以找到入口,Cortex-M系列处理器是通过中断向量表来确定程序入口。

  • 在 Cortex-M 系列 MCU 中,如果设置为从 flash 启动,flash 前1K 用于存放中断向量表,其中第一个字为程序栈顶指针,第二个字为复位向量,即 ResetHandler 的地址,boot会将第一个字用于初始化栈顶,第二个字作为中保存的地址作为程序跳转地址,从而跳转到程序中运行,详细说明可参考异常模型, 该机制为Cortex-M 系列 MCU 中独有,与此相对的,不同的芯片在系统启动时对可执行程序都有一定的格式要求,需要在程序的头部添加符合操作规范的头。

  • 上电启动后的第一段程序必须符合相应的规范才能被正确执行。

zImage 和 uImage

  • 程序经过编译后会生成一个elf格式的可执行程序,再通过objcopy工具转换为bin文件,但是其体积较大,通过对文件进行压缩并在头部添加一段压缩代码成为 zImage。使用的时候,通过zImage镜像头部的解压缩代码进行自解压,然后执行解压出来的内核镜像。
  • uboot为了启动linux内核,发明了一种内核格式叫uImage。uImage是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。
  • 芯片上电后通常是不能识别 zImage 和 uImage 的,运行的第一段程序通常是 bootloader,例如我们熟知的 u-boot,u-boot 运行之后,可以解析 zImage 和 uImage 加载内核。
  • 在一些虚拟仿真系统中,其本身就是软件模拟芯片运行,且最终目的也是为了加载内核,它可以跳过bootloader 直接加载 zImage 和 uImage。

系统初始化

  • zephyr/arch/arm/core/aarch32/cortex_m/reset.S 中定义了 __start,程序从此处开始往后执行
SECTION_SUBSEC_FUNC(TEXT,_reset_section,__start)#if defined(CONFIG_DEBUG_THREAD_INFO)/* Clear z_sys_post_kernel flag for RTOS aware debuggers */movs.n r0, #0ldr r1, =z_sys_post_kernelstrb r0, [r1]
#endif /* CONFIG_DEBUG_THREAD_INFO */#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)/* 复位CONTROL寄存器,在特权模式和非特权模式均使用MSP,* 当切换栈指针之后必须使用ISB指令刷新流水线,* 以保证在ISB之后执行的指令都使用新的栈*/movs.n r0, #0msr CONTROL, r0isb
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)/* 将MSP和PSP设置为0 */movs.n r0, #0msr MSPLIM, r0msr PSPLIM, r0
#endif /* CONFIG_CPU_CORTEX_M_HAS_SPLIM */#endif /* CONFIG_INIT_ARCH_HW_AT_BOOT */#if defined(CONFIG_PM_S2RAM)/* 低功耗相关初始化 */bl arch_pm_s2ram_resume
#endif /* CONFIG_PM_S2RAM */#if defined(CONFIG_PLATFORM_SPECIFIC_INIT)/* 针对内存,cache,jtag,时钟,中断的一些特殊配置  */bl z_arm_platform_init
#endif#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)
#if defined(CONFIG_CPU_HAS_ARM_MPU)/* 操作系统未运行之前使用平坦内存模型,所有内存均不受保护,* 为避免在初始化过程中触发读写保护进入异常,一定要关闭MPU*/movs.n r0, #0ldr r1, =_SCS_MPU_CTRLstr r0, [r1]dsb
#endif /* CONFIG_CPU_HAS_ARM_MPU *//* 将MSP指向 z_main_stack 的末尾,以便后续进行函数调用 */ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZEmsr msp, r0/* 清除MPU配置,关闭所有中断,清除被挂起的中断,重置Cache配置等 */bl z_arm_init_arch_hw_at_boot
#endif /* CONFIG_INIT_ARCH_HW_AT_BOOT *//* 屏蔽中断 */
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE)cpsid i
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)movs.n r0, #_EXC_IRQ_DEFAULT_PRIOmsr BASEPRI, r0
#else
#error Unknown ARM architecture
#endif#ifdef CONFIG_WDOG_INIT/* 开启看门狗 */bl z_arm_watchdog_init
#endif#ifdef CONFIG_INIT_STACKS/* 将栈全部设置为0xaa,可用于监测剩余栈容量 */ldr r0, =z_interrupt_stacksldr r1, =0xaaldr r2, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZEbl z_early_memset
#endif/* 初始化PSP,将CONTROL的SPSEL位设置为1(在特权模式下使用MSP,非特权模式下使用PSP)* 后续操作将使用 z_interrupt_stacks 作为栈进行初始化*/ldr r0, =z_interrupt_stacksldr r1, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZEadds r0, r0, r1msr PSP, r0mrs r0, CONTROLmovs r1, #2orrs r0, r1 /* CONTROL_SPSEL_Msk */msr CONTROL, r0isbbl z_arm_prep_c

初始化C语言运行环境

  • 在C语言运行之前有一些步骤必须要完成
    • 将bss段清0
    • 从ROM中取出data段初始值并初始化data段
    • 初始化栈指针
    • 开启中断
void z_arm_prep_c(void)
{relocate_vector_table();
#if defined(CONFIG_CPU_HAS_FPU)z_arm_floating_point_init();
#endifz_bss_zero();z_data_copy();
#if ((defined(CONFIG_ARMV7_R) || defined(CONFIG_ARMV7_A)) && defined(CONFIG_INIT_STACKS))z_arm_init_stacks();
#endifz_arm_interrupt_init();z_cstart();CODE_UNREACHABLE;
}
  • 对于不同的平台还需要增加额外的操作,例如浮点寄存器的初始化,中断向量表的重定向,在系统启动之前还运行了一段芯片内置程序,这段程序将中断向量表的位置设置为 0x00000000,当程序从boot跳转到指定存储器运行之后,首先需要立即关闭中断,避免中断产生并跳转到错误的中断响应函数中,重设中断向量表偏移位置之后,再重新开启中断。

初始化内核开启任务调度

FUNC_NO_STACK_PROTECTOR
FUNC_NORETURN void z_cstart(void)
{/* 代码覆盖率测试相关 */gcov_static_init();/* 调用初始化级别为 INIT_LEVEL_EARLY 的函数进行初始化 */z_sys_init_run_level(INIT_LEVEL_EARLY);/* z_arm_interrupt_stack_setup 初始化MSP* z_arm_exc_setup 初始化PENDSV、SysTick等中断的优先级,其中PENDSV优先级为最低* z_arm_fault_init 初始化 Fault 中断。* z_arm_cpu_idle_init 初始化 idle 线程* z_arm_clear_faults 清除所有故障标志* z_arm_mpu_init 初始化MPU* z_arm_mmu_init 初始化MMU*/arch_kernel_init();/* 日志初始化 */LOG_CORE_INIT();#if defined(CONFIG_MULTITHREADING)/* Note: The z_ready_thread() call in prepare_multithreading() requires* a dummy thread even if CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN=y*/struct k_thread dummy_thread;z_dummy_thread_init(&dummy_thread);
#endif/* 初始化驱动中的静态节点 */z_device_state_init();/* 其他的硬件初始化 */z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);#ifdef CONFIG_STACK_CANARIES/* CONFIG_STACK_CANARIES 用于开启堆栈金丝雀功能,这是一种安全特性,有助于监测堆栈溢出,* 当启动该功能时,系统启动时会生成一个随机数并保存在 __stack_chk_guard 中,* 在函数返回之前会检查该值确保它没有被缓冲区溢出所覆盖。*/uintptr_t stack_guard;z_early_boot_rand_get((uint8_t *)&stack_guard, sizeof(stack_guard));__stack_chk_guard = stack_guard;__stack_chk_guard <<= 8;
#endif	/* CONFIG_STACK_CANARIES */#ifdef CONFIG_TIMING_FUNCTIONS_NEED_AT_BOOT/* timing_init 函数用于初始化系统计时器 */timing_init();timing_start();
#endif#ifdef CONFIG_MULTITHREADING/* CONFIG_MULTITHREADING为y时,使用多线程,否则只会有一个 main 线程,* 默认情况下都启用多线程,通过将 main 线程添加到就绪队列中然后开启任务调度*/switch_to_main_thread(prepare_multithreading());
#else
#ifdef ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING/* Custom ARCH-specific routine to switch to main()* in the case of no multi-threading.*/ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING(bg_thread_main,NULL, NULL, NULL);
#elsebg_thread_main(NULL, NULL, NULL);/* LCOV_EXCL_START* We've already dumped coverage data at this point.*/irq_lock();while (true) {}/* LCOV_EXCL_STOP */
#endif
#endif /* CONFIG_MULTITHREADING *//** Compiler can't tell that the above routines won't return and issues* a warning unless we explicitly tell it that control never gets this* far.*/CODE_UNREACHABLE; /* LCOV_EXCL_LINE */
}
  • 在进入 z_cstart 之后,首先调用平台相关的内核初始化函数,然后使用模块自动初始化机制根据不同优先级调用相关初始化函数
  • 需要注意的是,同一优先级的模块之间不应该存在依赖关系,被依赖的模块应该先初始化。
  • 在调度器启动之前会调用优先级为 INIT_LEVEL_PRE_KERNEL_1 和 INIT_LEVEL_PRE_KERNEL_2 的初始化函数,此时调度器还未运行,因此这些函数中不应该使用操作系统提供的功能。
  • 在初始化完成之后通过将main线程添加到就绪队列中并开启调度器,main 线程从 bg_thread_main 函数开始运行。

main 线程

启动第一个线程

void arch_switch_to_main_thread(struct k_thread *main_thread, char *stack_ptr,k_thread_entry_t _main)
{z_arm_prepare_switch_to_main();/* 将_current 指向 main_thread, _current 始终指向正在运行的线程 */_current = main_thread;#if defined(CONFIG_THREAD_LOCAL_STORAGE) && defined(CONFIG_CPU_CORTEX_M)/* On Cortex-M, TLS uses a global variable as pointer to* the thread local storage area. So this needs to point* to the main thread's TLS area before switching to any* thread for the first time, as the pointer is only set* during context switching.*/extern uintptr_t z_arm_tls_ptr;z_arm_tls_ptr = main_thread->tls;
#endif#ifdef CONFIG_INSTRUMENT_THREAD_SWITCHINGz_thread_mark_switched_in();
#endif/* the ready queue cache already contains the main thread */#if defined(CONFIG_MPU_STACK_GUARD) || defined(CONFIG_USERSPACE)/** If stack protection is enabled, make sure to set it* before jumping to thread entry function*/z_arm_configure_dynamic_mpu_regions(main_thread);
#endif#if defined(CONFIG_BUILTIN_STACK_GUARD)/* Set PSPLIM register for built-in stack guarding of main thread. */
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)__set_PSPLIM(main_thread->stack_info.start);
#else
#error "Built-in PSP limit checks not supported by HW"
#endif
#endif /* CONFIG_BUILTIN_STACK_GUARD *//* 设置PSP,然后开启中断,然后跳转到 z_thread_entry,z_thread_entry有四个参数* 其中_main和stack_ptr作为调用 z_thread_entry 函数的前2个参数,后面两个参数为0*/__asm__ volatile ("mov   r0,  %0\\n\\t"	/* Store _main in R0 */
#if defined(CONFIG_CPU_CORTEX_M)"msr   PSP, %1\\n\\t"	/* __set_PSP(stack_ptr) */
#endif"movs r1, #0\\n\\t"
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE) \\|| defined(CONFIG_ARMV7_R) \\|| defined(CONFIG_AARCH32_ARMV8_R) \\|| defined(CONFIG_ARMV7_A)"cpsie i\\n\\t"		/* __enable_irq() */
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)"cpsie if\\n\\t"		/* __enable_irq(); __enable_fault_irq() */"msr   BASEPRI, r1\\n\\t"	/* __set_BASEPRI(0) */
#else
#error Unknown ARM architecture
#endif /* CONFIG_ARMV6_M_ARMV8_M_BASELINE */"isb\\n\\t""movs r2, #0\\n\\t""movs r3, #0\\n\\t""bl z_thread_entry\\n\\t"	/* z_thread_entry(_main, 0, 0, 0); */:: "r" (_main), "r" (stack_ptr): "r0" /* not to be overwritten by msr PSP, %1 */);CODE_UNREACHABLE;
}

运行线程

  • 在 main 线程启动之后,并没有直接运行main函数,首先会进入一个所有线程的入口函数 z_thread_entry,其第一个参数是线程入口,第二三四个参数是需要传递给线程的参数,如果线程结束时没有调用 k_thread_abrot 结束线程,最终会回到返回此处进行销毁。
FUNC_NORETURN void z_thread_entry(k_thread_entry_t entry,void *p1, void *p2, void *p3)
{
#ifdef CONFIG_THREAD_LOCAL_STORAGEz_tls_current = z_current_get();
#endifentry(p1, p2, p3);k_thread_abort(k_current_get());/** Compiler can't tell that k_thread_abort() won't return and issues a* warning unless we tell it that control never gets this far.*/CODE_UNREACHABLE; /* LCOV_EXCL_LINE */
}
  • main 线程的入口函数为 bg_thread_main 函数, 因此 z_thread_entry 中的 entry 即为 bg_thread_main,该线程用于做一些必要的初始化工作。
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{ARG_UNUSED(unused1);ARG_UNUSED(unused2);ARG_UNUSED(unused3);#ifdef CONFIG_MMU/* Invoked here such that backing store or eviction algorithms may* initialize kernel objects, and that all POST_KERNEL and later tasks* may perform memory management tasks (except for z_phys_map() which* is allowed at any time)*/z_mem_manage_init();
#endif /* CONFIG_MMU */z_sys_post_kernel = true;/* 调用优先级为INIT_LEVEL_POST_KERNEL的初始化函数,* 此时内核已经开始运行,可以使用操作系统API*/z_sys_init_run_level(INIT_LEVEL_POST_KERNEL);
#if CONFIG_STACK_POINTER_RANDOMz_stack_adjust_initialized = 1;
#endif/* 从控制台输出系统启动标识 */boot_banner();#if defined(CONFIG_CPP)/* 初始化CPP运行环境 */void z_cpp_init_static(void);z_cpp_init_static();
#endif/* 调用优先级为 INIT_LEVEL_APPLICATION 的初始化函数 */z_sys_init_run_level(INIT_LEVEL_APPLICATION);/* Zephyr支持静态创建线程,线程对应的信息在编译时确定,* 随代码一起被编译到程序中,系统启动之后从对应地址将线程的信息从flash中读出,* 创建并初始化线程并将其添加到就绪队列中等待操作系统调度。*/z_init_static_threads();#ifdef CONFIG_KERNEL_COHERENCE__ASSERT_NO_MSG(arch_mem_coherent(&_kernel));
#endif#ifdef CONFIG_SMPif (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) {z_smp_init();}z_sys_init_run_level(INIT_LEVEL_SMP);
#endif#ifdef CONFIG_MMUz_mem_manage_boot_finish();
#endif /* CONFIG_MMU */#ifdef CONFIG_CPP_MAINextern int main(void);
#elseextern void main(void);
#endif/* 跳转到main函数 */(void)main();/* Mark nonessential since main() has no more work to do */z_main_thread.base.user_options &= ~K_ESSENTIAL;#ifdef CONFIG_COVERAGE_DUMP/* Dump coverage data once the main() has exited. */gcov_coverage_dump();
#endif
} 
  • 其中包含几个系统运行的重要的操作:
    • 配置MMU(如果存在)
    • 调用优先级为 INIT_LEVEL_POST_KERNEL 的初始化函数
    • CPP运行环境的初始化
    • 调用优先级为 INIT_LEVEL_APPLICATION 的初始化函数
    • 创建通过宏静态创建的线程,并添加到就绪队列。
    • 初始化对称多核处理(如果存在多个处理器并启用了多核调度)。
  • 在这些准备工作完成后跳转到用户编写的main函数中,如果main函数执行并返回,最终会返回到 z_thread_entry 被销毁。