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

2023-05-16

目录

1.准备工作

2.触发系统调用

2.1依据&分析

2.2构造代码

2.3触发系统调用

3.分析系统调用 

3.1中断处理分析(保存现场)

 3.2内核堆栈pt_regs(保存现场)

3.3中断处理分析(恢复现场 )

 3.4总结


1.准备工作

(1)首先环境配置  安装arm64环境:(配置环境真的很痛苦我找了好几篇配环境教程,光配环境就配了大约一天)

放上我正确配置的一篇教程做个记录吧/(ㄒoㄒ)/~~:参考

        按照这篇知乎上的教程,安装完环境,其中继续使用之前的linux5.4.34内核,其余基本一致。这里记录下安装过程中出现的问题:

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

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

 sudo mknod console c 5 1

测试一下,成功了:

(2)复习ppt:

在ARM64系统环境下是通过X8寄存器传递系统调用号,在基于华为鲲鹏处理器的openEuler操作系统云主机环境下分析静态编译反汇编代码可以发现C库函数time内部封装的是gettimeofday系统调用,系统调用号为0xa9(169),通过查阅Linux内核源代码中的include\uapi\asm-generic\unistd.h可以找到169号gettimeofday系统调用对应的内核处理函数为sys_gettimeofday。

打开/include/uapi/asm-generic/unistd.h,找169号果然找到了gettimeofday系统调用且对应的内核处理函数是sys_gettimeofday:

 接下来开始分析这个系统调用。

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

2.触发系统调用

2.1依据&分析

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

其中,需要注意的是:

(1)(32位和64位x86中分别使用int $0x80和syscall汇编指令触发系统调用)arm64用svc触发系统调用。

(2)(32位和64位X86都是使用EAX寄存器传递系统调用号)ARM64系统调用的参数传递是采用X0-X5这6个寄存器,系统调用号放在X8寄存器里传递。Linux系统调用最多有6个参数,ARM64函数调用参数可以使用X0-X7这8个寄存器。

 这里回忆一下内嵌汇编的格式(可以看第二章的ppt中好几个例子)

在标准库sys/time.h文件中定义了gettimeofday的函数原型如下:

#include<sys/time.h>
int gettimeofday(struct timeval*tv, struct timezone *tz );

 gettimeofday函数会把时间包装为timeval和timezone结构体返回,timeval中包括秒和微妙值,timezone中包括时区等信息。gettimeofday系统调用比time系统调用提供的时间信息更多也更精确,timeval结构体中的tv_sec是与time系统调用的返回值是相同的,我们这里只需要使用tv_sec的值。


2.2构造代码

结合老师的ppt上的代码及解释:

“我们这里分别使用了gettimeofday库函数(对应后面代码#if 0 处)和内联ARM64汇编代码(#if 0 不执行会执行#else )的方式触发系统调用了。” 

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

显然#if #endif是预编译指令,老师给出的代码应该是在#if 0 xxx... #else xxx... 和 #if 1 xxx...  #else xxx...中切换来分别使用内联汇编代码以及库函数方式来触发系统调用的。这个用法下图解释得很清楚:

 而注释得也差不多很清楚了(只是这条"add x0, x29, 16\n\t"传递参数&tv的指令课上就没听懂= = 老师说x29是基址寄存器,基址寄存器加上偏移就是?我之前搜x29理解的是栈底指针,arm64指令字长是4字节,那它+16是往高地址去四个位置啊,咋回事....我之前理解的肯定不对,以后懂了再说吧)

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

2.3触发系统调用

把test.c进行交叉编译:

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

 

然后把test复制到根文件系统中:

重新编译下:(比较麻烦不如上一个实验老师的ppt中的教程直接打包成镜像文件)

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

很好,接下来启动虚拟机,打断点,就可以跟踪系统调用了:

 

???咋回事???没这个函数???(╯▔皿▔)╯

。。。。。试了下start_kernel能正常断点啊。。。。

 (然后我在各种找错,各种怀疑,怀疑是内核编译的问题,怀疑qemu版本问题,甚至我怀疑我的ubuntu版本问题,最后怀疑我没有配置成功arm64那些教程都是错的但我也不知道咋配了。。。。因为直接百度也没有相关教程,用了各种控制变量法找错,改来改去改来改去,这里卡了得有两天。。)

最后我觉得我的起点可能就是错误的了。难道在ubuntu里只能用x86?难道我这个系统还是64位x86?于是我试了下64位x86的内核处理函数:

 还是不行。

直到我重新做了下qemu启动64位x86中试了下给他的time系统调用对应的内核处理函数打断点,居然可以成功?

那说明我的arm64环境大概率没问题啊,那就是系统调用的问题。顺着64位成功断点的位置:

 “Breakpoint 1 at 0xffffffff810d6f50: file kernel/time/time.c, line 62.”

我查看了下这个文件:居然是在这里打的,

所以我们的arm64的gettimeofday大概率是在:

感觉关键是这个SYSCALL_DEFINE2,看到这里我突然想到了ppt上面的一些内容:

 

 确实这里很关键!

百度搜到了一篇帖子:syscall SYSCALL_DEFINE*()实现

结合帖子,在vscode中点开SYSCALL_DEFINE2:(在include/linux/syscalls.h中:)

 这里果然有:

打开asm/syscall_wrapper.h:(arch/arm64/include/asm/syscall_wrapper.h)

 我好像懂了,arm64的time系统调用对应的内核函数其实是__arm64_sys_gettimeofday,而sys_gettimeofday是arm32的,这与前面x86的64位是__x64_sys_time和32位的sys_time好像是差不多的意思。

赶紧试一下:

 成功了!!!o(* ̄▽ ̄*)ブ

可以按计划走了,重新启动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 

 启动成功。

然后再开一个窗口,设置好断点:

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

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

(gdb) c

 

 输入./test来运行之前写好的test触发系统调用:

[root@bryant ]# ./test

3.分析系统调用 

 查看一下此时的堆栈状况:

(gdb)bt

结合ppt回顾下上课讲的内容这里进行分析:

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

3.1中断处理分析(保存现场)

参考arm64系统调用分析arm64系统调用分析

el0_sync主要分为两部分:

第一部分实现从用户空间到内核空间的上下文切换: kernel_entry 0;

第二部是根据异常症状寄存器esr_el1判断异常原因,然后再进入具体处理函数。

系统调用是用户态执行SVC指令导致的,因此要进入el0_svc处理函数。

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

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

 这里el0_sync首先执行kernel_entry 0,kernel_entry对应的代码(代码有点多...这里放上老师ppt里选出的关键代码:

	.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过程。

 3.2内核堆栈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是发生异常时保存的处理器现场,用于异常处理完后来恢复现场)

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

 

 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:

 老师的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寄存器。

3.3中断处理分析(恢复现场 )

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

 3.4总结

 通过此次实验理解了arm64用户态执行系统调用切换到内核态的总体过程,更好地复习了下上课的知识,受益匪浅。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34 的相关文章

随机推荐

  • ipython notebook 如何打开.ipynb文件?

    三种方法查看 ipynb 文件 xff1a 1 xff0c GitHub 中可以直接打开 ipynb 文件 2 xff0c 可以把 ipynb 文件对应的下载链接复制到 https nbviewer jupyter org 中查看 3 xf
  • 解析IOS二进制格式的bplist

    关于二进制格式的plist xff0c 搜到一篇博客 详解Binary Plist格式 xff0c 介绍的很详细 xff0c 但是结合github上关于一份解析bplist的代码通过结果实际来看 xff0c 博客中解析对象表的说明出现了问题
  • 'gbk' codec can't encode character解决方法

    Friom https www cnblogs com themost p 6603409 html 使用Python写文件的时候 xff0c 或者将网络数据流写入到本地文件的时候 xff0c 大部分情况下会遇到 xff1a Unicode
  • Python中str与bytes互相转换

    快速转换方式 str to bytes my str 61 34 hello world 34 my str as bytes 61 str encode my str type my str as bytes ensure it is b
  • Python关于%matplotlib inline

    在github代码中经常会看到这样的代码 xff1a import numpy import matplotlib pyplot as plt from pandas import read csv import math from ker
  • Jupyter Notebook介绍、安装及使用教程

    目录 一 什么是Jupyter Notebook xff1f 1 简介 Jupyter Notebook是基于网页的用于交互计算的应用程序 其可被应用于全过程计算 xff1a 开发 文档编写 运行代码和展示结果 Jupyter Notebo
  • Python读取XML

    From http www cnblogs com fnng p 3581433 html 关于python读取xml文章很多 xff0c 但大多文章都是贴一个xml文件 xff0c 然后再贴个处理文件的代码 这样并不利于初学者的学习 xf
  • matlab解决中文显示乱码

    matlab很多函数在读取中文后显示乱码 xff0c 为了显示中文 xff0c 应改为UTF 8方式或其他支持中文的编码方式 xff0c 这在Matlab中的操作为 xff1a slCharacterEncoding 39 UTF 8 39
  • Matlab写TIFF格式文件(多于3波段)

    1 起因 通常情况下 xff0c 使用MATLAB做图像处理后 xff0c 使用下面的命令就可以保存处理结果为图片 imwrite im 39 im bmp 39 而如果需要保存的图像为single或者double类型 xff0c 或保存的
  • Python包设置清华源(pip, anaconda等)

    pip设置清华源 pypi 镜像每 5 分钟同步一次 临时使用 pip install i https pypi tuna tsinghua edu cn simple some package 注意 xff0c simple 不能少 是
  • shapefile字符集编码设置

    http zhihu esrichina com cn article 3 在 ArcGIS Desktop ArcMap ArcCatalog and ArcToolbox 中 xff0c 有编码页转换功能 xff08 CODE PAGE
  • pyhton 遍历文件夹,筛选文件

    如果我们需要遍历一个文件夹下的所有文件 xff0c 子文件夹里的内容 xff0c 用Python来实现 xff0c 很方便 xff0c 主要使用os walk folder xff0c 其中folder 是文件夹的路径 xff1a 先看代码
  • VINS 详解

    VINS是视觉与IMU融合SLAM的代表 xff0c 其实现了一个较为完整的SLAM工作 xff0c 开源地址为 xff1a GitHub HKUST Aerial Robotics VINS Mono A Robust and Versa
  • Python OS 文件/目录方法

    From http www runoob com python os file methods html os 模块提供了非常丰富的方法用来处理文件和目录 常用的方法如下表所示 xff1a 序号方法及描述1 os access path m
  • Python 异常处理

    From http www runoob com python python exceptions html python提供了两个非常重要的功能来处理python程序在运行中出现的异常和错误 你可以使用该功能来调试python程序 异常处
  • deeplabV3+源码分解学习

    From horsetif https www jianshu com p d0cc35b3f100 github上deeplabV3 43 的源码是基于tensorflow xff08 slim xff09 简化的代码 xff0c 是一款
  • 常用颜色名称与RGB数值对照表

    From http xh 5156edu com page z1015m9220j18754 html 颜色名 中文名称 Hex RGB 十进制 Decimal LightPink 浅粉红 FFB6C1 255 182 193 Pink 粉
  • c#调用C++DLL EntryPointNotFoundException 找不到入口点

    From http www voidcn com article p kqogmify rh html c 程序调用C 43 43 的dll的时候 xff0c 经常出现这样的问题 xff1a System EntryPointNotFoun
  • 混洗numpy.random.shuffle()与numpy.random.permutation()的区别

    参考API xff1a https docs scipy org doc numpy reference routines random html 1 numpy random shuffle API中关于该函数是这样描述的 xff1a M
  • 以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34

    目录 1 准备工作 2 触发系统调用 2 1依据 amp 分析 2 2构造代码 2 3触发系统调用 3 分析系统调用 3 1中断处理分析 xff08 保存现场 xff09 3 2内核堆栈pt regs xff08 保存现场 xff09 3