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

搭建Linux ARM64环境

        配置环境的过程主要是参考VSCode+GDB+Qemu调试ARM64 linux内核 - 知乎 (zhihu.com)过程搭建的,其中主要遇到问题的地方是执行这条命令时无法复制console这个字符设备。

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

于是我去linux-5.4.34的root文件夹下重新执行命令

 sudo mknod console c 5 1

经过测试能够成功启动:

编写test函数触发系统调用

        1、本例主要是用gettimeofday触发系统调用,于是参照老师的ppt编写出以下c代码段,169号系统调用可在include\uapi\asm-generic\unistd.h找到

 

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

         这里笔者主要不明白的地方在add x0,x29,16这句汇编语言,经查阅资料得知使用 add 指令将栈指针寄存器 x29 和常量 16 相加,得到的结果就是结构体变量 tv 在栈中的存储地址。因此,这里的参数 16 实际上是用来表示结构体变量在栈中的偏移量的,表示结构体变量 tv 相对于栈指针寄存器 x29 的偏移量。在 ARM64 架构下,栈是向上增长的,栈帧的大小是 16 字节的倍数。这是由于 ARM64 架构采用的是 AArch64 指令集,该指令集中的基本单位是 8 字节。在 ARM64 架构下,每个栈帧都包含了调用函数时需要保存的返回地址和其他寄存器的值,因此栈帧大小是固定的,一般为 16 字节的倍数。使用 16 字节的偏移量是因为结构体变量 "struct timeval" 的大小为 16 个字节,使用 16 字节的偏移量可以确保结构体变量 tv 的起始地址和栈指针寄存器 x29 指向的栈帧的起始地址对齐。

        2、进行交叉编译

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

        3、将test复制到root中以便启动时能够进行加载

        4、重新编译

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

 启动内核分析系统调用

         1、启动内核(在linux-5.4.34文件夹下)

 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 

 可以看到内核已经启动成功。

        2、启用gdb在内核中断点

查找gettimeofday的arm64的内核函数名叫__arm64_sys_gettimeofday,与老师ppt的有出入。

另开终端进入linux-5.4.34文件夹下输入以下命令:

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

可以看到已经成功打上断点。

输入命令

(gdb) c

        3、触发系统调用 

运行test文件,可以看到程序成功暂停,打开gdb看一看。

 看一看堆栈的调用情况。

 用户态程序执行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,这样就从异常向量空间跳转同步异常处理程序的入口。

 (1)用户态发生的中断处理接口为el0_sync

 可以看到该代码段调用的第一个就是kernel_entry 0

	.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寄存器的所有值都全部压栈,在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寄存器中,最后也要将lr寄存器压栈。

(2)el0_svc

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

可以看到通过bl指令跳转到了el0_svc_handler

在el0_svc_handler又跳转到了el0_svc_common函数接着执行 

el0_svc_common函数第1个参数regs是内核堆栈栈底的部分,主要是传递过来了系统调用参数x0-x5;第2个参数regs->regs[8]是指x8寄存器传递过来的系统调用号;第3个参数__NR_syscalls是指当前系统的系统调用总数,我们目前分析的ARM64 Linux 5.4.34内核的系统调用总数为436个;第4个参数sys_call_table则是以系统调用号作为下标的系统调用内核处理函数的数组。

 

 (3)invoke_syscall

 从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寄存器。

(4)ret_to_user

 (5)ret_to_user的最后是kernel_exit 0负责恢复现场,与保存现场kernel_entry 0相对应。

从上述函数可以看出kernel_exit 0负责恢复现场的代码,恢复各寄存器的值,kernel_exit 0的最后会执行eret指令系统调用返回。eret指令所做的工作与svc指令相对应,eret指令会将ELR_EL1寄存器里值恢复到程序指针寄存器PC中,把SPSR_EL1寄存器里的值恢复到PSTATE处理器状态中,同时会从内核态转换到用户态,在用户态堆栈栈顶指针sp代表的是sp_el0寄存器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值