一、ARM64 Linux系统调用过程
(1)svc指令触发系统调用。
(2)保存现场(el0_sync处的内核汇编代码保存异常发生时程序的执行现场),然后根据异常发生的原因(ESR_EL1寄存器)跳转到el0_svc,el0_svc处。
(3)执行系统调用内核处理函数sys_syz()。
(4)系统调用内核处理函数执行完成后,系统调用返回前,需要恢复异常发生时程序的执行现场(恢复现场),包括主动设置ELR_EL1和SPSR_EL1的值:是异常会发生嵌套,一旦发生异常嵌套ELR_EL1和SPSR_EL1的值就会随之发生改变,所以当系统调用返回时,需要恢复之前保存的ELR_EL1和SPSR_EL1的值。
(5)内核调用异常返回指令eret,CPU自动把ELR_EL1写回PC,把SPSR_EL1写回PSTATE,并返回到用户态程序里,继续运行程序。
二、用qemu搭建arm64实验环境
制作根文件系统、并打包–>配置内核,将架构调成arm64,并用arm64的交叉编译工具进行内核的编译–>用qemu启动内核
三、构造代码使用内嵌汇编触发 time/gettimeofday 系统调用
test.c如下
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
int main()
{
time_t tt;
struct timeval tv;
struct tm *t;
#if 0
gettimeofday(&tv,NULL); // 使用库函数的方式触发系统调用
#else
asm volatile( // 使用内嵌汇编的方式触发系统调用
"add x0, x29, 16\n\t" //X0寄存器用于传递参数&tv
"mov x1, #0x0\n\t" //X1寄存器用于传递参数NULL
"mov x8, #0xa9\n\t" //使用X8传递系统调用号169
"svc #0x0\n\t" //触发系统调用
);
#endif
tt = tv.tv_sec; //tv是保存获取时间结果的结构体
t = localtime(&tt); //将世纪秒转换成对应的年月日时分秒
printf("time: %d/%d/%d %d:%d:%d\n",
t->tm_year + 1900,
t->tm_mon,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec);
return 0;
}
静态编译一下:
aarch64-linux-gnu-gcc -o test test.c -static
在test.c中打断点调试:
四、分析系统调用的执行过程
ARM64 架构的 CPU 中,Linux 系统调用(同步异常)和其他异常的处理过程大致相同。异常发生时:
CPU 首先把异常的原因放在 ESR_EL1 寄存器里;
1、把当前的处理器状态(PSTATE)放入 SPSR_EL1 寄存器里;
2、把当前程序指针寄存器 PC 的值存入 ELR_EL1 寄存器里(保存断点),然后 CPU 3、通过异常向量表(vectors)基地址和异常的类型计算出异常处理程序的入口地址,即 4、VBAR_EL1 寄存器加上偏移量取得异常处理的入口地址;
接着开始执行异常处理入口的第一行代码。这一过程是 CPU 硬件自动完成的,不需要程序干预。
5、随后保存异常发生时程序的执行现场(保存现场,即用户栈、通用寄存器等),然后根据异常发生的原因(ESR_EL1 寄存器中的内容)跳转到 el0_svc,el0_svc 调用 el0_svc_handler、el0_svc_common 函数,将 X8 寄存器(regs->regs[8])中存放的系统调用号传递给 invoke_syscall 函数。
6、系统调用内核处理函数执行完成后,会将系统调用的返回值存放在 X0 寄存器中。
7、系统调用返回前,需要恢复异常发生时程序的执行现场(恢复现场),其中就包括恢复 ELR_EL1 和 SPSR_EL1 的值。最后内核调用异常返回指令 eret,CPU 硬件把 ELR_EL1 写回 PC,把 SPSR_EL1 写回 PSTATE,返回用户态继续执行用户态程序。如下图所示,该部分操作由 ret_to_user 函数中的 kernel_exit 0 完成。