实验要求:以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34
- 应包含用户态内嵌汇编触发系统调用
- 系统调用的参数传递和系统调用号
- ARM64处理保存现场和恢复现场
- 内核堆栈pt_regs等
实验原理
调用过程
系统调用从用户态陷入内核态时,从用户态堆栈转换到内核态堆栈,把esp、eip、标志寄存器等保存到内核堆栈,保存现场。系统调用入口会通过调用号执行内核处理函数,最后恢复现场和将esp、eip、标志寄存器等从内核堆栈中恢复到对应寄存器中,并回到用户态,继续执行下一条指令。
1.syscall
指令(或int $0x80
)
2.系统调用处理入口entry_SYSCALL_64
(或entry_INT80_32
)
a) 保存现场,保存中断发生时当前程序的esp、状态字、eip
b) do_syscall_64
(或do_int80_syscall_32
),系统调用sys_call_table
数组(内核处理函数组成),syscall_return_slowpath(regs)
一直跟踪到schedule函数
c) 恢复现场
3.系统调用返回iret
(或sysret
)
4.继续执行下一条指令
实验准备
1.安装编译工具链
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev build-essential git bison flex libssl-dev
2.制作根文件系统
2.1编译busybox
wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
tar -xjf busybox-1.33.1.tar.bz2
cd busybox-1.33.1
make menuconfig
2.2编译
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make
make install
2.3定制内存根镜像
cd _install
mkdir etc dev lib
cd etc
touch profile inittab fstab
mkdir init.d
touch init.d/rcS
创建profile
export HOSTNAME=bryant
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
创建inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
创建fstab
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
创建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
3.编译内核
3.1配置内核
make defconfig ARCH=arm64
3.2配置config文件
CONFIG_DEBUG_INFO=y
CONFIG_INITRAMFS_SOURCE="./root"
CONFIG_INITRAMFS_ROOT_UID=0
CONFIG_INITRAMFS_ROOT_GID=0
cp -r ../busybox-1.33.1/_install root
sudo mknod root/dev/console c 5 1
3.3编译
make ARCH=arm64 Image -j8 CROSS_COMPILE=aarch64-linux-gnu-
4.qemu
4.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
tar -xf 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
4.2启动
qemu-system-aarch64 -m 128M -smp 1 -cpu cortex-a57 -machine virt -kernel linux-5.4.34/arch/arm64/boot/Image -initrd rootfs.cpio.gz -append "rdinit=/init console=ttyAMA0 loglevel=8" -nographi
使用内嵌汇编触发 time/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;
}
交叉编译
aarch64-linux-gnu-gcc -o test test.c -static
分析 time/gettimeofday 执行过程
ARM64 架构下 Linux 系统调用由 svc 指令触发,在用户态(El0)调用gettimeofday()
会先把系统调用的参数依次放入 X0-X5,系统调用号放在 X8 ,执行 svc 指令,进入内核态(El1)
-
异常发生时,先把异常的原因放在
ESR_EL1
寄存器里; -
把处理器状态(
PSTATE
)放入SPSR_EL1
寄存器里; -
把 PC 的值存入
ELR_EL1
寄存器里,VBAR_EL1
寄存器加上偏移量取得异常处理的入口地址,执行异常处理入口的代码 -
el0_sync
处的内核代码保存异常发生时程序的执行现场(保存现场,即用户栈、通用寄存器等), -
根据异常发生的原因(
ESR_EL1
中的内容)跳转到el0_svc
,el0_svc
会调用el0_svc_handler()
、el0_svc_common()
,将 X8 寄存器(regs->regs[8])中存放的系统调用号传递给invoke_syscall
函数
-
执行
invoke_syscall()
,将通用寄存器中的内容传入syscall_fn()
,引出系统调用内核处理函数__arm64_sys_gettimeofday
-
系统调用返回前,恢复现场(包含
ELR_EL1
和SPSR_EL1
) -
内核调用返回指令
eret
,ELR_EL1
写回 PC,把SPSR_EL1
写回 PSTATE,继续执行用户态程序。