前言
Linux应用程序中使用的open/close等函数属于glibc库提供的系统函数,调用这些函数最后都需要进入内核。所以完整的open实现可分为两部分,一部分在USER space的glibc中实现;另一部分在内核中实现。这里只分析进入内核时USER栈的保存与恢复。系统调用的基本逻辑可以参考:[https://www.cnblogs.com/pengdonglin137/p/3878316.html]
软硬件基础
ARM由好几种处理器模式。系统调用是通过swi指令来触发arm产生异常进入SVC模式的。借用网上的图,各模式下寄存器access情况如下。
因为用到了USER和SVC模式,所以重点关注这两种模式下的寄存器。
user和svc模式下寄存器的差异
1.USER和SVC共用r0-r12, PC, CPSR寄存器
2.USER和SVC有自己独立的r13(sp) r14(lr)
3.SVC比USER多一个SPSR寄存器。
lr寄存器用法
大多数人都知道lr用来存放返回地址。用mov pc, lr就可以返回到调用点。但是异常发生时,lr也会被设置。当异常中断发生时,该异常模式下的r14被设置成了该异常模式将要返回的的地址!
也就是说使用swi指令触发的异常,lr_swi中存放的是USER模式下的下一条准备执行的指令。理解这点就能理解为什么 ldr pc,lr_svc能返回到USER模式。
spsr寄存器用法
CPSR是程序状态寄存器,SPSR则是备份程序状态寄存器。顾名思义,SPSR是用来备份CPSR的。当SWI指令触发异常时,ARM core会将当前user状态的CPSR的状态备份到SPSR_svc中。当系统调用完毕准备从kernel返回时,只需将SPSR复制到CPSR中即能恢复异常触发时CPSR的状态,也就恢复user 模式现场的CPSR寄存器。
USER栈的保存
因为跳过了USER space的实现,所以这里假设USER space通过swi + nr号触发了异常。
1218 __vectors_start:
1219 W(b) vector_rst
1220 W(b) vector_und
1221 W(ldr) pc, __vectors_start + 0x1000
1222 W(b) vector_pabt
1223 W(b) vector_dabt
1224 W(b) vector_addrexcptn
1225 W(b) vector_irq
1226 W(b) vector_fiq
swi异常会进入1221行,执行__vectors_start + 0x1000处代码。怪异的地址,直接看System.map查看这个地方链接时存放的是啥。
1 00000000 t __vectors_start
2 00000024 A cpu_ca8_suspend_size
3 00000024 A cpu_v7_suspend_size
4 0000002c A cpu_ca9mp_suspend_size
5 00001000 t __stubs_start
6 00001004 t vector_rst
7 00001020 t vector_irq
8 000010a0 t vector_dabt
9 00001120 t vector_pabt
10 000011a0 t vector_und
11 00001220 t vector_addrexcptn
12 00001240 t vector_fiq
13 00001240 T vector_fiq_offset
14 80004000 A swapper_pg_dir
15 80008000 T _text
16 80008000 T stext
可以看到__vectors_start+0x1000处存放的是__stubs_start。省去中间步骤直接到了vector_swi。
120 ENTRY(vector_swi)
121 #ifdef CONFIG_CPU_V7M
122 v7m_exception_entry
123 #else
124 sub sp, sp, #S_FRAME_SIZE
125 stmia sp, {r0 - r12} @ Calling r0 - r12
126 ARM( add r8, sp, #S_PC )
127 ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
128 THUMB( mov r8, sp )
129 THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr
130 mrs r8, spsr @ called from non-FIQ mode, so ok.
131 str lr, [sp, #S_PC] @ Save calling PC
132 str r8, [sp, #S_PSR] @ Save CPSR
133 str r0, [sp, #S_OLD_R0] @ Save OLD_R0
134 #endif
这里可以看到,现开辟了S_FRAME_SIZE(72Byte=18个32bit register)。然后先保存了r0到r12。第127行因为由^,所以保存了USER模式下的sp和lr。接着保存了lr_svc,根据前面的解释,这个lr_svc就是USER模式的下一条pc。然后是保存SPSR_svc,这个SPSR_svc等于USER模式的CPSR。最后保存r0.
内核栈最终的形状就如上图所示。
USER栈的恢复
在USER栈保存之后,内核便可以开始执行系统调用的相关操作了。
当系统调用完成准备返回USER空间时,内核是怎么操作的呢?先看一个正常的还是调用过程。
1 #include <unistd.h>
2 #include <stdio.h>
3
4 void func1() {
5 printf("In func1.\n");
6 }
7
8 int main ()
9 {
10 func1();
11
12 return 0;
13 }
对应的反汇编如下:
598 000103c8 <func1>:
599 103c8: b580 push {r7, lr}
600 103ca: af00 add r7, sp, #0
601 103cc: f240 403c movw r0, #1084 ; 0x43c
602 103d0: f2c0 0001 movt r0, #1
603 103d4: f7ff ef70 blx 102b8 <puts@plt>
604 103d8: bf00 nop
605 103da: bd80 pop {r7, pc}
606
607 000103dc <main>:
608 103dc: b580 push {r7, lr}
609 103de: af00 add r7, sp, #0
610 103e0: f7ff fff2 bl 103c8 <func1>
611 103e4: 2300 movs r3, #0
612 103e6: 4618 mov r0, r3
613 103e8: bd80 pop {r7, pc}
可以看到在进入子函数的时候会将lr push到栈中,在子函数返回的时候通过pop将栈中lr寄存器的内容送到了pc寄存器,完成从子函数返回到main的操作。
同样的,在系统调用完成,返回用户空间的过程也通过类似的操作来实现。在vector_swi中可以看到跳到系统调用对应的内核实现函数之前,内核会把lr寄存器设置为ret_fast_syscall。所以内核完成系统调用后,会跳转到这个函数。
201 adr lr, BSYM(ret_fast_syscall) @ return address
202 ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine
看这个ret_fast_syscall的实现。
32 ret_fast_syscall:
33 UNWIND(.fnstart )
34 UNWIND(.cantunwind )
35 disable_irq @ disable interrupts
36 ldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing
37 tst r1, #_TIF_SYSCALL_WORK
38 bne __sys_trace_return
39 tst r1, #_TIF_WORK_MASK
40 bne fast_work_pending
41 asm_trace_hardirqs_on
42
43 /* perform architecture specific actions before user return */
44 arch_ret_to_user r1, lr
45 ct_user_enter
46
47 restore_user_regs fast = 1, offset = S_OFF
48 UNWIND(.fnend ) ```
这个函数中有和调度相关的代码,也有当前平台压根没实现的函数,比如arch_ret_to_user就是空的。和恢复USER现场相关只有最后一个restore_user_reegs函数。
```C
255 .macro restore_user_regs, fast = 0, offset = 0
256 mov r2, sp
257 ldr r1, [r2, #\offset + S_PSR] @ get calling cpsr
258 ldr lr, [r2, #\offset + S_PC]! @ get pc
259 msr spsr_cxsf, r1 @ save in spsr_svc
260 #if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
261 @ We must avoid clrex due to Cortex-A15 erratum #830321
262 strex r1, r2, [r2] @ clear the exclusive monitor
263 #endif
264 .if \fast
265 ldmdb r2, {r1 - lr}^ @ get calling r1 - lr
266 .else
267 ldmdb r2, {r0 - lr}^ @ get calling r0 - lr
268 .endif
269 mov r0, r0 @ ARMv5T and earlier require a nop
270 @ after ldm {}^
271 add sp, sp, #\offset + S_FRAME_SIZE
272 movs pc, lr @ return & move spsr_svc into cpsr
273 .endm
这个函数很简单不展开分析了,作用就是从内核栈中将之前保存的值恢复到user模式下的寄存器中。
其中S_OFF是因为EABI规范的需要,需要额外8byte。
另外,因为lr_svc就是user模式中下一条被执行指令,所以这里通过movs pc,lr就直接返回用户空间了。