深入理解系统调用

深入理解系统调用

目录
  • 深入理解系统调用
    • 1. 实验要求
    • 2. 实验目标
    • 3. 搭建Linux内核调试环境
      • 3.1 按照下面的命令下载内核源码并配置内核选项,进行编译。
      • 3.2 借助BusyBox构造根文件系统,提供基本的用户态可执行程序。
    • 4. 查看系统调用并编写调用汇编代码
      • 4.1 根据学号选择相应的系统调用
      • 4.2 触发系统调用
      • 4.3 通过gdb跟踪系统调用
    • 5. 实验总结

1. 实验要求

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

2. 实验目标

  • 理解Linux操作系统调用;
  • 了解系统调用过程中内核堆栈状态的变化过程。

3. 搭建Linux内核调试环境

3.1 按照下面的命令下载内核源码并配置内核选项,进行编译。

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
make -j4
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

在配置内核选项中需要注意(1)打开debug相关选项 (2)关闭KASLR,否则会导致打断点失败

深入理解系统调用_第1张图片 深入理解系统调用_第2张图片

运行最后一条命令时会kernel panic,因为没有文件系统,下面制作根文件系统。

3.2 借助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
make -j4 && 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/

在make menuconfig时要注意设置编译成静态链接,不⽤动态链接库。

深入理解系统调用_第3张图片

编写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
# 挂载根文件系统运行:
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage-initrd rootfs.cpio.gz
深入理解系统调用_第4张图片

4. 查看系统调用并编写调用汇编代码

4.1 根据学号选择相应的系统调用

本人的学号后两位为25,通过查看系统调用表(./arch/x86/entry/syscalls/syscall_64.tbl)得知25号系统调用是shmget函数。其对应的系统调用为__x64_sys_shmget,其对应的API函数为shmget。

深入理解系统调用_第5张图片

shmget函数主要的功能是创建共享内存来实现进程之间的通信。也就是从内存中获得一段共享内存区域。

通过查看man手册得到shmget的函数签名为int shmget(key_t key, size_t size, int shmflg);

shmget函数详情参考

  • key:标识符的规则。key标识共享内存的键值。其中IPC_PRIVATE:会建立新共享内存对象
  • size:共享存储段的字节数
  • shmflg:读写的权限,包括IPC_CREAT和IPC_EXCL
    - IPC_CREAT: 创建共享内存,如果共享内存已经存在,就获取该共享内存的标识号。
    - IPC_EXCL: 与宏IPC_CREAT一起使用,单独使用无意义,此时只能创建一个不存在的共享内存,如果内存已存在,则调用失败。
  • 返回值:成功返回共享存储的标识符,失败返回-1

深入理解系统调用_第6张图片

4.2 触发系统调用

通过了解shmget的功能以及对各个参数的了解,使用c语言编写测试程序进行调用:

#include 
#include 
#include 
#define SIZE 1024
int main(){
  int shmid;
  shimd = shmeget(IPC_PRIVATE,IPC_CREAT|0600);
  if(shmid<0){
    printf("error");
    return -1;
  }
  printf("%d\n",shmid);
  return 0;
}

使用gcc进行编译:

gcc test.c -o test
./test

接着编写对应的汇编语言:

#include 

#include 

#include 

#define SIZE 1024

int main(){
	int shmid;
	asm volatile(
        	"movl %1, %%edi\n\t"      //将第一个参数放入edi
        	"movl %2, %%rsi\n\t"      //将第二个参数放入rsi
		      "movl %3, %%esi\n\t"      //将第三个参数放入esi
        	"movl $0x1D, %%eax\n\t"   //中断号29放入eax
        	"syscall\n\t"             //产生中断信号进行系统调用
        	"movl %%eax, %0\n\t"
        	:"=m"(shmid)              //将结果放入shmid中
        	:"b"(&IPC_PRIVATE),"c"(&SIZE),"d"(&SIZE,IPC_CREAT|0600)
    	);
	if(shmid<0){
		printf("error");
		return -1;
	}
	printf("%d\n",shmid);
	return 0;
}

将形成的可执行文件放到rootfs/home/目录下,然后重新打包rootfs文件夹

$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

4.3 通过gdb跟踪系统调用

执行下列命令,重新打包根文件系统,开启虚拟机gdb调试。

$ qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
# 开启新的terminal
$ cd linux-5.4.34
$ gdb vmlinux
$ target remote:1234
$ c

结果如下:

深入理解系统调用_第7张图片

使用bt命令查看系统调用栈:

深入理解系统调用_第8张图片

根据上图可以看出,系统调用的进入点为entry_SYSCALL_64,我们找到系统调用的入口并查看相应汇编代码。

ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
	/*
	 * Interrupts are off on entry.
	 * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
	 * it is too small to ever cause noticeable irq latency.
	 */

	swapgs  /*swapgs指令切换gs寄存器从用户态到内核态*/
	/* tss.sp2 is scratch space. */
	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /*保存中断上下文中的rsp寄存器的值*/
	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp      /*切换到内核堆栈空间*/
	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp  

	/* Construct struct pt_regs on stack */
	pushq	$__USER_DS				/* pt_regs->ss */
	pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
	pushq	%r11					/* pt_regs->flags */
	pushq	$__USER_CS				/* pt_regs->cs */
	pushq	%rcx					/* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
	pushq	%rax					/* pt_regs->orig_ax */

	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

	TRACE_IRQS_OFF           /*关闭中断追踪*/

	/* IRQs are off. */
	movq	%rax, %rdi      /*保存系统调用号到rdi*/
	movq	%rsp, %rsi      /*保存内核堆栈地址在rsi*/
	call	do_syscall_64		/* returns with IRQs disabled */

swapgs指令主要的作用是将保存现场和恢复现场时的寄存器保存起来,然后将pt_regs中的相关字段保存到内核栈中。
代码最后调用了do_syscall_64。该函数包含两个参数,其中rdi传递的是系统调用号,rsi传递的是内核堆栈地址。函数do_syscall_64()的代码如下:

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
	} else if (likely((nr & __X32_SYSCALL_BIT) &&
			  (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
		nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
					X32_NR_syscalls);
		regs->ax = x32_sys_call_table[nr](regs);
#endif
	}

	syscall_return_slowpath(regs);
}

首先通过传入的系统调用号nr找到相应的系统调用,并将返回值保存在regs的ax中。
调用结束后,执行syscall_return_slowpath,进行返回。然后在gdb单步调试中,我们可以看到从syscall_return_slowpath返回后,开始恢复现场。
主要是将之前保存在栈中的寄存器的值,重新恢复到原来的寄存器中。继续单步执行,直到恢复现场完成:

深入理解系统调用_第9张图片

5. 实验总结

通过实验可知,系统调用的大致过程为:首先,通过汇编指令中的系统调用号找到系统调用入口ENTRY(entry_SYSCALL_64),然后在ENTRY(entry_SYSCALL_64)中,执行swapgs保存现场。随后执行do_syscall_64方法,该方法中根据传参nr保存的的系统调用号跳转至具体的系统调用函数,系统调用函数执行完毕后,执行syscall_return_slowpath,进行返回,最后进行现场恢复。

你可能感兴趣的:(深入理解系统调用)