目录
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
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
下载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-a57
cpu 为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寄存器。