深入理解Linux系统调用
Posted chengjin96
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Linux系统调用相关的知识,希望对你有一定的参考价值。
@
1 操作说明
- 本次实验采用20号系统调用
wirtev
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及关注系统调用过程中内核堆栈状态的变化
2 知识预备
我们知道,中断是操作系统的一个重要概念,是操作系统并发操作的的基石。下面是中断的大致分类。
中断
- 外部中断(硬件中断)
- 内部中断(软件中断)/异常
- 故障(fault)
- 陷阱(trap)-----------系统调用从用户态进入内核态的方式
系统调用
系统调?的库函数就是我们使?的操作系统提供的 API(应?程序编程接?),API 只是 函数定义。系统调?是通过特定的软件中断(陷阱 trap) 向内核发出服务请求,int $0x80 和syscall指令的执?就会触发?个系统调?。C库函数内部使?了系统调?的封装例程, 其主要?的是发布系统调?,使程序员在写代码时不需要?汇编指令和寄存器传递参数来 触发系统调?。?般每个系统调?对应?个系统调?的封装例程,函数库再?这些封装例 程定义出给程序员调?的 API ,这样把系统调?终封装成?便程序员使?的C库函数。
Linux系统调用过程
- 当?户态进程调??个系统调?时,
CPU切换到内核态
并开始执?system_call(entry_INT80_32或entry_SYSCALL_64)
汇编代码,其 中根据系统调?号
调?对应的内核处理函数 - 保存现场,执行中断函数,恢复现场,中断返回(简要来说就是这么些)
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 ?ex 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 defcon?g # Default con?guration is based on ‘x86_64_defcon?g‘
make menucon?g
# 打开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 menucon?g
#记得要编译成静态链接,不?动态链接库。
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
#打包成内存根?件系统镜像
?nd . -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
",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
" // 参数3
"movq %1, %%rsi
" // 参数2
"movq $0x1, %%rdi
" // 参数1
"movl $0x14,%%eax
" // 传递系统调用号
"syscall
" // 系统调用
"movq %%rax,%0
" // 结果存到%0 就是str_len中
:"=m"(str_len) // 输出
:"g"(vec) // 输入
);
puts("");
printf("Write bytes: %d
",str_len);
return 0;
}
gcc编译后查看执行结果,改写成功。
gcc -o write-file-asm wrrite-file-asm.c -static
5 gdb调试与分析
重新打包根文件目录(因为新编写了wirite-file.c
和 write-file-asm.c
,gcc后产生了可执行文件wirite-file
write-file-asm
)
?nd . -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
指令,恢复了rdi
和rsp
寄存器。系统调用完成。
6 总结
到了这里我们来整理一下整个系统调用的过程。
- 汇编指令syscall 触发系统调用,通过MSR寄存器找到了中断函数入口(具体细节不考虑),此时,代码执行到
/home/cj/linux-5.4.34/arch/x86/entry/entry_64.S
目录下的ENTRY(entry_SYSCALL_64)
入口,然后开始通过swapgs
和压栈
动作保存现场。
- 然后跳转到了
/linux-5.4.34/arch/x86/entry/common.c
目录下的do_syscall_64
函数,在ax寄存器中获取到系统调用号,然后去执行系统调用内容
- 然后程序跳转到
/linux-5.4.34/fs/read_write.c
下的do_writev
函数,开始执行
- 函数执行完后回到步骤3中的
syscall_return_slowpath(regs);
准备进行现场恢复操作,
- 接着程序再次回到
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
写在后面
本文在某些地方不一定足够严谨,如有错误,恳请指正。
以上是关于深入理解Linux系统调用的主要内容,如果未能解决你的问题,请参考以下文章