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

本次实验:以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34

前期准备工作

安装编译工具链:
由于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,方式和lab3类似

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


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

busybox 作为 linuxrc 启动后, 会读取 /etc/profile, 这里面设置了一些环境变量和 shell 的属性
根据 /etc/fstab 提供的挂载信息, 进行文件系统的挂载
busybox 会从 /etc/inittab 中读取 sysinit 并执行, 这里 sysinit 指向了 /etc/init.d/rcS
/etc/init.d/rcS 中 ,mdev -s 这条命令扫描 /sys 目录,查找字符设备和块设备,并在 /dev下 mknod

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

执行编译
指定 target 为 Image 增加编译速度, 这样会只编译 kernel, 不会编译 modules。

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

启动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后, 串口打印如下:

[    3.154374] registered taskstats version 1
[    3.154752] Loading compiled-in X.509 certificates
[    3.178034] input: gpio-keys as /devices/platform/gpio-keys/input/input0
[    3.185886] rtc-pl031 9010000.pl031: setting system clock to 2023-03-27T12:33:12 UTC (1679920392)
[    3.192638] ALSA device list:
[    3.193262]   No soundcards found.
[    3.200666] uart-pl011 9000000.pl011: no DMA platform data
[    3.605537] Freeing unused kernel memory: 8576K
[    3.608903] Run /linuxrc as init process
can't run '/etc/init.d/rcS': Permission denied

Please press Enter to activate this console. 

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

理论基础
在 ARM64 架构下 Linux 系统调用由同步异常 svc 指令触发。当用户态(EL0级)程序调用库函数 xyz() 从而触发系统调用的时候,先把系统调用的参数依次放入X0-X5 这 6 个寄存器( Linux 系统调用最多有 6 个参数,ARM64 函数调用参数可以使用 X0-X7 这 8 个寄存器),然后把系统调用号放在 X8 寄存器 里,最后执行 svc 指令,CPU 即进入内核态(EL1级)。svc 指令一般会带一个立即数参数,一般是 0x0,但并没有被 Linux 内核使用,而是把系统调用号放到了 X8 寄存器 里。
gettimeofday 是 C 库提供的函数(不是系统调用),它封装了内核里的sys_gettimeofday 系统调用,就是说,归根到底是系统调用。
以 time 系统调用为例,分别使用 C 库函数 和 int $0x80/syscall 和 svc 指令汇编代码触发系统调用。
C 库函数 time_t time(time_t *seconds) 返回自 1970-01-01 00:00:00(UTC国际时区)起经过的时间,以秒为单位。如果 seconds 不为空,则返回值也存储在变量 seconds 中。
C库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 stuct tm 结构体。timer 的值被分解到 stuct tm 结构体中,并用本地时区表示。
arm64 的 time 系统调用对应的内核函数其实是 __arm64_sys_gettimeofday
通过查阅 Linux 内核源代码中的 include\uapi\asm-generic\unistd.h 可以找到 169 号 gettimeofday 系统调用对应的内核处理函数为 sys_gettimeofday。

uapi 文件夹,里面放了很多 Linux Kernel 各个模块的头文件。

在这里插入图片描述
查看 kernel/time/time.c

文件头注释给了 这么一句说明

 *  This file contains the interface functions for the various time related

 *  system calls: time, stime, gettimeofday, settimeofday, adjtime

可以看到使用了系统调用 gettimeofday

在基于华为鲲鹏处理器的 openEuler 操作系统云主机环境下 C 库函数 time 内部使用的是 gettimeofday 系统调用,将分别使用了 gettimeofday 库函数和内联 ARM64 汇编代码的方式触发系统调用了。
转到 gettimeofday 定义,可以看到:
在这里插入图片描述
gettimeofday 函数会把时间包装为 timeval 和 timezone 结构体返回,timeval 中包括秒和微妙值,timezone 中包括时区等信息。gettimeofday 系统调用比 time 系统调用提供的时间信息更多也更精确,timeval 结构体中的 tv_sec 是与 time 系统调用的返回值是相同的,我们这里只需要使用 tv_sec 的值

继续往下找可以找到 gettimeofday 系统调用的内核处理函数,代码如下。
在这里插入图片描述

构造代码

使用 PPT 代码

使用了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;
}

调试准备
然后把test移动到根文件系统中:

mv test root/

把test.c进行交叉编译:

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

需要静态编译,动态链接库找不到 time 的系统调用,只能找到函数:

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

加上断点:
在这里插入图片描述
跟踪分析该系统调用的内核处理函数
点击开始调试

执行编译好的程序:./test

在这里插入图片描述

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

// arch/arm64/kernel/entry.S
/*
 * EL0 mode handlers.
 */
el0_sync:
	kernel_entry 0
	mrs	x25, esr_el1			// read the syndrome register
	lsr	x24, x25, #ESR_ELx_EC_SHIFT	// exception class
	cmp	x24, #ESR_ELx_EC_SVC64		// SVC in 64-bit state
	b.eq	el0_svc
	...
ENDPROC(el0_sync)


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

arch/arm64/kernel/entry.S
在这里插入图片描述
arch/arm64/kernel/syscall.c
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
SYSCALL_DEFINE2 宏定义是将 gettimeofday 系统调用转换为 sys_gettimeofday。这是由一组非常复杂的宏定义实现的。

转到 SYSCALL_DEFINE2 定义
在这里插入图片描述
可以看出关键是 __SYSCALL_DEFINEx,转到 __SYSCALL_DEFINEx 定义

如果有 define CONFIG_ARCH_HAS_SYSCALL_WRAPPER,将会用 arch 那边的实现,比如我这边的是 arch64 的,则是使用如下这个: arch/arm64/include/asm/syscall_wrapper.h

在这里插入图片描述
看以看出 SYSCALL_DEFINEx 后的数字表示这个 syscall API 参数列表里有几个参数,允许的参数数量为1-6,最多为6个。

asmlinkage long __arm64_sys##name(const struct pt_regs *regs);

系统调用入口的保存现场

// arch/arm64/kernel/entry.S
	.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


在 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 寄存器中。保存现场的主要工作如上代码所示,是保存 x0-x30 及 sp 、pc 和 pstate,这和 struct pt_regs 数据结构的起始部分正好一一对应。

系统调用返回
系统调用处理完毕,先来看看 ret_to_user 系统调用返回相关的代码。(由 el0_svc 调用)

// arch/arm64/kernel/entry.S
/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
	mov	x0, sp				// 'regs'
	bl	do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on		// enabled while in userspace
#endif
	ldr	x1, [tsk, #TSK_TI_FLAGS]	// re-check for single-step
	b	finish_ret_to_user
/*
 * "slow" syscall return path.
 */
ret_to_user:
	disable_daif
	gic_prio_kentry_setup tmp=x3
	ldr	x1, [tsk, #TSK_TI_FLAGS]
	and	x2, x1, #_TIF_WORK_MASK
	cbnz	x2, work_pending
finish_ret_to_user:
	enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
	bl	stackleak_erase
#endif
	kernel_exit 0
ENDPROC(ret_to_user)

从如上代码中可以看到从系统调用返回前会处理一些工作(work_pending),比如处理信号、判断是否需要进程调度等,ret_to_user 的最后是 kernel_exit 0 负责恢复现场。

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

.macro	kernel_exit, el
	...
	msr	sp_el0, x23
	msr	elr_el1, x21			// set up the return data
	msr	spsr_el1, x22
	ldp	x0, x1, [sp, #16 * 0]
	ldp	x2, x3, [sp, #16 * 1]
	ldp	x4, x5, [sp, #16 * 2]
	ldp	x6, x7, [sp, #16 * 3]
	...
	ldp	x24, x25, [sp, #16 * 12]
	ldp	x26, x27, [sp, #16 * 13]
	ldp	x28, x29, [sp, #16 * 14]
	ldr	lr, [sp, #S_LR]
	...
	eret


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值