1.搭建环境
1.1 安装编译工具链
由于Ubuntu是X86架构,为了编译arm64的文件,需要安装交叉编译工具链
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev build-essential git bison flex libssl-dev
1.2 制作根文件系统
linux的启动需要配合根文件系统,这里我们利用busybox来制作一个简单的根文件系统
编译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
Settings --->
[*] Build static binary (no shared libs)
指定编译工具
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
编译
make
make install
编译完成,在busybox目录下生成_install目录
1.3定制文件系统
为了init进程能正常启动, 需要再额外进行一些配置
根目录添加etc、dev和lib目录
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ mkdir etc dev lib
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ ls
bin dev etc lib linuxrc sbin usr
在etc分别创建文件:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:13]
$ cat profile
#!/bin/sh
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
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:16]
$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:19]
$ cat 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
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:26]
$ ls init.d
rcS
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:30]
$ cat 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
这里对这几个文件做一点说明:
- 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
dev目录:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/dev [1:17:36]
$ sudo mknod console c 5 1
这一步很重要, 没有console这个文件, 用户态的输出没法打印到串口上
lib目录:拷贝lib库,支持动态编译的应用程序运行:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/lib [1:18:43]
$ cp /usr/aarch64-linux-gnu/lib/*.so* -a .
2.触发系统调用
在ARM64系统环境下是通过X8寄存器传递系统调用号,在基于华为鲲鹏处理器的openEuler操作系统云主机环境下分析静态编译反汇编代码可以发现C库函数time内部封装的是gettimeofday系统调用,系统调用号为0xa9(169),通过查阅Linux内核源代码中的include\uapi\asm-generic\unistd.h可以找到169号gettimeofday系统调用对应的内核处理函数为sys_gettimeofday。
2.1写一个用户态触发系统调用的程序。
#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;
}
————————————————
版权声明:本文为优快云博主「yuyanyz」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/double113217/article/details/129621007
2.2重新编译内核使用gdb-multiarch vmlinux命令启动虚拟机
在arm64的time系统调用对应的内核函数__arm64_sys_gettimeofday上打断点:
打断点成功!
2.3下载 4.2.1版本的qemu
wget https://download.qemu.org/qemu-4.2.1.tar.xz
tar xvJf 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:
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
2.4用test.c启动系统调用
3.系统调用的参数传递和系统调用号
用户态程序执行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,这样就从异常向量空间跳转同步异常处理程序的入口。
4.ARM64处理保存现场和恢复现场
参考arm64系统调用分析arm64系统调用分析
el0_sync主要分为两部分:
第一部分实现从用户空间到内核空间的上下文切换: kernel_entry 0;
第二部是根据异常症状寄存器esr_el1判断异常原因,然后再进入具体处理函数。
系统调用是用户态执行SVC指令导致的,因此要进入el0_svc处理函数。
用户态发生的中断处理接口为el0_sync(内核态发生的中断处理接口是el1_sync)
首先将通用寄存器x0~x29保存到当前进程的内核栈,然后是从SP_EL0、SPSR_EL1、ELR_EL1寄存器中读取用户栈栈顶地址、发生异常时的处理器状态和返回地址,将这三个值以及发生异常时的LR寄存器中的值都保存到当前进程的内核栈中以struct pt_regs结构体的格式保存在当前进程内核栈的栈底,这样就完成了硬件上下文的save过程。
4.1内核堆栈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是发生异常时保存的处理器现场,用于异常处理完后来恢复现场)
el0_sync在完成保存现场的工作之后,会根据ESR_EL1寄存器确定同步异常产生的原因,同步异常产生的原因很多,在ARM64 Linux中最常见的原因是svc指令触发了系统调用,所以排在最前面的就是条件判断跳转到el0_svc,el0_svc中主要负责调用C代码的el0_svc_handler处理系统调用和ret_to_user系统调用返回。
4.2 恢复现场
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寄存器。