使用内嵌汇编触发 time/gettimeofday 系统调用
使用内嵌汇编触发 gettimeofday 的用户态 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;
}
将上述代码保存为 test.c,运行以下命令将其编译为 ARM64 下的可执行文件。注意要使用静态编译,因为默认的动态链接编译产生的二进制文件并不会有 gettimeofday 系统调用的入口,只有相应的库函数。
aarch64-linux-gnu-gcc -o test test.c -static
在 VSCode 中启动调试。首先在窗口左下角的断点设置处新增断点 __arm64_sys_gettimeofday(32 位 ARM 下是 sys_gettimeofday,注意不要搞错了),然后在终端中执行 test 命令,就可以看到调试器成功在对应的内核函数处停下来。
系统调用的参数传递和系统调用号在 ARM64 架构中通常遵循以下规则:
系统调用号(System Call Number):系统调用号是一个标识符,用于指定要调用的特定系统调用功能。不同的系统调用具有不同的系统调用号。例如,在 ARM64 架构中,gettimeofday 的系统调用号为 96。系统调用号通常存储在通用寄存器 x8 中。
参数传递:ARM64 架构使用通用寄存器来传递系统调用的参数。前几个参数通常存储在寄存器 x0、x1、x2 等中。具体参数在不同的系统调用中可能有所不同,需要查看相应的系统调用文档来确定正确的参数传递方式。
在给定的示例代码中,我们使用了 gettimeofday 系统调用。它接受两个参数:一个指向 struct timeval 结构的指针和一个指向 struct timezone 结构的指针。在示例代码中,我们将时间结构体的地址作为第一个参数传递给 x0 寄存器,将第二个参数设置为 0(NULL),并使用 svc 0 指令触发系统调用。
ARM64架构
ARM64架构的CPU中,处理Linux系统调用(同步异常)和其他异常的处理过程大致相同。当异常发生时,CPU会执行以下步骤:
将异常的原因(例如通过执行svc指令触发的系统调用)存储在ESR_EL1寄存器中。
将当前的处理器状态(PSTATE)存储在SPSR_EL1寄存器中。
将当前程序指针寄存器PC的值存储在ELR_EL1寄存器中,即保存断点。
CPU通过异常向量表(vectors)的基地址和异常类型计算出异常处理程序的入口地址。计算方法是将VBAR_EL1寄存器的值与偏移量相加,以获取异常处理的入口地址。
CPU开始执行异常处理入口的第一行代码。
这个过程是由CPU硬件自动完成的,不需要程序干预。
接下来,以svc指令对应的el0_sync为例,el0_sync处的内核汇编代码首先会执行以下操作:
保存异常发生时程序的执行现场,包括用户栈、通用寄存器等。
根据异常发生的原因(存储在ESR_EL1寄存器中)跳转到el0_svc。
el0_svc会调用el0_svc_handler和el0_svc_common函数,将存放在X8寄存器(regs->regs[8])中的系统调用号传递给invoke_syscall函数。
通过这个过程,异常处理程序能够正确保存和恢复现场,并根据异常的类型执行相应的处理代码。整个过程由CPU硬件自动完成,程序员不需要手动干预。
内核堆栈pt_reg
内核堆栈ptreg的代码结构如下。
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
...
};
在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寄存器中。
el0_sync在完成保存现场的工作之后,会根据ESR_EL1寄存器确定同步异常产生的原因(svc指令触发了系统调用),所以排在最前面的就是条件判断跳转到el0_svc,el0_svc中主要负责调用C代码的el0_svc_handler处理系统调用和ret_to_user系统调用返回。
从系统调用返回前会处理一些工作(work_pending),比如处理信号、判断是否需要进程调度等,ret_to_user的最后是kernel_exit 0负责恢复现场,与保存现场kernel_entry 0相对应,kernel_exit 0的最后会执行eret指令系统调用返回。eret指令所做的工作与svc指令相对应,eret指令会将ELR_EL1寄存器里值恢复到程序指针寄存器PC中,把SPSR_EL1寄存器里的值恢复到PSTATE处理器状态中,同时会从内核态转换到用户态,在用户态堆栈栈顶指针sp代表的是sp_el0寄存器。