sudo apt install build-essential
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
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
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即可取消选中该选项
make -j$(nproc)
#用qemu虚拟机测试运行linux内核,因为没有挂在跟文件系统,qemu窗口最后会提示kenel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
为了简化实验环境,仅仅利用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. 整个实验的文件目录结构
通过查阅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内核系统调用对照表
在终端输入:
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路径中的目录存在但却非真正的目录
以下是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;
}
静态编译c语言程序:
gcc -o readlinktest readlinktest.c -static
将编译好的readlinktest
可执行程序复制到rootfs/home
目录下,并且重新制作根文件系统镜像。
cd rootfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
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
。
在调试控制台上,可以通过以下命令进行调试:
n
: 单步跳过list
: 列出当前位置之后的10行代码bt(backtrace)
:列出调用栈br
: 设置断点调试的过程如图5所示:
图5. 调试readlinktest程序
通过命令bt,查看了系统调用过程中的调用的函数,可以看到:
__x64_sys_readlink()
函数;arch/x86/entry/common.c
程序中的do_syscall_64()
函数;??()
函数,这个函数应该是对应的系统调用的中断处理程序,位于内核态。知道了函数调用关系,接下来就可以通过(gdb) n
的命令顺序查看代码的执行情况,根据终端中提示的代码位置,用vscode
编程工具查看了对应的内核源码,结果如下:
在保护现场时,使用的swapgs
指令用于快速地保护现场,将一些重要寄存器的值保存到特定的寄存器中,然后一系列的pushq
操作,将一些寄存器和内存中的值压入内核堆栈,其中pt_regs
是一个结构体,其中包含的信息有:
恢复现场同样采用的是swapgs
指令,中断返回采用的是sysretq
指令。
最后附上孟老师的Linux syscall过程分析(万字长文),感谢阅读!
原文:https://www.cnblogs.com/Luck-365/p/12976854.html