以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34

安装arm环境

cp -r ../busybox-1.33.1/_install root

无法复制console那个文件(其他都复制过去了,可能console用了sudo命令创建的),于是直接在linux-5.4.34/root/dev文件夹下重新:

 sudo mknod console c 5 1

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第1张图片
成功安装

打开/include/uapi/asm-generic/unistd.h,根据老师在课上讲的,gettimeofday系统调用的处理函数为sys_gettimeofday,系统调用号为0xa9(169)以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第2张图片
找169号,gettimeofday系统调用且对应的内核处理函数是sys_gettimeofday:

但系统调用没法压栈,因为他有两个栈:不知道压在用户态还是内核态?所以它是通过寄存器来传参,这样又比压栈传参性能好,因为压栈要访问内存,和寄存器的访问速度相比来说慢了些

触发系统调用

fork的系统调用之前可以直接用start_kernel跟踪,可以写一个fork的程序触发跟踪,而time这里老师上课说过:time系统调用需要写一个用户态触发系统调用的程序去跟踪到内核。

查看标准库sys/time.h文件中定义了gettimeofday的函数原型

#include
int gettimeofday(struct timeval*tv, struct timezone *tz );

构造调用代码

首先需要用到标准库:#include(以及localtime的依赖 ),而动态链接库找不到time的系统调用,只能找到函数;所以需要静态编译(所以后面编译命令要加-static),这样函数标准库里的函数也会放在里面,于是这就需要用到了预编译指令:新建test.c代码:

#include 
#include 
#include 
 
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;
}

关键的嵌入汇编代码如下

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"            //触发系统调用
      );

将代码进行交叉编译

aarch64-linux-gnu-gcc -o test test.c -static

将执行文件赋值到根文件系统中
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第3张图片
重新编译

make ARCH=arm64 Image -j8  CROSS_COMPILE=aarch64-linux-gnu-

接下来启动虚拟机,跟踪系统调用
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第4张图片
可以看到断点出现在这里
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第5张图片
看到下面有
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第6张图片
正好对应老师ppt上的
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第7张图片

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第8张图片
arm64的time系统调用对应的内核函数其实是__arm64_sys_gettimeofday,而之前我们的sys_gettimeofday是arm32的,这与前面x86的64位是__x64_sys_time和32位的sys_time好像是差不多的意思。

重新开始打断点
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第9张图片
成功
重启qemu,由于不需要跟踪启动过程,不需要-S

 qemu-system-aarch64 -m 512M -smp 4 -cpu cortex-a57 -machine virt -kernel arch/arm64/boot/Image -append "rdinit=/linuxrc nokaslr console=ttyAMA0 loglevel=8" -nographic -s 

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第10张图片
启动成功

此时再新开一个窗口,设置新的断点

gdb-multiarch vmlinux
(gdb) target remote:1234
(gdb) b __arm64_sys_gettimeofday

接下来输入continue命令,就可以在启动起来的虚拟机里输入命令了:

(gdb) c

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第11张图片
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第12张图片
输入./test来运行之前写好的test触发系统调用:

[root@lhy ]# ./test

分析系统调用

(gdb)bt

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第13张图片
回顾老师课上内容:
1)用户态程序执行svc指令,CPU会把当前程序指针寄存器PC放入ELR_EL1寄存器里,把PSTATE放入SPSR_EL1寄存器里,把异常产生的原因(这里是调用了svc指令触发系统调用)放在ESR_EL1寄存器里。这时CPU是知道异常类型和异常向量表的起始地址的,所以可以自动把VBAR_EL1寄存器的值(vectors),和第3组Synchronous的偏移量0x400相加,即vectors + 0x400,得出该异常向量空间的入口地址,然后跳转到那里执行异常向量空间里面的指令。

每个异常向量空间仅有128个字节,最多可以存储32条指令(每条指令4字节),而且异常向量空间最后一条指令是b指令,对于系统调用来说会跳转到el0_sync,这样就从异常向量空间跳转同步异常处理程序的入口。

中断处理过程

el0_sync主要分为两部分:
第一部分实现从用户空间到内核空间的上下文切换: kernel_entry 0;
第二部是根据异常症状寄存器esr_el1判断异常原因,然后再进入具体处理函数。
系统调用是用户态执行SVC指令导致的,因此要进入el0_svc处理函数

用户态发生的中断处理接口为el0_sync(内核态发生的中断处理接口是el1_sync)

查看el0_sync的代码:(arch/arm64/kernel/entry.S)

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第14张图片
这里el0_sync首先执行kernel_entry 0,kernel_entry对应的代码

	.macro	kernel_entry, el, regsize = 64
      ...
	stp	x0, x1, [sp, #16 * 0]
	stp	x2, x3, [sp, #16 * 1]
	...
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]
      ...
	mrs	x21, sp_el0
	mrs	x22, elr_el1
	mrs	x23, spsr_el1
      stp	   	lr, x21, [sp, #S_LR]      // lr is x30
      stp		x22, x23, [sp, #S_PC]
      ...
	.endm

首先将通用寄存器x0~x29保存到当前进程的内核栈,然后是从SP_EL0、SPSR_EL1、ELR_EL1寄存器中读取用户栈栈顶地址、发生异常时的处理器状态和返回地址,将这三个值以及发生异常时的LR寄存器中的值都保存到当前进程的内核栈中以struct pt_regs结构体的格式保存在当前进程内核栈的栈底,这样就完成了硬件上下文的save过程。

内核堆栈pt_regs保存现场过程

保存现场的主要工作如上代码所示,是保存x0-x30及sp、pc和pstate,这和struct pt_regs数据结构的起始部分正好一一对应。

pt_regs的结构:

struct pt_regs {
        union {
                struct user_pt_regs user_regs;
                struct {
                        u64 regs[31];
                        u64 sp;
                        u64 pc;
                        u64 pstate;
                };
        };
        ...
};

(pt_regs是发生异常时保存的处理器现场,用于异常处理完后来恢复现场)

注意:el0_sync在完成保存现场的工作之后,会根据ESR_EL1寄存器确定同步异常产生的原因,同步异常产生的原因很多,在ARM64 Linux中最常见的原因是svc指令触发了系统调用,所以排在最前面的就是条件判断跳转到el0_svc,el0_svc中主要负责调用C代码的el0_svc_handler处理系统调用和ret_to_user系统调用返回。
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第15张图片
svc_handler代码

asmlinkage void el0_svc_handler(struct pt_regs *regs)
{
	sve_user_discard();
	el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}

可以看到svc_handler又执行了svc_common,这里正好与前面我们gdb调试查看堆栈对应上了,

svc_common:
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第16张图片

这里找到老师上课的ppt:

从invoke_syscall函数中我们可以看到当系统调用号(scno)小于系统调用总个数(sc_nr)时,会找到系统调用号作为下标的syscall_table数组中的函数指针(syscall_fn)。注意这里syscall_table数组就是sys_call_table数组,只是实参和形参传递过程中改了个名字哦。然后通过__invoke_syscall函数执行该系统调用内核处理函数,即将__invoke_syscall函数的两个参数regs和syscall_fn变为调用syscall_fn(regs),regs中存储着系统调用参数(regs->regs[0-5])和系统调用号(regs->regs[8]),从而执行该系统调用内核处理函数。最后将系统系统调用内核处理函数的返回值保存到内核堆栈里保存x0的位置,以便将返回值在恢复现场系统调用返回时可以传递到用户态x0寄存器。

恢复现场过程分析

ret_to_user:
以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34_第17张图片

从系统调用返回前会处理一些工作(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寄存器。

总结

这次的实验加深了arm64用户态执行系统调用切换到内核态的总体过程的理解

你可能感兴趣的:(linux,运维,服务器)