Linux实验二:深入理解系统调用

1. 实验要求

  • 找一个系统调用,系统调用号为学号最后2位(我是89号)相同的系统调用
  • 通过汇编指令触发该系统调用
  • 通过gdb跟踪该系统调用的内核处理过程
  • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

2. 实验流程

  1. 配置实验环境;
  2. 编写含有系统调用函数的c语言程序,并编译生成可执行程序;
  3. 将上述可执行程序和根文件系统一起打包成根文件系统镜像;
  4. 在纯命令行下,用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所示:
Linux实验二:深入理解系统调用

图1. qemu正常挂载根文件系统截图

最后附上目录结构图(目录结构不唯一),如图2所示:
Linux实验二:深入理解系统调用

图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所示:
Linux实验二:深入理解系统调用

图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所示:
Linux实验二:深入理解系统调用

图4. 纯命令行下启动qemu虚拟机

此时在调试控制台对readline系统调用打断点:

(gdb) b __x64_sys_readlink
(gdb) c

然后在qemu终端上运行可执行程序readlinktest

5.2 跟踪调试readlinktest程序

在调试控制台上,可以通过以下命令进行调试:

  • n: 单步跳过
  • list: 列出当前位置之后的10行代码
  • bt(backtrace):列出调用栈
  • br: 设置断点

调试的过程如图5所示:
Linux实验二:深入理解系统调用

图5. 调试readlinktest程序

通过命令bt,查看了系统调用过程中的调用的函数,可以看到:

  1. 首先调用了位于fs/stat.c程序中的__x64_sys_readlink()函数;
  2. 再调用了位于arch/x86/entry/common.c程序中的do_syscall_64()函数;
  3. 最后调用了??()函数,这个函数应该是对应的系统调用的中断处理程序,位于内核态。

知道了函数调用关系,接下来就可以通过(gdb) n 的命令顺序查看代码的执行情况,根据终端中提示的代码位置,用vscode编程工具查看了对应的内核源码,结果如下:
Linux实验二:深入理解系统调用
在保护现场时,使用的swapgs指令用于快速地保护现场,将一些重要寄存器的值保存到特定的寄存器中,然后一系列的pushq操作,将一些寄存器和内存中的值压入内核堆栈,其中pt_regs是一个结构体,其中包含的信息有:
Linux实验二:深入理解系统调用
恢复现场同样采用的是swapgs指令,中断返回采用的是sysretq指令。

最后附上孟老师的Linux syscall过程分析(万字长文),感谢阅读!

Linux实验二:深入理解系统调用

上一篇:ubuntu 20.04版本更新软件源为国内源(清华、网易、阿里云等等)


下一篇:深入理解Linux系统调用