点击上方蓝字 江湖评谈设为关注/星标
前言
大部分人玩的都是X86/x64那一套,移动端玩的倒是Arm,不过偏向于应用层级。本篇深入Arm64内核层级,看下Linuxkernel是如何调用Arm64的用户态。
Arm64
1.前置概念
Arm64要运行用户态的程序,必须要内核调用才行。任何程序都是如此,不过大部分编译器对内核方面进行了深度隐藏,应用层级的程序基本上无感进行了调用。
先看下用户态的入口,注意linux下面用户态的入口不是main函数,而是在elf二进制文件里面的EntryPoint Addres(后面简称:EA)项,可以通过如下命令查看:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x400680
可以看到以上EA是:0x400680,这个数值才是内核态最先调用的用户态地址。
那么内核态如何调用的EA的呢?
Arm64的linux内核有一个调度(__schedule)功能,有一个类似于队列(Queue)的变量。队列用来存储一些需要被内核调度的功能/进程/函数/用户模块/用户入口等等。当我们运行可执行的elf二进制文件的时候,linux用户态会通过一些既定的函数进入内核态,把elf二进制文件的入口EA存储到内核的队列变量里面,当内核进行队列扫描的时候,扫描到了用户态EA,就会进行调度,运行EA。EA进而运行用户态的main函数,从而运行整个应用层级的程序。
以上是大致的概念,但实际上的操作会充满荆棘。
2.异常的引出
上面的调度功能是在vmlinux(完整的linux内核映像),它有一个函数__schedule,这个函数会对内核队列变量存储的内容进行调度,当需要调度用户态的时候,它会通过ret_from_fork函数调用ret_to_user函数,ret_to_user如下:
//inux-6.11.7/arch/arm64/kernel/entry.S:612
SYM_CODE_START_LOCAL(ret_to_user)
ldr x19, [tsk, #TSK_TI_FLAGS] // re-check for single-step
enable_step_tsk x19, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
bl stackleak_erase_on_task_stack
#endif
kernel_exit 0
SYM_CODE_END(ret_to_user)
ret_to_user函数通过指令ERET负责从内核模式(EL1
)返回到用户模式(EL0
)。注意这里的EL,ERET,他们都是特权寄存器/指令。EL在内核态它的模式为ELR_EL1,在低级的用户态它则为ELR_EL0。内核态的ELR_EL1保存了异常处理返回后的地址(也即是被调度填充的EA地址),当ret_to_user通过ERET返回的地址即是ELR_EL1(也即是当一个程序发生异常,系统处理完成这个异常之后,需要返回的地址就保存在了ELR_EL1)。但是这个地方需要注意,ELR_EL1是被调度填充的值EA,而非某个地方异常填充的值。这点可以通过硬软断点检测,除了软件断点硬件读写断点无法断下来。
(lldb) watc s exp -s 4 -w read_write -- 0x400680
(lldb) b 0x400680
通过代码验证下上面的说法。当EA第一次被调用的时候,ELR_EL1存储了EA地址。通过ERET跳转到EA。下面EA第一次被调用的状态:
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x0000000000400680
error: memory read failed for 0x400600
(lldb) re r pc ELR_EL1 VBAR
pc = 0x0000000000400680
ELR_EL1 = 0x0000000000400680
VBAR = 0xffff800080010800 vmlinux`vectors
Qemu-ASM在EA被调用的上一条指令:
----------------IN: 0xffff8000800121d8:
OBJD-T: fe7b40f9ff4305911f2003d51f2003d5e0039fd6
其代码如下:
(lldb) di -s 0xffff8000800121d8 -b
vmlinux`ret_to_user:
0xffff8000800121d8 <+264>: 0xf9407bfe ldr x30, [sp, #0xf0]
0xffff8000800121dc <+268>: 0x910543ff add sp, sp, #0x150
0xffff8000800121e0 <+272>: 0xd503201f nop
0xffff8000800121e4 <+276>: 0xd503201f nop
0xffff8000800121e8 <+280>: 0xd69f03e0 eret
0xffff8000800121ec <+284>: 0xd503379f dsb nsh
0xffff8000800121f0 <+288>: 0xd5033fdf isb
0xffff8000800121f4: 0xd503201f nop
我们看到上一条指令刚好运行到了eret处,而这条指令处在ret_to_user函数里。因EA是用户态的地址,在内核是无法被运行的,所以导致了一个Arm64里面的向量异常。
这里需要注意ERET同时会恢复 SP_EL0(用户模式堆栈指针)、ELR_EL1(用户模式程序计数器)、SPSR_EL1(用户模式程序状态寄存器)等寄存器的值。这些寄存器在发生异常或系统调用时被保存到内核栈中。用以修改内核态模式为用户态模式,为运行EA做准备。上面说了,这个地方的SP_EL0,ELR_EL1,SPSR_EL1寄存器值是被调度时候填充的,而非异常引起的填充。
3.内核态异常类别
linuxkernel的arm64向量异常选项分为如下几种:
//linux-6.11.7/arch/arm64/kernel/entry.S:520
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1t
kernel_ventry 1, t, 64, error // Error EL1t
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)
这每一项的偏移是0x80大小,比如Synchronous EL1t为0偏移,则IRQ EL1t的偏移是0x80。
kernel_ventry标记所有项都是一样的。1表示内核态,0表示用户态。t表示64位地址,h表示32位地址。64表示指令集的宽度。后面是向量异常选项的类别,如下:
Sync:同步异常(如未定义指令、预取中止、数据中止等)。IRQ:中断请求。FIQ:快速中断请求。Error:系统错误(通常是硬件错误或不可屏蔽中断)。
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
entry_handler 1, t, 64, sync
entry_handler 1, t, 64, irq
entry_handler 1, t, 64, fiq
entry_handler 1, t, 64, error
entry_handler 1, h, 64, sync
entry_handler 1, h, 64, irq
entry_handler 1, h, 64, fiq
entry_handler 1, h, 64, error
entry_handler 0, t, 64, sync //这个地方时异常选项的处理程序
entry_handler 0, t, 64, irq
entry_handler 0, t, 64, fiq
entry_handler 0, t, 64, error
entry_handler 0, t, 32, sync
entry_handler 0, t, 32, irq
entry_handler 0, t, 32, fiq
entry_handler 0, t, 32, error
entry_handler 0, t, 64, sync
(lldb) n
Process 1 stopped
* thread #1, stop reason = step over
frame #0: 0xffff80008109e130 vmlinux`el0t_64_sync_handler(regs=0xffff800082b9beb0) at entry-common.c:726:22
723
724 asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
725 {
-> 726 unsigned long esr = read_sysreg(esr_el1);
727
728 switch (ESR_ELx_EC(esr)) {
729 case ESR_ELx_EC_SVC64:
(lldb) p/x regs->pc
(u64) $1 = 0x0000000000400680
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #2, stop reason = breakpoint 3.1
frame #0: 0x0000000000400680
-> 0x400680: nop
0x400684: mov x29, #0x0
0x400688: mov x30, #0x0
0x40068c: mov x5, x0
(lldb) re r pc ELR_EL1 VBAR
pc = 0x0000000000400680
ELR_EL1 = 0x0000000000400680
VBAR = 0xffff800080010800 vmlinux`vectors
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync //这里就是VBAR的基地址
//中间省略便于观看
kernel_ventry 0, t, 64, sync //这里即是内核调用用户态的向量异常选项
//省略便于观看
SYM_CODE_END(vectors)
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x0000000000400680
error: memory read failed for 0x400600
(lldb) re r pc ELR_EL1 VBAR
pc = 0x0000000000400680
ELR_EL1 = 0x0000000000400680
VBAR = 0xffff800080010800 vmlinux`vectors
结尾
这里需要注意的是,其一用户态的调用是被内核态调度填充的特权寄存器导致的,而非某个地方异常填充特权寄存器。其二Qemu-asm可以向上推导,其三以下Register,以及指令ERET也需注意:
(lldb) re r ELR_EL1 ESR_EL1 VBAR PC
ELR_EL1 = 0x0000000000400680
ESR_EL1 = 0x0000000096000044
VBAR = 0xffff800080010800 vmlinux`vectors
pc = 0x0000000000400680
(lldb) di -s 0xffff8000800121d8 -b
vmlinux`ret_to_user:
0xffff8000800121d8 <+264>: 0xf9407bfe ldr x30, [sp, #0xf0]
0xffff8000800121dc <+268>: 0x910543ff add sp, sp, #0x150
0xffff8000800121e0 <+272>: 0xd503201f nop
0xffff8000800121e4 <+276>: 0xd503201f nop
0xffff8000800121e8 <+280>: 0xd69f03e0 eret
那么总体来说,Arm64用户态的调用有以下步骤:1.通过内核态的调度,2.调度填充相关的特权寄存器ELR_EL1,且把EA赋给ELR_EL1 ,3.通过指令ERET返回到ELR_EL1(EA),切换内核态到用户态环境,4.EA不能在内核态运行,调用向量异常选项,5.调用向量异常处理程序。6.调用EA。
可以看到,Arm64跟X64的是有大大的不同的。
以上就是本篇内容了,欢迎关注,点赞。
往期精彩回顾
Arm32-Linux子系统的互相Notify
.Net JIT支持的Risc-V/La/Arm
关注公众号↑↑↑:江湖评谈