前言
之前学汇编发现教材和实际的有出入, 书上写的int, 但是汇编不通过,而gcc 反汇编的结果是调用syscall。
原来这是两种方式调用方式即: int 0x80 和 syscall
除此之外还有一个名词是vdso, 很多elf文件会链接这个vdso库
1 | ldd a.out √ 19:03:30 |
词汇说明
int 0x80
即80中断, 是最老的系统函数调用方式syscall/sysret
是amd64 制定的标准, 也是目前的x86 64位的标准,即amd64
sysenter/syssysexit
是inter制定的x86 64位标准, 目前已被放弃vdso
是linux内核虚拟出的so, 实现了int 80 和 syscall,调用方式为 vsyscall
系统函数调用路径
系统调用多被封装成库函数提供给应用程序调用,应用程序调用库函数后,由 glibc 库负责进入内核调用系统调用函数。
即用户函数-> glibc -> 系统调用
int 0x80
int 即是interrupt 中断, 0x80是IDT上注册的中断向量, 每个编号对应一个处理函数handle, linux的0x80的handle即是内核,即系统调用。
所以不同的系统设置的0x80的handle可能不同
调用方式:首先是将参数复制到寄存器, 参数包括系统调用编号和传入参数,然后执行 init 0x80
例如,以下的进程退出的系统调用
1 | .data |
vdos
vdos即 linux-vdso.so.1, 几乎很多elf 都会链接这个库,但其实他并不是真实存在的so文件,
而是由内核虚拟的文件,再映射到用户的进程来调用。
vdos 是对以下几个函数的实现,称作快速调用
1 | #define __NR_gettimeofday 96 //0x60 |
对比
所以以上总结其实就3种方式, int ,syscall/sysret , vdso
int 0x80 方式很慢,所以出现了syscall 即快速调用
执行区别
1 | 在 x86 保护模式中,处理 INT 中断指令时,CPU 首先从中断描述表 IDT 取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别 DPL 和 INT 指令调用者的级别 CPL,当 CPL<=DPL 也就是说 INT 调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用 IRET 指令返回,IRET 指令恢复用户栈,并跳转会低级别的代码。 |
返回的区别
1 | 在 Intel 的手册中,还提到了 sysenter/sysexit 和 int n/iret 指令的一个区别,那就是 sysenter/sysexit 指令并不成对,sysenter 指令并不会把 SYSEXIT 所需的返回地址压栈,sysexit 返回的地址并不一定是 sysenter 指令的下一个指令地址。调用 sysenter/sysexit 指令地址的跳转是通过设置一组特殊寄存器实现的。 |
vdos的局限(syscall)
1 | 而"快速系统调用指令"比起中断方式的系统调用方式,还存在一定局限,例如无法在一个系统调用处理过程中再通过"快速系统调用指令"调用别的系统调用。因此,并不一定每个系统调用都需要通过"快速系统调用指令"来实现。比如,对于复杂的系统调用例如 fork,两种系统调用方式的时间差和系统调用本身运行消耗的时间来比,可以忽略不计,此处采取"快速系统调用指令"方式没有什么必要。而真正应该使用"快速系统调用指令"方式的,是那些本身运行时间很短,对时间精确性要求高的系统调用,例如 getuid、gettimeofday 等等。 |
最后总结
int 是最老的方式,目前用amd64的 syscall 方式, 而vdso是基于syscall实现的快速调用。
只有在调用clock_gettime、gettimeofday、getcpu、time这些系统调用时,才会使用vdso,其他系统调用是通过syscall实现的