文章目录
- 进入内核流程
- arch/arm/kernel/vmlinux.lds
- asm-offsets.c: 为汇编代码生成C结构体偏移量
- arch/arm/include/asm/glue-proc.h
- PROC_INFO 架构信息
- THREAD_SIZE 分配栈大小
- arch/arm/include/asm/assembler.h
- arch/arm/include/asm/v7m.h
- arch/arm/kernel/entry-header.S
- arch/arm/kernel/entry-common.S
- arch/arm/kernel/entry-v7m.S
- arch/arm/mm/proc-v7m.S
- arch/arm/kernel/head-nommu.S
- arch/arm/kernel/head-common.S
- --------------------------------------------
- arch/arm/include/asm/cputype.h
- arch/arm/kernel/devtree.c
- arch/arm/kernel/early_printk.c 早期printk打印
- arch/arm/kernel/setup.c
- arch/arm/include/asm/switch_to.h
https://github.com/wdfk-prog/linux-study
进入内核流程
- 从解压代码中解压代码后跳转进入
stext
汇编代码开始执行 __lookup_processor_type
循环查找processor_type中匹配的cpu信息- 没有找到匹配的处理器类型,调用
__error_p
函数打印错误信息并进入死循环 - 找到匹配的处理器类型,调用具体的cpu_flush函数,完成处理器的初始化
- V7M 处理器调用
__v7m_cm7_setup
函数
- 异常向量表的地址存储在SCB的VTOR寄存器中,以便处理器能够正确地跳转到异常处理程序。
- 启用UsageFault、BusFault和MemManage异常,以便在发生这些异常时能够进行处理。
- 设置SVC(超级用户调用)和PendSV(挂起的系统服务调用)的优先级,以便在异常发生时能够正确地处理这些异常。
- 通过SVC指令切换到线程模式,并设置堆栈指针(sp)指向init_thread_union + THREAD_START_SP,以便为线程模式准备好堆栈。
- 分配THREAD_SIZE大小的栈空间
- 计算异常返回值,设置控制寄存器(CONTROL)为非特权模式,以便在异常返回时能够正确地恢复处理器状态。
- 配置缓存(如果硬件支持),以提高系统性能。
- 配置系统控制寄存器以确保8字节堆栈对齐,以满足ARM Cortex-M7的对齐要求。
- V7M 处理器调用
- 进入
__after_proc_init
函数,根据配置禁用数据缓存、分支预测和指令缓存。最后,将控制寄存器的值存储到SCB中,并将异常返回值传递给__mmap_switched
函数,以继续执行后续的启动流程。 - 调用
__mmap_switched
函数,完成内核的初始化工作。
arch/arm/kernel/vmlinux.lds
- 通过lds链接脚本可知入口函数为
stext
ENTRY(stext)
asm-offsets.c: 为汇编代码生成C结构体偏移量
此文件不是一个常规的内核驱动程序, 而是一个在内核编译期间运行的特殊工具。它的核心作用是计算Linux内核中常用C语言结构体 (struct
) 内部各个成员的内存偏移量 (offset), 并将这些偏移量定义为汇编语言可以理解和使用的常量。
在单核无MMU的STM32H750平台上的原理与作用
对于STM32H750这样的平台, 内核的许多底层操作, 特别是上下文切换、异常处理和启动代码, 都是用高度优化的汇编语言编写的。这些汇编代码需要直接访问C语言定义的内核数据结构, 例如 task_struct
(任务描述符) 或 thread_info
(线程信息栈)。
然而, 汇编语言本身并不知道C结构体的布局。例如, 汇编代码无法直接理解 current_task->thread_info->flags
这样的表达式。它需要知道 flags
成员相对于 thread_info
结构体起始地址的精确字节偏移量。这个偏移量可能会因为内核版本的变化、配置选项的不同或编译器的差异而改变。
asm-offsets.c
这个工具就是为了解决这个问题而存在的。在每次编译内核时:
- 它会被编译并执行。
- 它利用C语言的
offsetof()
宏来计算出所有需要的偏移量。 - 它将这些计算结果以汇编语法 (
.equ
或#define
) 的形式输出到一个头文件中 (通常是asm-offsets.h
或offsets.h
)。 - 内核的汇编文件 (
.S
文件) 会包含这个自动生成的头文件, 从而获得所有结构体成员的最新、最准确的偏移量。
对于STM32H750平台, 这尤其重要, 因为:
- MPU配置:
#ifdef CONFIG_ARM_MPU
块中的定义对于配置内存保护单元至关重要。汇编代码需要知道MPU相关结构体的布局, 以便正确地将值写入MPU寄存器。 - 异常处理:
pt_regs
结构体的布局定义了在发生异常时, CPU寄存器在堆栈上的保存顺序。异常处理的汇编代码必须知道每个寄存器(如PC
,SP
,R0
)的准确偏移量才能正确地保存和恢复现场。 - 上下文切换: 汇编代码需要知道
thread_info
中cpu_context
的位置, 以便保存和恢复任务切换时的寄存器上下文。
总之, 这个文件是连接C语言世界和底层汇编世界的桥梁, 它为汇编代码提供了访问内核核心数据结构的精确"地址地图", 保证了内核在特定硬件平台上的正确运行。
/*
* 确保编译器和目标兼容. APCS-26是旧的ARM过程调用标准, 内核需要32位的APCS.
*/
#if defined(__APCS_26__)
#error Sorry, your compiler targets APCS-26 but this kernel requires APCS-32
#endif
// 主函数. 这个程序在编译内核的主机上运行.
int main(void)
{
// DEFINE宏(在kbuild头文件中定义)会打印出 " #define <name> <value> " 格式的字符串.
// BLANK宏打印一个空行, 用于格式化输出.
// --- task_struct 结构体偏移量 ---
DEFINE(TSK_ACTIVE_MM, offsetof(struct task_struct, active_mm)); // active_mm指向进程的内存描述符.
#ifdef CONFIG_STACKPROTECTOR // 如果启用了栈保护
DEFINE(TSK_STACK_CANARY, offsetof(struct task_struct, stack_canary)); // 栈"金丝雀"值, 用于检测栈溢出.
#endif
BLANK();
// --- thread_info 结构体偏移量, 这是与任务紧密相关的底层信息 ---
DEFINE(TI_FLAGS, offsetof(struct thread_info, flags)); // 线程标志位 (如: 是否有信号待处理).
DEFINE(TI_PREEMPT, offsetof(struct thread_info, preempt_count)); // 内核抢占计数器.
DEFINE(TI_CPU, offsetof(struct thread_info, cpu)); // 当前任务运行在哪个CPU上 (单核系统上恒为0).
DEFINE(TI_CPU_DOMAIN, offsetof(struct thread_info, cpu_domain)); // CPU的电源域.
DEFINE(TI_CPU_SAVE, offsetof(struct thread_info, cpu_context)); // CPU上下文保存区域, 上下文切换时使用.
DEFINE(TI_ABI_SYSCALL, offsetof(struct thread_info, abi_syscall)); // 系统调用ABI相关.
DEFINE(TI_TP_VALUE, offsetof(struct thread_info, tp_value)); // TLS线程指针值.
DEFINE(TI_FPSTATE, offsetof(struct thread_info, fpstate)); // 浮点/SIMD单元状态.
#ifdef CONFIG_VFP // 如果配置了VFP(矢量浮点)协rocessor, STM32H750有
DEFINE(TI_VFPSTATE, offsetof(struct thread_info, vfpstate)); // VFP状态.
#ifdef CONFIG_SMP // 如果是多核系统 (STM32H750不是)
DEFINE(VFP_CPU, offsetof(union vfp_state, hard.cpu)); // VFP状态属于哪个CPU.
#endif
#endif
DEFINE(SOFTIRQ_DISABLE_OFFSET,SOFTIRQ_DISABLE_OFFSET); // 软中断禁用偏移量.
#ifdef CONFIG_ARM_THUMBEE // 如果配置了ThumbEE扩展
DEFINE(TI_THUMBEE_STATE, offsetof(struct thread_info, thumbee_state));
#endif
#ifdef CONFIG_IWMMXT // 如果配置了Intel WMMX扩展
DEFINE(TI_IWMMXT_STATE, offsetof(struct thread_info, fpstate.iwmmxt));
#endif
BLANK();
// --- pt_regs 结构体偏移量, 定义了异常发生时寄存器在栈上的保存布局 ---
DEFINE(S_R0, offsetof(struct pt_regs, ARM_r0));
DEFINE(S_R1, offsetof(struct pt_regs, ARM_r1));
DEFINE(S_R2, offsetof(struct pt_regs, ARM_r2));
DEFINE(S_R3, offsetof(struct pt_regs, ARM_r3));
DEFINE(S_R4, offsetof(struct pt_regs, ARM_r4));
DEFINE(S_R5, offsetof(struct pt_regs, ARM_r5));
DEFINE(S_R6, offsetof(struct pt_regs, ARM_r6));
DEFINE(S_R7, offsetof(struct pt_regs, ARM_r7)); // R7可能是帧指针或系统调用号.
DEFINE(S_R8, offsetof(struct pt_regs, ARM_r8));
DEFINE(S_R9, offsetof(struct pt_regs, ARM_r9));
DEFINE(S_R10, offsetof(struct pt_regs, ARM_r10));
DEFINE(S_FP, offsetof(struct pt_regs, ARM_fp)); // R11, 帧指针(Frame Pointer).
DEFINE(S_IP, offsetof(struct pt_regs, ARM_ip)); // R12, 过程调用间scratch寄存器.
DEFINE(S_SP, offsetof(struct pt_regs, ARM_sp)); // R13, 堆栈指针(Stack Pointer).
DEFINE(S_LR, offsetof(struct pt_regs, ARM_lr)); // R14, 链接寄存器(Link Register).
DEFINE(S_PC, offsetof(struct pt_regs, ARM_pc)); // R15, 程序计数器(Program Counter).
DEFINE(S_PSR, offsetof(struct pt_regs, ARM_cpsr)); // 当前程序状态寄存器.
DEFINE(S_OLD_R0, offsetof(struct pt_regs, ARM_ORIG_r0)); // 原始的R0值 (在系统调用中可能被修改).
DEFINE(PT_REGS_SIZE, sizeof(struct pt_regs)); //整个pt_regs结构体的大小.
DEFINE(SVC_DACR, offsetof(struct svc_pt_regs, dacr)); // 在SVC模式下保存的域访问控制寄存器.
DEFINE(SVC_TTBCR, offsetof(struct svc_pt_regs, ttbcr)); // 在SVC模式下保存的转换表基址控制寄存器.
DEFINE(SVC_REGS_SIZE, sizeof(struct svc_pt_regs)); // SVC模式保存区域的大小.
BLANK();
// --- 信号处理帧的偏移量 ---
DEFINE(SIGFRAME_RC3_OFFSET, offsetof(struct sigframe, retcode[3]));
DEFINE(RT_SIGFRAME_RC3_OFFSET, offsetof(struct rt_sigframe, sig.retcode[3]));
BLANK();
#ifdef CONFIG_CACHE_L2X0 // 如果配置了L2X0二级缓存控制器
// --- L2X0 缓存控制器寄存器结构体的偏移量 ---
DEFINE(L2X0_R_PHY_BASE, offsetof(struct l2x0_regs, phy_base));
DEFINE(L2X0_R_AUX_CTRL, offsetof(struct l2x0_regs, aux_ctrl));
// ... 其他L2缓存寄存器
#endif
BLANK();
#ifdef CONFIG_CPU_HAS_ASID // 如果CPU支持ASID(地址空间ID)
DEFINE(MM_CONTEXT_ID, offsetof(struct mm_struct, context.id.counter));
BLANK();
#endif
// --- 内存管理相关结构体偏移量 ---
DEFINE(VMA_VM_MM, offsetof(struct vm_area_struct, vm_mm)); // VMA指向其所属的mm_struct.
DEFINE(VMA_VM_FLAGS, offsetof(struct vm_area_struct, vm_flags)); // VMA的标志位.
BLANK();
DEFINE(VM_EXEC, VM_EXEC); // VM_EXEC标志位的值本身.
BLANK();
DEFINE(PAGE_SZ, PAGE_SIZE); // 系统页大小 (通常是4096).
BLANK();
DEFINE(SYS_ERROR0, 0x9f0000); // 内核错误码的起始值.
BLANK();
// --- 处理器和机器描述符的偏移量, 用于早期启动 ---
DEFINE(SIZEOF_MACHINE_DESC, sizeof(struct machine_desc));
DEFINE(MACHINFO_TYPE, offsetof(struct machine_desc, nr));
DEFINE(MACHINFO_NAME, offsetof(struct machine_desc, name));
BLANK();
DEFINE(PROC_INFO_SZ, sizeof(struct proc_info_list));
DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));
DEFINE(PROCINFO_MM_MMUFLAGS, offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
DEFINE(PROCINFO_IO_MMUFLAGS, offsetof(struct proc_info_list, __cpu_io_mmu_flags));
BLANK();
#ifdef MULTI_DABORT // 如果支持多种数据异常处理
DEFINE(PROCESSOR_DABT_FUNC, offsetof(struct processor, _data_abort));
#endif
#ifdef MULTI_PABORT // 如果支持多种预取异常处理
DEFINE(PROCESSOR_PABT_FUNC, offsetof(struct processor, _prefetch_abort));
#endif
#ifdef MULTI_CPU // 如果支持多种CPU类型
DEFINE(CPU_SLEEP_SIZE, offsetof(struct processor, suspend_size));
DEFINE(CPU_DO_SUSPEND, offsetof(struct processor, do_suspend));
DEFINE(CPU_DO_RESUME, offsetof(struct processor, do_resume));
#endif
#ifdef MULTI_CACHE // 如果支持多种缓存操作
DEFINE(CACHE_FLUSH_KERN_ALL, offsetof(struct cpu_cache_fns, flush_kern_all));
#endif
#ifdef CONFIG_ARM_CPU_SUSPEND // 如果支持CPU挂起
DEFINE(SLEEP_SAVE_SP_SZ, sizeof(struct sleep_save_sp));
DEFINE(SLEEP_SAVE_SP_PHYS, offsetof(struct sleep_save_sp, save_ptr_stash_phys));
DEFINE(SLEEP_SAVE_SP_VIRT, offsetof(struct sleep_save_sp, save_ptr_stash));
#endif
// --- ARM SMCCC (安全监控调用约定) 相关的偏移量 ---
DEFINE(ARM_SMCCC_QUIRK_ID_OFFS, offsetof(struct arm_smccc_quirk, id));
DEFINE(ARM_SMCCC_QUIRK_STATE_OFFS, offsetof(struct arm_smccc_quirk, state));
BLANK();
// --- DMA方向的定义 ---
DEFINE(DMA_BIDIRECTIONAL, DMA_BIDIRECTIONAL);
DEFINE(DMA_TO_DEVICE, DMA_TO_DEVICE);
DEFINE(DMA_FROM_DEVICE, DMA_FROM_DEVICE);
BLANK();
// --- 缓存行大小相关的定义 ---
DEFINE(CACHE_WRITEBACK_ORDER, __CACHE_WRITEBACK_ORDER);
DEFINE(CACHE_WRITEBACK_GRANULE, __CACHE_WRITEBACK_GRANULE);
BLANK();
#ifdef CONFIG_ARM_MPU // 如果配置了ARM MPU, 这对STM32H750至关重要
// --- MPU区域信息结构体的偏移量 ---
DEFINE(MPU_RNG_INFO_RNGS, offsetof(struct mpu_rgn_info, rgns)); // 指向MPU区域数组.
DEFINE(MPU_RNG_INFO_USED, offsetof(struct mpu_rgn_info, used)); // 已使用的区域数量.
// --- 单个MPU区域结构体的偏移量和大小 ---
DEFINE(MPU_RNG_SIZE, sizeof(struct mpu_rgn)); // 单个MPU区域描述符的大小.
DEFINE(MPU_RGN_DRBAR, offsetof(struct mpu_rgn, drbar)); // 区域基地址寄存器的值.
DEFINE(MPU_RGN_DRSR, offsetof(struct mpu_rgn, drsr)); // 区域大小和使能寄存器的值.
DEFINE(MPU_RGN_DRACR, offsetof(struct mpu_rgn, dracr)); // 区域访问控制寄存器的值.
DEFINE(MPU_RGN_PRBAR, offsetof(struct mpu_rgn, prbar)); // (Cortex-A) 保护区基地址.
DEFINE(MPU_RGN_PRLAR, offsetof(struct mpu_rgn, prlar)); // (Cortex-A) 保护区限制地址.
#endif
// --- kexec (内核重启动) 相关的偏移量 ---
DEFINE(KEXEC_START_ADDR, offsetof(struct kexec_relocate_data, kexec_start_address));
DEFINE(KEXEC_INDIR_PAGE, offsetof(struct kexec_relocate_data, kexec_indirection_page));
DEFINE(KEXEC_MACH_TYPE, offsetof(struct kexec_relocate_data, kexec_mach_type));
DEFINE(KEXEC_R2, offsetof(struct kexec_relocate_data, kexec_r2));
return 0; // 正常退出
}
arch/arm/include/asm/glue-proc.h
- 根据不同架构执行不同的代码
#ifdef CONFIG_CPU_V7M
# ifdef CPU_NAME
# undef MULTI_CPU
# define MULTI_CPU
# else
# define CPU_NAME cpu_v7m
# endif
#endif
#ifndef MULTI_CPU
#define cpu_proc_init __glue(CPU_NAME,_proc_init)
#define cpu_proc_fin __glue(CPU_NAME,_proc_fin)
#define cpu_reset __glue(CPU_NAME,_reset)
#define cpu_do_idle __glue(CPU_NAME,_do_idle)
#define cpu_dcache_clean_area __glue(CPU_NAME,_dcache_clean_area)
#define cpu_do_switch_mm __glue(CPU_NAME,_switch_mm)
#define cpu_set_pte_ext __glue(CPU_NAME,_set_pte_ext)
#define cpu_suspend_size __glue(CPU_NAME,_suspend_size)
#define cpu_do_suspend __glue(CPU_NAME,_do_suspend)
#define cpu_do_resume __glue(CPU_NAME,_do_resume)
#endif
PROC_INFO 架构信息
arch/arm/include/asm/vmlinux.lds.h
//arch/arm/kernel/vmlinux.lds.S
/*
- 这些值绝不能为空
- 如果你必须注释掉这两个 assert 语句,则你的 binutils 太旧了(还有其他原因)
*/
ASSERT((__proc_info_end - __proc_info_begin), "missing CPU support")
// arch/arm/include/asm/vmlinux.lds.h
#define PROC_INFO \
. = ALIGN(4); \
__proc_info_begin = .; \
KEEP(*(.proc.info.init)) \
__proc_info_end = .;
arch/arm/include/uapi/asm/hwcap.h “hardware capabilities”(硬件能力)
HWCAP flags “hardware capabilities”(硬件能力)
- hwcap 是 “hardware capabilities”(硬件能力)的缩写,用于描述处理器支持的硬件特性或扩展功能。
/*
* HWCAP flags - for elf_hwcap (in kernel) and AT_HWCAP
*/
/* 描述: 支持 SWP 指令(交换寄存器和内存的值)。
用途: 用于原子操作,但在现代 ARM 处理器中已被更高效的指令替代。 */
#define HWCAP_SWP (1 << 0)
/* 描述: 支持半字(16 位)加载和存储指令。
用途: 提高对 16 位数据的操作效率。 */
#define HWCAP_HALF (1 << 1)
/* 描述: 支持 Thumb 指令集(16 位压缩指令)。
用途: 提高代码密度,减少内存占用。 */
#define HWCAP_THUMB (1 << 2)
/* 描述: 支持 26 位地址模式(已过时)。
用途: 仅用于兼容旧的 ARM 处理器。 */
#define HWCAP_26BIT (1 << 3)
/* 描述: 支持快速乘法指令。
用途: 提高整数乘法运算的性能。 */
#define HWCAP_FAST_MULT (1 << 4)
/* 描述: 支持浮点协处理器(FPA)。
用途: 处理浮点运算,但已被 VFP 替代。 */
#define HWCAP_FPA (1 << 5)
/* 描述: 支持向量浮点(VFP)指令集。
用途: 提高浮点运算性能 */
#define HWCAP_VFP (1 << 6)
/* 描述: 支持增强的 DSP 指令集。
用途: 提高数字信号处理性能 */
#define HWCAP_EDSP (1 << 7)
/* 描述: 支持 Jazelle(Java 加速)。
用途: 提高 Java 字节码的执行效率。 */
#define HWCAP_JAVA (1 << 8)
/* 描述: 支持 Intel Wireless MMX 技术。
用途: 提高多媒体处理性能。 */
#define HWCAP_IWMMXT (1 << 9)
/* 描述: 支持 Crunch 协处理器(已废弃)。
用途: 处理浮点运算,但已过时。 */
#define HWCAP_CRUNCH (1 << 10)
/* 描述: 支持 ThumbEE(Thumb Execution Environment)。
用途: 提高嵌入式环境的性能 */
#define HWCAP_THUMBEE (1 << 11)
/* 描述: 支持 NEON SIMD 指令集。
用途: 提高多媒体和信号处理性能。 */
#define HWCAP_NEON (1 << 12)
/* 描述: 支持 VFPv3 浮点指令集。
用途: 提高浮点运算性能。 */
#define HWCAP_VFPv3 (1 << 13)
/* 描述: 支持 VFPv3-D16(16 个浮点寄存器)。
用途: 提供较小的浮点寄存器集,适用于资源受限的设备。 */
#define HWCAP_VFPv3D16 (1 << 14)
/* 描述: 支持线程本地存储(TLS)。
用途: 提高多线程程序的性能。 */
#define HWCAP_TLS (1 << 15)
/* 描述: 支持 VFPv4 浮点指令集。
用途: 提供更高效的浮点运算。 */
#define HWCAP_VFPv4 (1 << 16)
/* 在 ARM 架构的早期版本中,整数除法通常需要通过软件模拟来实现,这会导致性能开销较大。
* 从 ARMv7 版本开始,部分处理器引入了硬件支持的整数除法指令:
描述: 支持整数除法指令(ARM 模式)。
用途: 提高整数除法性能。
*/
#define HWCAP_IDIVA (1 << 17)
/* 描述: 支持整数除法指令(Thumb 模式)。
用途: 提高整数除法性能。 */
#define HWCAP_IDIVT (1 << 18)
/* 描述: 支持 32 个浮点寄存器(而非 16 个)。
用途: 提供更大的寄存器集以支持复杂计算。 */
#define HWCAP_VFPD32 (1 << 19)
/* 描述: 同时支持 HWCAP_IDIVA 和 HWCAP_IDIVT。
用途: 表示整数除法在 ARM 和 Thumb 模式下均可用。 */
#define HWCAP_IDIV (HWCAP_IDIVA | HWCAP_IDIVT)
/* 描述: 支持大物理地址扩展(Large Physical Address Extension)。
用途: 支持超过 4GB 的物理内存。 */
#define HWCAP_LPAE (1 << 20)
/* 描述: 支持事件流(Event Stream)。
用途: 用于性能监控和调试。 */
#define HWCAP_EVTSTRM (1 << 21)
/* 描述: 支持半精度浮点运算(FPHP)。
用途: 提高半精度浮点运算性能。 */
#define HWCAP_FPHP (1 << 22)
/* 描述: 支持半精度 SIMD 运算。
用途: 提高 SIMD 半精度运算性能。 */
#define HWCAP_ASIMDHP (1 << 23)
/* 描述: 支持双精度 SIMD 运算。
用途: 提高 SIMD 双精度运算性能。 */
#define HWCAP_ASIMDDP (1 << 24)
/* 描述: 支持浮点 FMLA(Fused Multiply-Add)运算。
用途: 提高浮点乘加运算性能 */
#define HWCAP_ASIMDFHM (1 << 25)
/* 描述: 支持 BF16(bfloat16)格式的 SIMD 运算。
用途: 提高机器学习和神经网络计算性能。 */
#define HWCAP_ASIMDBF16 (1 << 26)
/* 描述: 支持 8 位整数矩阵乘法(I8MM)。
用途: 提高低精度矩阵运算性能,常用于机器学习。 */
#define HWCAP_I8MM (1 << 27)
/*
* HWCAP2 flags - for elf_hwcap2 (in kernel) and AT_HWCAP2
*/
/* 描述: 支持 AES(高级加密标准)指令集。
用途: 提供硬件加速的 AES 加密和解密操作,显著提高对称加密算法的性能。
应用场景: 数据加密、VPN、SSL/TLS 等。 */
#define HWCAP2_AES (1 << 0)
/* 描述: 支持 PMULL(多项式乘法)指令。
用途: 提供硬件加速的多项式乘法运算,通常用于 Galois/Counter Mode (GCM) 加密模式。
应用场景: GCM 模式的 AES 加密 */
#define HWCAP2_PMULL (1 << 1)
/* 描述: 支持 SHA-1 哈希指令。
用途: 提供硬件加速的 SHA-1 哈希计算,显著提高哈希运算性能。
应用场景: 数据完整性校验、数字签名(尽管 SHA-1 已逐渐被弃用)。 */
#define HWCAP2_SHA1 (1 << 2)
/* 描述: 支持 SHA-2 哈希指令(包括 SHA-224 和 SHA-256)。
用途: 提供硬件加速的 SHA-2 哈希计算,显著提高安全哈希算法的性能。
应用场景: 数据完整性校验、数字签名、区块链等。 */
#define HWCAP2_SHA2 (1 << 3)
/* 描述: 支持 CRC32(循环冗余校验)指令。
用途: 提供硬件加速的 CRC32 校验计算,用于快速检测数据传输中的错误。
应用场景: 网络通信、存储设备的数据完整性校验。 */
#define HWCAP2_CRC32 (1 << 4)
/* 描述: 支持 Speculation Barrier(推测屏障)指令。
用途: 防止推测执行漏洞(如 Spectre 攻击),通过插入屏障指令限制处理器的推测执行行为。
应用场景: 安全性增强,防止侧信道攻击。 */
#define HWCAP2_SB (1 << 5)
/* 描述: 支持 Speculative Store Bypass Safe(推测存储旁路安全)功能。
用途: 防止 Speculative Store Bypass(推测存储旁路)漏洞,通过硬件机制限制推测执行的影响。
应用场景: 安全性增强,防止侧信道攻击。
*/
#define HWCAP2_SSBS (1 << 6)
arch/arm/include/asm/procinfo.h
/*
*注意! 如果我们使用 Struct 处理器,则始终定义 MULTI_CPU,否则此条目未使用,但仍然存在。
*
*注意!以下结构由汇编语言定义,而不是 C 代码。 有关更多信息,请查看:arch/arm/mm/proc-*。S 和 arch/arm/kernel/head 的 S 和 arch/arm/kernel/head 中。S
*/
/*
* Match ARM Cortex-M7 processor.
struct proc_info_list {
unsigned int cpu_val; = 0x410fc270
unsigned int cpu_mask; = 0xff0ffff0
unsigned long __cpu_mm_mmu_flags; = 0
unsigned long __cpu_io_mmu_flags; = 0
unsigned long __cpu_flush; = __v7m_cm7_setup()
const char *arch_name; = "armv7m"
const char *elf_name; = "v7m"
unsigned int elf_hwcap; = HWCAP_EDSP
const char *cpu_name; = "ARMv7-M"
struct processor *proc; = cm7_processor_functions()
struct cpu_tlb_fns *tlb; = NULL
struct cpu_user_fns *user; = NULL
struct cpu_cache_fns *cache; = v7m_cache_fns()
};
*/
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
arch/arm/mm/proc-macros.S
initfn
- 这段代码的逻辑是通过 initfn 宏计算两个符号(func 和 base)之间的偏移量,并将结果存储为一个 32 位值。结合 initfn \initfunc, \name 的调用,具体逻辑如下:
.macro initfn, func, base
.long \func - \base
.endm
arch/arm/mm/proc-v7m.S
ARMv7-M 处理器定义块
此代码片段是Linux内核中的一段ARM汇编代码。其核心作用是定义一系列数据结构和函数指针, 以便内核能够识别和正确操作基于ARMv7-M架构的CPU (如此处特指的Cortex-M7)。它特别为无MMU (内存管理单元) 的单核系统 (如STM32H750) 进行了配置, 通过宏来生成一个处理器信息表 (proc_info_list
) 中的条目。内核在启动时会查询这个表, 以找到与当前CPU匹配的配置, 并据此设置正确的中断/异常处理器、缓存操作函数以及电源管理函数。
/*
* Cortex-M7 处理器函数
*/
/*
* globl_equ: 是一个汇编指令, 它将一个符号定义为全局的, 并使其等同于另一个符号.
* 这里的几行指令是为了代码复用, 将为更通用的 "v7m" (ARMv7-M) 架构编写的函数,
* 直接用作更具体的 "cm7" (Cortex-M7) CPU 的实现函数.
* 这意味着 Cortex-M7 的处理器初始化、复位、空闲处理和"内存切换"功能都使用通用的 ARMv7-M 实现.
*/
globl_equ cpu_cm7_proc_init, cpu_v7m_proc_init // 将 cm7 的初始化函数指向 v7m 的初始化函数
globl_equ cpu_cm7_reset, cpu_v7m_reset // 将 cm7 的复位函数指向 v7m 的复位函数
globl_equ cpu_cm7_do_idle, cpu_v7m_do_idle // 将 cm7 的空闲(idle)函数指向 v7m 的空闲函数
globl_equ cpu_cm7_switch_mm, cpu_v7m_switch_mm // 将 cm7 的内存管理(mm)切换函数指向 v7m 的对应函数. 在无MMU系统上, 此函数通常为空操作.
/*
* define_processor_functions: 这是一个宏, 用于生成一个包含核心处理器操作函数指针的结构体.
* @ v7m: 宏的第一个参数, 用于构成生成的结构体名称 (例如 v7m_processor_functions).
* @ dabort=nommu_early_abort: 指定数据访问异常(Data Abort)的处理器为 nommu_early_abort. 函数名明确指出这是用于无MMU系统的异常处理.
* @ pabort=legacy_pabort: 指定指令预取异常(Prefetch Abort)的处理器为 legacy_pabort.
* @ nommu=1: 这是一个关键参数, 向宏明确指出这是为无MMU系统生成的配置.
* 下面一行对 cm7 的定义复用了完全相同的配置, 表明其处理方式与通用的 v7m 一致.
*/
define_processor_functions v7m, dabort=nommu_early_abort, pabort=legacy_pabort, nommu=1
define_processor_functions cm7, dabort=nommu_early_abort, pabort=legacy_pabort, nommu=1
/*
* .section ".rodata": 这是一个汇编指令, 用于将其后的数据放入 ".rodata" (只读数据) 段中.
*/
.section ".rodata"
/*
* string: 这可能是一个自定义宏, 用于定义一个空字符结尾的字符串.
* 下面几行定义了几个将在处理器信息结构体中引用的字符串常量.
*/
string cpu_arch_name, "armv7m" // 定义字符串 "armv7m", 代表CPU架构名称.
string cpu_elf_name "v7m" // 定义字符串 "v7m", 代表ELF文件格式的架构名称.
string cpu_v7m_name "ARMv7-M" // 定义字符串 "ARMv7-M", 用于显示给用户.
/*
* .section ".proc.info.init", "a": 切换到一个特殊的、名为 ".proc.info.init" 的数据段.
* "a" 标志表示该段是可分配的(allocatable).
* 链接器会收集所有目标文件中的这个段, 并将它们连接在一起, 形成一个处理器信息表.
* 内核启动时会遍历这个表来找到与当前硬件匹配的处理器信息.
*/
.section ".proc.info.init", "a"
/*
* .macro __v7m_proc ... .endm: 定义一个名为 __v7m_proc 的汇编宏.
* 这个宏是一个模板, 用于方便地生成一个完整的 proc_info_list 结构体条目.
* @ name, initfunc, cache_fns, hwcaps, proc_fns: 这些是宏可以接受的参数.
*/
.macro __v7m_proc name, initfunc, cache_fns = nop_cache_fns, hwcaps = 0, proc_fns = v7m_processor_functions
/*
* .long: 这是一个汇编指令, 用于在当前位置插入一个32位的长字(long word).
* 下面的每一条 .long 指令都对应 C语言中 proc_info_list 结构体的一个成员.
* 对应 proc_info_list.__cpu_mm_mmu_flags 成员. 对于无MMU系统, 此值为0.
*/
.long 0
/*
* 对应 proc_info_list.__cpu_io_mmu_flags 成员. 对于无MMU系统或没有IOMMU的系统, 此值为0.
*/
.long 0
/*
* initfn: 这可能是另一个宏, 用于生成指向特定初始化函数的引用.
* \initfunc 和 \name 是从宏参数中代入的.
*/
initfn \initfunc, \name
/*
* 插入一个指向 cpu_arch_name 字符串 ("armv7m") 的指针.
*/
.long cpu_arch_name
/*
* 插入一个指向 cpu_elf_name 字符串 ("v7m") 的指针.
*/
.long cpu_elf_name
/*
* 插入一个表示硬件能力的32位标志字.
* HWCAP_HALF: 支持半字(16位)加载/存储指令.
* HWCAP_THUMB: 支持Thumb指令集 (ARMv7-M只运行在Thumb-2模式).
* HWCAP_FAST_MULT: 支持32位乘法指令.
* \hwcaps: 允许在调用宏时传入额外的硬件能力标志.
*/
.long HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \hwcaps
/*
* 插入一个指向 cpu_v7m_name 字符串 ("ARMv7-M") 的指针.
*/
.long cpu_v7m_name
/*
* 插入一个指向处理器函数表 (由 define_processor_functions 宏生成) 的指针.
* 默认值为 v7m_processor_functions.
*/
.long \proc_fns
/*
* 插入一个指向TLB(快表)操作函数表的指针. TLB是MMU的一部分, 在无MMU系统上此值为0.
*/
.long 0
/*
* 插入一个指向用户空间辅助函数表的指针.
*/
.long 0
/*
* 插入一个指向缓存(cache)操作函数表的指针.
* 默认值为 nop_cache_fns, 表示缓存操作为空操作(no-operation),
* 这可能意味着系统没有缓存, 或者缓存管理由硬件自动完成, 不需要软件干预.
*/
.long \cache_fns
.endm
/*
* Match ARM Cortex-M7 processor.
struct proc_info_list {
unsigned int cpu_val; = 0x410fc270
unsigned int cpu_mask; = 0xff0ffff0
unsigned long __cpu_mm_mmu_flags; = 0
unsigned long __cpu_io_mmu_flags; = 0
unsigned long __cpu_flush; = __v7m_cm7_setup()
const char *arch_name; = "armv7m"
const char *elf_name; = "v7m"
unsigned int elf_hwcap; = HWCAP_EDSP
const char *cpu_name; = "ARMv7-M"
struct processor *proc; = cm7_processor_functions()
struct cpu_tlb_fns *tlb; = NULL
struct cpu_user_fns *user; = NULL
struct cpu_cache_fns *cache; = v7m_cache_fns()
};
*/
.type __v7m_cm7_proc_info, #object
__v7m_cm7_proc_info:
.long 0x410fc270 /* ARM Cortex-M7 0xC27 */
.long 0xff0ffff0 /* Mask off revision, patch release */
__v7m_proc __v7m_cm7_proc_info, __v7m_cm7_setup, hwcaps = HWCAP_EDSP, cache_fns = v7m_cache_fns, proc_fns = cm7_processor_functions
.size __v7m_cm7_proc_info, . - __v7m_cm7_proc_info
THREAD_SIZE 分配栈大小
__v7m_cm7_setup
函数中分配THREAD_SIZE大小的栈空间
ldr sp, =init_thread_union + THREAD_START_SP @ 设置堆栈指针SP
- arch/arm/include/asm/thread_info.h 中定义了
THREAD_SIZE
为8192-8字节,预留8字节用于填充magic,用于溢出检测
#ifdef CONFIG_KASAN
/*
* KASan uses a lot of extra stack space so the thread size order needs to
* be increased.
*/
#define THREAD_SIZE_ORDER 2
#else
#define THREAD_SIZE_ORDER 1
#endif
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) //PAGE_SIZE 1<<12
#define THREAD_START_SP (THREAD_SIZE - 8) //预留8字节用于填充magic,用于溢出检测
- arch/arm/kernel/vmlinux.lds 中定义了
init_thread_union
// include/asm-generic/vmlinux.lds.h
#define INIT_TASK_DATA(align) \
. = ALIGN(align); \
__start_init_stack = .; \
init_thread_union = .; \
init_stack = .; \
KEEP(*(.data..init_thread_info)) \
. = __start_init_stack + THREAD_SIZE; \
__end_init_stack = .;
.data : AT(ADDR(.data) - 0)
{
.= ALIGN(((1 << 12) << 1));
__start_init_stack =.;
init_thread_union =.;
init_stack =.;
KEEP(*(.data..init_task))
KEEP(*(.data..init_thread_info)).=
__start_init_stack + ((1 << 12) << 1);
__end_init_stack =.;
}
arch/arm/include/asm/assembler.h
setmode 用于在引导期间断言处于 SVC 模式
#if defined(CONFIG_CPU_V7M)
/*
* setMode 用于在引导期间断言处于 SVC 模式。对于 v7-M,这是在 __v7m_setup 中完成的,因此 setmode 在这里可以为空。
*/
.macro setmode, mode, reg
.endm
#elif defined(CONFIG_THUMB2_KERNEL)
#else
#endif
safe_svcmode_maskall 干净地进入 SVC 模式并屏蔽中断
/*
* 帮助宏干净地进入 SVC 模式并屏蔽中断。reg 是宏要覆盖的暂存寄存器。
*
* 此宏用于在启动时强制 CPU 进入 SVC 模式。
* 您无法返回到原始模式。
*/
.macro safe_svcmode_maskall reg:req
#if __LINUX_ARM_ARCH__ >= 6 && !defined(CONFIG_CPU_V7M)
#else
//对于 v7-M,这是在 __v7m_setup 中完成的,因此 setmode 在这里可以为空。
#endif
.endm
str_va 将一个 32 位字存储到 \sym 的虚拟地址
/*
* str_va - 将一个 32 位字存储到 \sym 的虚拟地址
*/
.macro str_va, rn:req, sym:req, tmp:req, cond
__ldst_va str, \rn, \tmp, \sym, \cond, 0
.endm
set_current 存储此 CPU 当前任务的任务指针
- 将寄存器中的值存储到符号 __current 所表示的虚拟地址中
/*
* set_current - 存储此 CPU 当前任务的任务指针
*/
.macro set_current, rn:req, tmp:req
#if defined(CONFIG_CURRENT_POINTER_IN_TPIDRURO) || defined(CONFIG_SMP)
#ifdef CONFIG_CPU_V6
#endif
#else
//将寄存器中的值存储到符号 __current 所表示的虚拟地址中
str_va \rn, __current, \tmp
#endif
.endm
disable_irq_notrace enable_irq_notrace
/*
* Enable and disable interrupts
*/
#if __LINUX_ARM_ARCH__ >= 6
.macro disable_irq_notrace
cpsid i
.endm
.macro enable_irq_notrace
cpsie i
.endm
#else
#endif
ldr_va 从符号\sym的虚拟地址加载一个32位的字
/*
* 声明一个名为 __ldst_va 的汇编宏。
* 它接受6个参数:
* op: 要执行的操作,如 ldr (加载), str (存储)。
* reg: 目标/源寄存器。
* tmp: 一个临时寄存器,用于存放符号的基地址。
* sym: 目标符号(如一个全局变量名)。
* cond: 条件码,如 eq (相等), ne (不等),若无则为空。
* offset: 地址偏移量。
*/
.macro __ldst_va, op, reg, tmp, sym, cond, offset
/*
* Part 1: 获取符号的虚拟地址
* ----------------------------
*/
#if __LINUX_ARM_ARCH__ >= 7 || \
!defined(CONFIG_ARM_HAS_GROUP_RELOCS) || \
(defined(MODULE) && defined(CONFIG_ARM_MODULE_PLTS))
/*
* 这是现代或特定配置下的路径,通常更高效。
* 适用于:
* - ARMv7及以上架构。
* - 或,内核配置不支持“组重定位”。
* - 或,正在编译一个内核模块,并且该模块使用PLT(过程链接表)。
*
* mov_l 是另一个宏,它会展开为一条或两条指令(如movw+movt或adr+ldr)
* 来将32位的符号地址'sym'加载到临时寄存器'tmp'中。
* 'cond'参数会被传递给mov_l内部的指令。
*/
mov_l \tmp, \sym, \cond
#else
/*
* 这是针对较旧架构(如ARMv6)且在特定配置下的备用路径。
* 这种情况下,获取地址的方式可能更复杂,例如需要通过全局偏移表(GOT)。
* (此代码片段中省略了这部分实现,但现实中它会存在)。
*/
#endif
/*
* ldr_va - 从符号\sym的虚拟地址加载一个32位的字。
*/
/*
* 定义一个名为ldr_va的汇编宏。
* rd: 必需参数,目标寄存器。
* sym: 必需参数,要加载的变量符号名。
* cond: 可选参数,指令的条件码(如eq, ne)。
* tmp: 可选参数,用于地址计算的临时寄存器。
* offset: 可选参数,地址偏移量,默认为0。
*/
.macro ldr_va, rd:req, sym:req, cond, tmp, offset=0
/*
* .ifnb \tmp: 如果调用时传入的'tmp'参数不是空的(not blank)。
*/
.ifnb \tmp
/*
* 如果提供了临时寄存器'tmp',则调用内部宏__ldst_va。
* 参数传递:
* - ldr: 指定操作是加载(load)。
* - \rd: 最终的目标寄存器。
* - \tmp: 用于地址计算的临时寄存器。
* - ...其他参数照常传递...
*/
__ldst_va ldr, \rd, \tmp, \sym, \cond, \offset
.else
/*
* 如果调用时没有提供'tmp'参数。
*/
/*
* 同样调用内部宏__ldst_va,但这次将'tmp'参数的位置
* 传入了'\rd'。这等于告诉__ldst_va:“你没有别的临时寄存器可用,
* 就用目标寄存器rd自己来做地址计算的中转吧”。
*/
__ldst_va ldr, \rd, \rd, \sym, \cond, \offset
.endif
.endm
ldr_this_cpu 从每个 CPU 的变量 ‘sym’ 加载一个 32 位字 插入寄存器 ‘rd’
/*
* ldr_this_cpu - 从每CPU变量'sym'中加载一个32位字
* 到寄存器'rd'中,'rd'可以是堆栈指针。
* 使用't1'和't2'作为通用临时寄存器。如果'rd'不是sp,
* 它们可以和'rd'重叠。
*/
/*
* 定义一个名为ldr_this_cpu的汇编宏,它接受四个必需的参数:
* rd: 目标寄存器
* sym: 要访问的per-CPU变量的符号名
* t1, t2: 临时寄存器
*/
.macro ldr_this_cpu, rd:req, sym:req, t1:req, t2:req
/*
* ---- 分支一:单核(UP)系统 ----
* 如果内核没有以SMP模式编译(即单核)。
*/
#ifndef CONFIG_SMP
/*
* 对于单核系统,per-CPU变量就是普通的全局变量。
* ldr_va是一个宏,它会展开为一条简单的LDR指令,
* 直接从'sym'变量的地址加载值到'rd'寄存器。
* temp=\t1表示使用临时寄存器t1来存储中间结果。
*/
ldr_va \rd, \sym, tmp=\t1
#elif __LINUX_ARM_ARCH__ >= 7 || \
!defined(CONFIG_ARM_HAS_GROUP_RELOCS) || \
(defined(MODULE) && defined(CONFIG_ARM_MODULE_PLTS))
#else
#endif
.endm
get_thread_info 获取当前线程信息
/*
* get_current - load the task pointer of this CPU's current task
*/
.macro get_current, rd:req
#if defined(CONFIG_CURRENT_POINTER_IN_TPIDRURO) || defined(CONFIG_SMP)
#else
ldr_va \rd, __current
#endif
.endm
/*
* Get current thread_info.
*/
.macro get_thread_info, rd
/* thread_info is the first member of struct task_struct */
get_current \rd
.endm
arch/arm/include/asm/v7m.h
//arch/arm/include/asm/assembler.h
#define IOMEM(x) (x)
/*
* 当分支到具有位 [31:28] == 0xf 的地址时,将发生异常返回。
位 [27:5] 是保留的 (SBOP)。
如果处理器实现 FP 扩展位 [4] 定义异常帧是否为 FP 状态信息分配了空间,否则为 SBOP。
位 [3] 定义返回的模式(0 -> 处理程序模式;1 -> 线程模式)。
位 [2] 定义使用哪个 sp (0 -> msp;1 -> psp)。
位 [1:0] 固定为 0b01。
*/
#define EXC_RET_STACK_MASK 0x00000004
#define EXC_RET_THREADMODE_PROCESSSTACK (3 << 2)
/*
* Common defines for v7m cpus
*/
#define V7M_SCS_ICTR IOMEM(0xe000e004)
#define V7M_SCS_ICTR_INTLINESNUM_MASK 0x0000000f
#define BASEADDR_V7M_SCB IOMEM(0xe000ed00)
#define V7M_SCB_CPUID 0x00
#define V7M_SCB_ICSR 0x04
#define V7M_SCB_ICSR_PENDSVSET (1 << 28)
#define V7M_SCB_ICSR_PENDSVCLR (1 << 27)
#define V7M_SCB_ICSR_RETTOBASE (1 << 11)
#define V7M_SCB_ICSR_VECTACTIVE 0x000001ff
#define V7M_SCB_VTOR 0x08
#define V7M_SCB_AIRCR 0x0c
#define V7M_SCB_AIRCR_VECTKEY (0x05fa << 16)
#define V7M_SCB_AIRCR_SYSRESETREQ (1 << 2)
#define V7M_SCB_SCR 0x10
#define V7M_SCB_SCR_SLEEPDEEP (1 << 2)
#define V7M_SCB_CCR 0x14
#define V7M_SCB_CCR_STKALIGN (1 << 9)
#define V7M_SCB_CCR_DC (1 << 16)
#define V7M_SCB_CCR_IC (1 << 17)
#define V7M_SCB_CCR_BP (1 << 18)
#define V7M_SCB_SHPR2 0x1c
#define V7M_SCB_SHPR3 0x20
#define V7M_SCB_SHCSR 0x24
#define V7M_SCB_SHCSR_USGFAULTENA (1 << 18)
#define V7M_SCB_SHCSR_BUSFAULTENA (1 << 17)
#define V7M_SCB_SHCSR_MEMFAULTENA (1 << 16)
#define V7M_xPSR_FRAMEPTRALIGN 0x00000200
#define V7M_xPSR_EXCEPTIONNO V7M_SCB_ICSR_VECTACTIVE
arch/arm/kernel/entry-header.S
/*
* ARMv7-M 异常进入/退出宏。
*
* xPSR、ReturnAddress()、LR (R14)、R12、R3、R2、R1 和 R0 是
* 之前自动保存在当前堆栈(32 个单词)上
* 切换到异常堆栈 (SP_main)。
*
* 如果在用户模式下出现异常,则SP_main
*空。否则,SP_main 会自动对齐到 64 位
* (CCR.STKALIGN 集)。
*
* Linux 假定在输入
* 异常处理程序,否则可能会 BUG。中断
* 在进入时被禁用,并在 Exit 宏中重新启用。
*
* 从 SVC 或 PendSV 返回时使用 v7m_exception_slow_exit。
* 返回内核模式时,我们不会从 exception 返回。
*/
v7m_exception_entry 进入中断
v7m_exception_entry
宏的主要目的是为ARMv7-M架构的异常处理程序提供一个安全且一致的运行环境。ARMv7-M处理器在发生异常时会自动保存部分寄存器状态,但这并不足以满足Linux内核的需求。Linux内核需要:- 保存更多的寄存器状态:ARMv7-M自动保存的寄存器(如
xPSR
、R0-R3
等)不足以满足内核的上下文切换需求,因此需要额外保存R4-R11
等寄存器。 - 禁用中断:Linux内核假定在进入异常处理程序时中断已被禁用,以避免中断干扰异常处理。
- 对齐堆栈:ARMv7-M可能会自动对堆栈进行8字节对齐,Linux内核需要根据对齐情况调整堆栈指针。
- 为异常处理程序准备上下文:将寄存器状态和其他必要信息存储到堆栈中,供异常处理程序使用。
- 保存更多的寄存器状态:ARMv7-M自动保存的寄存器(如
v7m_exception_entry
宏的作用可以总结为以下几点:
- 确定异常堆栈:根据异常发生时的模式(用户模式或内核模式),选择主堆栈(
SP_main
)或进程堆栈(PSP
)。 - 保存寄存器状态:将所有需要的寄存器(包括
R0-R11
、R12
、LR
、PC
和xPSR
)保存到堆栈中。 - 禁用中断:确保异常处理程序在执行时不会被中断干扰。
- 调整堆栈对齐:根据
xPSR
中的对齐标志调整堆栈指针,确保堆栈对齐符合ARMv7-M架构的要求。 - 为异常处理程序准备上下文:将寄存器状态和其他必要信息存储到堆栈中,供异常处理程序使用。
/*
* ARMv7-M 异常进入/退出宏。
*
在进入异常时,处理器会自动将xPSR、返回地址(ReturnAddress())、LR (R14)、R12、R3、R2、R1和R0保存到当前堆栈(共32字节),然后切换到异常堆栈(SP_main)。
如果异常发生在用户模式下,SP_main为空;否则,SP_main会自动对齐到64位(通过CCR.STKALIGN设置)。
Linux假定在进入异常处理程序时中断已被禁用。如果不是这种情况,可能会导致BUG。在入口宏中禁用中断,并在退出宏中重新启用。
v7m_exception_slow_exit用于从SVC或PendSV返回。当返回到内核模式时,不会从异常中返回。
*/
.macro v7m_exception_entry
/*
确定异常发生时处理器保存的寄存器位置。根据CPU进入异常时的模式,寄存器可能保存在主堆栈(SP_main)或进程堆栈(PSP)中。lr寄存器中的EXC_RETURN的第2位决定使用哪个堆栈。
*/
//TST lr, #EXC_RET_STACK_MASK检查lr寄存器的第4位。如果该位为1,则使用进程堆栈;否则,使用主堆栈
tst lr, #EXC_RET_STACK_MASK
mrsne r12, psp //从PSP加载堆栈指针到r12。
moveq r12, sp //从SP_main加载堆栈指针到r12
//由于尾链(tail-chaining)的存在,r0-r3和r12的值可能与异常帧中保存的不一致,因此需要重新加载。
ldmia r12!, {r0-r3}
@ Linux假定中断在进入异常处理程序时已被禁用。在分配堆栈空间之前禁用中断。
cpsid i // CPSID i指令禁用中断(IRQ)
//调整堆栈指针以分配空间,并将r0-r11寄存器的值压入堆栈
sub sp, #PT_REGS_SIZE-S_IP //为保存寄存器分配堆栈空间。
stmdb sp!, {r0-r11} //将r0-r11的值存储到堆栈中,并更新堆栈指针
// 加载保存的r12、lr、返回地址和xPSR。从现在开始,r0-r7用于信号处理,不会被修改,而r8-r12可以被覆盖。
mov r9, r12
ldmia r9!, {r8, r10-r12}
// 计算原始堆栈指针值。r9当前指向自动保存的xPSR上方的内存位置。如果CPU自动对堆栈进行了8字节对齐(xPSR的第9位设置),则堆栈中会额外包含一个32位值。
tst r12, V7M_xPSR_FRAMEPTRALIGN
addne r9, r9, #4 //如果设置了对齐位,则调整r9以跳过额外的32位值
@ 将保存的r12存储到堆栈中,并将sp加上偏移量存储到r8中,作为后续STM操作的基地址
str r8, [sp, #S_IP]
add r8, sp, #S_SP
@ store r13-r15, xPSR
stmia r8!, {r9-r12}
@ store old_r0
str r0, [r8]
.endm
v7m_exception_slow_exit 退出中断
- 读取当前内核栈上的pt_regs结构体,从中提取出被中断任务的核心寄存器(SP, LR, PC, xPSR等),然后在目标任务的栈上(这里是PSP)手动重建一个硬件能够识别的、最小化的异常帧。最后,通过BX LR指令,触发硬件自动完成剩余的恢复工作。
/*
* PendSV和SVC被配置为相同优先级。因为内核线程在SVC优先级运行,
* 它永远不会被抢占,所以这个返回路径永远不需要返回到一个内核线程。
* 这意味着,此宏处理的总是返回到用户模式(使用进程堆栈指针PSP)的场景。
*/
.macro v7m_exception_slow_exit ret_r0
/*
* Step 1: 准备返回环境。
* ----------------------
* 首先关闭中断,确保接下来的上下文恢复操作是原子的。然后从全局变量
* exc_ret 中加载特殊的EXC_RETURN值到LR寄存器。这个值是触发硬件
* 异常返回机制的“钥匙”,它告诉硬件返回时应使用PSP,并进入线程模式。
*/
cpsid i
ldr lr, =exc_ret
ldr lr, [lr]
/*
* Step 2: 从内核栈上的pt_regs中提取核心寄存器。
* ----------------------------------------------------
* 内核栈(sp)上保存着一个完整的pt_regs结构体。我们首先需要从中读取出
* 被中断任务的几个关键寄存器,以便后续在目标任务的栈上重建硬件帧。
*
* add r12, sp, #S_IP: 计算出pt_regs中ARM_ip字段的地址,放入r12。
* ldmia r12, {r1-r5}: 从该地址开始,连续加载5个32位值到寄存器中。
* - r1 (临时) <-- pt_regs.ARM_ip (原始r12)
* - r2 (临时) <-- pt_regs.ARM_sp (原始SP,即我们要恢复的PSP)
* - r3 (临时) <-- pt_regs.ARM_lr (原始LR)
* - r4 (临时) <-- pt_regs.ARM_pc (原始PC)
* - r5 (临时) <-- pt_regs.ARM_cpsr (原始xPSR)
*/
add r12, sp, #S_IP
ldmia r12, {r1-r5}
/*
* Step 3: 精确重建硬件要求的栈对齐状态。
* --------------------------------------------
* 硬件在异常返回时,需要通过xPSR的第9位得知目标栈是否是8字节对齐的。
* 我们必须根据从pt_regs中读出的原始SP值(在r2中),来正确设置这个标志位。
*
* tst r2, #4: 检查原始SP的第2位是否为1(即地址是否是...100b)。
* subne r2, r2, #4: 这行代码的意图可能是处理某些对齐的边界情况,
* 确保硬件帧的基地址正确。
* orrne r5, V7M_xPSR_FRAMEPTRALIGN: 如果SP不是8字节对齐,则在xPSR(r5)中
* 设置对齐标志位。
* biceq r5, V7M_xPSR_FRAMEPTRALIGN: 如果SP是8字节对齐的,则清除xPSR(r5)中
* 的对齐标志位。
*
* 经过这一步,r2(目标SP)和r5(目标xPSR)已经完全符合硬件要求。
*/
tst r2, #4
subne r2, r2, #4
orrne r5, V7M_xPSR_FRAMEPTRALIGN
biceq r5, V7M_xPSR_FRAMEPTRALIGN
/*
* Step 4: 清理PC值并开始在目标栈上构建硬件帧。
* ------------------------------------------------
* 硬件要求PC值的最低位(Thumb位)必须为1,但地址本身必须是偶数。
* 此处bic r4, #1是为了确保地址是偶数,硬件在跳转时会自动处理Thumb位。
*
* stmdb r2!, {r1, r3-r5}: 这是构建硬件帧的第一部分。在目标SP(r2)上,
* 按照硬件要求的顺序,压入r1(原始r12), r3(原始LR),
* r4(原始PC), r5(原始xPSR)。
*/
bic r4, #1
stmdb r2!, {r1, r3-r5}
/*
* Step 5: 从内核栈上加载剩余的低位寄存器。
* ----------------------------------------------
* 现在,我们需要从内核栈的pt_regs中加载原始的r0-r3。
* ldmia sp, {r1, r3-r5}: 这一步的理解是,它从内核栈的pt_regs的
* r1,r3,r4,r5位置加载值到临时寄存器。
* 这可能是为了准备下一步的写入。
*/
ldmia sp, {r1, r3-r5}
/*
* Step 6: 完成硬件帧的构建(写入r0-r3)。
* ------------------------------------------
* 这是最精妙的部分。根据宏参数ret_r0 (来自fast),决定硬件帧中r0-r3的内容。
*
* .if \ret_r0: ret_r0非零,意味着fast=0,需要恢复所有寄存器。
* - stmdb r2!, {r0, r3-r5}: 这里的r0是当前CPU的r0,它应该是在上一步
* 从pt_regs的r0位置加载而来的。这行指令将原始的r0, r1, r2, r3
* 压入目标栈,完成硬件帧。这里的代码模式{r0, r3-r5}可能是一种
* 写法技巧或特定版本的语法,其意图是压入r0-r3这四个值。
*
* .else: ret_r0为零,意味着fast=1,不需要恢复r0 (用作返回值)。
* - stmdb r2!, {r1, r3-r5}: 此时,硬件帧中r0的位置被写入了r1的值。
* 因为此时的r0是系统调用的返回值,不能被覆盖。
*
* 经过这一步,一个完整的8寄存器硬件帧在目标栈上构建完毕。
*/
.if \ret_r0
stmdb r2!, {r0, r3-r5}
.else
stmdb r2!, {r1, r3-r5}
.endif
/*
* Step 7: 恢复CPU状态。
* ---------------------
* msr psp, r2: 将我们构建好的目标栈指针r2,写入到物理的进程堆栈指针PSP中。
*
* ldmia sp!, {r0-r11}: 从内核栈(sp)上一次性恢复r0-r11到CPU中。
* 这既恢复了被中断任务的r4-r11,也恢复了被用作
* 临时的r0-r3。同时,sp指针也向前移动,
* 相当于释放了pt_regs中这部分空间。
*
* add sp, sp, #PT_REGS_SIZE-S_IP: 彻底恢复内核栈指针,释放整个
* pt_regs结构体的空间。
*
* cpsie i: 重新开启中断。
*/
msr psp, r2
ldmia sp!, {r0-r11}
add sp, sp, #PT_REGS_SIZE-S_IP
cpsie i
/*
* Step 8: 执行硬件返回。
* ---------------------
* bx lr: 这是最后的魔法。LR寄存器中是我们第一步加载的EXC_RETURN值。
* CPU看到这个指令和LR中的特殊值,就会启动硬件自动恢复机制:
* 它会从PSP指向的地址(我们刚刚构建的硬件帧)弹出8个寄存器,
* 恢复CPU模式和PC,然后无缝地从被中断的地方继续执行。
*/
bx lr
.endm
restore_user_regs 恢复寄存器.
- restore_user_regs 的唯一职责是,将在内核栈上保存的pt_regs结构体中的值,准确无误地加载回CPU的物理寄存器中,并执行最后的返回指令,让CPU从被中断的地方继续运行。
.macro restore_user_regs, fast = 0, offset = 0
#ifndef CONFIG_THUMB2_KERNEL
#elif defined(CONFIG_CPU_V7M)
@ V7M恢复。
@ 我们不需要在这里执行clrex(清除独占监视器),
@ 因为这是异常进入和退出序列的一部分,由硬件自动处理。
/*
* 1. 处理栈偏移
*/
.if \offset
/* 如果调用时传入的offset参数非零,就执行这条指令。*/
add sp, #\offset
.endif
/*
* 2. 委托给专家宏执行恢复
*/
v7m_exception_slow_exit ret_r0 = \fast
#else
#endif /* !CONFIG_THUMB2_KERNEL */
.endm
scno (系统调用号) tbl (系统调用表指针) why (Linux syscall) tsk (当前线程信息)
/*
* 这些是系统调用处理程序中使用的寄存器,理论上允许我们
* 向一个函数传递多达7个参数 - r0 到 r6。
* 对于Thumb模式,r7被保留用于存放系统调用号。
* tbl == why 是有意为之的。
* 在调用 ret_with_reschedule 时,我们必须至少设置 "tsk" 和 "why"。
*/
/*
* scno .req r7:
* 定义 r7 寄存器的别名为 scno (syscall number)。
* 在ARM EABI的系统调用规范中,r7通常用于传递系统调用号。
* 当用户空间代码执行 SVC #0 指令时,内核的SVC Handler会从r7中
* 读取这个编号,以确定用户请求的是哪个系统调用。
*/
scno .req r7 @ syscall number
/*
* tbl .req r8:
* 定义 r8 寄存器的别名为 tbl (table)。
* 注释表明它用作“syscall table pointer”(系统调用表指针)。
* 内核在获取到系统调用号(scno)后,会以tbl(r8)为基地址,
* scno(r7)为索引,在系统调用表中查找到对应的C函数处理程序。
*/
tbl .req r8 @ syscall table pointer
/*
* why .req r8:
* 再次将 r8 寄存器定义为别名 why。
* 注释“Linux syscall (!= 0)”暗示了它的用途。why通常用于区分
* 不同的内核返回路径。当从一个常规的Linux系统调用返回时,why会被
* 设置为一个非零值。这与“tbl == why is intentional”的注释相呼应,
* 表明在系统调用处理的某个阶段,r8的角色会从“表指针”转变为
* “返回路径标识符”。这是一个寄存器复用的例子。
*/
why .req r8 @ Linux syscall (!= 0)
/*
* tsk .req r9:
* 定义 r9 寄存器的别名为 tsk (task)。
* 它用于存放当前任务的 thread_info 结构体的指针。
* 在ARM Linux内核中,r9通常被保留作为一个“钦定”的寄存器,
* 用于快速访问当前任务的信息,相当于current_thread_info()。
*/
tsk .req r9 @ current thread_info
invoke_syscall (调用系统调用)
- 参数校验:首先,它会检查用户传入的系统调用号(nr)是否在一个合法的范围内,防止恶意或错误地调用未定义的服务,这是一个基本的安全措施。
- 设置返回点:它会预先将系统调用执行完毕后应该返回的地址(由ret参数指定)加载到LR(链接寄存器)中。这样,当内核函数执行BX LR或MOV PC, LR时,就能正确地回到系统调用处理的后续流程中。
- 参数重载(可选):在某些情况下(如系统调用被信号中断后重启),需要从栈上的pt_regs中重新加载参数。reload参数控制是否执行这个操作。
- 查表与跳转:这是最核心的一步。它以系统调用表(table)的基地址为准,以系统调用号(nr)为索引,计算出目标内核函数(如sys_read, sys_write)的地址,然后直接通过LDR PC, […]指令跳转到该函数去执行。
/*
* 声明一个名为invoke_syscall的汇编宏,它接受table, nr, tmp, ret, reload五个参数。
*/
.macro invoke_syscall, table, nr, tmp, ret, reload=0
/*
* 如果内核配置了CONFIG_CPU_SPECTRE,则编译这部分代码以缓解Spectre漏洞。
*/
#ifdef CONFIG_CPU_SPECTRE
/* 将系统调用号(nr)移入一个临时寄存器(tmp)。*/
mov \tmp, \nr
/* 比较系统调用号是否超出了系统定义的上限(NR_syscalls)。*/
cmp \tmp, #NR_syscalls
/* movcs: 如果比较结果为CS(Carry Set, 表示nr >= NR_syscalls),则将tmp清零。
* 这是一种安全措施,使得无效的调用号最终会跳转到sys_ni_syscall。*/
movcs \tmp, #0
/* csdb: 条件同步屏障,是Spectre v1的缓解措施之一,用于防止恶意分支预测。*/
csdb
/* badr: 将返回地址(由ret标签指定)加载到LR寄存器。*/
badr lr, \ret
/* .if \reload: 如果宏的reload参数非零,则执行以下代码块。
* 这通常用于被信号中断后重启的系统调用。*/
.if \reload
/* r1 = ®s->r0,计算出栈上pt_regs中r0的地址。*/
add r1, sp, #S_R0 + S_OFF
/* ldmiacc: 从r1指向的地址,一次性加载r0-r6到CPU寄存器,恢复参数。*/
ldmiacc r1, {r0 - r6}
/* stmiacc: 将r4, r5重新存入栈上,更新某些栈上传递的参数。*/
stmiacc sp, {r4, r5}
.endif
/* ldrcc: 如果cmp的结果为CC(Carry Clear, 表示nr < NR_syscalls),则执行跳转。
* 这是核心的分发指令:
* pc = table_base_addr + (tmp * 4)
* 它以系统调用表(table)为基址,系统调用号(tmp)为索引,
* 计算出目标内核函数的地址,并直接跳转过去执行。*/
ldrcc pc, [\table, \tmp, lsl #2]
#else
/*
* 如果没有配置CONFIG_CPU_SPECTRE,则编译这部分不含缓解措施的、更高效的代码。
*/
/* 比较系统调用号是否超出了系统定义的上限。*/
cmp \nr, #NR_syscalls
/* 将返回地址(由ret标签指定)加载到LR寄存器。*/
badr lr, \ret
/* 如果reload参数非零,则执行参数重载。*/
.if \reload
add r1, sp, #S_R0 + S_OFF
ldmiacc r1, {r0 - r6}
stmiacc sp, {r4, r5}
.endif
/* ldrcc: 如果nr有效,则计算目标函数地址并跳转执行。
* pc = table_base_addr + (nr * 4) */
ldrcc pc, [\table, \nr, lsl #2]
#endif
/* 宏定义结束。*/
.endm
好的,我们来详细探讨一下Spectre(幽灵)漏洞,这是一个深刻影响了现代处理器设计的安全问题。
Spectre漏洞是什么?一个生动的比喻
想象一位非常性急但能干的图书管理员,他叫“预测执行”(Speculative Execution)。
用户请求:你(一个程序)向管理员要一本书,但需要提供一个复杂的索引号
X
。这个X
的计算很慢,而且你可能没有权限访问它。管理员的“预测”:管理员不想干等,他非常聪明,会猜测你最终会要哪本书。比如,他根据你以前常借的书,猜测你这次可能要的是《烹饪大全》。
提前拿书(预测执行):在等你计算出
X
的时候,他就已经跑去书库,把《烹饪大全》拿了出来,甚至翻开了第一页放在桌上。这个过程非常快,但都是在“幕后”悄悄进行的。最终检查:你终于计算出了索引号
X
,并把它交给管理员。- 情况A(猜对了):如果
X
正好对应《烹饪大全》,管理员直接把已经准备好的书给你,大大节省了时间。 - 情况B(猜错了或无权限):如果
X
对应的是《黑客秘籍》,而你没有权限访问,管理员会立刻意识到错误。他会撤销之前的操作,把《烹饪大全》放回书架,擦掉桌上的痕迹,然后正式地告诉你:“抱歉,你无权访问这本书。”
- 情况A(猜对了):如果
漏洞在哪里?
从表面上看,一切都安全无虞。你最终没有拿到不该拿的书。但问题在于,管理员在“预测执行”时,虽然最后撤销了操作,但他的行为留下了一些微小的、可被观察到的“副作用”(Side Effects)。
- 副作用:比如,他去拿《烹饪大全》时,那个书架上的灰尘被多擦掉了一点;或者他回来时,因为拿了本厚书,心跳稍微快了一点。
一个精明的攻击者(另一个恶意程序)虽然看不到书的内容,但可以通过精确测量这些副作用(例如,通过测量访问某个内存地址的速度,即缓存攻击),来反推出管理员曾经预测性地访问过哪些数据。
Spectre漏洞的核心:攻击者可以诱导处理器去预测性地执行一些它本不该执行的代码(比如访问内核或其他进程的敏感数据),即使这些操作最终会被撤销,但其执行过程中在CPU缓存(Cache)中留下的痕迹,会像“幽灵”一样泄露出本应保密的数据。它利用的是现代CPU为了追求性能而广泛使用的“预测执行”机制。
Spectre漏洞的两个主要变种
变种1:分支目标注入 (Bounds Check Bypass)
- 原理:攻击者通过重复训练CPU的分支预测器,让它相信一个if条件判断总是会走向某个分支。然后,攻击者提供一个恶意的、会导致数组越界访问的索引。CPU在正式计算出索引越界之前,会预测性地执行那个分支,用恶意索引去访问内存中的敏感数据,从而将数据加载到缓存中。
- 比喻:诱导图书管理员相信你总是借阅A区1-10号书架的书,然后突然给他一个11号书架的请求,在他反应过来“哦,越界了”之前,他已经预测性地跑去11号书架看了一眼。
变种2:分支历史注入 (Branch Target Injection)
- 原理:这个变种更复杂,它利用了CPU中间接跳转(Indirect Branch)的预测机制。攻击者可以“毒化”分支目标缓冲器(BTB),让CPU在执行一个间接跳转时,错误地预测并跳转到攻击者指定的一小段代码(称为gadget)上,这段代码会访问敏感数据。
- 比喻:在图书馆的索引卡片上(比如“烹饪类 -> 见C区”),攻击者用一种特殊的方式涂改,让管理员在查找“烹饪类”时,错误地以为应该跑去“禁书区”看一眼。
如何修复(或更准确地说,缓解)?
Spectre是CPU微架构的根本性设计问题,无法像普通软件Bug一样被“修复”,只能通过多种手段进行缓解(Mitigation),而这些缓解措施通常会带来性能损失。
软件缓解:
- 增加屏障指令 (Fences):在关键代码路径上插入内存或指令屏障,强制CPU停止预测,等待前序操作完成后再继续。这会显著影响性能。我们之前分析的
CSDB
(条件同步屏障)就是一种轻量级的屏障。 - Retpoline(返回蹦床):这是针对变种2的一种非常有效的软件缓解技术。它将间接跳转替换为一个安全的、不会被恶意预测的无限循环(即“蹦床”),然后通过
ret
指令从这个循环中“弹出”到真正的目标地址。这阻止了CPU进行恶意的分支预测,但也会带来一定的性能开销。 - 清除分支预测器历史:在特权级切换时(如从内核返回用户态),清除分支预测器的历史记录,防止用户态程序“毒化”的预测影响到内核。
- 增加屏障指令 (Fences):在关键代码路径上插入内存或指令屏障,强制CPU停止预测,等待前序操作完成后再继续。这会显著影响性能。我们之前分析的
编译器缓解:
- 编译器可以自动在代码中插入缓解措施。例如,在数组访问前插入掩码操作,确保索引不会越界,从而防止变种1的攻击。
微码(Microcode)更新:
- CPU厂商(Intel, AMD, ARM)可以发布微码更新,为CPU增加新的指令(如IBRS, IBPB)来更好地控制预测行为。操作系统可以通过调用这些新指令来增强安全性。
硬件修复:
- 这是最根本的解决方案。自Spectre被发现以来,新设计的CPU都在硬件层面加入了缓解措施,例如改进分支预测器使其更难被“毒化”,或者提供更细粒度的预测行为控制。但这需要购买新的硬件。
总结:修复Spectre漏洞是一个系统性的工程,需要在操作系统、编译器、CPU微码和硬件设计等多个层面协同工作。这是一个在安全和性能之间不断进行艰难权衡的过程。您在内核代码中看到的#ifdef CONFIG_CPU_SPECTRE
和csdb
指令,正是这个宏大斗争在代码层面的具体体现。
arch/arm/kernel/entry-common.S
ret_to_user ret_to_user_from_irq ret_slow_syscall
- ret_to_user (系统调用的返回路径)
当一个任务执行系统调用(例如 read(), write(), fork())并且该调用执行完毕后,内核需要返回到该任务。它就会跳转到ret_to_user。
/*
* "慢速"系统调用返回路径。"why"寄存器告诉我们这是否是一个真正的系统调用。
* 此处中断可能是开启的,所以我们总是先关闭它。注意我们用"notrace"版本
* 来避免不必要的追踪代码调用。do_work_pending()会在必要时更新状态。
*/
ENTRY(ret_to_user)
ret_slow_syscall:
#if IS_ENABLED(CONFIG_DEBUG_RSEQ)
#endif
/*
* 关键过渡点:在进入共享的返回路径之前,必须确保中断是关闭的,
* 因为共享路径假定它在中断关闭的上下文中运行。
*/
disable_irq_notrace @ 关闭中断
ENTRY(ret_to_user_from_irq)
/*
* 这是整个返回路径的核心检查点。
* 它用极高的效率判断是否有任何“慢速工作”需要处理。
*/
/* r1 = current->thread_info->flags */
ldr r1, [tsk, #TI_FLAGS]
/* 这是一个非常精妙的3指令序列,用于检查标志位 */
/* 1. 将flags字段左移16位。所有需要慢速处理的标志位(如_TIF_NEED_RESCHED)
都被定义在32位flags的高16位。*/
/* 2. 's'后缀会根据操作结果更新CPSR状态寄存器中的Z(零)标志位。
如果所有高16位都为0,则操作结果为0,Z位置1。
如果任何高16位为1,则操作结果非0,Z位清0。*/
movs r1, r1, lsl #16
/* bne: Branch if Not Equal (to zero) / 如果Z标志为0,则跳转。
* 如果Z标志为0,说明movs结果非0,意味着有慢速工作待处理。
* 因此,跳转到slow_work_pending去处理。*/
bne slow_work_pending
no_work_pending:
/* 如果代码执行到这里,说明flags中没有任何待处理工作。*/
/* 通知追踪系统,即将重新开启硬件中断。*/
//asm_trace_hardirqs_on save = 0
/* 通知上下文追踪系统,即将返回到非中断上下文。*/
// ct_user_enter save = 0
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
/* 如果开启了栈泄漏检测,则调用函数擦除栈上可能残留的敏感数据。*/
// bl stackleak_erase_on_task_stack
#endif
/*
* 这是最终的返回指令宏。它负责从当前任务的内核栈上恢复所有
* 之前保存的CPU寄存器(包括硬件自动保存和软件手动保存的),
* 并将控制权交还给被中断的代码。这是整个异常处理的终点。
*/
restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)
vector_swi 用户空间的程序调用内核时
- 建立内核环境:在手动保存了用户态寄存器(简化代码中省略了这部分)后,vector_swi首先会切换到内核的运行环境,最重要的一步是开启中断,允许更高优先级的中断来抢占正在执行的系统调用。
- 获取调用号:它遵循EABI规范,假定系统调用号已经由用户空间的C库放入了R7寄存器。这避免了旧ABI中需要反汇编SWI指令来解码调用号的复杂过程。
- 查表与分发:它以R7中的调用号为索引,在主系统调用表sys_call_table中找到对应的内核C函数地址。
- 调用与返回:通过invoke_syscall宏,它调用了查找到的C函数来执行具体的内核服务。这个宏还内置了对系统调用被信号中断后自动重启的支持。
集成追踪:在调用C函数前后,它都检查了追踪标志位,如果任务正在被strace等工具追踪,它会跳转到专门的__sys_trace路径,以便记录系统调用的参数和返回值。
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7M
v7m_exception_entry
#else
#endif
/*
* Step 1: 内核态环境准备。
* --------------------------
* 在处理系统调用之前,内核需要完成一系列准备工作,并开启中断。
*/
//reload_current r10, ip /* 宏:加载当前任务的thread_info到r10。*/
//zero_fp /* 宏:清理帧指针寄存器(r11)。*/
//alignment_trap r10, ip, cr_alignment /* 宏:处理非对齐访问陷阱。*/
//asm_trace_hardirqs_on save=0 /* 宏:通知追踪器,中断即将开启。*/
enable_irq_notrace /* 指令:开启可屏蔽中断(IRQ)。*/
//ct_user_exit save=0 /* 宏:通知上下文追踪器,已退出用户态。*/
/*
* Step 2: 获取系统调用号。
* --------------------------
* 在EABI规范下,这个过程非常简单。
*/
/* #if defined(CONFIG_OABI_COMPAT) -> false */
/* #elif defined(CONFIG_AEABI) -> true */
#if defined(CONFIG_OABI_COMPAT)
#elif defined(CONFIG_AEABI)
/*
* 注释:纯EABI用户空间总是将系统调用号放入scno(r7)寄存器中。
* 在这里,内核什么都不用做。系统调用号已经在r7中了。
*/
#elif defined(CONFIG_ARM_THUMB)
#else
#endif
/*
* Step 3: 准备查表并调用C函数。
* --------------------------------
*/
/* 宏:禁用用户空间内存访问,防止在内核态发生意外的页错误。*/
// uaccess_disable tbl
/* 宏:获取当前任务的thread_info结构体指针。*/
get_thread_info tsk
/* 指令:加载内核主系统调用表(sys_call_table)的地址到tbl寄存器。*/
adr tbl, sys_call_table
/* #if defined(CONFIG_OABI_COMPAT) -> false */
/* #elif !defined(CONFIG_AEABI) -> false */
/* #else -> true */
#if defined(CONFIG_OABI_COMPAT)
#elif !defined(CONFIG_AEABI)
#else
/* 对于EABI,我们仅需要将r7中的系统调用号保存到任务的thread_info中,
* 以便追踪或调试。scno是r7的别名。*/
str scno, [tsk, #TI_ABI_SYSCALL]
#endif
/* 宏:如果开启了追踪,从栈上重新加载r0-r3,因为它们可能在
* 入口处的追踪代码中被修改过。*/
TRACE( ldmia sp, {r0 - r3} )
/*
* Step 4: 执行系统调用并处理重启逻辑。
* --------------------------------------
*/
local_restart:
/* 检查是否有追踪相关的标志位(如strace)。*/
ldr r10, [tsk, #TI_FLAGS]
/* 系统调用C函数最多使用6个参数(r0-r5)。前4个已在寄存器中,
* 这里将第5、6个参数(在r4, r5中)也压入栈,以备C函数通过栈来寻址。*/
stmdb sp!, {r4, r5}
/* 检查_TIF_SYSCALL_WORK标志位,判断是否正在被追踪。*/
tst r10, #_TIF_SYSCALL_WORK
/* 如果是,则跳转到专门的追踪处理路径__sys_trace。*/
bne __sys_trace
/*
* 宏:这是核心的调用动作。
* 它会以scno(r7)为索引,在tbl(sys_call_table)中找到函数指针,
* 然后跳转执行。执行完毕后,C函数的返回值在r0中。
* 如果系统调用被信号中断,返回时会跳转到local_restart重新执行。
* 如果正常返回,则跳转到__ret_fast_syscall。
*/
invoke_syscall tbl, scno, r10, __ret_fast_syscall
/*
* Step 5: 处理专有或无效的系统调用。
* --------------------------------------
* 如果invoke_syscall因为某些原因(如专有调用)返回到这里,
* 则进行额外处理。
*/
add r1, sp, #S_OFF
2: /* 比较系统调用号是否在ARM专有范围内。*/
cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE
/* 如果是,则跳转到arm_syscall处理。*/
bcs arm_syscall
/* 否则,这是一个无效的系统调用。*/
mov why, #0
/*
kernel/sys_ni.c
Non-implemented system calls get redirected here.
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
*/
b sys_ni_syscall @ 跳转到“系统调用未实现”的处理函数
/* #if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI) -> false */
#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
#endif
ENDPROC(vector_swi)
slow_work_pending 异常和系统调用统一返回路径上的慢速工作处理程序
/*
* 慢速工作处理的入口点。
*/
slow_work_pending:
/* 准备调用C函数do_work_pending的参数。*/
/* 第一个参数 r0 = 当前栈指针sp,即指向pt_regs结构体的指针。*/
mov r0, sp /* 注释:'regs' */
/* 第二个参数 r2 = why寄存器的值,用于区分入口是来自系统调用还是中断。*/
mov r2, why /* 注释:'syscall' */
/* 调用C函数do_work_pending。该函数负责处理所有慢速工作,
* 如信号传递和任务调度。*/
bl do_work_pending
/* 检查do_work_pending的返回值(在r0寄存器中)。*/
cmp r0, #0
/* beq: Branch if Equal。如果返回值为0,说明所有工作已处理完毕,
* 跳转到no_work_pending,执行快速返回流程。*/
beq no_work_pending
/* 如果返回值不为0(通常小于0),则处理系统调用重启。*/
/* movlt: Move if Less Than。如果r0 < 0,执行mov。
* 将特殊的重启系统调用号__NR_restart_syscall的值加载到scno寄存器。*/
movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
/* 将新的系统调用号写入任务的thread_info结构体中,以确保追踪工具(如ptrace)
* 能观察到这次状态更新。*/
str scno, [tsk, #TI_ABI_SYSCALL] /* 注释:确保追踪器能看到更新 */
/* 由于do_work_pending可能已修改了寄存器,必须从栈上的pt_regs中
* 重新加载系统调用的原始参数(r0-r6)。*/
ldmia sp, {r0 - r6} /* 注释:必须重载r0-r6 */
/* 无条件跳转到local_restart标签处,该标签下的代码会重新触发
* 一个系统调用(现在是__NR_restart_syscall)。*/
b local_restart /* 注释:... 然后我们开始 */
ret_fast_syscall __ret_fast_syscall
/*
* 这是一个快速系统调用返回路径。我们在这里尽可能少做事,
* 比如避免将r0写入堆栈。我们只在禁用了追踪、上下文追踪和RSEQ调试时
* 才使用此路径——因为这些特性的开销会使此路径变得效率低下。
*/
/*
* =============================================================================
* 版本一:当追踪等特性被禁用时,编译此部分,追求极致性能。
* =============================================================================
*/
#if !(IS_ENABLED(CONFIG_TRACE_IRQFLAGS) || IS_ENABLED(CONFIG_CONTEXT_TRACKING_USER) || \
IS_ENABLED(CONFIG_DEBUG_RSEQ))
ret_fast_syscall:
__ret_fast_syscall:ret_slow_syscall
/* UNWIND宏用于提供栈回溯信息给调试器。*/
UNWIND(.fnstart )
UNWIND(.cantunwind )
/* 关闭中断。*/
disable_irq_notrace
/* 重新检查thread_info->flags,看是否有系统调用追踪等标志被设置。*/
ldr r1, [tsk, #TI_FLAGS]
/* 使用 lsl #16 和 's' 后缀,高效地检查所有高16位的慢速工作标志。*/
movs r1, r1, lsl #16
/* bne: 如果有标志位被设置,则跳转到慢速处理路径。*/
bne fast_work_pending
/*
* 快速路径:没有待处理工作。调用寄存器恢复宏,并传入fast=1,
* 这会跳过对r0(系统调用返回值)的恢复,从而提高效率。
* restore_user_regs 宏执行完毕后,CPU的控制权已经通过 BX LR 指令返回到了用户空间
*/
restore_user_regs fast = 1, offset = S_OFF
UNWIND(.fnend )
ENDPROC(ret_fast_syscall)
/* 如果快速路径检查失败,则进入慢速路径。*/
fast_work_pending:
/* 首先,必须将r0中的系统调用返回值保存回内核栈上的pt_regs结构体中。
* '!'表示基址寄存器sp在操作后会被更新。*/
str r0, [sp, #S_R0+S_OFF]!
/* 注释:然后顺着代码执行下去,“掉入”到work_pending处理路径。
* (work_pending路径通常紧跟在这段代码之后,它会包含slow_work_pending的逻辑) */
/* fall through to work_pending */
#else
#endif
/* 关闭中断。*/
disable_irq_notrace
/* 检查是否有慢速工作标志。*/
ldr r1, [tsk, #TI_FLAGS]
movs r1, r1, lsl #16
/* beq: 如果没有慢速工作,则直接跳转到no_work_pending,执行快速返回。
* 注意,此时因为r0已经保存,所以可以直接使用标准的返回路径。*/
beq no_work_pending
UNWIND(.fnend )
ENDPROC(ret_fast_syscall)
ret_from_fork 新创建的任务,在首次被调度器调度后,所执行的第一个函数
- 是所有通过叉()、克隆()或线程创建(kthread_create)系统调用新创建的任务,在首次被调度器调度后,所执行的第一个函数
/*
* 注释:这是从fork返回的方式。
*/
/*
* 定义一个全局可用的汇编函数入口点,名为ret_from_fork。
*/
ENTRY(ret_from_fork)
/*
* 调用C函数schedule_tail。此函数负责执行调度器在上下文切换后
* 必需的收尾操作,例如释放前一个任务的运行队列锁。
*/
bl schedule_tail
/*
* 比较r5寄存器的值与0,并根据结果设置CPSR中的条件码标志位。
* 在copy_thread中,内核线程的r5被设置为其启动函数的地址,
* 而用户进程的r5被设置为0。
*/
cmp r5, #0
/*
* 以下三条指令均为条件执行指令,后缀'ne'(Not Equal)表示
* 仅当上一条cmp指令的结果不为0时才执行。
*/
/* 如果r5不为0(即为内核线程),将r4的值移动到r0。
* r4中保存着创建时传递给内核线程的参数,r0是C调用约定的第一个参数寄存器。*/
movne r0, r4
/* 如果r5不为0,将标签'1'的地址加载到LR寄存器。
* 这是为后续的retne指令设置返回地址,以防内核线程函数返回。*/
badrne lr, 1f
/* 如果r5不为0,执行'bx r5',跳转到r5寄存器中的地址执行。
* 该地址即为内核线程的入口函数。*/
retne r5
/*
* 如果代码执行到标签'1',说明cmp r5, #0的结果为0(即为用户进程)。
*/
/*
* ===================================================================
* 内核线程在这里长时间运行...
* 当它的主函数执行完毕,会执行一条标准的函数返回指令(如 BX LR)。
* 此时,CPU会跳转到LR寄存器中存放的地址。
* ===================================================================
*/
1: /* 6. 内核线程主函数返回后,执行会从这里继续!*/
/*
* get_thread_info tsk: 获取当前任务的thread_info指针。
* b ret_slow_syscall: 跳转到系统调用返回的慢速路径。
* 这条路径看似是为用户进程准备的,但为什么内核线程也要走?
* 因为在这条路径的更深处,会检查到当前任务正在退出,并最终调用do_exit()。
*/
/* 获取当前任务的thread_info结构体指针,并存入tsk寄存器(通常是r9)。*/
get_thread_info tsk
/* 无条件跳转到ret_slow_syscall标签。
* 这是系统调用的通用慢速返回路径,它将处理后续的返回用户空间事宜。*/
b ret_slow_syscall
/* ret_from_fork过程结束。*/
ENDPROC(ret_from_fork)
arch/arm/kernel/entry-v7m.S
Vector table
- ARMv7-M架构的异常向量表定义了处理器在发生各种异常时应执行的处理程序地址。每个异常都有一个对应的入口点,处理器在发生异常时会跳转到这些入口点执行相应的处理逻辑。
- 在Linux内核中,异常向量表通常位于一个特定的汇编文件中,并且需要确保其自然对齐(即每个入口点的地址都应是4字节对齐的)。以下是一个典型的ARMv7-M异常向量表的定义示例:
vector_table
是异常向量表的入口点,包含了各种异常的处理程序地址。每个异常都有一个对应的处理函数,如果没有定义特定的处理函数,则使用__invalid_entry
作为默认处理函数。__invalid_entry
函数用于处理未定义的异常,它会打印异常信息并进入死循环,以防止系统继续运行。__irq_entry
函数是外部硬件中断的统一入口点,它会保存当前任务的寄存器状态,切换到安全的IRQ栈,并调用通用的中断处理逻辑。处理完中断后,如果有需要推迟的工作,会触发PendSV异常来处理这些工作。exc_ret
是一个全局符号,用于存储异常返回的地址。它通常被用作异常返回时的跳转目标。__pendsv_entry
是PendSV异常的入口点,用于处理需要推迟的工作。它会在中断处理完毕后被调用,以确保系统能够正确地处理这些工作。vector_swi
是SVC(Supervisor Call)异常的入口点,用于处理系统调用。它会在用户空间程序请求内核服务时被调用。
/*
* Vector table (Natural alignment need to be ensured)
*/
ENTRY(vector_table)
.long 0 @ 0 - Reset stack pointer
.long __invalid_entry @ 1 - Reset
.long __invalid_entry @ 2 - NMI
.long __invalid_entry @ 3 - HardFault
.long __invalid_entry @ 4 - MemManage
.long __invalid_entry @ 5 - BusFault
.long __invalid_entry @ 6 - UsageFault
.long __invalid_entry @ 7 - Reserved
.long __invalid_entry @ 8 - Reserved
.long __invalid_entry @ 9 - Reserved
.long __invalid_entry @ 10 - Reserved
.long vector_swi @ 11 - SVCall
.long __invalid_entry @ 12 - Debug Monitor
.long __invalid_entry @ 13 - Reserved
.long __pendsv_entry @ 14 - PendSV
.long __invalid_entry @ 15 - SysTick
//循环CONFIG_CPU_V7M_NUM_IRQ次,每个中断都执行__irq_entry
.rept CONFIG_CPU_V7M_NUM_IRQ
.long __irq_entry @ External Interrupts
.endr
.align 2
.globl exc_ret
exc_ret:
.space 4
__invalid_entry 打印异常信息进入死循环
__invalid_entry:
v7m_exception_entry
#ifdef CONFIG_PRINTK //启用printk 支持。删除它从内核映像中消除大部分消息字符串
adr r0, strerr //将异常信息字符串的地址加载到r0寄存器中,作为_printk的第一个参数
mrs r1, ipsr //将当前异常号(从IPSR寄存器中获取)加载到r1,表示触发异常的具体类型
mov r2, lr //将链接寄存器(LR)的值加载到r2,表示异常返回地址
bl _printk //调用_printk函数,打印上述信息。
#endif
mov r0, sp //将当前堆栈指针(SP)的值加载到r0,作为show_regs的参数
bl show_regs //调用show_regs函数,显示当前寄存器的状态
1: b 1b // 无限循环
ENDPROC(__invalid_entry)
__irq_entry
- 标准化现场:当中断发生时,首先调用v7m_exception_entry宏,在当前任务的内核栈上创建一个标准的struct pt_regs寄存器帧。
- 切换到安全栈:为了防止中断服务程序耗尽任务内核栈,它会临时将CPU的栈指针切换到一个独立的、专用的IRQ栈。
- 调用通用处理逻辑:在安全的IRQ栈上,调用高层的、用C语言实现的generic_handle_arch_irq函数。这个C函数会根据中断号,分发并执行由设备驱动程序注册的真实中断服务例程(ISR)。
- 检查并推迟工作:在C函数处理完毕后,它会检查是否有因这次中断而产生的、需要稍后处理的慢速工作(如任务调度)。如果有,它并不立即执行,而是通过触发一个低优先级的PendSV异常来“委托”这项工作。
- 快速返回:最后,执行一个简化的硬件返回序列,尽快退出中断,将CPU控制权交还。
/*
* 外部硬件中断(IRQ)的统一入口点。
*/
__irq_entry:
/*
* 首先,调用通用异常入口宏,在当前任务的内核栈上
* 创建一个标准的pt_regs寄存器保存帧。
*/
v7m_exception_entry
/*
* 注释:调用IRQ处理程序。
*/
/* 将当前SP(指向pt_regs)保存到r0。*/
mov r0, sp
/* 将当前CPU的专用IRQ栈顶指针加载到SP寄存器,实现栈切换。*/
ldr_this_cpu sp, irq_stack_ptr, r1, r2
/*
* 注释:如果我们是在内核运行时发生中断,可能已经在使用IRQ栈了,
* 这种情况下就恢复到原始值。
*/
/* 检查当前是否已在IRQ栈上(处理中断嵌套)。*/
subs r2, sp, r0 /* SP是否高于IRQ栈底?*/
rsbscs r2, r2, #THREAD_SIZE /* ... 并且低于IRQ栈顶?*/
/* 如果是,则取消切换,恢复sp为r0。*/
movcs sp, r0
/* 在IRQ栈上保存LR(EXC_RETURN)和原始SP(r0),因为C函数调用会覆盖它们。*/
push {r0, lr}
/* 注释:调用C函数,r0 = 指向pt_regs的指针。*/
/* 调用通用的C语言中断处理函数。*/
bl generic_handle_arch_irq
/* 从IRQ栈上恢复LR和原始SP。*/
pop {r0, lr}
/* 将SP切回到原始任务的内核栈。*/
mov sp, r0
/*
* 注释:检查返回用户模式时是否有任何挂起的工作。
*/
/* 加载系统控制块(SCB)的基地址。*/
ldr r1, =BASEADDR_V7M_SCB
/* 读取中断控制和状态寄存器(ICSR)。*/
ldr r0, [r1, V7M_SCB_ICSR]
/* 检查RETTOBASE位,判断是否是嵌套中断返回。*/
tst r0, V7M_SCB_ICSR_RETTOBASE
/* 如果是嵌套中断,则直接跳转到末尾,不检查调度。*/
beq 2f
/* 如果不是嵌套中断,则检查当前任务是否有慢速工作标志(如_TIF_NEED_RESCHED)。*/
get_thread_info tsk
ldr r2, [tsk, #TI_FLAGS]
movs r2, r2, lsl #16
/* 如果没有慢速工作,直接跳转到末尾。*/
beq 2f
/* 如果有慢速工作,则向ICSR写入,手动触发一次PendSV异常来处理它。*/
mov r0, #V7M_SCB_ICSR_PENDSVSET
str r0, [r1, V7M_SCB_ICSR] /* 注释:触发PendSV */
2:
/*
* 注释:r0-r3和r12会在异常返回时自动恢复。r4-r7在v7m_exception_entry
* 中也没有被破坏,所以严格来说不需要恢复。所以这里只需要恢复r8-r11。
* 但最简单的方法是把r0-r11一起恢复了。
*/
/* 从任务内核栈上的pt_regs中恢复r0-r11。*/
ldmia sp!, {r0-r11}
/* 调整SP,释放整个pt_regs结构体的空间。*/
add sp, #PT_REGS_SIZE-S_IP
/* 重新开启中断。*/
cpsie i
/* 执行硬件异常返回,硬件会自动利用栈上的硬件帧完成恢复。*/
bx lr
/* __irq_entry过程结束。*/
ENDPROC(__irq_entry)
__pendsv_entry pensv中断进入
/* 这段代码是PendSV异常的入口点。*/
__pendsv_entry:
/*
* 这是一个宏,用于执行标准的异常入口操作。
* 主要工作是:在硬件已经自动压栈(xPSR, PC, LR, r12, r3-r0)的基础上,
* 将剩余的通用寄存器(r4-r11)也压入当前任务的内核栈中。
* 执行完毕后,完整的CPU上下文都保存在了当前任务的栈上。
*/
v7m_exception_entry
/*
* 以下三行代码是关键:清除PendSV的挂起状态。
* 如果不清除,一旦我们从这个异常中返回,它会立刻再次触发,导致系统死锁。
*/
@ 将系统控制块(SCB)的基地址加载到r1。
ldr r1, =BASEADDR_V7M_SCB
@ 将“清除PendSV”的标志位值加载到r0。
mov r0, #V7M_SCB_ICSR_PENDSVCLR
@ 将r0的值写入SCB的ICSR寄存器,硬件上清除PendSV的挂起请求。
str r0, [r1, V7M_SCB_ICSR] @ 清除PendSV
@ 这个宏的作用是获取当前正在运行任务的thread_info结构体指针,
@ 并将其放入一个预定义的寄存器中(通常是r9,别名为tsk)。
get_thread_info tsk
@ 将寄存器why设置为0(这可能用于调试或追踪,表示一个普通的异常退出)。
mov why, #0
@ b: Branch指令,无条件跳转。
@ 跳转到ret_to_user_from_irq,这是所有中断和异常处理完毕后,
@ 返回到被中断代码(无论是内核线程还是用户进程)的通用路径。
b ret_to_user_from_irq
ENDPROC(__pendsv_entry)
__switch_to
// arch/arm/include/asm/thread_info.h
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};
/*
* 适用于ARMv7-M处理器的寄存器切换函数。
* 调用约定:
* r0 = 指向前一个任务的 task_struct 结构体的指针 (previous)
* r1 = 指向前一个任务的 thread_info 结构体的指针 (previous)
* r2 = 指向后一个任务的 thread_info 结构体的指针 (next)
*
* 内核保证 "previous" 和 "next" 不会是同一个任务。
*/
ENTRY(__switch_to)
.fnstart @ 标记函数开始,供调试器使用。
.cantunwind @ 告诉调试工具,此处的栈帧无法安全地“回溯”,因为我们正在手动修改栈。
/*
* ==========================================================
* == Part 1: 保存前一个任务 (previous task) 的CPU上下文 ==
* ==========================================================
*/
@ 计算'previous'任务的CPU寄存器保存区域的地址。
@ ip(r12) = r1 (previous->thread_info) + TI_CPU_SAVE (cpu_save字段的偏移量)。
add ip, r1, #TI_CPU_SAVE
@ stmia: Store Multiple Increment After,多寄存器存数,地址增量在后。
@ ip!: '!'表示ip寄存器的值在操作后会被更新。
@ 将 r4 到 r11 这8个寄存器(被调用者需要保存的寄存器)的值,连续存入ip指向的内存中。
@ 存完后,ip的值会增加 8 * 4 = 32字节。
stmia ip!, {r4 - r11}
@ 将当前栈指针(sp)的值存入ip指向的内存,然后ip地址加4。
str sp, [ip], #4
@ 将链接寄存器(lr)的值(即返回地址)存入ip指向的内存,然后ip地址加4。
str lr, [ip], #4
/*
* ==========================================================
* == Part 2: 准备切换,并恢复下一个任务 (next task) 的上下文 ==
* ==========================================================
*/
@ 保存'previous'任务的task_struct指针(r0)到r5,因为r0-r3在函数调用时可能会被修改。
mov r5, r0
@ 保存'next'任务的thread_info指针(r2)到r6,同样是为了保护它不被接下来的函数调用破坏。
mov r6, r2
@ 计算'next'任务的CPU寄存器保存区域的地址。
@ r4 = r2 (next->thread_info) + TI_CPU_SAVE (cpu_save字段的偏移量)。
add r4, r2, #TI_CPU_SAVE
@ 调用内核的通知链,告知其他子系统(如profiling工具)将要发生任务切换。
@ 这是个可选的扩展点,不影响切换的核心逻辑。
ldr r0, =thread_notify_head @ r0 = &thread_notify_head (第一个参数)
mov r1, #THREAD_NOTIFY_SWITCH @ r1 = THREAD_NOTIFY_SWITCH (第二个参数)
bl atomic_notifier_call_chain @ 调用C函数 atomic_notifier_call_chain
@ 恢复之前保存在r5和r6中的值。
mov r0, r5
mov r1, r6
@ ldmia: Load Multiple Increment After,多寄存器取数。
@ 从r4指向的内存(即'next'任务的保存区域)中,加载所有之前保存的寄存器。
@ 包括 r4-r11, ip(r12) 和 lr(r14)。
@ 此时,CPU的通用寄存器和链接寄存器已经是'next'任务的状态了。
ldmia r4, {r4 - r12, lr}
@ 这是一个宏,用于更新系统中标识当前运行任务的指针。
@ 它会把'next'任务的thread_info指针(r1)设置到专门的寄存器或内存位置,
@ 这样内核中任何地方调用 current 或 smp_processor_id() 都能获取到正确的值。
set_current r1, r2
@ 'ip'寄存器(r12)中存放的是'next'任务的栈顶指针(sp),是上一步ldmia加载进来的。
@ 这条指令将'next'任务的栈顶指针恢复到CPU的sp寄存器中。
@ 从这一刻起,栈已经切换完成。
mov sp, ip
@ bx: Branch and Exchange, 跳转并切换指令集(此处主要是跳转)。
@ 跳转到lr寄存器中的地址。因为lr已经被恢复为'next'任务的返回地址,
@ 所以这条指令执行后,CPU会从'next'任务上次被中断的地方继续执行。
@ 上下文切换的最后一步,也是最神奇的一步,完成!
bx lr
.fnend @ 标记函数结束,供调试器使用。
ENDPROC(__switch_to) @ 声明函数__switch_to结束。
arch/arm/mm/proc-v7m.S
__v7m_cm7_setup V7M处理器的初始化函数
__v7m_cm7_setup
函数的主要作用是配置ARM Cortex-M7处理器的系统控制块(SCB)和异常处理机制。它设置了异常向量表、异常优先级、堆栈指针等,并根据需要配置缓存。
异常向量表的地址存储在SCB的VTOR寄存器中,以便处理器能够正确地跳转到异常处理程序。
启用UsageFault、BusFault和MemManage异常,以便在发生这些异常时能够进行处理。
设置SVC(超级用户调用)和PendSV(挂起的系统服务调用)的优先级,以便在异常发生时能够正确地处理这些异常。
通过SVC指令切换到线程模式,并设置堆栈指针(sp)指向init_thread_union + THREAD_START_SP,以便为线程模式准备好堆栈。
- 分配THREAD_SIZE大小的栈空间
计算异常返回值,设置控制寄存器(CONTROL)为非特权模式,以便在异常返回时能够正确地恢复处理器状态。
配置缓存(如果硬件支持),以提高系统性能。
配置系统控制寄存器以确保8字节堆栈对齐,以满足ARM Cortex-M7的对齐要求。
__v7m_cm7_setup:
mov r8, #(V7M_SCB_CCR_DC | V7M_SCB_CCR_IC| V7M_SCB_CCR_BP)
b __v7m_setup_cont
__v7m_setup_cont:
@ 配置 vector table 基地址
ldr r0, =BASEADDR_V7M_SCB //加载V7M_SCB的基地址
ldr r12, =vector_table //加载中断向量表的地址
str r12, [r0, V7M_SCB_VTOR] //将中断向量表的地址存储到SCB的VTOR寄存器中
@启用了UsageFault、BusFault和MemManage异常
ldr r5, [r0, #V7M_SCB_SHCSR]
orr r5, #(V7M_SCB_SHCSR_USGFAULTENA | V7M_SCB_SHCSR_BUSFAULTENA | V7M_SCB_SHCSR_MEMFAULTENA)
str r5, [r0, #V7M_SCB_SHCSR]
@ 降低了SVC(超级用户调用)和PendSV(挂起的系统服务调用)的优先级
mov r5, #0x80000000
str r5, [r0, V7M_SCB_SHPR2] @ set SVC priority
mov r5, #0x00800000
str r5, [r0, V7M_SCB_SHPR3] @ set PendSV priority
@ 通过SVC指令切换到线程模式,并设置栈指针(sp)指向init_thread_union + THREAD_START_SP
badr r1, 1f
ldr r5, [r12, #11 * 4] @ 读取SVC向量表的原始值。
str r1, [r12, #11 * 4] @ 将1标签地址写入向量表
dsb @ 数据同步屏障,确保所有内存操作完成
mov r6, lr @ 保存链接寄存器
ldr sp, =init_thread_union + THREAD_START_SP @ 设置堆栈指针SP
cpsie i @ 启用中断
svc #0 @ 触发SVC异常,切换到处理模式。
1: cpsid i @ 禁用中断
/* 计算exc_ret*/
orr r10, lr, #EXC_RET_THREADMODE_PROCESSSTACK //计算异常返回值,切换到线程模式
ldmia sp, {r0-r3, r12}
str r5, [r12, #11 * 4] @ 恢复原始SVC向量表值。
mov lr, r6 @ 恢复链接寄存器
@专用控制寄存器
mov r1, #1
msr control, r1 @ 将CONTROL寄存器设置为非特权模式
@ 配置缓存(如果硬件支持)
teq r8, #0 //检查r8是否为零
stmiane sp, {r0-r6, lr} @ r8 不为零时,将寄存器的值保存到栈中,以便调用 v7m_invalidate_l1 时不会破坏这些寄存器的内容。
blne v7m_invalidate_l1 //配置一级缓存(L1 Cache
teq r8, #0 @重新评估条件
ldmiane sp, {r0-r6, lr} //从栈中恢复之前保存的寄存器值。
@ 配置 System Control Register 以确保 8 字节堆栈对齐
@ 请注意,STKALIGN 位是 RW 或 RAO。
ldr r0, [r0, V7M_SCB_CCR] @ system control register
orr r0, #V7M_SCB_CCR_STKALIGN
orr r0, r0, r8
ret lr
ENDPROC(__v7m_setup)
cpu_v7m_do_idle 进入空闲状态
/*
* cpu_v7m_do_idle()
*
* 处理器空闲(例如,等待中断)。
*
* IRQ 已禁用。
*/
SYM_TYPED_FUNC_START(cpu_v7m_do_idle)
wfi
ret lr
SYM_FUNC_END(cpu_v7m_do_idle)
arch/arm/kernel/head-nommu.S
stext 内核启动入口点
/*
* 内核启动入口点。
* ---------------------------
* 这通常是从 decompressor 代码中调用的。 要求是:MMU = 关闭,D-cache = 关闭,I-cache = 不关心,r0 = 0,r1 = 机器 nr。
*
* 参见 linux/arch/arm/tools/mach-types 获取 r1 的完整机器编号列表。
*
*/
//include/linux/init.h
//#define __HEAD .section ".head.text","ax"
__HEAD
#ifdef CONFIG_CPU_THUMBONLY //V7M使用
.thumb
ENTRY(stext)
#else
#endif
#ifdef CONFIG_ARM_VIRT_EXT
bl __hyp_stub_install
#endif
//V7M 这里是一个空函数
@ 确保 SVC 模式和所有中断都被屏蔽
safe_svcmode_maskall r9 @ 并且 IRQ 已禁用
#if defined(CONFIG_CPU_CP15)
#elif defined(CONFIG_CPU_V7M)
ldr r9, =BASEADDR_V7M_SCB
ldr r9, [r9, V7M_SCB_CPUID]
#else
#endif
// 循环查找processor_type中匹配的cpu信息
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
// r5 = 0 进入
beq __error_p @ yes, error 'p'
#ifdef CONFIG_ARM_MPU
bl __setup_mpu
#endif
//将标签1的地址保存到lr寄存器中,以便后续返回
badr lr, 1f @ return (PIC) address
ldr r12, [r10, #PROCINFO_INITFUNC] //加载PROCINFO_INITFUNC的偏移量
add r12, r12, r10 //得到函数的实际地址
ret r12 //跳转执行该函数 V7M执行 __v7m_cm7_setup()
1: ldr lr, =__mmap_switched //将符号 __mmap_switched 的函数地址加载到链接寄存器 lr 中
b __after_proc_init //跳转到 __after_proc_init,继续执行后续的启动流程
ENDPROC(stext)
__after_proc_init 内核启动后处理函数
__after_proc_init
函数的主要作用是设置控制寄存器(Control Register)和读取处理器ID。它在内核启动后执行,确保处理器处于正确的状态,并为后续的操作做好准备。- 该函数首先加载SCB的基地址,然后根据配置禁用数据缓存、分支预测和指令缓存。最后,将控制寄存器的值存储到SCB中,并将异常返回值传递给
__mmap_switched
函数,以继续执行后续的启动流程。 - 该函数的实现依赖于ARM Cortex-M7架构的特性,使用了特定的寄存器和指令来完成这些操作。
/*
* Set the Control Register and Read the process ID.
*/
.text
__after_proc_init:
//加载BASEADDR_V7M_SCB到R12
M_CLASS(movw r12, #:lower16:BASEADDR_V7M_SCB)
M_CLASS(movt r12, #:upper16:BASEADDR_V7M_SCB)
#ifdef CONFIG_ARM_MPU
#endif
#ifdef CONFIG_CPU_CP15
#elif defined (CONFIG_CPU_V7M)
/* 对于 V7M 系统,我们希望像修改 SCTLR 一样修改 CCR*/
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #V7M_SCB_CCR_DC //r0, =V7M_SCB_CCR
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #V7M_SCB_CCR_BP
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #V7M_SCB_CCR_IC
#endif
str r0, [r12, V7M_SCB_CCR] //r0=V7M_SCB_CCR
/* 将 exc_ret 传递给 __mmap_switched*/
mov r0, r10
#endif /* CONFIG_CPU_CP15 elif CONFIG_CPU_V7M */
ret lr //跳转到__mmap_switched函数,继续执行后续的启动流程
ENDPROC(__after_proc_init)
arch/arm/kernel/head-common.S
__error 死循环
- 死循环
__error:
1: mov r0, r0
b 1b
ENDPROC(__error)
__error_p
- 打印R9 CPUID 值,并显示错误信息不支持的CPU架构
- 跳转进入
__error
,死循环
__error_p:
#ifdef CONFIG_DEBUG_LL
adr r0, str_p1
bl printascii
mov r0, r9
bl printhex8
adr r0, str_p2
bl printascii
b __error //死循环
str_p1: .asciz "\nError: unrecognized/unsupported processor variant (0x"
str_p2: .asciz ").\n"
.align
#endif
ENDPROC(__error_p)
__lookup_processor_type 循环查找processor_type中匹配的cpu信息
/*
c=* 该函数读取处理器 ID 寄存器(CP#15, CR0),并在链接器生成的处理器信息列表中查找与当前处理器匹配的条目。
* 由于此时 MMU 尚未启用,无法使用绝对地址,因此需要通过计算偏移量来访问处理器信息列表。
*
* r9 = CPUID
*返回:
* R3、R4、R6 损坏
* r5 = 物理地址空间中的 proc_info 指针
* r9 = cpuid(保留)
*/
__lookup_processor_type:
/*
* Look in <asm/procinfo.h> for information about the __proc_info
* structure.
*/
adr_l r5, __proc_info_begin
adr_l r6, __proc_info_end
1: ldmia r5, {r3, r4} @ r3 = value, r4 = mask
and r4, r4, r9 @ 提取感兴趣的位
teq r3, r4 @ 比较提取的位与处理器标识值。如果匹配,则跳转到标签 2
beq 2f
@ 将 r5 移动到下一个处理器信息条目
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6 @ 检查是否到达列表末尾。
blo 1b @ 如果未到达,则继续循环。
mov r5, #0 @ 如果遍历完整个列表仍未找到匹配项,将 r5 设置为 0,表示未知处理器
2: ret lr @ 返回调用函数
ENDPROC(__lookup_processor_type)
lookup_processor_type 查找处理器类型
/*
* This provides a C-API version of __lookup_processor_type
*/
ENTRY(lookup_processor_type)
stmfd sp!, {r4 - r6, r9, lr}
mov r9, r0
bl __lookup_processor_type
mov r0, r5
ldmfd sp!, {r4 - r6, r9, pc}
ENDPROC(lookup_processor_type)
__mmap_switched_data 内核启动后数据段
- 包含内核启动后需要初始化的数据段和BSS段的起始和结束地址,以及一些其他信息,如处理器ID、机器类型、ATAG指针等。
- 该数据段在内核启动后被
__mmap_switched
函数使用,以便在内核初始化过程中进行必要的设置和清理。
.align 2
.type __mmap_switched_data, %object
__mmap_switched_data:
#ifdef CONFIG_XIP_KERNEL
#ifndef CONFIG_XIP_DEFLATED_DATA
#endif
#endif
.long __bss_start @ r0
.long __bss_stop @ r1
.long init_thread_union + THREAD_START_SP @ sp
.long processor_id @ r0
.long __machine_arch_type @ r1
.long __atags_pointer @ r2
#ifdef CONFIG_CPU_CP15
#else
M_CLASS(.long exc_ret) @ r3
AR_CLASS(.long 0) @ r3
#endif
.size __mmap_switched_data, . - __mmap_switched_data
__FINIT
.text
__mmap_switched 内核启动后处理函数
__mmap_switched
函数的主要作用是完成内核启动后的初始化工作.
- 清除未初始化的BSS段
- 保存处理器ID和机器类型等信息。它在内核启动过程中被调用,以确保内核在正确的状态下运行。
/*
* 以下代码片段在 MMU 模式下开启 MMU 的情况下执行,
* 并使用绝对地址;这与位置无关。
*
* r0 = cp#15 控制寄存器(M 类为 exc_ret)
* r1 = 计算机 ID
* R2 = ATAG/DTB 指针
* r9 = 处理器 ID
*/
__INIT
__mmap_switched:
mov r7, r1
mov r8, r2
mov r10, r0
adr r4, __mmap_switched_data
mov fp, #0
#if defined(CONFIG_XIP_DEFLATED_DATA)
#elif defined(CONFIG_XIP_KERNEL)
#endif
ARM( ldmia r4!, {r0, r1, sp} )
/*
r0 = __bss_start
r1 = __bss_stop
r3 = sp
*/
THUMB( ldmia r4!, {r0, r1, r3} ) //从寄存器 r4 指向的内存地址加载 r0、r1 和r3,并更新 r4 的值(! 表示地址自增)
THUMB( mov sp, r3 ) //将 r3 的值移动到栈指针(sp)
sub r2, r1, r0 //计算 .bss 段的大小,结果存储在 r2 中,r1 是 .bss 段的结束地址,r0 是起始地址。
mov r1, #0
bl __memset @.bss 是未初始化的全局变量和静态变量所在的内存区域,清零是启动流程中的标准操作。
//加载 init_task 的地址到 r0,这是内核的初始任务结构(task_struct
adr_l r0, init_task @ get swapper task_struct
//将__current存储到符号 init_task 所表示的虚拟地址中
//init/init_task.c struct task_struct init_task __aligned(L1_CACHE_BYTES) = {}
//arch/arm/kernel/process.c asmlinkage struct task_struct *__current;
set_current r0, r1 //设置当前任务为 init_task,这是内核调度器的起点。
/*
r0 = processor_id
r1 = __machine_arch_type
r2 = __atags_pointer
r3 = exc_ret
*/
ldmia r4, {r0, r1, r2, r3} //从 r4 指向的内存地址加载 r0、r1、r2 和 r3,这些寄存器将用于保存硬件相关信息。
str r9, [r0] @ Save processor ID
str r7, [r1] @ Save machine type
str r8, [r2] @ Save atags pointer
cmp r3, #0 @ 检查 exc_ret 是否为 0
strne r10, [r3] @ 如果 exc_ret 不为0,将控制寄存器值(r10)存储到 r3 指向的地址。保存控制寄存器值
#ifdef CONFIG_KASAN
bl kasan_early_init
#endif
mov lr, #0 //将链接寄存器(lr)清零,确保返回地址无效。
b start_kernel //跳转到内核的启动函数 start_kernel,继续执行内核初始化过程。
ENDPROC(__mmap_switched)
--------------------------------------------
arch/arm/include/asm/cputype.h
read_cpuid_id 读取处理器ID
#if defined(CONFIG_CPU_V7M)
static inline unsigned int __attribute_const__ read_cpuid_id(void)
{
return readl(BASEADDR_V7M_SCB + V7M_SCB_CPUID);
}
static inline unsigned int __attribute_const__ read_cpuid_cachetype(void)
{
return readl(BASEADDR_V7M_SCB + V7M_SCB_CTR);
}
static inline unsigned int __attribute_const__ read_cpuid_mputype(void)
{
return readl(BASEADDR_V7M_SCB + MPU_TYPE);
}
#endif
cpuid_feature_extract 处理器特征提取
//从SCB读取处理器ID寄存器的值
static inline unsigned int __attribute_const__ read_cpuid_ext(unsigned offset)
{
return readl(BASEADDR_V7M_SCB + offset);
}
static inline int __attribute_const__ cpuid_feature_extract_field(u32 features,
int field)
{
int feature = (features >> field) & 15;
/* 特征寄存器是有符号值 */
if (feature > 7)
feature -= 16;
return feature;
}
#define cpuid_feature_extract(reg, field) \
cpuid_feature_extract_field(read_cpuid_ext(reg), field)
arch/arm/kernel/devtree.c
__arch_info 架构信息
arch/arm/include/asm/mach/arch.h
- 定义了
__arch_info
结构体,表示机器描述符的基本信息,包括机器类型、名称、DTB兼容性等。 - 该结构体用于在内核启动时识别和匹配不同的机器类型,以便进行适当的初始化和配置。
/*
* Machine type table - also only accessible during boot
*/
extern const struct machine_desc __arch_info_begin[], __arch_info_end[];
#define for_each_machine_desc(p) \
for (p = __arch_info_begin; p < __arch_info_end; p++)
/*
* Set of macros to define architecture features. This is built into
* a table by the linker.
*/
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__section(".arch.info.init") = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__section(".arch.info.init") = { \
.nr = ~0, \
.name = _namestr,
- arch/arm/kernel/vmlinux.lds
- 其中定义了相关变量存储机器描述符信息的起始和结束地址。
- 使用
.arch.info.init
节来存储机器描述符信息,并使用KEEP
指令确保这些信息在链接时不会被丢弃。
.init.arch.info : {
__arch_info_begin = .;
KEEP(*(.arch.info.init))
__arch_info_end = .;
}
- 例如
st,stm32h750
的机器描述符信息如下:
//arch/arm/mach-stm32/board-dt.c
static const char *const stm32_compat[] __initconst = {
"st,stm32f429",
"st,stm32f469",
"st,stm32f746",
"st,stm32f769",
"st,stm32h743",
"st,stm32h750",
"st,stm32mp131",
"st,stm32mp133",
"st,stm32mp135",
"st,stm32mp151",
"st,stm32mp157",
NULL
};
DT_MACHINE_START(STM32DT, "STM32 (Device Tree Support)")
.dt_compat = stm32_compat,
#ifdef CONFIG_ARM_SINGLE_ARMV7M
.restart = armv7m_restart,
#endif
MACHINE_END
arch_get_next_mach 获取下一个机器描述符
static const void * __init arch_get_next_mach(const char *const **match)
{
static const struct machine_desc *mdesc = __arch_info_begin;
const struct machine_desc *m = mdesc;
if (m >= __arch_info_end)
return NULL;
mdesc++;
*match = m->dt_compat;
return m;
}
setup_machine_fdt 将 dtb 传递到内核时的计算机设置,返回struct machine_desc信息
/**
* setup_machine_fdt - Machine setup when an dtb was passed to the kernel
* @dt_virt: virtual address of dt blob
*
* If a dtb was passed to the kernel in r2, then use it to choose the
* correct machine_desc and to setup the system.
*/
const struct machine_desc * __init setup_machine_fdt(void *dt_virt)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
DT_MACHINE_START(GENERIC_DT, "Generic DT based system")
.l2c_aux_val = 0x0,
.l2c_aux_mask = ~0x0,
MACHINE_END
mdesc_best = &__mach_desc_GENERIC_DT;
if (!dt_virt || !early_init_dt_verify(dt_virt, __pa(dt_virt)))
return NULL;
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
if (!mdesc) {
const char *prop;
int size;
unsigned long dt_root;
early_print("\nError: unrecognized/unsupported "
"device tree compatible list:\n[ ");
dt_root = of_get_flat_dt_root();
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
while (size > 0) {
early_print("'%s' ", prop);
size -= strlen(prop) + 1;
prop += strlen(prop) + 1;
}
early_print("]\n\n");
dump_machine_table(); /* does not return */
}
/* 我们真的不想这样做,但有时固件会提供有问题的数据 */
if (mdesc->dt_fixup)
mdesc->dt_fixup();
early_init_dt_scan_nodes(); //扫描设备树节点
/* 更改机器编号以匹配我们使用的 mdesc*/
__machine_arch_type = mdesc->nr;
return mdesc;
}
arm_dt_init_cpu_maps 初始化CPU映射
/*
* arm_dt_init_cpu_maps - 函数从设备树中检索 CPU 节点,并构建包含与逻辑 CPU 相关的 MPIDR 值的 cpu 逻辑映射数组
*
* 使用解析的 cpu 节点数更新 cpu 可能的掩码
*/
void __init arm_dt_init_cpu_maps(void)
{
/*
* 临时逻辑映射使用被视为无效逻辑映射条目的 UINT_MAX 值进行初始化,因为逻辑映射必须包含 MPIDR[23:0] 值列表,其中 MPIDR[31:24] 必须读取为 0。
*/
struct device_node *cpu, *cpus;
int found_method = 0;
u32 i, j, cpuidx = 1;
u32 mpidr = is_smp() ? read_cpuid_mpidr() & MPIDR_HWID_BITMASK : 0;
if (!cpus)
return;
}
arch/arm/kernel/early_printk.c 早期printk打印
- 调用.s文件进行printk打印
extern void printascii(const char *);
static void early_write(const char *s, unsigned n)
{
char buf[128];
while (n) {
unsigned l = min(n, sizeof(buf)-1);
memcpy(buf, s, l);
buf[l] = 0;
s += l;
n -= l;
printascii(buf);
}
}
static void early_console_write(struct console *con, const char *s, unsigned n)
{
early_write(s, n);
}
static struct console early_console_dev = {
.name = "earlycon",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
static int __init setup_early_printk(char *buf)
{
early_console = &early_console_dev;
register_console(&early_console_dev);
return 0;
}
early_param("earlyprintk", setup_early_printk);
arch/arm/kernel/setup.c
smp_setup_processor_id 处理器ID设置函数
void __init smp_setup_processor_id(void)
{
int i;
u32 mpidr = is_smp() ? read_cpuid_mpidr() & MPIDR_HWID_BITMASK : 0; //非SMP:0
u32 cpu = MPIDR_AFFINITY_LEVEL(mpidr, 0); //非SMP:0
cpu_logical_map(0) = cpu; //cpu_logical_map[0] = 0
for (i = 1; i < nr_cpu_ids; ++i)
cpu_logical_map(i) = i == cpu ? 0 : i;
/*
* clear __my_cpu_offset on boot CPU to avoid hang caused by
* using percpu variable early, for example, lockdep will
* access percpu variable inside lock_release
*/
set_my_cpu_offset(0); //非SMP:do {} while(0)
pr_info("Booting Linux on physical CPU 0x%x\n", mpidr);
}
lookup_processor 查找处理器类型
struct proc_info_list *lookup_processor(u32 midr)
{
struct proc_info_list *list = lookup_processor_type(midr);
if (!list) {
pr_err("CPU%u: configuration botched (ID %08x), CPU halted\n",
smp_processor_id(), midr);
while (1)
/* can't use cpu_relax() here as it may require MMU setup */;
}
return list;
}
__get_cpu_architecture 获取CPU架构
//arch/arm/include/asm/system_info.h
#define CPU_ARCH_UNKNOWN 0
#define CPU_ARCH_ARMv3 1
#define CPU_ARCH_ARMv4 2
#define CPU_ARCH_ARMv4T 3
#define CPU_ARCH_ARMv5 4
#define CPU_ARCH_ARMv5T 5
#define CPU_ARCH_ARMv5TE 6
#define CPU_ARCH_ARMv5TEJ 7
#define CPU_ARCH_ARMv6 8
#define CPU_ARCH_ARMv7 9
#define CPU_ARCH_ARMv7M 10
#ifdef CONFIG_CPU_V7M
static int __get_cpu_architecture(void)
{
return CPU_ARCH_ARMv7M;
}
#endif
cpuid_init_hwcaps 设置CPU硬件能力
static void __init cpuid_init_hwcaps(void)
{
int block;
u32 isar5;
u32 isar6;
u32 pfr2;
if (cpu_architecture() < CPU_ARCH_ARMv7)
return;
//识别是否支持整数除法指令
block = cpuid_feature_extract(CPUID_EXT_ISAR0, 24);
if (block >= 2)
elf_hwcap |= HWCAP_IDIVA; //支持整数除法指令
if (block >= 1)
elf_hwcap |= HWCAP_IDIVT; //Thumb 模式下的整数除法指令
/* LPAE 表示原子 ldrd/strd 指令 */
block = cpuid_feature_extract(CPUID_EXT_MMFR0, 0);
if (block >= 5)
elf_hwcap |= HWCAP_LPAE;
/*检查支持的v8加密指令 */
isar5 = read_cpuid_ext(CPUID_EXT_ISAR5);
block = cpuid_feature_extract_field(isar5, 4);
if (block >= 2)
elf_hwcap2 |= HWCAP2_PMULL;
if (block >= 1)
elf_hwcap2 |= HWCAP2_AES;
block = cpuid_feature_extract_field(isar5, 8);
if (block >= 1)
elf_hwcap2 |= HWCAP2_SHA1;
block = cpuid_feature_extract_field(isar5, 12);
if (block >= 1)
elf_hwcap2 |= HWCAP2_SHA2;
block = cpuid_feature_extract_field(isar5, 16);
if (block >= 1)
elf_hwcap2 |= HWCAP2_CRC32;
/* 查看 Speculation barrier 指令 */
isar6 = read_cpuid_ext(CPUID_EXT_ISAR6);
block = cpuid_feature_extract_field(isar6, 12);
if (block >= 1)
elf_hwcap2 |= HWCAP2_SB;
/* 检查 Speculative Store Bypassing 控件 */
pfr2 = read_cpuid_ext(CPUID_EXT_PFR2);
block = cpuid_feature_extract_field(pfr2, 4);
if (block >= 1)
elf_hwcap2 |= HWCAP2_SSBS;
}
cacheid_init 缓存ID初始化函数
static void __init cacheid_init(void)
{
unsigned int arch = cpu_architecture();
if (arch >= CPU_ARCH_ARMv6) {
unsigned int cachetype = read_cpuid_cachetype();
if ((arch == CPU_ARCH_ARMv7M) && !(cachetype & 0xf000f)) {
cacheid = 0;
} else if ((cachetype & (7 << 29)) == 4 << 29) {
/* ARMv7 register format */
arch = CPU_ARCH_ARMv7;
cacheid = CACHEID_VIPT_NONALIASING;
switch (cachetype & (3 << 14)) {
case (1 << 14):
cacheid |= CACHEID_ASID_TAGGED;
break;
case (3 << 14):
cacheid |= CACHEID_PIPT;
break;
}
} else {
arch = CPU_ARCH_ARMv6;
if (cachetype & (1 << 23))
cacheid = CACHEID_VIPT_ALIASING;
else
cacheid = CACHEID_VIPT_NONALIASING;
}
if (cpu_has_aliasing_icache(arch))
cacheid |= CACHEID_VIPT_I_ALIASING;
} else {
cacheid = CACHEID_VIVT;
}
pr_info("CPU: %s data cache, %s instruction cache\n",
cache_is_vivt() ? "VIVT" :
cache_is_vipt_aliasing() ? "VIPT aliasing" :
cache_is_vipt_nonaliasing() ? "PIPT / VIPT nonaliasing" : "unknown",
cache_is_vivt() ? "VIVT" :
icache_is_vivt_asid_tagged() ? "VIVT ASID tagged" :
icache_is_vipt_aliasing() ? "VIPT aliasing" :
icache_is_pipt() ? "PIPT" :
cache_is_vipt_nonaliasing() ? "VIPT nonaliasing" : "unknown");
}
setup_processor 处理器设置函数
static void __init setup_processor(void)
{
unsigned int midr = read_cpuid_id(); //读取处理器 ID 寄存器的值
struct proc_info_list *list = lookup_processor(midr); //查找处理器类型
cpu_name = list->cpu_name;
__cpu_architecture = __get_cpu_architecture();
init_proc_vtable(list->proc); //需要 MULTI_CPU
#ifdef MULTI_TLB
cpu_tlb = *list->tlb;
#endif
#ifdef MULTI_USER
cpu_user = *list->user;
#endif
#ifdef MULTI_CACHE
cpu_cache = *list->cache; //v7m_cache_fns
#endif
snprintf(init_utsname()->machine, __NEW_UTS_LEN + 1, "%s%c",
list->arch_name, ENDIANNESS);
snprintf(elf_platform, ELF_PLATFORM_SIZE, "%s%c",
list->elf_name, ENDIANNESS);
elf_hwcap = list->elf_hwcap;
cpuid_init_hwcaps(); //设置CPU硬件能力
//CPU_32v7 使用
patch_aeabi_idiv(); //修补 AEABI IDIV 函数
#ifndef CONFIG_ARM_THUMB
elf_hwcap &= ~(HWCAP_THUMB | HWCAP_IDIVT);
#endif
#ifdef CONFIG_MMU
init_default_cache_policy(list->__cpu_mm_mmu_flags);
#endif
erratum_a15_798181_init();
elf_hwcap_fixup();
//根据寄存器读取的值设置 CPU 的缓存 ID
cacheid_init(); //cacheid 初始化函数
cpu_init(); //非CONFIG_CPU_V7M进行设置
}
request_standard_resources 请求标准资源
/*
* Standard memory resources
*/
static struct resource mem_res[] = {
{
.name = "Video RAM",
.start = 0,
.end = 0,
.flags = IORESOURCE_MEM
},
{
.name = "Kernel code",
.start = 0,
.end = 0,
.flags = IORESOURCE_SYSTEM_RAM
},
{
.name = "Kernel data",
.start = 0,
.end = 0,
.flags = IORESOURCE_SYSTEM_RAM
}
};
#define video_ram mem_res[0]
#define kernel_code mem_res[1]
#define kernel_data mem_res[2]
static void __init request_standard_resources(const struct machine_desc *mdesc)
{
phys_addr_t start, end, res_end;
struct resource *res;
u64 i;
kernel_code.start = virt_to_phys(_text);
kernel_code.end = virt_to_phys(__init_begin - 1);
kernel_data.start = virt_to_phys(_sdata);
kernel_data.end = virt_to_phys(_end - 1);
for_each_mem_range(i, &start, &end) {
unsigned long boot_alias_start;
/*
* 在 memblock 中,end 指向范围后的第一个字节,而在资源中,end 指向范围中的最后一个字节。
*/
res_end = end - 1;
/*
* 某些系统有一个仅用于--*启动的特殊内存别名。 我们需要向 kexec-tools 公布这个区域,以便它们知道可启动 RAM 的位置。
*/
boot_alias_start = phys_to_idmap(start);
//CONFIG_MMU
if (arm_has_idmap_alias() && boot_alias_start != IDMAP_INVALID_ADDR) {
res = memblock_alloc_or_panic(sizeof(*res), SMP_CACHE_BYTES);
res->name = "System RAM (boot alias)";
res->start = boot_alias_start;
res->end = phys_to_idmap(res_end);
res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;
request_resource(&iomem_resource, res);
}
//malloc指针地址
res = memblock_alloc_or_panic(sizeof(*res), SMP_CACHE_BYTES);
res->name = "System RAM";
res->start = start;
res->end = res_end;
res->flags = IORESOURCE_SYSTEM_RAM | IORESOURCE_BUSY;
//插入资源树
request_resource(&iomem_resource, res);
//插入资源树
if (kernel_code.start >= res->start &&
kernel_code.end <= res->end)
request_resource(res, &kernel_code);
if (kernel_data.start >= res->start &&
kernel_data.end <= res->end)
request_resource(res, &kernel_data);
}
if (mdesc->video_start) {
video_ram.start = mdesc->video_start;
video_ram.end = mdesc->video_end;
request_resource(&iomem_resource, &video_ram);
}
/*
* Some machines don't have the possibility of ever
* possessing lp0, lp1 or lp2
*/
if (mdesc->reserve_lp0)
request_resource(&ioport_resource, &lp0);
if (mdesc->reserve_lp1)
request_resource(&ioport_resource, &lp1);
if (mdesc->reserve_lp2)
request_resource(&ioport_resource, &lp2);
}
setup_arch 设置体系结构
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc = NULL;
void *atags_vaddr = NULL;
if (__atags_pointer)
atags_vaddr = FDT_VIRT_BASE(__atags_pointer);
setup_processor(); //处理器设置函数
if (atags_vaddr) {
//返回struct machine_desc信息
//setup_machine_fdt函数会根据传入的dtb地址,返回对应的机器描述符信息
mdesc = setup_machine_fdt(atags_vaddr);
if (mdesc)
//预留fdt大小的空间
memblock_reserve(__atags_pointer,
fdt_totalsize(atags_vaddr));
}
if (!mdesc)
/* 使用atag方式尝试进行设置
CONFIG_ATAGS没有配置直接报错
early_print("no ATAGS support: can't continue\n");
while (true);
unreachable(); */
mdesc = setup_machine_tags(atags_vaddr, __machine_arch_type);
if (!mdesc) {
early_print("\nError: invalid dtb and unrecognized/unsupported machine ID\n");
early_print(" r1=0x%08x, r2=0x%08x\n", __machine_arch_type,
__atags_pointer);
if (__atags_pointer)
early_print(" r2[]=%*ph\n", 16, atags_vaddr);
/* early_print("Available machine support:\n\nID (hex)\tNAME\n");
for_each_machine_desc(p)
early_print("%08x\t%s\n", p->nr, p->name);
early_print("\nPlease check your kernel config and/or bootloader.\n");
while (true) */
dump_machine_table();
}
machine_desc = mdesc;
machine_name = mdesc->name;
dump_stack_set_arch_desc("%s", mdesc->name);
if (mdesc->reboot_mode != REBOOT_HARD)
reboot_mode = mdesc->reboot_mode;
setup_initial_init_mm(_text, _etext, _edata, _end);
/*也填充 cmd_line 供以后使用,保留boot_command_line */
//boot_command_line从FDT中获取的参数
strscpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
//MMU 无使用
early_fixmap_init();
early_ioremap_init();
parse_early_param(); //解析早期参数并设置内核参数
#ifdef CONFIG_MMU
early_mm_init(mdesc);
#endif
setup_dma_zone(mdesc);
xen_early_init();
arm_efi_init();
/*
* 在保留/分配任何内存之前,请确保正确设置 lowmem/highmem 的计算
*/
adjust_lowmem_bounds(); //设置memblock的限制地址
arm_memblock_init(mdesc);
/* 内存可能已被删除,因此请重新计算边界。 */
adjust_lowmem_bounds();
early_ioremap_reset(); //CONFIG_MMU
paging_init(mdesc); //初始化页表
kasan_init();
request_standard_resources(mdesc);
if (mdesc->restart) { //armv7m_restart
__arm_pm_restart = mdesc->restart;
register_restart_handler(&arm_restart_nb);
}
unflatten_device_tree();
//单核不需要执行
arm_dt_init_cpu_maps();
/* PSCI(Power State Coordination Interface,电源状态协调接口)是由 ARM 定义的一种标准化接口,
用于在 ARM 架构的多核处理器中管理电源状态。它为操作系统(OS)和固件之间提供了一种统一的通信机制,
主要用于实现 CPU 的电源管理功能,例如 CPU 的开关、挂起、休眠和系统关机等操作。 */
psci_dt_init();
if (!is_smp())
hyp_mode_check();
reserve_crashkernel();
if (mdesc->init_early)
mdesc->init_early();
}
}
customize_machine: 执行特定于开发板的初始化回调
此函数是Linux内核ARM架构移植层中的一个标准化初始化钩子(hook)。它的核心作用是在内核启动的早期阶段, 调用一个由特定开发板或平台提供的、名为init_machine
的C语言回调函数。这个回调函数用于执行那些无法通过设备树(Device Tree)来描述的、非常特殊的板级硬件初始化操作, 例如以编程方式注册平台设备。
在单核无MMU的STM32H750平台上的原理与作用
在像STM32H750这样的现代嵌入式系统上, 硬件的描述和初始化几乎完全由设备树(Device Tree)来驱动。内核会解析设备树, 并自动创建和注册其中描述的所有设备。因此, init_machine
这种传统的、基于C代码的板级初始化方法基本上已被弃用。
对于一个标准的、基于设备树的STM32H750内核配置, machine_desc->init_machine
这个函数指针几乎总是NULL
。machine_desc
结构体本身是ARM架构的遗留产物。
因此, 在STM32H750的启动过程中, customize_machine
这个函数虽然会因为arch_initcall
的注册而被调用, 但其内部的if
条件判断将为假, 函数会直接返回0, 不执行任何实际操作。真正的平台设备初始化将由内核后续的设备树解析代码来完成。
这段代码的存在主要是为了保持对那些非常古老的、没有使用设备树的ARM开发板的向后兼容性。
/*
* 这是一个静态的初始化函数.
* __init 宏表示此函数仅在内核启动期间执行, 其占用的内存之后可以被回收.
* @return: 总是返回0, 表示成功.
*/
static int __init customize_machine(void)
{
/*
* 原始注释翻译:
* 自定义平台设备, 或者添加新的设备.
* 在基于设备树(DT)的机器上, 如果没有提供回调函数, 我们会退回到从设备树填充机器信息,
* 否则我们将总是需要一个 init_machine 回调.
*/
/*
* machine_desc 是一个指向 struct machine_desc 的全局指针, 这个结构体描述了当前运行的机器(开发板)的特性.
* 它是ARM架构的一个历史悠久的组成部分.
*
* 检查 machine_desc->init_machine 这个函数指针是否不为NULL.
* init_machine 是一个 void (*)(void) 类型的函数指针, 用于指向一个特定于板级的C代码初始化函数.
*
* 在一个完全依赖设备树的现代STM32H750系统上, 不会有代码去设置这个指针, 因此它将保持为NULL.
* 这个 if 条件将不成立.
*/
if (machine_desc->init_machine)
/*
* 如果 init_machine 指针有效(仅在非设备树的旧式开发板上),
* 则调用它所指向的函数, 以执行硬编码的板级初始化.
*/
machine_desc->init_machine();
/*
* 对于 initcall, 返回0表示初始化成功.
*/
return 0;
}
/*
* 使用 arch_initcall() 宏将 customize_machine 函数注册为一个初始化调用.
* arch_initcall 是级别为 "3" 的 initcall.
* 这意味着 customize_machine 函数将在内核启动过程中一个相对较早的、用于体系结构相关设置的阶段被调用.
* 这确保了这个板级定制化的钩子能在任何依赖它的驱动程序被初始化之前执行.
*/
arch_initcall(customize_machine);
arch/arm/include/asm/switch_to.h
switch_to 切换任务
#define __complete_pending_tlbi()
/*
* switch_to(prev, next) 应从任务 'prev' 切换到 'next'
* 'prev' 永远不会与 'next' 相同。 schedule() 本身
* 包含内存屏障,告诉 GCC 不要缓存 'current'。
*/
//根据不同架构执行不同的切换任务函数
//arch/arm/kernel/entry-v7m.S
extern struct task_struct *__switch_to(struct task_struct *, struct thread_info *, struct thread_info *);
#define switch_to(prev,next,last) \
do { \
__complete_pending_tlbi(); \
if (IS_ENABLED(CONFIG_CURRENT_POINTER_IN_TPIDRURO) || is_smp()) \
__this_cpu_write(__entry_task, next); \
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
} while (0)