本文主要基于clone系统调用分析在Arm64中代码流如何从用户态进入内核态,如何从内核态返回用户态,以及如何实现一次调用两次返回。
Arm64总共有4个异常级别,这里主要讨论EL0和EL1这两个异常级别。当程序运行在用户态时是EL0,当程序运行在内核态时一般是EL1. 寄存器有两种,一种是普通寄存器,一种是特殊寄存器。汇编代码种常用的x0、x1等就是普通寄存器。而栈指针寄存器、程序状态寄存器、异常连接寄存器等就是特殊寄存器。在EL0级别下栈指针寄存器是SP_EL0,在EL1级别下就是SP_EL1,当在不同的异常级别下切换时,SP就代表SP_EL0或者SP_EL1. 当然在EL1级别下也能访问到SP_EL0,但在EL0下无法访问SP_EL1。程序状态寄存器SPSR_EL1保存从EL0转到EL1级别时的状态寄存器。ELR_EL1异常连接寄存器保存EL0转到EL1级别时异常代码也就是PC的位置。由于不会有发生异常时将cpu核心的状态转到EL0级别(只会有处理完异常后返回EL0级别),所以没有SPSR_EL0和ELR_EL0。汇编指令svc是用于从EL0转到EL1异常级别。ARM64异常级别的一些介绍
另外,ELR_EL1保存的是哪一个指令的位置呢,是产生异常的指令还是产生异常的下一个指令?当一个异常是由专门异常生成指令产生的时候,比如svc指令,它是专门用来生成一个异常然后从EL0切换到EL1的,ELR_EL1保存的就是svc指令的下一条指令位置。当一个异常是同步异常但不是由专门的生成异常指令触发的时候,ELR_EL1保存的是产生异常的那个指令位置,比如一个指针访问了一个没有映射过的地址,mmu找不到对应的页表而产生了一个异常,这时ELR_EL1保存的是访问这个指针的指令的地址。
下面通过代码一步步分析具体实现。
// pid_t __bionic_clone(int flags, void* child_stack, pid_t* parent_tid, void* tls, pid_t* child_tid, int (*fn)(void*), void* arg);
ENTRY_PRIVATE(__bionic_clone)
# Push 'fn' and 'arg' onto the child stack.
stp x5, x6, [x1, #-16]!
# Make the system call.
mov x8, __NR_clone //将对应的系统调用号保存到x8
svc #0 //转到EL1异常级别,PC保存到ELR_EL1中,程序状态如零标志位,溢出标志位等保存在SPSR_EL1中。
# Are we the child?
cbz x0, .L_bc_child
# Set errno if something went wrong.
cmn x0, #(MAX_ERRNO + 1)
cneg x0, x0, hi
b.hi __set_errno_internal
ret
.L_bc_child:
# We're in the child now. Set the end of the frame record chain.
mov x29, #0
# Setting x30 to 0 will make the unwinder stop at __start_thread.
mov x30, #0
# Call __start_thread with the 'fn' and 'arg' we stored on the child stack.
ldp x0, x1, [sp], #16
b __start_thread
END(__bionic_clone)
clone的调用过程,首先将新线程的执行方法和参数保存到之前创建好的栈中,将系统调用号保存到x8寄存器,然后执行svc进入EL1异常级别。进入EL1之后执行那里的代码呢,