文章目录
程序入口
- 使用gcc进行交叉编译时,可以通过链接脚本中添加 ENTRY 指令用于指定程序入口,通过该指令指定的标签即为程序入口,下面是 zephyr/include/zephyr/arch/arm/aarch32/cortex_m/scripts/linker.ld:85 中指定的程序入口:
ENTRY(CONFIG_KERNEL_ENTRY)
- 此处的程序入口可通过 KConfig 修改,下面是其默认值:
config KERNEL_ENTRY
string "Kernel entry symbol"
default "__start"
help
Code 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, #0
ldr r1, =z_sys_post_kernel
strb r0, [r1]
#endif /* CONFIG_DEBUG_THREAD_INFO */
#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)
/* 复位CONTROL寄存器,在特权模式和非特权模式均使用MSP,
* 当切换栈指针之后必须使用ISB指令刷新流水线,
* 以保证在ISB之后执行的指令都使用新的栈
*/
movs.n r0, #0
msr CONTROL, r0
isb
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
/* 堆栈限制寄存器分别限制 MSP 和 PSP 寄存器可以下降的程度,此处设置为0 */
movs.n r0, #0
msr MSPLIM, r0
msr 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, #0
ldr r1, =_SCS_MPU_CTRL
str r0, [r1]
dsb
#endif /* CONFIG_CPU_HAS_ARM_MPU */
/* ARM32使用满递减栈,栈顶指针初始时刻指向高地址,
* 将MSP指向 z_main_stack 的末尾,以便后续进行函数调用
*/
ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZE
msr 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_PRIO
msr 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_stacks
ldr r1, =0xaa
ldr r2, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
bl z_early_memset
#endif
/* 初始化PSP,将CONTROL的SPSEL位设置为1(在特权模式下使用MSP,非特权模式下使用PSP)
* 后续操作将使用 z_interrupt_stacks 作为栈进行初始化
*/
ldr r0, =z_interrupt_stacks
ldr r1, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
adds r0, r0, r1
msr PSP, r0
mrs r0, CONTROL
movs r1, #2
orrs r0, r1 /* CONTROL_SPSEL_Msk */
msr CONTROL, r0
isb
bl 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();
#endif
z_bss_zero();
z_data_copy();
#if ((defined(CONFIG_ARMV7_R) || defined(CONFIG_ARMV7_A)) && defined(CONFIG_INIT_STACKS))
z_arm_init_stacks();
#endif
z_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);
#else
bg_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 线程
从裸机切换到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_SWITCHING
z_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)
/* 堆栈限制寄存器分别限制 MSP 和 PSP 寄存器可以下降的程度,此处设置为main线程的栈起始地址 */
__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;
}
Zephyr 线程入口
- 在 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_STORAGE
z_tls_current = z_current_get();
#endif
entry(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_RANDOM
z_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_SMP
if (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) {
z_smp_init();
}
z_sys_init_run_level(INIT_LEVEL_SMP);
#endif
#ifdef CONFIG_MMU
z_mem_manage_boot_finish();
#endif /* CONFIG_MMU */
#ifdef CONFIG_CPP_MAIN
extern int main(void);
#else
extern 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 被销毁。