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

1. 配置ARM64环境

参考 VSCode+GDB+Qemu调试ARM64 linux内核 进行环境配置

准备工作

安装编译工具链

由于Ubuntu是X86架构,为了编译arm64的文件,需要安装交叉编译工具链

sudo apt install gcc-aarch64-linux-gnu
sudo apt install libncurses5-dev  build-essential git bison flex libssl-dev

制作根文件系统

linux的启动需要配合根文件系统,这里我们利用busybox来制作一个简单的根文件系统

编译busybox
cd ~/linux_lab/lab4

wget  https://busybox.net/downloads/busybox-1.33.1.tar.bz2

extract busybox-1.33.1.tar.bz2

cd busybox-1.33.1

打开静态库编译选项

make menuconfig
Settings --->
 [*] Build static binary (no shared libs) 

指定编译工具

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

编译

make
make install

编译完成,在busybox目录下生成_install目录

定制文件系统

为了init进程能正常启动, 需要再额外进行一些配置

根目录添加etc、dev和lib目录

cd _install
mkdir etc dev lib

结果如下

$ ls
bin  dev  etc  lib  linuxrc  sbin  usr
etc

在etc分别创建文件:profile、inittab、fstab、init.d/rcS

  1. busybox 作为 linuxrc 启动后, 会读取 /etc/profile, 这里面设置了一些环境变量和 shell 的属性
  2. 根据 /etc/fstab 提供的挂载信息, 进行文件系统的挂载
  3. busybox 会从 /etc/inittab 中读取 sysinit 并执行, 这里 sysinit 指向了 /etc/init.d/rcS
  4. /etc/init.d/rcS 中 ,mdev -s 这条命令扫描 /sys 目录,查找字符设备和块设备,并在 /devmknod
cd etc
code profile
#!/bin/sh
export HOSTNAME=imingz
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
code inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
code fstab
#device  mount-point    type     options   dump   fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0
mkdir -p init.d
code init.d/rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
dev
  • console : 将用户态的输出打印到串口上
sudo mknod console c 5 1
lib

拷贝lib库,支持动态编译的应用程序运行:

cp /usr/aarch64-linux-gnu/lib/*.so*  -a .

编译内核

配置内核

linux内核源码可以用之前的。
根据 arch/arm64/configs/defconfig 文件生成 .config

cd ~/linux_lab/lab4/linux-5.4.34
make defconfig ARCH=arm64

将下面的配置加入.config文件下面

code .config
CONFIG_DEBUG_INFO=y 
CONFIG_INITRAMFS_SOURCE="./root"
CONFIG_INITRAMFS_ROOT_UID=0
CONFIG_INITRAMFS_ROOT_GID=0
  • CONFIG_DEBUG_INFO 是为了方便调试
  • CONFIG_INITRAMFS_SOURCE 是指定 kernel ramdisk 的位置,这样指定之后 ramdisk 会直接被编译到 kernel 镜像中。
    将之前制作好的根文件系统cp到root目录下
cp -r ../busybox-1.33.1/_install root
sudo mknod root/dev/console c 5 1
执行编译

指定 targetImage 增加编译速度, 这样会只编译 kernel, 不会编译 modules。

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

启动qemu

下载qemu

需要注意的,qemu 最好源码编译, 用 apt-get 直接安装的 qemu 可能版本过低,导致无法启动 arm64 内核。使用 4.2.1 版本的 qemu

sudo apt install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev python-pip python-capstone virtualenv
wget https://download.qemu.org/qemu-4.2.1.tar.xz
extract qemu-4.2.1.tar.xz
cd qemu-4.2.1
./configure --target-list=x86_64-softmmu,x86_64-linux-user,arm-softmmu,arm-linux-user,aarch64-softmmu,aarch64-linux-user --enable-kvm
make 
sudo make install

编译完成之后,qemu在 /usr/local/bin目录下

$ /usr/local/bin/qemu-system-aarch64 --version
QEMU emulator version 4.2.1
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developer
启动linux内核
/usr/local/bin/qemu-system-aarch64 -m 512M -smp 4 -cpu cortex-a57 -machine virt -kernel ~/linux_lab/lab4/linux-5.4.34/arch/arm64/boot/Image -append "rdinit=/linuxrc nokaslr console=ttyAMA0 loglevel=8" -nographic -s
  • -m 512M 内存为512M
  • -smp 4 4核
  • -cpu cortex-a57cpu 为cortex-a57
  • -kernel kernel镜像文件
  • -append传给kernel 的cmdline参数。其中rdinit指定了init进程;nokaslr 禁止内核起始地址随机化,这个很重要, 否则GDB调试可能有问题;console=ttyAMA0指定了串口,没有这一步就看不到linux的输出;
  • -nographic禁止图形输出
  • -s监听gdb端口, gdb程序可以通过1234这个端口连上来。

成功启动Linux:
在这里插入图片描述

2. 触发系统调用

2.1 相关知识

fork的系统调用需要写一个fork程序触发跟踪,而time系统调用需要写一个用户态触发系统调用程序跟踪到内核。
其中,需要注意的是:
(1)32位和64位X86架构下int $0x80或syscall指令的执行就会触发一个系统调用,ARM64架构svc指令的执行就会触发一个系统调用。系统调用返回指令,X86架构是中断返回指令iret或sysret,ARM64架构是异常返回指令eret
一般系统调用会在堆栈上保存一些寄存器的值,会保存中断发生时当前执行程序的栈顶地址、当时的状态字、当时的指令指针寄存器的值。同时会将当前进程内核态的栈顶地址、内核态的状态字放入 CPU 对应的寄存器,并且指令指针寄存器的值会指向中断处理程序的入口,对于系统调用来讲是指向系统调用处理的入口。
(2)32位和64位X86都是使用EAX寄存器传递系统调用号,而ARM64系统调用的参数传递是采用X0-X5这6个寄存器,系统调用号放在X8寄存器里传递。Linux系统调用最多有6个参数,ARM64函数调用参数可以使用X0-X7这8个寄存器。

2.2 构造代码

  • 使用gettimeofday 库函数(目前)
  • 或者使用内联 ARM64 汇编代码的方式触发系统调用(老师给的)
#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;
}

2.3 触发系统调用

对test.c进行交叉编译

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

在这里插入图片描述
将编译生成的test复制到root中
在这里插入图片描述
在这里插入图片描述
重新编译一遍

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

重新启动虚拟机后启动gdb进行跟踪调用

gdb-multiarch vmlinux

在这里插入图片描述

在gettimeofday函数处打断点
在这里插入图片描述
发现可以成功进行断点。
参考了这位同学的博客 以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34

重新启动qemu(不设置-S停止命令),并在另一个终端中使用gdb打断点。

 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

在gdb中输入c命令后,内核继续运行,可以在其中输入命令。

在启动的内核中输入./test命令运行准备好的test来触发系统调用。

在这里插入图片描述
可以看到程序停在了断点处。

在这里插入图片描述

3. 分析系统调用

ARM64 Linux系统调用的执行过程

在这里插入图片描述

在gdb中使用bt命令可以查看当前堆栈状况

(gdb)bt

在这里插入图片描述
根据堆栈中的函数以及课堂上所学的知识进行系统调用分析。

3.1 保存现场

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

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

el0_sync函数代码如下:

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

3.2 内核堆栈pt_regs

保存现场的主要工作如上节代码所示,保存x0-x30及sp、pc、pstate,与struct user_pt_regs结构体的起始部分一一对应。
user_ pt_regs的结构:(pt_regs是发生异常时保存的处理器现场,用于异常处理完后来恢复现场)

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

在这里插入图片描述
svc_handler:

在这里插入图片描述
svc_common:

在这里插入图片描述
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寄存器。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值