深入理解系统调用
Posted caihn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解系统调用相关的知识,希望对你有一定的参考价值。
深入理解系统调用
1. 实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用,我的学号尾号为97.
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
2. 什么是系统调用
操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,为了达到这个目的,内核提供一系列具备预定功能的多内核函数,通过一组称为系统调用(system call)的接口呈现给用户。系统调用把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。
现代的操作系统通常都具有多任务处理的功能,通常靠进程来实现。由于操作系统快速的在每个进程间切换执行,所以一切看起来就会像是同时的。同时这也带来了很多安全问题,例如,一个进程可以轻易的修改进程的内存空间中的数据来使另一个进程异常或达到一些目的,因此操作系统必须保证每一个进程都能安全的执行。这一问题的解决方法是在处理器中加入基址寄存器和界限寄存器。这两个寄存器中的内容用硬件限制了对储存器的存取指令所访问的储存器的地址。这样就可以在系统切换进程时写入这两个寄存器的内容到该进程被分配的地址范围,从而避免恶意软件。
为了防止用户程序修改基址寄存器和界限寄存器中的内容来达到访问其他内存空间的目的,这两个寄存器必须通过一些特殊的指令来访问。通常,处理器设有两种模式:“用户模式”与“内核模式”,通过一个标签位来鉴别当前正处于什么模式。一些诸如修改基址寄存器内容的指令只有在内核模式中可以执行,而处于用户模式的时候硬件会直接跳过这个指令并继续执行下一个。
同样,为了安全问题,一些I/O操作的指令都被限制在只有内核模式可以执行,因此操作系统有必要提供接口来为应用程序提供诸如读取磁盘某位置的数据的接口,这些接口就被称为系统调用。
当操作系统接收到系统调用请求后,会让处理器进入内核模式,从而执行诸如I/O操作,修改基址寄存器内容等指令,而当处理完系统调用内容后,操作系统会让处理器返回用户模式,来执行用户代码。
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
? [*] Provide GDB scripts for kernel debugging
? [*] Kernel debugging
? # 关闭KASLR,否则会导致打断点失败
? Processor type and features ---->
? [] Randomize the address of the kernel image (KASLR)
在make menuconfig后会出现下面的界面,参考上面的选项完成即可。
3.4 编译和运行内核
? make -j$(nproc) # nproc gives the number of CPU cores/threads
available
? # 测试?下内核能不能正常加载运?,因为没有?件系统最终会kernel
panic
? qemu-system-x86_64 -kernel arch/x86/boot/bzImage
3.5 制作根文件系统
? ?先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核?样先配置编译,并安装。
? 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)
? 然后编译安装,默认会安装到源码?录下的 _install ?录中。
? make -j$(nproc) && make install
? 然后制作内存根?件系统镜像,?致过程如下:
? 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/
3.6 准备init脚本文件并打包
? 准备init脚本?件放在根?件系统跟?录下(rootfs/init),添加如下内容到init?件。
? #!/bin/sh
? mount -t proc none /proc
? mount -t sysfs none /sys
? echo "Wellcome MengningOS!"
? echo "--------------------"
? cd home
? /bin/sh
? 给init脚本添加可执?权限
? chmod +x init
在完成上述步骤后,就可以查看系统调用,编写调用汇编代码了。
? 打包成内存根?件系统镜像
? find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../
rootfs.cpio.gz
? 测试挂载根?件系统,看内核启动完成后是否执?init脚本
? qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage
-initrd rootfs.cpio.gz
在执行完上述指令后,我们可以看到以下输出, 说明我们的init脚本执行成功。
4. 系统调用
首先根据学号后两位在/arch/x86/entry/syscalls/syscall_64.tbl 中找到对应的系统调用函数__x64_sys_getrlimit。
4.1 __x64_sys_getrlimit介绍
功能描述:获取或设定资源使用限制。每种资源都有相关的软硬限制,软限制是内核强加给相应资源的限制值,硬限制是软限制的最大值。非授权调用进程只可以将其软限制指定为0~硬限制范围中的某个值,同时能不可逆转地降低其硬限制。授权进程可以任意改变其软硬限制。RLIM_INFINITY的值表示不对资源限制。
总结来说就是:每个进程都有一组资源限制,其中某一些可以用getrlimit函数查询
4.2 __x64_sys_getrlimit用法
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
其中resource的参数是可选的,
resource:可能的选择有
RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
RLIMIT_DATA //进程数据段的最大值。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
RLIMIT_NPROC //用户可拥有的最大进程数。
RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
RLIMIT_STACK //最大的进程堆栈,以字节为单位。
然后使用以下代码测试 __x64_sys_getrlimit的功能
#include <stdio.h>
#include <sys/resource.h>
void pr_limits(char* name, int resource){
struct rlimit limit;
if(getrlimit(resource, &limit) <0){
perror("getrlimit");
}
printf("%-15s",name);
if(limit.rlim_cur == RLIM_INFINITY){
printf("(infinite) ");
}else{
printf("%-15ld",limit.rlim_cur);
}
if(limit.rlim_max == RLIM_INFINITY){
printf("(infinite) ");
}else{
printf("%-15ld",limit.rlim_max);
}
printf("
");
}
int main(void){
pr_limits("LIMIT_AS",RLIMIT_AS);
pr_limits("RLIMIT_CORE",RLIMIT_CORE);
pr_limits("RLIMIT_CPU",RLIMIT_CPU);
pr_limits("RLIMIT_DATA",RLIMIT_DATA);
pr_limits("RLIMIT_FSIZE",RLIMIT_FSIZE);
pr_limits("RLIMIT_LOCKS",RLIMIT_LOCKS);
pr_limits("RLIMIT_MEMLOCK",RLIMIT_MEMLOCK);
pr_limits("RLIMIT_NOFILE",RLIMIT_NOFILE);
pr_limits("RLIMIT_NPROC",RLIMIT_NPROC);
pr_limits("RLIMIT_RSS",RLIMIT_RSS);
pr_limits("RLIMIT_STACK",RLIMIT_STACK);
return 0;
}
运行结果如下,可以看出对于线程的相关的限制:
4.3 __x64_sys_getrlimit调试
下面选用另一个代码来进行追踪调试,代码如下
int main()
{
asm volatile(
"movl $0x61,%eax
" //使?EAX传递系统调?号97
"syscall
" //触发系统调?
);
return 0;
}
纯命令行启动qemu
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
? 再打开?个窗?,启动gdb,把内核符号表加载进来,建?连接:
? cd linux-5.4.34/
? gdb vmlinux
? (gdb) target remote:1234
? (gdb) b __x64_sys_getrlimit
上述步骤完成后,然后单步追踪调试,结果如下:
然后在其中我们可以看到系统调用函数的开始在entry_SYSCALL_64是系统调用的入口,接下来在vscode中找到对应的函数,其中ENTRY是系统入口,swapgs是保存现场,然后call函数,开始调用
接下来,就是一系列的堆栈操作,完成相关的系统调用
最后我在vscode中发现系统会经过以下的操作,恢复原有的现场,并且切换回用户态,完成本次系统调用
5. 总结
本次实验通过对97号系统调用的实现,学习了对Linux内核进行断点调试的相关技巧,了解了__x64_sys_getrlimit系统调用的作用,使得我加深了对系统调用相关知识的理解。
以上是关于深入理解系统调用的主要内容,如果未能解决你的问题,请参考以下文章