• 1. Linux内核源代码导读 中国科学技术大学计算机系 陈香兰(0551-3606864) xlanchen@ustc.edu.cn Spring 2009
  • 2. 一个典型的Linux操作系统的结构用户应用程序System call对硬件资源的管理Shell,libKernel implementation10/16/20182Linux内核源代码导读
  • 3. 系统调用的意义操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用 把用户从底层的硬件编程中解放出来 极大的提高了系统的安全性 使用户程序具有可移植性10/16/20183Linux内核源代码导读
  • 4. API和系统调用应用编程接口(application program interface, API) 和系统调用是不同的 API只是一个函数定义 系统调用通过“软”中断向内核发出一个明确的请求 在x86中为int指令 Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用) 一般每个系统调用对应一个封装例程 库再用这些封装例程定义出给用户的API10/16/20184Linux内核源代码导读
  • 5. 不是每个API都对应一个特定的系统调用。 API可能直接提供用户态的服务 如,一些数学函数 一个单独的API可能调用几个系统调用 不同的API可能调用了同一个系统调用 返回值 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用 -1在多数情况下表示内核不能满足进程的请求 Libc中定义的errno变量包含特定的出错码10/16/20185Linux内核源代码导读
  • 6. 系统调用程序及服务例程当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。 在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常 (回忆,trap_init中系统调用入口的初始化) 传参: 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数 使用eax寄存器10/16/20186Linux内核源代码导读
  • 7. 系统调用的返回值所有的系统调用返回一个整数值。 正数或0表示系统调用成功结束 负数表示一个出错条件 这里的返回值与封装例程返回值的约定不同 内核没有设置或使用errno变量 封装例程在系统调用返回取得返回值之后设置errno 当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序10/16/20187Linux内核源代码导读
  • 8. 系统调用处理程序也和其他异常处理程序的结构类似 在进程的内核态堆栈中保存大多数寄存器的内容 (即保存恢复进程到用户态执行所需要的上下文) 调用相应的系统调用服务例程处理系统调用 sys_xxx 通过syscall_exit从系统调用返回10/16/20188Linux内核源代码导读
  • 9. 应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系10/16/20189Linux内核源代码导读
  • 10. 为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。 这个表存放在sys_call_table数组中, 有nr_syscalls个表项:第n个表项对应了系统调用号为n的服务例程的入口地址的指针 观察sys_call_table 观察nr_syscalls的定义10/16/201810Linux内核源代码导读
  • 11. 初始化系统调用内核初始化期间调用trap_init()函数建立IDT表中向量128对应的表项,语句如下: 其中,SYSCALL_VECTOR被定义为0x80 该调用把下列值存入这个系统门描述符的相应字段: segment selector:内核代码段__KERNEL_CS的段选择符 offset:指向system_call()异常处理程序的入口地址 type:置为15。表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断 DPL(描述符特权级):置为3。这就允许用户态进程访问这个门,即在用户程序中使用int $0x80是合法的10/16/201811Linux内核源代码导读
  • 12. CPU执行int 0x80指令过程类似中断 在进入system_call 之前,堆栈情况为10/16/201812Linux内核源代码导读
  • 13. system_call()函数10/16/201813Linux内核源代码导读
  • 14. 观察SAVE_ALL宏SAVE_ALL 在堆栈上依次保存了3个段寄存器fs、es、ds的值,以及ax、bp、di、si、dx、cx、bx这7个寄存器的值。 并对段寄存器ds、es、fs赋予适当的值10/16/201814Linux内核源代码导读
  • 15. 系统调用的参数传递系统调用也需要输入输出参数,例如 实际的值 用户态进程地址空间的变量的地址 甚至是包含指向用户态函数的指针的数据结构的地址 system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号 例如: 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号10/16/201815Linux内核源代码导读
  • 16. 很多系统调用需要不止一个参数 普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈用户态堆栈用户态C函数内核态堆栈内核态C函数?10/16/201816Linux内核源代码导读
  • 17. 在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的用户态堆栈用户态C函数内核态堆栈内核态C函数寄存器10/16/201817Linux内核源代码导读
  • 18. 回想一下在进入中断和异常处理程序前,在内核态堆栈中保存的pt_regs结构,此时pt_regs结构中的一些寄存器被用来传递参数或者pt_regs结构本身就是参数 使用寄存器传参的限制 一个参数最长32位 根据pt_regs结构,最多可以传递6个参数10/16/201818Linux内核源代码导读
  • 19. 参数传递举例处理write系统调用的sys_write服务例程声明如下 该函数期望在栈顶找到fd,buf和count参数 在封装sys_write()的封装例程中,将会在ebx、ecx和edx寄存器中分别填入这些参数的值,然后在进入system_call时,SAVE_ALL会把这些寄存器保存在堆栈中,进入sys_write服务例程后,就可以在相应的位置找到这些参数 举例观察asmlinkage使得编译器不通过寄存器(x=0)而 使用堆栈传递参数10/16/201819Linux内核源代码导读
  • 20. SAVE_ALL后的 堆栈情况10/16/201820Linux内核源代码导读
  • 21. 在有些系统调用中,C库接口中尽管没有提供参数,但是在内核中却会依赖保存的pt_regs信息,例如fork()sys_clone()do_frok()10/16/201821Linux内核源代码导读
  • 22. 传递返回值服务例程的返回值是将会被写入eax寄存器中 这个是在执行“return”指令时,由编译器自动完成的10/16/201822Linux内核源代码导读
  • 23. 验证参数在内核打算满足用户的请求之前,必须仔细的检查所有的系统调用参数 比如前面的write()系统调用,fd参数是一个文件描述符,sys_write()必须检查这个fd是否确实是以前已打开文件的一个文件描述符,进程是否有向fd指向的文件的写权限,如果有条件不成立,那这个处理程序必须返回一个负数10/16/201823Linux内核源代码导读
  • 24. 只要一个参数指定的是地址,那么内核必须检查它是否在这个进程的地址空间之内,有两种验证方法: 验证这个线性地址是否属于进程的地址空间,并且是否有合理的访问权限 仅仅验证这个线性地址小于PAGE_OFFSET(用户态地址,粗粒度) 对于第一种方法: 费时 大多数情况下,不必要 对于第二种方法: 高效 在后续的执行过程中,会自然的捕获到出错情况(配合缺页异常) 从linux2.2开始执行第二种检查10/16/201824Linux内核源代码导读
  • 25. 对用户地址参数的粗略验证在内核中,可以访问到所有的内存 要防止用户将一个内核地址作为参数传递给内核,这将导致它借用内核代码来读写任意内存 检查方法: Addr+size是否超出当前进程的地址边界对于用户进程:不大于3G 对于内核线程:可以使用整个4G10/16/201825Linux内核源代码导读
  • 26. 访问进程的地址空间系统调用服务例程需要非常频繁的读写进程地址空间的数据10/16/201826Linux内核源代码导读
  • 27. 举例get_userget_user__get_user_x10/16/201827Linux内核源代码导读
  • 28. 访问进程地址空间时的缺页内核对进程传递的地址参数只进行粗略的检查 访问进程地址空间时的缺页,可以有多种情况 合理的缺页:来自虚存技术 由于错误引起的缺页 由于非法引起的缺页10/16/201828Linux内核源代码导读
  • 29. 非法缺页的判定内核中,只有少数几个函数/宏会访问用户地址空间 对于一次非法缺页,一定来自于这些函数/宏 可以将访问用户地址空间的指令的地址一一列举出来,当发生非法缺页时,根据引起出错的指令地址来定位 若定位到:则说明系统调用参数有问题 否则,可能是更严重的bug引起的 Linux使用了异常表的概念 __ex_table, __start___ex_table, __stop___ex_table10/16/201829Linux内核源代码导读
  • 30. __ex_table的表项 哪条指令访问了用户地址空间如果这条指令引起了非法缺页,该怎么处理Fixup所指向的代码,称为修正代码 通常为汇编代码10/16/201830Linux内核源代码导读
  • 31. 举例get_user相关文件getuser_32.S10/16/201831Linux内核源代码导读
  • 32. 缺页异常对非法缺页的处理在缺页异常do_page_fault中,若最后发现是非法缺页,就会执行下面的操作 假设找到了修正代码,会发生什么事情?该操作使用引起出错的代码地址在异常表中进行查找,若找到,就返回 相应的修正代码地址,填写在regs->eip中10/16/201832Linux内核源代码导读
  • 33. IDT表, 进入异 常处理某内核函数缺页缺页处理作为一次故障,要重新执行引起出错的代码正常情况下,这个eip在发生异常时, 由硬件保存到堆栈中 因此,正常情况下,返回此处非法异常修改堆栈中的eip,指向修正代码 因此,非法缺页时,返回此处修正代码异常处理后,返回eip指定的位置执行10/16/201833Linux内核源代码导读
  • 34. 系统调用的返回RESTORE_REGS10/16/201834Linux内核源代码导读
  • 35. 中断、异常、系统调用小结用户态内核态IDT表中断异常系统调用iretreturn_from_intr return_from_exception syscall_exit10/16/201835Linux内核源代码导读
  • 36. Project自选: 分析中断、异常和系统调用的代码之一,提交分析报告 采用某种方法截获一个中断,例如键盘中断 采用某种方法制造缺页异常 自己编写一个系统调用 提交project,详细说明你的分析或者实验10/16/201836Linux内核源代码导读