一、Linux 系统调用
Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信息。Linux 只占用一个中断向量号,即 0x80
,处理器执行指令 int 0x80
时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前,Linux 在寄存器 eax 中写入子功能号。当用户程序通过 int 0x80
进行系统调用时,对应的中断处理例程会根据 eax 的值来判断用户进程申请那种系统调用。
我们来看 syscall 的 man 手册
1 | syscall - indirect system call |
这里的 syscall 是间接的(indirect system call
)。就是 C 库函数
1 | _syscall - invoking a system call without library support (OBSOLETE) |
这里的 _syscall
是系统调用,但是他是过时的(obsolete)
二、系统调用实现
一个系统功能调用分为两部分
- 一部分是暴露给用户进程的接口函数,它属于用户空间,此部分知识用户进程使用系统调用的途径,只负责发需求
- 另一部分是与之对应的内核具体实现,它属于内核空间,此部分完成的是功能需求,就是系统调用子功能处理函数
系统调用的实现思路
- 用中断门实现系统调用,效仿 Linux 用 0x80 号中断作为系统调用的入口
- 在 IDT 中安装 0x80 号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程
- 建立系统调用子功能表 syscall_table,利用 eax 寄存器中的子功能号在该表中索引相应的处理函数
- 用宏实现用户空间系统调用接口
_syscall
,最大支持 3 个参数的系统调用。使用寄存器传参,eax 为子功能号,ebx、ecx、edx 依次保存参数
系统调用的接口如下:
1 | #define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ |
这段内联汇编,输入参数,eax 表示子功能号,ebx、ecx、edx 分别是三个参数,调用 0x80 中断,最终结果通过 eax 输出。
我们再来看一下 0x80 号中断处理例程:
1 | syscall_handler: |
先压入中断错误码、然后保存任务的上下文,接着显式压入了中断号 0x80。
接下来是为子功能函数准备参数,由于目前只支持 3 个参数的系统调用,因此只压入了 3 个参数。按照 C 调用约定,最右边的参数先入栈,因此压入参数的顺序是 edx、ecx、ebx。注意,我们不管具体系统调用中参数是几个,一律压入 3 个参数。因为在函数体中,编译器生成的取参数指令是从栈顶往上(跨过栈顶的返回地址,由高地址方向)获取参数的,参数个数是通过函数声明事先确定好的,因此并不会获取到错误的参数,从而保证了多余的参数用不上。因此尽管压入了 3 个参数,但对于那些参数少于 3 个的函数也不会出错,仅仅是浪费了一点栈空间。
寄存器 eax 中是系统调用子功能号,数组 syscall_table 中存储的是函数地址,每个成员是 4 个字节大小。因此通过偏移量来调用子功能处理函数。调用完之后,通过 add esp, 12
跨过这三个参数(edx、ecx、ebx)。
mov [esp + 8*4], eax
就是将返回值写到了栈(此时是内核栈)中保存 eax 的那个内核空间。8*4
相当于跳过了 0x80,ECX,EDX,EBX,ESP,EBP,ESI,EDI
这些,找到了 eax 的位置。为什么这么做呢?
我们要把返回值传给用户进程,但是从内核态退出时,要从内核栈中恢复寄存器上下文,这会将当前 eax 的返回值覆盖。那么,我们把寄存器 eax 的值回写到内核栈中用于保存 eax 的内存处,这样从内核返回时,popd 指令也只是用该返回值重新覆盖一个 eax 寄存器,返回到用户态时,用户进程便获取到了系统调用函数的返回值。
最后从中断出口函数 intr_exit
返回。