Lab4 分析 time/gettimeofday 系统调用在 ARM64 Linux 中的执行过程

使用内嵌汇编触发 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寄存器。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值