1. 在执行系统调用时,内核一般返回0表示成功,负数表示失败。用户态的包装函数将内核返回的错误码取负,设置到errno中。
2. 对于没有实现的系统调用,sys_call_table中会存放sys_ni_syscall函数,返回-ENOSYS。
3. 进入系统调用有两种方法:
(1) int 0x80
(2) sysenter
退出也有两种方式:
(1) iret
(2) sysexit
4. int 0x80 方式:
当系统调用结束后,将返回值写入eax所在的内核栈中,之后内核会检查thread_info中的flags,看是否有重新调用等额外的工作要做。最后恢复用户态的寄存器,然后通过
iret返回用户态。
5. sysenter 方式:
当调用sysenter时,必须填充三个寄存器 (1) SYSENTER_CS_MSR, 内核代码段选择子 (2) SYSENTER_EIP_MSR,内核入口地址 (3)SYSENTER_ESP_MSR,
内核栈指针。
6. 内核的堆栈段和数据段是共用的,并且紧挨着内核代码段。
7. enable_sep_cpu在内核初始化的过程中初始化将__KERNEL_CS写入SYSENTER_CS_MSR寄存器,将sysenter_entry函数的线性地址写入SYSENTER_CS_EIP寄存器,
将当前cpu TSS的末尾地址写入SYSENTER_CS_ESP中。之后发生系统调用时,切换到内核态后,系统调用处理函数读取esp,然后获取TSS 中esp0的地址,再从esp0中
读取真正的内核栈地址。
8.为了解决两种调用方式的兼容问题,在内核初始化过程中sysenter_setup函数创建了一个页,叫vsyscall page。里面包含有一个小的ELF共享库。当进程通过execve系统调用 来起动一个elf程序时,vsyscall page中的代码就会动态第链接到进程的地址空间。vsyscall page中的代码会选择最优的方式发起系统调用。
9. vsyscall page 中定义了__kernel_vsyscall和 SYSENTER_RETURN,根据是否支持sysenter来实现该函数。用户态的库发起系统调用时最终会调用__kernel_vsyscall, 退出系统调用时切换回用户态后从SYSENTER_RETURN 开始执行。
10. 系统调用时,参数会先被保存到cpu寄存器中,然后切换到内核态后,从寄存器中复制到内核栈中,供内核的C函数使用。参数不能超过6个,大于6个的话,需要传递栈指针。
11. 在进行参数检查时,内核只需要确定用户传递的地址是否属于用户态的地址范围。更精细的检查会在页故障中进行。
12. 内核通过函数access_ok(verify_area)来检查地址是否合法,该函数内部会检查地址是否越过addr_limit.seg,这个值一般是0xbfffffff。通过get_fs 和set_fs 可以更改这个值。
13. 发生页故障的三种情况:
(1)内核去访问一个属于该进程的页,但是该页面不存在或者是只读,内核必须分配一个新页。
(2)内核去访问一个属于该进程的页,但是该页对应的页表不存在,内核必须建立页表。
(3)内核bug或者是硬件的错误,内核会oops。
(4)内核在处理系统调用过程中,去读去用户穿过来的地址,但是该地址并不在进程的地址空间内。
14. 由于内核访问用户空间的函数比小少,因此可以将这些函数的指令地址记录在一个异常表中,当发生页故障时,只要检查出错的指令是否在异常表中,就可以确定是否是由于系统调用参数异常导致。
15. exception_table_entry结构中包含一个指令的地址和fixup代码的地址。