以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34
用户态内嵌汇编触发系统调用
触发系统调用的方法
以time系统调用为例,分别使用C库函数和int $0x80/syscall和svc指令汇编代码触发系统调用。
C库函数 time_t time(time_t *seconds) 返回自1970-01-01 00:00:00(UTC国际时区)起经过的时间,以秒为单位。如果 seconds 不为空,则返回值也存储在变量 seconds 中。
C库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 stuct tm 结构体。timer 的值被分解到 stuct tm 结构体中,并用本地时区表示。
使用C库函数触发time系统调用
范例代码
gcc -o time time.c -static
objdump -S time > time64.S
64位机器上编出32位代码需加-m32
sudo apt-get install gcc-multilib
gcc -o time time.c -static -m32
objdump -S time > time32.S
调试的时候忘截图了,虚拟机太卡,调试截图略
系统调用的参数传递和系统调用号
系统调用表
Linux源代码中的arch/x86/entry/syscalls/syscall_32.tbl和arch/x86/entry/syscalls/syscall_64.tbl 分别定义了32位x86和x86-64的系统调用内核处理函数,它们最终通过脚本转换按照系统调用号依次存入ia32_sys_call_table和sys_call_table数组中。而系统调用内核处理函数则是由系统调用入口entry_INT80_32和entry_SYSCALL_64分别调用的do_int80_syscall_32和do_syscall_64来调用执行。do_int80_syscall_32和do_syscall_64的代码如arch/x86/entry/common.c。
在start_kernel函数开始执行之前是用汇编代码初始化CPU,其中非常重要的就是将异常向量表的基地址(vectors)配置到VBAR_EL1寄存器中,从arch\arm64\kernel\head.S中可以找到代码,这段代码配置了不仅配置了异常向量表,还配置了0号进程的内核堆栈和进程描述符。
异常向量表分为4组,每组有4个向量入口地址,分别处理4种不同类型的异常。每个向量入口空间128字节,也就是说,在这个异常向量空间里可以放入32条指令(每条指令4字节)。举个例子,如果用户态程序执行了svc指令,这时CPU自动把VBAR_EL1寄存器的值(vectors),和第3组Synchronous的偏移量0x400相加,即vectors + 0x400,得出该异常向量空间的入口地址,然后跳转到那里,执行里面的第一条指令。
ARM64处理保存现场和恢复现场
保存现场
在Linux系统中系统调用发生时,CPU会把当前程序指针寄存器PC放入ELR_EL1寄存器里,把PSTATE放入SPSR_EL1寄存器里,同时Linux系统从用户态切换到内核态(从EL0切换到EL1),这时SP指的是SP_EL1寄存器,用户态堆栈的栈顶地址依然保存在SP_EL0寄存器中。也就是说异常(这里是指系统调用)发生时CPU的关键状态sp、pc和pstate分别保存在SP_EL0、ELR_EL1和SPSR_EL1寄存器中。保存现场的主要工作如上代码所示,是保存x0-x30及sp、pc和pstate,这和struct pt_regs数据结构的起始部分正好一一对应。
恢复现场
kernel_exit 0负责恢复现场的代码和kernel_entry 0负责保存现场的代码相对应
内核堆栈pt_regs等
pt_regs 的数据结构如下: