系统调用介绍
间接调用
Linux系统调用是用中断门来实现的,通过 int 0x80 来实现。在之前要在寄存器eax 中写入子功能号。syscall的原型是 int syscall(int number,...),所有的系统调用都可以通过一个函数完成。函数syscall()并不是由操作系统提供的,它是由C运行库 glibc 提供的。syscall属于“间接”的,它是库函数实现了对系统调用接口的封装。系统调用的本质就是:提供子功能号给eax和参数给要求寄存器,然后int 0x80即可。函数 syscall( ) 是将这些封装在函数实现内部。如:syscall(SYS_getpid)//SYS_getpid 是个宏,表示20
直接调用
直接的系统调用是利用操作系统提供的一系列宏函数:_syscall[x](现在已经不使用了)。它是一系列的宏函数,用_syscallX来表示,X表示系统调用中参数的个数,原型为:
_syscallX(type,name,type1,arg1,type2,arg2,...)
Linux实现了7个不同的宏,分布是_syscall[0-6],因此对于参数个数不同的系统调用,要调用不同的宏来完成。其中 type 是系统调用的返回值类型,name是系统调用的名称(字符串),最后会转换为数值型的子功能号。typeN 和 argN配对出现,表示变量的类型和变量名。
在Linux中系统调用是用寄存器来传递参数的,参数需要按照从左到右的顺序存入不同的寄存器中。其中,eax用来保存子功能号,ebx保存第一个参数,ecx保存第2个参数,edx保存第3个参数,esi保存第4个参数,edi保存第5个参数。
系统调用的实现
一个系统调用分为两个部分,一部分是给用户进程的接口函数,属于用户空间。另一部分是与之对应的内核具体实现,属于内核空间,此部分完成的是功能需求,即我们一直所说的系统调用子功能处理函数。为了区分,内核空间的函数名要在用户空间的函数名前面加 “ sys_” 。所以当我们实现时候,是要在用户空间通过 int 0x80,产生中断进入中断入口程序,然后再次进入真正的中断处理函数中。所以我们要编写中断向量号 0x80 的入口程序,即在 idt 中断描述符表中写好中断程序的选择子和偏移地址。
增加 0x80 中断向量号
make_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler);
注意:这个中断描述符的DPL=3,这样用户进程的int 中断才能进去,之前的时钟中断、键盘中断等的中断描述符的DPL都是0特权级的,其实用3特权级就可以了。
实现系统调用接口
Linux中是实现 _syscall[0-6]进行系统调用。我们实现_syscall[0-3]这几个够用即可。
只写三个参数的系统调用,其他的和这个类似。大括号括住的宏函数最后一个语句的值会作为大括号代码块的返回值,并且要在最后一个语句后添加分号;
/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3) \
: "memory" \
); \
retval; \
})
增加0x80号中断处理例程
CPU首先进入中断入口程序,然后作为主调函数:压入参数push,call 调用函数的地址,add esp xxx。因为系统调用参数的不一样,有时候是3个参数,有时候是一个,但是我们调用函数时候都统一在栈中压入3个参数。在调用中断处理函数时候,eax中储存的是子功能号,因此我们利用这个特性去调用子功能号函数。事先我们将这些子功能号函数都写好,然后将地址都分别填入 syscall_table[] 数组中,然后 call [syscall_table+eax*4]即可。
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境
push 0 ; 压入0, 使栈中格式统一
push ds
push es
push fs
push gs
pushad
push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式
;2 为系统调用子功能传入参数
push edx ; 系统调用中第3个参数
push ecx ; 系统调用中第2个参数
push ebx ; 系统调用中第1个参数
;3 调用子功能处理函数
call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数
add esp, 12 ; 跨过上面的三个参数
;4 将call调用后的返回值存入待当前内核栈中eax的位置
mov [esp + 8*4], eax
jmp intr_exit ; intr_exit返回,恢复上下文
然后我们是通过用户进程来调用系统调用的,系统调用封装在C语言的库函数中,比如
uint32_t getpid()
{
return _syscall0(SYS_GETPID)
}
通过 getpid()将调用系统调用接口,_syscall[0-3](xxx),函数参数是子功能号和所需参数。因此要提前分配好函数的子功能号并且实现好子功能号中断函数。
总结
1,为将要实现的系统调用分配子功能