深入理解系统调用
Posted hesetone
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解系统调用相关的知识,希望对你有一定的参考价值。
一、熟悉对应的系统调用
本人学号尾号86,对应的系统调用号是link,在./arch/x86/entry/syscall_64.tbl得到如下信息:
# # 64-bit system call numbers and entry vectors # # The format is: # <number> <abi> <name> <entry point> # # The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls # # The abi is "common", "64" or "x32" for this file. # ... 86 common link __x64_sys_link ...
因此,可以看到,86号系统调用,对应link指令,在shell下,简写为ln,ln是linux中一个非常重要命令,它的功能是为某一个文件在另外一个位置建立一个同不的链接,这个命令常用的可选项参数是-s,具体用法是:ln –s 源文件 目标文件。当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间。ln的链接又软链接和硬链接两种,软链接只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接没有参数-s,它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化,
二、反汇编代码讨论
int link(const char *, const char *);
SYSCALL_DEFINE2(link, const char __user *, oldname, const char __user *, newname) { return do_linkat(AT_FDCWD, oldname, AT_FDCWD, newname, 0); }
可以发现,它调用了一个do_linkat函数,并且把两个参数都传递给了它,继续追溯do_linkat函数的定义,在./fs/namei.c中找到其对应的函数实现如下:
int do_linkat(int olddfd, const char __user *oldname, int newdfd, const char __user *newname, int flags) { struct dentry *new_dentry; struct path old_path, new_path; struct inode *delegated_inode = NULL; int how = 0; int error; if ((flags & ~(AT_SYMLINK_FOLLOW | AT_EMPTY_PATH)) != 0) return -EINVAL; /* * To use null names we require CAP_DAC_READ_SEARCH * This ensures that not everyone will be able to create * handlink using the passed filedescriptor. */ if (flags & AT_EMPTY_PATH) { if (!capable(CAP_DAC_READ_SEARCH)) return -ENOENT; how = LOOKUP_EMPTY; } if (flags & AT_SYMLINK_FOLLOW) how |= LOOKUP_FOLLOW; retry: error = user_path_at(olddfd, oldname, how, &old_path); if (error) return error; new_dentry = user_path_create(newdfd, newname, &new_path, (how & LOOKUP_REVAL)); error = PTR_ERR(new_dentry); if (IS_ERR(new_dentry)) goto out; error = -EXDEV; if (old_path.mnt != new_path.mnt) goto out_dput; error = may_linkat(&old_path); if (unlikely(error)) goto out_dput; error = security_path_link(old_path.dentry, &new_path, new_dentry); if (error) goto out_dput; error = vfs_link(old_path.dentry, new_path.dentry->d_inode, new_dentry, &delegated_inode); out_dput: done_path_create(&new_path, new_dentry); if (delegated_inode) { error = break_deleg_wait(&delegated_inode); if (!error) { path_put(&old_path); goto retry; } } if (retry_estale(error, how)) { path_put(&old_path); how |= LOOKUP_REVAL; goto retry; } out: path_put(&old_path); return error; }
因为SYSCALL_DEFINE2(link,...)等同于asmlinkage long sys_link(const char __user *oldname,const char __user *newname);,因此调用sys_link()等价于调用前者。接下来,触发syscall指令调用的时候,会从MSR寄存器里面拿出该函数的首地址来调用,也就是调用 entry_SYSCALL_64,这个入口在/arch/x86/entry/entry.S里面:
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 /* tss.sp2 is scratch space. */ movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) 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 movq %rsp, %rsi call do_syscall_64 /* returns with IRQs disabled */ TRACE_IRQS_IRETQ /* we‘re about to change IF */ /* * Try to use SYSRET instead of IRET if we‘re returning to * a completely clean 64-bit userspace context. If we‘re not, * go to the slow exit path. */ movq RCX(%rsp), %rcx movq RIP(%rsp), %r11 cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */ jne swapgs_restore_regs_and_return_to_usermode
END(entry_SYSCALL_64)
显然,可以看到,此处是保存了相当多的结构体pt_regs的相关内容,其中就包含栈段、代码段、指令指针、栈指针、标志寄存器和ax的值,最后的ax寄存器实际上存储了要执行的系统调用号码,接下来执行call do_syscall_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); }
二、编程触发系统调用(含汇编)
首先,编写出测试程序execl_ln.c:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { link("./execl_ln.c", "./a.c"); return 0; }
分别按照软/硬连接处理得到a.c文件,上面的是软连接,只占有一极小部分内存,下面是硬连接,和源文件占有同样多的内存:
接下来我们汇编实现它,因为它是86号系统调用,因此我们需要预先存储0x56给eax寄存器:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { const char *oldpath = "execl_ln.c"; const char *newpath = "a.c"; short int err_flag; asm volatile( "movl $0x56,%%eax " "movl %1, %%ebx " "movl %2, %%ecx " "int 0x80 " "movl %%eax,%0 " : "=m"(err_flag) : "b"(oldpath), "c"(newpath)); if (!err_flag) printf("link successful."); return 0; }
使用命令编译为32位汇编源码,gcc -S -o a32.s execl_ln.c -static -m32,首先,运行execl_ln.c编译链接完毕的可执行文件,可以查看到可以在当前目录查看到产生了一个a.c文件,以硬链接的方式链接到execl_ln.c,在a32.s中可以查看到相关的代码组织结构,很明显,在main函数中调用link函数的时候,事实上是调用了__link()函数,这个函数的位置在0x0806dad0,我们继续追踪,可以查看到从地址0x0806dad0开始执行系统调用例程,且执行结果存储在eax中,如果eax中的返回值小于0xfffff001,那么就会引起触发__syscall_error,引起报错,展示如下:
显然,可以看到,触发系统调用的操作,成功使程序进入到__link()函数中,查看对应地址的内存,发现0x080eaf90地址的数据已经超出了当前文件m32.S的存储范围,这是因为我们编译源文件的时候,采用了静态链接,所以0x0806dadf地址会存储一条等同于call *_dl_sysinfo的指令,继续查看0xf7ffcc80地址的内存,可以发现,其实最终调用的是VDSO(linux-gate.so.1)中的__kernel_vsyscall函数,显然其包含sysenter指令,syscall()函数也是类似的,根据静态/动态链接的不同分别采用的不同的指令,最终调用__kernel_vsyscall函数。
三、系统调用栈空间分析
在具有函数调用的处理例程当中,各个函数的栈空间关系如下图所示:
在所有的寄存器中,%rax 通常用于存储函数调用的返回结果,%rsp 是堆栈指针寄存器,它会一直指向栈顶位置,堆栈的pop和push操作就是通过改变%rsp 的值即移动堆栈指针的位置来实现出栈和压栈操作的。%rbp 是栈帧指针,用于标识当前栈帧的起始位置,剩余的%rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数,一般不带有6个以上的参数。在整个系统调用过程中,调用方主要做的操作有:
1、先把参数保存在寄存器edi和esi中(通过寄存器传参数)
2、调用callq,其中callq保存下一条指令的地址,用于函数返回继续执行,之后再跳转到子函数地址。
3、处理返回值,函数返回值通常存放在%eax。
被调用方的操作通常有:
1、上一个函数的帧指针%ebp入栈
2、栈指针%esp保存到帧指针
3、从寄存器(%edi和%esi)取出参数到栈中
4、参数参与运算
5、把计算结果保存在%eax
6、弹出帧指针(还原前一个函数的%rbp)
7、函数返回,取从%eip下一跳指令继续执行
从main函数调用link函数的时候,可以查看到如下信息:
指令push %ebp和mov %esp,%ebp,保存了上一个函数(main)的栈帧指针,同时,让%ebp重新指向新的栈帧的起始地址。之所以保存返回地址和保存上一栈帧的%ebp,都是为了函数返回时,恢复上一个函数的栈帧结构。事实上,保存返回地址和跳转到子函数处执行由call一条指令完成,在call 指令执行完成时,已经进入了子程序中,因而将上一栈帧%ebp 压栈的操作,需要由子程序来完成,%ebp的压栈的内容也是保存在被调用函数的栈空间之中。
在调用函数例程结束返回时,我们只要知道函数的运行结果存储在%eax中,之后就需要将栈的结构恢复到启动函数调用之前的状态,并跳转到上一个函数的返回地址处继续执行下一条指令。由于函数调用时已经保存了返回地址和父函数栈帧的起始地址,为了恢复子函数调用之前的上一个栈帧状态,我们只需要执行mov %ebp,%esp和pop %ebp指令就行,前一条使得%esp和%ebp都指向子函数栈的起始处,然后出栈%ebp的值,赋值给%ebp,此时,%esp也会自动上移一个位置,指向上一个函数的栈结尾处。此外,在上图中,可以看到,并没有这两条指令出现,这是因为x86_64 架构中提供了leave指令来实现上述两条命令的功能,而且可以看出,leave指令用于恢复父函数的栈帧,ret用于跳转到返回地址处,也就是上一个函数栈帧的结尾处,leave和ret配合共同完成了子函数的返回。当执行完成 ret 后,%esp 指向的是上一个函数栈帧的结尾处。
以上是关于深入理解系统调用的主要内容,如果未能解决你的问题,请参考以下文章