中国科学技术大学计算机系 陈香兰(0512-87161312) xlanchen@ustc.edu.cn Spring 2011 Linux操作系统分析 中国科学技术大学计算机系 陈香兰(0512-87161312) xlanchen@ustc.edu.cn Spring 2011
系统调用
系统调用的意义 操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用 把用户从底层的硬件编程中解放出来 极大的提高了系统的安全性 使用户程序具有可移植性 2018/11/23 Linux OS Analysis
API和系统调用 应用编程接口(application program interface, API) 和系统调用是不同的 系统调用通过软中断向内核发出一个明确的请求 Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用) 一般每个系统调用对应一个封装例程 库再用这些封装例程定义出给用户的API 2018/11/23 Linux OS Analysis
不是每个API都对应一个特定的系统调用。 如,一些数学函数 一个单独的API可能调用几个系统调用 不同的API可能调用了同一个系统调用 返回值 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用 -1在多数情况下表示内核不能满足进程的请求 Libc中定义的errno变量包含特定的出错码 2018/11/23 Linux OS Analysis
系统调用程序及服务例程 当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。 在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常 (回忆,trapinit中系统调用入口的初始化) Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持(本课程不考虑这个) 传参: 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数 使用eax寄存器 2018/11/23 Linux OS Analysis
所有的系统调用返回一个整数值。 这里的返回值与封装例程返回值的约定不同 正数或0表示系统调用成功结束 负数表示一个出错条件 内核没有设置或使用errno变量 封装例程在系统调用返回取得返回值之后设置这个变量 当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序 2018/11/23 Linux OS Analysis
系统调用处理程序也和其他异常处理程序的结构类似 在进程的内核态堆栈中保存大多数寄存器的内容 (即保存恢复进程到用户态执行所需要的上下文) 调用相应的系统调用服务例程处理系统调用 sys_xxx 通过ret_from_sys_call()从系统调用返回 2018/11/23 Linux OS Analysis
应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系 syscall_exit: 2018/11/23 Linux OS Analysis
为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。 这个表存放在sys_call_table数组中,有若干个表项(2.6.26中,是326): 第n个表项对应了系统调用号为n的服务例程的入口地址的指针 观察sys_call_table(syscall_table_32.S以及entry_32.S最后) 2018/11/23 Linux OS Analysis
关于系统调用表的大小 2018/11/23 Linux OS Analysis
初始化系统调用总入口 内核初始化期间调用trap_init()函数建立IDT表中向量128对应的表项,语句如下: 该调用把下列值存入这个系统门描述符的相应字段: segment selector 内核代码段__KERNEL_CS的段选择符 Offset 指向system_call()异常处理程序的入口地址 Type 置为15。表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断 DPL(描述符特权级) 置为3。这就允许用户态进程访问这个门,即在用户程序中使用int $0x80是合法的 2018/11/23 Linux OS Analysis
system_call()函数 参见entry_32.S 2018/11/23 Linux OS Analysis
参数传递 系统调用也需要输入输出参数,例如 实际的值 用户态进程地址空间的变量的地址 甚至是包含指向用户态函数的指针的数据结构的地址 system_call是linux中所有系统调用的入口点,每个系统 调用至少有一个参数,即由eax传递的系统调用号 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就 把eax寄存器的值置为2(即__NR_fork)。 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般 不关心系统调用号 进入sys_call之后,立即将eax的值压入内核堆栈 对C库进行反汇编,察看int $0x80 2018/11/23 Linux OS Analysis
很多系统调用需要不止一个参数 普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈 用户态C函数 用户态堆栈 内核态C函数 内核态堆栈 2018/11/23 Linux OS Analysis
在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的 用户态C函数 用户态堆栈 内核态C函数 内核态堆栈 寄存器 SAVE_ALL 2018/11/23 Linux OS Analysis
回想一下在进入中断和异常处理程序前,在内核态堆栈中保存的pt_regs结构,此时pt_regs结构中的一些寄存器被用来传递参数或者pt_regs结构本身就是参数 2018/11/23 Linux OS Analysis
参数传递举例 处理write系统调用的sys_write服务例程声明如下 该函数期望在栈顶找到fd,buf和count参数 在封装sys_write()的封装例程中,将会在ebx、ecx和edx 寄存器中分别填入这些参数的值,然后在进入system_call时, SAVE_ALL会把这些寄存器保存在堆栈中,进入sys_write服务 例程后,就可以在相应的位置找到这些参数 asmlinkage使得编译器不通过寄存器(x=0)而 使用堆栈传递参数 Include/asm-x86/linkage.h 参见SAVE_ALL 2018/11/23 Linux OS Analysis
反汇编/lib/libc.so.6(可能是其他版本号) 197897 000bed90 <__write>: 197898 bed90: 65 83 3d 0c 00 00 00 cmpl $0x0,%gs:0xc 197899 bed97: 00 197900 bed98: 75 1d jne bedb7 <__write+0x27> 197901 bed9a: 53 push %ebx 197902 bed9b: 8b 54 24 10 mov 0x10(%esp),%edx 197903 bed9f: 8b 4c 24 0c mov 0xc(%esp),%ecx 197904 beda3: 8b 5c 24 08 mov 0x8(%esp),%ebx 197905 beda7: b8 04 00 00 00 mov $0x4,%eax 197906 bedac: cd 80 int $0x80 197907 bedae: 5b pop %ebx 197908 bedaf: 3d 01 f0 ff ff cmp $0xfffff001,%eax 197909 bedb4: 73 2d jae bede3 <__write+0x53> 197910 bedb6: c3 ret ………… 反汇编/lib/libc.so.6(可能是其他版本号) 2018/11/23 Linux OS Analysis
1)每个参数的长度不能超过寄存器的长度 ,即32位 由此,使用寄存器传递参数具有如下限制 : 1)每个参数的长度不能超过寄存器的长度 ,即32位 2)在系统调用号(eax)之外,参数的个数 不能超过6个(ebx,ecx,edx,esi,edi, ebp) ?超过6个怎么办? 2018/11/23 Linux OS Analysis
传递返回值 服务例程的返回值是将会被写入eax寄存器中 这个是在执行“return”指令时,由编译器自动完 成的 2018/11/23 Linux OS Analysis
验证参数 在内核打算满足用户的请求之前,必须仔细的检查所有的 系统调用参数 比如前面的write()系统调用,fd参数是一个文件描述符, sys_write()必须检查这个fd是否确实是以前已打开文件的一个 文件描述符,进程是否有向fd指向的文件的写权限,如果有 条件不成立,那这个处理程序必须返回一个负数 2018/11/23 Linux OS Analysis
只要一个参数指定的是地址,那么内核必须检查它是否在 这个进程的地址空间之内,有两种验证方法: 验证这个线性地址是否属于进程的地址空间 仅仅验证这个线性地址小于PAGE_OFFSET 对于第一种方法: 费时 大多数情况下,不必要 对于第二种方法: 高效 可以在后续的执行过程中,很自然的捕获到出错的情况 从linux2.2开始执行第二种检查 2018/11/23 Linux OS Analysis
要防止用户将一个内核地址作为参数传递给内核 ,这将导致它借用内核代码来读写任意内存 对用户地址参数的粗略验证 在内核中,可以访问到所有的内存 要防止用户将一个内核地址作为参数传递给内核 ,这将导致它借用内核代码来读写任意内存 2018/11/23 Linux OS Analysis
2018/11/23 Linux OS Analysis
检查方法: 最高地址:addr+size-1 1、是否超出3G边界 2、是否超出当前进程的地址边界 对于用户进程:不大于3G 2018/11/23 Linux OS Analysis
访问进程的地址空间 系统调用服务例程需要非常频繁的读写进程地址空 间的数据 2018/11/23 Linux OS Analysis
访问进程地址空间时的缺页 内核对进程传递的地址参数只进行粗略的检查 访问进程地址空间时的缺页,可以有多种情况 合理的缺页:来自虚存技术 页框不存在或者写时复制 由于错误引起的缺页 由于非法引起的缺页 2018/11/23 Linux OS Analysis
非法缺页的判定 内核中,只有少数几个函数/宏会访问用户地址空间 对于一次非法缺页,一定来自于这些函数/宏 可以将访问用户地址空间的指令的地址一一列举出 来,当发生非法缺页时,根据引起出错的指令地址 来定位 Linux使用了异常表的概念 __ex_table, __start___ex_table, __stop___ex_table 2018/11/23 Linux OS Analysis
__ex_table的表项 哪条指令访问了用户地址空间 如果这条指令引起了非法缺页,该怎么处理 Fixup所指向的代码,称为修正代码 通常为汇编代码 2018/11/23 Linux OS Analysis
fixup_exception 2018/11/23 Linux OS Analysis
缺页异常对非法缺页的处理 在缺页异常do_page_fault中,若最后发现是非法缺页,就会执行下面的操作 假设找到了修正代码,会发生什么事情? 该操作使用引起出错的代码地址在异常表中进行查找,若找到,就返回 相应的修正代码地址,填写在regs->eip中 2018/11/23 Linux OS Analysis
缺页处理 某内核函数 IDT表, 进入异 常处理 缺页 非法异常 修改堆栈中的eip,指向修正代码 因此,非法缺页时,返回此处 作为一次故障,要重新执行引起出错的代码 修正代码 正常情况下,这个eip在发生异常时, 由硬件保存到堆栈中 因此,正常情况下,返回此处 2018/11/23 Linux OS Analysis
异常表的生成和修正代码 2018/11/23 Linux OS Analysis
举例 2018/11/23 Linux OS Analysis
系统调用的返回 参见中断中的返回 2018/11/23 Linux OS Analysis
中断、异常、系统调用小结 ret_from_fork ret_from_exception iret ret_from_intr syscall_exit iret 内核态 用户态 IDT表 中断 异常 系统调用 2018/11/23 Linux OS Analysis
Project 参见课程主页 2018/11/23 Linux OS Analysis