system_call 中断处理过程

来源:互联网 发布:php判断是不是正整数 编辑:程序博客网 时间:2024/05/23 12:33

Previously on the 系统调用初探

上次用 C 代码所实现的功能为:从当前目录的 src_file/src-asm_file 拷贝内容到 当前目录的 dest_file/dest-asm_file 文件中,具体源码如下: 

#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#define BUFFER_SIZE    1024/* 每次读写缓存大小,影响运行效率 */#define OFFSET    10240/* 拷贝的数据大小 */#define SRC_FILE_NAME    "src_file"        /* 源文件名 */#define DEST_FILE_NAME    "dest_file"        /* 目标文件名 */int main(){int src_fd, dest_fd;unsigned char buff[BUFFER_SIZE];int buff_len;/* 以只读的方式打开源文件 */src_fd = open(SRC_FILE_NAME, O_RDONLY);/* 以只读的方式打开目标文件,若此文件不存在则创建,访问权限为644 */dest_fd = open(DEST_FILE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);if (src_fd < 0 || dest_fd < 0){printf("Open File Error!\n");exit(1);}/* 将源文件的读写指针移到最后10KB的起始位置 */lseek(src_fd, -OFFSET, SEEK_END);/* 读取源文件的最后10KB数据并写到目标文件中,每次读写1KB */while ((buff_len = read(src_fd, buff, sizeof(buff))) > 0){write(dest_fd, buff, buff_len);}close(dest_fd);close(src_fd);return 0;}

注:嵌入汇编的代码就不再贴出,因为通过 C 程序可以很清楚地看出程序的功能,如果想看汇编代码建议点击上面链接。


        首先要明白一点,我们分析 system_call 中断处理的过程是分析什么?我们分析的是 entry_32.S 文件中的执行流程,那么到底是什么时候进入 entry_32.S 的呢?我们在触发系统调用时,即用 int 0x80 触发软中断时,就会跳转到 entry_32.S 文件中 system_call,然后根据相应的系统调用号分别进行处理。具体分析如下:


1. 用户空间到内核空间的转换

        根据系统调用初探中描述到,用户态与内核态的区别仅仅在于CPU的运行状态、运行权限得到提升。系统调用需要一个从用户空间到内核空间的转换,X86结构通过操作系统触发一个软中断来实现,即汇编指令 int $0x80. 通过软中断0x80 ,这种特定的指令称作操作系统的陷入(operating system trap)。此时操作系统就会陷入到内核态中,同时系统就会跳到一个预设的内核空间地址。这个预设的地址指向系统调用处理程序system_call(中断向量表),在arch/x86/kernel/entry_32.S 中以汇编语言编写,该过程主要有2个步骤。

(1)系统启动时,对int 0x080 进行一定初始化

/arch/x86/kernel/head32.S 中从 468 开始到 507 截至:

#include "verify_cpu.S"/* *  setup_once * *  The setup work we only want to run on the BSP. * *  Warning: %esi is live across this function. */__INITsetup_once:/* * Set up a idt with 256 entries pointing to ignore_int, * interrupt gates. It doesn't actually load idt - that needs * to be done on each CPU. Interrupts are enabled elsewhere, * when we can be relatively sure everything is ok. */movl $idt_table,%edimovl $early_idt_handlers,%eaxmovl $NUM_EXCEPTION_VECTORS,%ecx1:movl %eax,(%edi)movl %eax,4(%edi)/* interrupt gate, dpl=0, present */movl $(0x8E000000 + __KERNEL_CS),2(%edi)addl $9,%eaxaddl $8,%ediloop 1bmovl $256 - NUM_EXCEPTION_VECTORS,%ecxmovl $ignore_int,%edxmovl $(__KERNEL_CS << 16),%eaxmovw %dx,%ax/* selector = 0x0010 = cs */movw $0x8E00,%dx/* interrupt gate - dpl=0, present */2:movl %eax,(%edi)movl %edx,4(%edi)addl $8,%ediloop 2b


(2)设置中断描述符表

        start_kernel 函数(索引:init/main.c)中调用 trap_init 函数(索引:arch/x86/kernel/trap.c),设置中断描述符表。在 trap_init 函数里,实际上是通过调用函数set_system_trap_gate(SYSCALL_VECTOR, &system_call); (索引:arch/x86/kernel/traps.c)来完成该项的设置的,如下:

#ifdef CONFIG_X86_32         set_system_trap_gate(SYSCALL_VECTOR, &system_call);         set_bit(SYSCALL_VECTOR, used_vectors);#endif

        其中的 SYSCALL_VECTOR 就是 0x80,而 system_call 则是一个汇编子函数,它即是中断向量 0x80 的处理函数,主要完成两项工作:寄存器上下文的保存、跳转到系统调用处理函数。(至此,应该有了个大概的流程感受,流程图等会儿再放)


2. 系统调用函数的入口

        syscall_call 函数到系统调用服务例程通过系统调用号联系起来:在上面执行软中断 0x80 时,系统调用号会被放入eax寄存器(参数的传递),system_call 函数读取eax寄存器获取参数(当前系统调用的调用号),将其乘以4生成偏移地址。然后以中断向量表(sys_call_table)为基址,以系统调用号所确定的为偏移地址相加得到最后的物理地址:基址+偏移地址 => 系统调用服务例程的地址。其中 sys_call_table 基址在文件 arch/x86/kernel/syscall_table_32.S 中定义,同时表中每一项例程的地址占用4个字节,所以上面乘以4。

        这给我们的启示是,如果能改变中断向量表的基址(修改 syscall_table_32.S 文件),或者修改中断向量表中相应地址所对应的服务例程,我们就能定义自己的中断处理函数或者改写原来的中断处理函数,这样很容易被用来干坏事,于是这个文件被干掉了。从3.5开始系统调用号及与对应的内核处理函数的关联方式发生了变化,删除了 \arch\x86\kernel\syscall_table_32.S,增加了/arch/x86/syscalls/syscall_32.tbl,unistd_32.h中的系统调用号Macro也由 syscall_32.tbl 中的数据自动生成。

        到这儿system_call() 就到服务例程的地址了。然后另一个问题——参数传递需要解决。由于系统调用例程在定义时时用 asmlinkage 标记了的,所以编译器仅从堆栈中获取该函数的参数。在进入system_call函数前,用户应用会把参数存放到寄存器中,system_call 函数执行时会首先把这些寄存器压入堆栈。这样对系统调用服务例程可以直接从堆栈照片能够获取参数。


3. 系统调用函数的执行

open 函数会执行 sys_open 函数(/fs/open.c):

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode){if (force_o_largefile())flags |= O_LARGEFILE;return do_sys_open(AT_FDCWD, filename, flags, mode);}

而sys_open 函数里则会调用 do_sys_open 函数(/fs/open.c)

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode){struct open_flags op;int fd = build_open_flags(flags, mode, &op);struct filename *tmp;if (fd)return fd;tmp = getname(filename);if (IS_ERR(tmp))return PTR_ERR(tmp);fd = get_unused_fd_flags(flags);if (fd >= 0) {struct file *f = do_filp_open(dfd, tmp, &op);if (IS_ERR(f)) {put_unused_fd(fd);fd = PTR_ERR(f);} else {fsnotify_open(f);fd_install(fd, f);}}putname(tmp);return fd;}

fd = get unused_fd_flags(flags); 语句用来获取未被使用的文件描述符,在do_filp_open 函数中打开文件。其函数调用过程如下:

sys_open -> do_sys_open -> do_filp_open ->do_last-> nameidata_to_filp -> __dentry_open


4. 总结



(1)open 函数通过系统调用号与 sys_open 系统调用服务例程函数联系起来的

(2)int 0x80 通过中断向量(0x80)与 system_call 联系起来的

(3)上周的实验内容是关于第一条的,这周的实验内容是关于第二条的


参考文献:

[1]. 浅析linux中open系统调用

[2]. linux下的系统调用函数到内核函数的追踪 


陈金雷 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000






0 0