1. 实验要求
- 找一个系统调用,系统调用号为学号最后2位(我是89号)相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
2. 实验流程
- 配置实验环境;
- 编写含有系统调用函数的c语言程序,并编译生成可执行程序;
- 将上述可执行程序和根文件系统一起打包成根文件系统镜像;
- 在纯命令行下,用qemu虚拟机挂载根文件系统镜像,并使用gbd跟踪调试上述可执行程序。
3. 配置实验环境
3.1 安装开发工具
sudo apt install build-essential
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
3.2 下载内核源代码
sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34
3.3 配置内核选项
make defconfig # Default configuration is based on ‘x86_64_defconfig‘
make menuconfig
# 打开debug相关选项
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info # 输入Y即可选中该选项
[*] Provide GDB scripts for kernel debugging
[*] Kernel debugging
# 关闭KASLR,否则会导致打断点失败
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR) #输入N即可取消选中该选项
3.4 编译并运行内核
make -j$(nproc)
#用qemu虚拟机测试运行linux内核,因为没有挂在跟文件系统,qemu窗口最后会提示kenel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
3.5 制作根文件系统
为了简化实验环境,仅仅利用BusyBox制作内存根文件系统,没有去制作磁盘根文件系统。
cd ..
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
tar -jxvf busybox-1.31.1.tar.bz2
cd busybox-1.31.1
make menuconfig
# 在配置界面要改成静态链接
# Settings --->
# [*] Build static binary (no shared libs)
make -j$(nproc) && make install #编译安装,默认会安装到源码目录下的_install??文件中
cd ..
mkdir rootfs
cd rootfs
cp ../busybox-1.31.1/_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
vim init #按i进入编辑模式,输入以下代码,按esc退出编辑模式,输入“:wq”保存并退出init文件。
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome ******OS!"
echo "--------------------"
cd home
/bin/sh
chmod +x init#??给init脚本添加可执行权限
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz #打包成根文件系统镜像
cd ..
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz#测试挂载根文件系统
测试成功如图1所示:
图1. qemu正常挂载根文件系统截图
最后附上目录结构图(目录结构不唯一),如图2所示:
图2. 整个实验的文件目录结构
4. 编写含系统调用的c语言程序
4.1 查看89号系统调用
4.1.1 位置
通过查阅Linux源代码中的arch/x86/ entry/syscalls/syscall_32.tbl可以找到89号系统调用为readdir,在arch/x86/entry/ syscalls /syscall_64.tbl可以找到89号系统调用为readlink,本实验采用的是64位linux内核中89号系统调用readlink,如图3所示:
图3. 64位linux内核系统调用对照表
4.1.2 readlink系统调用
在终端输入:
readlink --help
结果显示:
Usage: readlink [OPTION]... FILE
Print value of a symbolic link or canonical file name //输出符号链接值或者权威文件名
-f, --canonicalize canonicalize by following every symlink in
every component of the given name recursively;
all but the last component must exist
-e, --canonicalize-existing canonicalize by following every symlink in
every component of the given name recursively,
all components must exist
-m, --canonicalize-missing canonicalize by following every symlink in
every component of the given name recursively,
without requirements on components existence
-n, --no-newline do not output the trailing newline
-q, --quiet,
-s, --silent suppress most error messages
-v, --verbose report error messages
--help display this help and exit
大概意思就是,readlink是linux系统中一个常用工具,主要用来找出符号链接所指向的位置。
用法如下:
相关函数: stat, lstat, symlink
表头文件: #include <unistd.h>
定义函数:int readlink(const char *path, char *buf, size_t bufsiz);
函数说明:readlink()
会将参数path的符号链接内容存储到参数buf所指的内存空间,返回的内容不是以\000作字符串结尾,但会将字符串的字符数返回,这使得添加\000变得简单。若参数bufsiz小于符号连接的内容长度,过长的内容会被截断,如果 readlink 第一个参数指向一个文件而不是符号链接时,readlink 设 置errno 为 EINVAL 并返回 -1。 readlink()函数组合了open()、read()和close()的所有操作。
返回值 :执行成功则传符号连接所指的文件路径字符串,失败返回-1, 错误代码存于errno
错误代码:
EACCESS 取文件时被拒绝,权限不够
EINVAL 参数bufsiz为负数
EIO O存取错误
ELOOP 欲打开的文件有过多符号连接问题
ENAMETOOLONG 参数path的路径名称太长
ENOENT 参数path所指定的文件不存在
ENOMEM 核心内存不足
ENOTDIR 参数path路径中的目录存在但却非真正的目录
4.2 编写含有readlink函数的程序
以下是readlinktest.c的程序,主要作用查看/usr/bin/awk
符号链接的位置,并打印输出,如果没有找到,就打印输出null。
#include <stdio.h>
#include <unistd.h>
char * get_exe_path( char * buf, int count)
{
int i;
int rslt = readlink("/usr/bin/awk", buf, count - 1);
if (rslt < 0 || (rslt >= count - 1))
{
return NULL;
}
buf[rslt] = ‘\0‘;
for (i = rslt; i >= 0; i--)
{
if (buf[i] == ‘/‘)
{
buf[i + 1] = ‘\0‘;
break;
}
}
return buf;
}
int main(int argc, char ** argv)
{
char path[1024];
printf("%s\n", get_exe_path(path, 1024));
return 0;
}
4.3 编译并重新制作根文件系统镜像
静态编译c语言程序:
gcc -o readlinktest readlinktest.c -static
将编译好的readlinktest
可执行程序复制到rootfs/home
目录下,并且重新制作根文件系统镜像。
cd rootfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
5. 通过gdb跟踪readlink系统调用的内核处理过程
5.1 纯命令行下启动虚拟机
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
其中参数含义:
-
-s
在TCP1234端口创建一个gdb-server,可以通过此端口连接到虚拟机进行接下来的调试。 -
-S
表示启动时暂停虚拟机,等待gdb执行continue指令。 -
-nographic -append "console=ttyS0"
表示不会弹出qemu虚拟机窗口
然后再打开一个终端窗口,启动gdb,把内核符号表加载进去,建立链接:
cd linux-5.4.34/
gdb vmlinux
(gdb) target remote:1234
(gdb) c
如图4所示:
图4. 纯命令行下启动qemu虚拟机
此时在调试控制台对readline
系统调用打断点:
(gdb) b __x64_sys_readlink
(gdb) c
然后在qemu
终端上运行可执行程序readlinktest
。
5.2 跟踪调试readlinktest程序
在调试控制台上,可以通过以下命令进行调试:
-
n
: 单步跳过 -
list
: 列出当前位置之后的10行代码 -
bt(backtrace)
:列出调用栈 -
br
: 设置断点
调试的过程如图5所示:
图5. 调试readlinktest程序
通过命令bt,查看了系统调用过程中的调用的函数,可以看到:
- 首先调用了位于fs/stat.c程序中的
__x64_sys_readlink()
函数; - 再调用了位于
arch/x86/entry/common.c
程序中的do_syscall_64()
函数; - 最后调用了
??()
函数,这个函数应该是对应的系统调用的中断处理程序,位于内核态。
知道了函数调用关系,接下来就可以通过(gdb) n
的命令顺序查看代码的执行情况,根据终端中提示的代码位置,用vscode
编程工具查看了对应的内核源码,结果如下:
在保护现场时,使用的swapgs
指令用于快速地保护现场,将一些重要寄存器的值保存到特定的寄存器中,然后一系列的pushq
操作,将一些寄存器和内存中的值压入内核堆栈,其中pt_regs
是一个结构体,其中包含的信息有:
恢复现场同样采用的是swapgs
指令,中断返回采用的是sysretq
指令。
最后附上孟老师的Linux syscall过程分析(万字长文),感谢阅读!