深入理解Linux系统调用

@

1 操作说明

  • 本次实验采用20号系统调用 wirtev
  • 通过汇编指令触发该系统调用
  • 通过gdb跟踪该系统调用的内核处理过程
  • 阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及关注系统调用过程中内核堆栈状态的变化

2 知识预备

我们知道,中断是操作系统的一个重要概念,是操作系统并发操作的的基石。下面是中断的大致分类。

中断

  • 外部中断(硬件中断)
  • 内部中断(软件中断)/异常
    • 故障(fault)
    • 陷阱(trap)-----------系统调用从用户态进入内核态的方式

系统调用
系统调⽤的库函数就是我们使⽤的操作系统提供的 API(应⽤程序编程接⼝),API 只是 函数定义。系统调⽤是通过特定的软件中断(陷阱 trap) 向内核发出服务请求,int $0x80 和syscall指令的执⾏就会触发⼀个系统调⽤。C库函数内部使⽤了系统调⽤的封装例程, 其主要⽬的是发布系统调⽤,使程序员在写代码时不需要⽤汇编指令和寄存器传递参数来 触发系统调⽤。⼀般每个系统调⽤对应⼀个系统调⽤的封装例程,函数库再⽤这些封装例 程定义出给程序员调⽤的 API ,这样把系统调⽤终封装成⽅便程序员使⽤的C库函数。

在这里插入图片描述
Linux系统调用过程

  1. 当⽤户态进程调⽤⼀个系统调⽤时,CPU切换到内核态并开始执⾏ system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码,其 中根据系统调⽤号调⽤对应的内核处理函数
  2. 保存现场,执行中断函数,恢复现场,中断返回(简要来说就是这么些)

Linux系统调用传参(为编写嵌入式汇编做准备)
32位x86体系结构下普通的函数调⽤是通过将参数压栈的⽅式传递的。系统调⽤从⽤户 态切换到内核态,在⽤户态和内核态这两种执⾏模式下使⽤的是不同的堆栈,即进程的⽤户态堆栈和进程的内核态堆栈,传递参数⽅法⽆法通过参数压栈的⽅式,⽽是通过寄存器 传递参数的方式。

32位x86体系结构下寄存器的⻓度⼤32位。除了EAX⽤于传递系统调⽤号外,参数按顺序赋值给EBX、ECX、EDX、ESI、EDI、EBP,参数的个数不能超过6个, 即上述6个寄存器。如果超过6个就把某⼀个寄存器作为指针,指向内存,就可以通过内 存来传递更多的参数。

64位x86体系结构下普通的函数调⽤和系统调⽤都是通过寄存器传递参数,RDI、RSI、RDX、RCX、R8、R9这6个寄存器⽤ 作函数/系统调⽤参数传递,依次对应第 1 参数到第 6 个参数。

3 环境准备

安装开发工具

sudo apt install build-essential
sudo apt install qemu # install QEMU 
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev

下载内核源代码

#pwd=~
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 
       [*]   Provide GDB scripts for kernel debugging
 [*] Kernel debugging 
# 关闭KASLR,否则会导致打断点失败
Processor type and features ----> 
   [] Randomize the address of the kernel image (KASLR)

编译和运行内核

make -j$(nproc) # nproc gives the number of CPU cores/threads available
# 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统终会kernel panic 
qemu-system-x86_64 -kernel arch/x86/boot/bzImage  #  此时应该不能正常运行

制作根⽂件系统

# pwd = ~
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
#pwd = ~
 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/

准备init脚本⽂件放在根⽂件系统跟⽬录下(rootfs/init),添加如下内容到init⽂件

 #!/bin/sh
 mount -t proc none /proc 
 mount -t sysfs none /sys
 echo "Wellcome MyOS!"
 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

在这里插入图片描述

4 查看系统调用表和汇编改写

打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,查看要选择进行实验的系统调用。
在这里插入图片描述
系统调用writev,函数入口为__x64_sys_writev。下面通过一个函数来简单了解writev的使用,

#include <stdio.h>
#include <sys/uio.h>
 
/*
struct iovec
{
	void *iov_base;		//指向一个char数组
	size_t iov_len;		//大小
};
*/
 
int main(int argc,char *argv[])
{
	struct iovec vec[2];
	char buf1[]="ABCDEFG";
	char buf2[]="1234567";
	int str_len;
 
	vec[0].iov_base=buf1;
	vec[0].iov_len=3;
	vec[1].iov_base=buf2;
	vec[1].iov_len=4;
 	//1 -标准输出
	// vec 缓冲区
	// 2 缓冲区长度
	str_len=writev(1,vec,2);	//调用writev()函数
	puts("");
	printf("Write bytes: %d \n",str_len);
	return 0;
}

gcc编译(这里采用静态编译)后运行,输出结果:

gcc -o write-file wrrite-file.c -static

在这里插入图片描述
简单分析,writev的作用是将多个缓冲区的内容一次性输出到某个位置/内容。


汇编改写手动触发系统调用
新建在 rootfs/home 目录下新建文件 write-flie-asm.c

#include <stdio.h>
#include <sys/uio.h>
 
/*
struct iovec
{
	void *iov_base;		//指向一个char数组
	size_t iov_len;		//大小
};
*/
 
int main(int argc,char *argv[])
{
	struct iovec vec[2];
	char buf1[]="ABCDEFG";
	char buf2[]="1234567";
	int str_len;
 
	vec[0].iov_base=buf1;
	vec[0].iov_len=3;
	vec[1].iov_base=buf2;
	vec[1].iov_len=4;
 	//1 -标准输出
	// vec 缓冲区
	// 2 缓冲区长度
	//str_len=writev(1,vec,2);	//调用writev()函数
	
	asm volatile(
		"movq $0x2, %%rdx\n\t"  // 参数3
		"movq %1, %%rsi\n\t"   //  参数2
		"movq $0x1, %%rdi\n\t"  //  参数1 
		"movl $0x14,%%eax\n\t" //  传递系统调用号
		"syscall\n\t"          //  系统调用
		"movq %%rax,%0\n\t"    //  结果存到%0 就是str_len中
		:"=m"(str_len) // 输出
		:"g"(vec) // 输入
	);
		
	puts("");
	printf("Write bytes: %d \n",str_len);
	return 0;
}

gcc编译后查看执行结果,改写成功。

gcc -o write-file-asm wrrite-file-asm.c -static

在这里插入图片描述

5 gdb调试与分析

重新打包根文件目录(因为新编写了wirite-file.cwrite-file-asm.c ,gcc后产生了可执行文件wirite-file write-file-asm

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" ,此时虚拟机会暂停在启动界面,
在这里插入图片描述
在另一个terminal中开启gdb调试 gdb vmlinux,连接进行调试,target remote:1234
在这里插入图片描述
在这里插入图片描述
gdb输入命令c,使得虚拟机继续执行,到初始界面
在这里插入图片描述
在这里插入图片描述
由之前分析可知,wirtev系统调用触发的函数是__x64_sys_writev,通过gdb在函数入口下断点,然后监听
在这里插入图片描述
在虚拟机中执行write-file,会卡住,在gdb界面查看断点分析,
在这里插入图片描述
在这里插入图片描述
使用 l 命令查看代码情况, n 命令单步执行, step 命令进入函数内部 bt查看堆栈

查看此时堆栈情况,有4层

  • 第一层/ 顶层 __x64_sys_writev 系统调用函数所在
  • 第二层 do_syscall_64 获取系统调用号, 前往系统调用函数
  • 第三层 entry_syscall_64 中断入口,做保存线程工作,调用 do_syscall_64
  • 第四层 OS相关(不讨论)
    在这里插入图片描述

首先断点定位为到/home/cj/linux-5.4.34/fs/read_write.c 1128行
在这里插入图片描述
进入do_writev函数查看,可知,这里是完成程序内容的地方,前期的保存现场工作已经完成

static ssize_t do_writev(unsigned long fd, const struct iovec __user *vec,
			 unsigned long vlen, rwf_t flags)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos, *ppos = file_ppos(f.file);
		if (ppos) {
			pos = *ppos;
			ppos = &pos;
		}
		ret = vfs_writev(f.file, vec, vlen, ppos, flags);
		if (ret >= 0 && ppos)
			f.file->f_pos = pos;
		fdput_pos(f);
	}

	if (ret > 0)
		add_wchar(current, ret);
	inc_syscw(current);
	return ret;
}

执行完这个函数,发现回到了函数堆栈上一层的do_sys_call_64 中 ,接下来要执行的 syscall_return_slowpath 函数要为恢复现场做准备。
在这里插入图片描述
继续执行,发现再次回到了函数堆栈的上一层,entry_SYSCALL_64 ,接下来执行的是用于恢复现场的汇编指令
在这里插入图片描述
在这里插入图片描述
最后伴随着两个pop指令,恢复了rdirsp寄存器。系统调用完成。
在这里插入图片描述

6 总结

到了这里我们来整理一下整个系统调用的过程。

  1. 汇编指令syscall 触发系统调用,通过MSR寄存器找到了中断函数入口(具体细节不考虑),此时,代码执行到/home/cj/linux-5.4.34/arch/x86/entry/entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口,然后开始通过swapgs压栈动作保存现场。
    在这里插入图片描述
  2. 然后跳转到了/linux-5.4.34/arch/x86/entry/common.c 目录下的 do_syscall_64 函数,在ax寄存器中获取到系统调用号,然后去执行系统调用内容
    在这里插入图片描述
  3. 然后程序跳转到/linux-5.4.34/fs/read_write.c 下的do_writev 函数,开始执行
    在这里插入图片描述
  4. 函数执行完后回到步骤3中的syscall_return_slowpath(regs); 准备进行现场恢复操作,
    在这里插入图片描述
    在这里插入图片描述
  5. 接着程序再次回到arch/x86/entry/entry_64.S,执行现场的恢复,最后两句,完成了堆栈的切换。
popq	%rdi
popq	%rsp

在这里插入图片描述

entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:184
184		movq	RCX(%rsp), %rcx
(gdb) n
185		movq	RIP(%rsp), %r11
(gdb) n
187		cmpq	%rcx, %r11	/* SYSRET requires RCX == RIP */
(gdb) n
188		jne	swapgs_restore_regs_and_return_to_usermode
(gdb) n
205		shl	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
206		sar	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
210		cmpq	%rcx, %r11
(gdb) n
211		jne	swapgs_restore_regs_and_return_to_usermode
(gdb) n
213		cmpq	$__USER_CS, CS(%rsp)		/* CS must match SYSRET */
(gdb) n
214		jne	swapgs_restore_regs_and_return_to_usermode
(gdb) n
216		movq	R11(%rsp), %r11
(gdb) n
217		cmpq	%r11, EFLAGS(%rsp)		/* R11 == RFLAGS */
(gdb) n
218		jne	swapgs_restore_regs_and_return_to_usermode
(gdb) n
238		testq	$(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
(gdb) n
239		jnz	swapgs_restore_regs_and_return_to_usermode
(gdb) n
243		cmpq	$__USER_DS, SS(%rsp)		/* SS must match SYSRET */
(gdb) n
244		jne	swapgs_restore_regs_and_return_to_usermode
(gdb) n
253		POP_REGS pop_rdi=0 skip_r11rcx=1
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:259
259		movq	%rsp, %rdi
(gdb) n
260		movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:262
262		pushq	RSP-RDI(%rdi)	/* RSP */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:263
263		pushq	(%rdi)		/* RDI */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:271
271		SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
(gdb) n
273		popq	%rdi
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:274
274		popq	%rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:275
275		USERGS_SYSRET64
(gdb) n
0x000000000044a221 in ?? ()
(gdb) n

写在后面

本文在某些地方不一定足够严谨,如有错误,恳请指正。

posted @ 2020-05-23 11:09  chengjin96  阅读(789)  评论(0编辑  收藏  举报