Interrupt Pipeline系列文章大纲-CSDN博客
2.3.2.1 概要
调用kernel_entry 0将进程运行的用户态现场按照struct pt_regs的格式存入进程内核栈。调用kernel_exit 0将已压入进程内核栈的用户态现场恢复到对应的寄存器。struct pt_regs定义在arch/arm64/include/asm/ptrace.h,其中用于保持用户态现场的结构为:
struct pt_regs { union { struct user_pt_regs user_regs; struct { u64 regs[31]; u64 sp; u64 pc; u64 pstate; }; }; u64 orig_x0; #ifdef __AARCH64EB__ u32 unused2; s32 syscallno; #else s32 syscallno; u32 unused2; #endif u64 orig_addr_limit; u64 unused; // maintain 16 byte alignment u64 stackframe[2]; }; |
在I-pipe中,没有对kernel_entry和kernel_exit做修改。如果已经对二者比较熟悉,可以跳过本节剩余章节。
2.3.2.2 kernel_entry 0
去掉32位的干扰,只保留入参el为0的情况,核心代码如下:
1 | .macro | kernel_entry, el, regsize = 64 |
2 | stp | x0, x1, [sp, #16 * 0] |
3 | stp | x2, x3, [sp, #16 * 1] |
4 | stp | x4, x5, [sp, #16 * 2] |
5 | stp | x6, x7, [sp, #16 * 3] |
6 | stp | x8, x9, [sp, #16 * 4] |
7 | stp | x10, x11, [sp, #16 * 5] |
8 | stp | x12, x13, [sp, #16 * 6] |
9 | stp | x14, x15, [sp, #16 * 7] |
10 | stp | x16, x17, [sp, #16 * 8] |
11 | stp | x18, x19, [sp, #16 * 9] |
12 | stp | x20, x21, [sp, #16 * 10] |
13 | stp | x22, x23, [sp, #16 * 11] |
14 | stp | x24, x25, [sp, #16 * 12] |
15 | stp | x26, x27, [sp, #16 * 13] |
16 | stp | x28, x29, [sp, #16 * 14] |
18 | clear_gp_regs | |
19 | mrs | x21, sp_el0 |
20 | ldr_this_cpu | tsk, __entry_task, x20 |
21 | ldr | x19, [tsk, #TSK_TI_FLAGS] |
disable_step_tsk x19, x20 | ||
23 | apply_ssbd 1, x22, x23 | |
24 | mrs | x22, elr_el1 |
25 | mrs | x23, spsr_el1 |
26 | stp | lr, x21, [sp, #S_LR] |
27 | ||
28 | stp | xzr, xzr, [sp, #S_STACKFRAME] |
29 | add | x29, sp, #S_STACKFRAME |
30 | ||
31 | stp | x22, x23, [sp, #S_PC] |
32 | ||
33 | mov | w21, #NO_SYSCALL |
34 | str | w21, [sp, #S_SYSCALLNO] |
35 | ||
36 | msr | sp_el0, tsk |
37 | ||
38 | .endm |
第1行,宏kernel_entry有两个参数el和regsize,其中regsize默认为64.
第2~16行,将X0~X29共30个通用寄存器压栈。栈空间是在kernel_ventry中按照struct pt_regs的大小预留好的,所以相当于压入struct pt_regs的reg[0]~reg[29]。
第18行,将X0~X29共30个通用寄存器清零。
第19行,将sp_el0存入X21。
第20行,将当前进程的task_struct结构指针存入tsk。其中tsk就是X28,定义在entry.S:
tsk .req x28 // current thread_info
第21~22行,检查TSK_TI_FLAGS中是否设置了TIF_SINGLESTEP标记。如果是,则清零MDSCR.SS。el0_irq在调用kernel_entry之后,会立刻打开debug中断,所以必须先清零MDSCR.SS,避免在同时打开debug中断和置位MDSCR.SS的情况下执行调度。TSK_TI_FLAGS定义在arch/arm64/kernel/asm-offsets.c:
DEFINE(TSK_TI_FLAGS, offsetof(struct task_struct, thread_info.flags));
disable_step_tsk定义在arch/arm64/include/asm/assembler.h:
.macro disable_step_tsk, flgs, tmp
tbz \flgs, #TIF_SINGLESTEP, 9990f
mrs \tmp, mdscr_el1
bic \tmp, \tmp, #DBG_MDSCR_SS
msr mdscr_el1, \tmp
isb // Synchronise with enable_dbg
9990:
.endm
第23行,与Speculative Store Bypass漏洞相关代码,可以忽略。
第24行,将elr_el1存入X22。当异常发生时,ARM64自动把返回地址保存在elr_e1寄存器即异常链接寄存器。当调用eret时,自动将elr放到pc寄存器。
第25行,将spsr_el1存入X23。当异常发生时,ARM64自动把PSTATE寄存器的值保存到SPSR_EL1中。
第26行,其中lr就是X30寄存器,X21代表了SP_EL0的值。S_PC定义在arch/arm64/kernel/asm-offsets.c:
DEFINE(S_LR, offsetof(struct pt_regs, regs[30]));
将X30压入struct pt_regs的regs[30],将SP_EL0压入struct pt_regs的变量sp。
第28行,将struct pt_regs末尾的变量u64 stackframe[2]清零。S_STACKFRAME,定义在arch/arm64/kernel/asm-offsets.c:
DEFINE(S_STACKFRAME, offsetof(struct pt_regs, stackframe));
第29行,x29为Frame Pointer寄存器。让X29指向SP+ S_STACKFRAME的位置,即图中的FP’. 栈帧寄存器,应该指向上一个caller的栈帧寄存器在callee中保持的位置。因为从用户栈切到内核栈,因此对于内核栈来说,不存在上一个caller的栈帧,所以pt_regs中定义的u64 stackframe[2]代表的FP/LR的值都设置为0.
第 31行,将X22和X23压栈,就是将elr_el1和spsr_el1的值压入struct pt_regs的pc变量和pstate变量。
第36行,将第20行得到的tsk存入SP_EL0。这样以后,宏定义current就可以通过SP_EL0直接找到当前的进程的task_struct结构,而不需要访存查找。宏定义current定义在arch/arm64/include/asm/current.h:
小Tip:精简代码时去掉了和Speculative Store Bypass漏洞相关代码.
2.3.2.3 kernel_exit 0
最终返回用户空间时,会调用kernel_exit宏,这里提前分析一下。仍然按照kernel_exit 0来精简代码。
1 | .macro | kernel_exit, el | |
2 | |||
3 | ldp | x21, x22, [sp, #S_PC] | // load ELR, SPSR |
4 | |||
5 | ldr | x23, [sp, #S_SP] | // load return stack pointer |
6 | msr | sp_el0, x23 | |
7 | |||
8 | msr | elr_el1, x21 | // set up the return data |
9 | msr | spsr_el1, x22 | |
10 | ldp | x0, x1, [sp, #16 * 0] | |
11 | ldp | x2, x3, [sp, #16 * 1] | |
12 | ldp | x4, x5, [sp, #16 * 2] | |
13 | ldp | x6, x7, [sp, #16 * 3] | |
14 | ldp | x8, x9, [sp, #16 * 4] | |
15 | ldp | x10, x11, [sp, #16 * 5] | |
16 | ldp | x12, x13, [sp, #16 * 6] | |
17 | ldp | x14, x15, [sp, #16 * 7] | |
18 | ldp | x16, x17, [sp, #16 * 8] | |
19 | ldp | x18, x19, [sp, #16 * 9] | |
20 | ldp | x20, x21, [sp, #16 * 10] | |
21 | ldp | x22, x23, [sp, #16 * 11] | |
22 | ldp | x24, x25, [sp, #16 * 12] | |
23 | ldp | x26, x27, [sp, #16 * 13] | |
24 | ldp | x28, x29, [sp, #16 * 14] | |
25 | ldr | lr, [sp, #S_LR] | |
26 | add | sp, sp, #S_FRAME_SIZE | // restore sp |
27 | |||
28 | eret | // tramp_exit->eret | |
29 | .endm |
第1行,宏定义kernel_exit,有一个参数el。此处根据el传入0来保留核心代码。
第3行,从内核栈中的pt_regs.pc恢复ELR到X21。从 pt_regs.pstate恢复SPSR到X22寄存器。
第5~6行,从内核栈中的pt_regs.sp恢复sp_el0。
第8~9行,结合第3行,恢复了elr_el1和spsr_el1。
第10~24行,从内核栈中的pt_regs.reg[0]~reg[29]恢复到X0~X29通用寄存器。
第25行,从内核栈中的pt_regs.reg[30]恢复到lr即X30通用寄存器。
第26行,内核栈指针sp_el1弹出,向上走#S_FRAME_SIZE,与kernel_entry压栈的大小一致。
第28行,通过eret从EL1异常级别返回到EL0,此时sp会使用sp_el0, ARM64会自动把elr_el1寄存器的值赋值到PC寄存器,并从PC指针继续执行。此处原始代码并非简单的eret,而是通过去掉AARCH64平台修复meltdown漏洞的KPTI补丁简化而来。涉及到的补丁:commit 4bf3286d arm64: entry: Hook up entry trampoline to exception vectors .
点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!