系统调用
预备知识
目标:了解系统调用的流程,在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。
对应章节:同济大学赵炯博士的《Linux内核0.11完全注释(修正版V3.0)》的第5.5节
下面就针对这一节做一些笔记
系统调用的概念
系统调用(通常称为 syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口。这些接口为了应用级的代码能够在不同的操作系统上都可以运行,就需要有标准去限制系统调用接口的标准。不同操作系统的系统调用接口都要按照这个标准来。
POSIX(Portable Operating System Interface for Computing Systems)
由IEEE开发,是一个标准族: 1003.1, 2003…于保证编制的应用程序可以在源代码一级上在多种操作系统上移植和运行。
那用户态程序想要访问内核资源怎么办呢?
- 从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断
int 0x80
- 并在
eax
寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。- 但是,这个太麻烦了还需要记住系统调用的功能号,还需设置中断,标准接口定义的 C 函数库中的函数间接地使用内核的系统调用,C 函数库已经帮封装好了
- 系统调用的实现:(C 函数库已经给封装好了)
- (1) 用户程序中写上一段包含int指令的代码
- (2) OS写中断处理代码,获取想调程序的编号(系统调用编号)
- (3) OS根据编号转去执行相应的代码
上面提到的系统调用的功能号
和系统调用编号
是什么?
指的是同一个东西:在 Linux 内核中,每个系统调用都具有唯一的一个系统调用功能号。
这些号定义在文件include/unistd.h 中第 60 行开始处。例如,write 系统调用的功能号是 4,定义为符号__NR_write。
// 以下是内核实现的系统调用符号常数,用于作为系统调用函数表中的索引值。( include/linux/sys.h ) #define __NR_setup 0 /* used only by init, to get system going */ /* __NR_setup 仅用于初始化,以启动系统 */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4
这些系统调用号对应于 include/linux/sys.h 中定义的系统调用处理程序指针数组表 sys_call_table[]中项的索引值。因此 write()系统调用的处理程序指针就位于该数组的项 4 处。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71) extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137) extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208) extern int sys_read (); // 读文件。 (fs/read_write.c, 55) extern int sys_write (); // 写文件。 (fs/read_write.c, 83) // 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open,省略了 };
这里有很多的系统调用处理函数,他们有几个相同点:
系统调用执行的结果返回值。通常负值表示错误,而 0 则表示成功。
在出错的情况下,错误的类型码被存放在全局变量 errno 中。通过调用库函数 perror(),我们可以打印出该错误码对应的出错字符串信息。
系统调用处理过程
现在以read
系统调用为例,来说明这个过程
首先,在
include/unistd.h
文件里面有个read函数的声明// 对应各系统调用的函数原型定义。int read(int fildes, char * buf, off_t count);
但是,操作系统是负责写系统调用的,是要提供
系统调用sys_read()
,在文件fs/read_write.c
中。接下来给出从read()->sys_read()
的具体流程。前面给出了
read()
的声明,下面得找到定义,Linux0.11的源码好像没有找到,但是在lib
文件夹有一些其他的类似的函数定义例如write()
。代码如下,这里用到了_syscall3宏
,把这个宏展开之后,得到write()
的定义。write.s文件 #include <set_seg.h> #define __LIBRARY__ // Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。 // 如定义了__LIBRARY__,则还包括系统调用号和内嵌汇编_syscall0()等。 #include <unistd.h> 写文件系统调用函数。 // 该宏结构对应于函数:int write(int fd, const char * buf, off_t count) // 参数:fd - 文件描述符;buf - 写缓冲区指针;count - 写字节数。 // 返回:成功时返回写入的字节数(0 表示写入0 字节);出错时将返回-1,并且设置了出错号。 _syscall3(int,write,int,fd,const char *,buf,off_t,count)展开之后: int write(int fd, const char *buf, off_t count) {long __res;__asm__ volatile ("int $0x80": "=a" (__res): "0" (__NR_write), "b" ((long)(fd)), "c" ((long)(buf)), "d" ((long)(count)));if (__res >= 0)return (int) __res;errno = -__res;return -1; }
接下来介绍
宏_syscalln()
:这个宏完成了c标准库函数的定义,也就是对相关的系统调用sys_函数名
的封装。其中 n 代表携带的参数个数,可以分别 0 至 3。最多可以直接传递 3 个参数。其中寄存器 eax 中存放着系统调用号,而携带的参数可依次存放在寄存器 ebx、ecx 和 edx 中。所以用户程序能够向内核最多直接传递3个参数,当然也可以不带参数。下面给出
_syscall3()
的代码#define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \ __asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \ if (__res>=0) \return (type) __res; \ errno=-__res; \ return -1; \ }
依然以read为例来说明宏_syscall3()的作用
GCC 内联汇编(AT&T格式)学习资源:https://zhuanlan.zhihu.com/p/606376595
#define --LIBRARY-- #include <unistd.h> -syscall3(int, read, int, fd, char *, buf, int, n) 展开之后int read(int fd, char *buf, int n) {// 存储系统调用返回值long __res;// 内联汇编触发系统调用,volatile:编译器不优化__asm__ volatile ("int $0x80" // 触发系统调用的中断指令: "=a" (__res) // 输出,系统调用返回值存入 __res "=a" 表示使用寄存器 eax 来传递返回值,"=" 表示只写操作: "0" (__NR_read), // 输入,"0" 表示使用和输出操作数列表中编号为 0 的操作数相同的寄存器(即 eax)"b" ((long)(fd)), // ebx 传递文件描述符"c" ((long)(buf)), // ecx 传递缓冲区指针"d" ((long)(n)) // edx 传递要读取的字节数);// 系统调用成功,返回读取字节数if (__res >= 0)return (int)__res;// 系统调用失败,设置错误码errno = -__res;return -1; }
接下来,将展开后的代码分成三部分
首先,传入给
eax
寄存器一个系统调用号__NR_read
,这个前文已经说明,__NR_read=3
是sys_read()
这个系统调用函数在sys_call_table[]
中项的索引值,有了这个才能定位到系统调用函数。输出的结果也使用寄存器eax
来传递返回值
int $0x80
:进入系统调用中断程序在kernel/system_call.s
中。下面是中断代码:`kernel/system_call.s`中 # ====================== 系统调用处理流程 ====================== # # 入口:用户态执行int 0x80后CPU跳转至此 .align 2 system_call:cmpl $nr_system_calls-1,%eax # 1. 校验系统调用号范围ja bad_sys_call # 超过最大值跳错误处理# ==== 保存用户态上下文 ==== #push %ds # 2. 保存用户数据段push %es # 保存用户附加段push %fs # 保存特殊用途段pushl %edx # 系统调用参数3pushl %ecx # 参数2pushl %ebx # 参数1# ==== 设置内核环境 ==== #movl $0x10,%edx # 3. 内核数据段选择子(GDT[2], RPL=0)mov %dx,%ds # 设置ds/es为内核数据段mov %dx,%esmovl $0x17,%edx # 用户数据段选择子(LDT[2], RPL=3)mov %dx,%fs # fs保持用户数据访问能力# ==== 执行系统调用 ==== #call *sys_call_table(,%eax,4) # 4. 查系统调用表执行对应函数# eax*4因为函数指针占4字节# ==== 调度检查 ==== #pushl %eax # 保存系统调用返回值movl current,%eax # 获取当前进程task_struct指针cmpl $0,state(%eax) # 5. 检查进程状态(0=就绪态)jne reschedule # 非就绪态跳调度cmpl $0,counter(%eax) # 检查时间片剩余je reschedule # 时间片耗尽跳调度# ==================== 系统调用返回路径 ==================== # ret_from_sys_call:movl current,%eax cmpl task,%eax # 6. 是否是初始任务0?je 3f # 是则跳过信号处理cmpw $0x0f,CS(%esp) # 检查原CS是否是用户态代码段jne 3fcmpw $0x17,OLDSS(%esp) # 检查原SS是否是用户态堆栈段jne 3f# ==== 信号处理 ==== #movl signal(%eax),%ebx # 7. 获取待处理信号位图movl blocked(%eax),%ecx # 获取阻塞信号掩码notl %ecx # 反转掩码(允许通过的信号)andl %ebx,%ecx # 计算有效信号bsfl %ecx,%ecx # 扫描最低有效位je 3f # 无信号则跳过btrl %ecx,%ebx # 清除已处理信号位movl %ebx,signal(%eax) # 更新信号位图incl %ecx # 信号编号转为1-basedpushl %ecx # 参数压栈call do_signal # 8. 调用信号处理函数popl %eax# ==== 恢复上下文 ==== # 3: popl %eax # 恢复系统调用返回值popl %ebx # 9. 逆向弹出寄存器popl %ecxpopl %edxpop %fspop %espop %dsiret # 10. 中断返回用户态# ====================== 错误处理路径 ====================== # .align 2 bad_sys_call:movl $-1,%eax # 返回-1表示错误iret # 直接返回用户态# ====================== 进程调度路径 ====================== # .align 2 reschedule:pushl $ret_from_sys_call # 将返回地址压栈jmp schedule # 跳转到调度函数
这里的int 0x80是系统调用中断是在哪设置呢?为什么int 0x80就跳到_system_call执行呢?
- 通常,异常中断处理过程(int0 --int 31)都在 traps.c 的初始化函数中进行了重新设置(kernl/traps.c,181)
- 而系统调用中断 int128 则在调度程序初始化函数中进行了重新设置(kernel/sched.c,385)。
// 调度程序的初始化子程序。 void sched_init (void) {// 设置时钟中断处理程序句柄(设置时钟中断门)。set_intr_gate (0x20, &timer_interrupt);// 修改中断控制器屏蔽码,允许时钟中断。outb (inb_p (0x21) & ~0x01, 0x21);// 设置系统调用中断门。set_system_gate (0x80, &system_call); }
上面的中断系统调用函数中
call [_sys_call_table + eax * 4]
,直接跳转到sys_read()函数位置完成了系统调用。下面给出整个
read()->sys_read()
的简易流程图┌─────────────┐ ┌───────────────┐ │ 用户态 │ │ 内核态 │ ├─────────────┤ ├───────────────┤ │ 1. read() │ │ │ └──────┬──────┘ │ │▼ │ │ ┌──────┴───────┐ │ │ │ 2. int 0x80 ├───中断───▶ │3. system_call│ │ (NR=3 in EAX)│ │ - 检查NR有效性 │ └──────────────┘ │ - 保存寄存器 │▼ │┌─────┴─────┐ ││4.查系统调用表 | │sys_call_table │ │[NR=3] → sys_read │└─────┬─────┘ ││ │▼ │┌─────┴─────┐ ││5.sys_read() │└─────┬─────┘ ││ │▼ │┌─────┴─────┐ ││ 返回用户态 │ ││ (iret指令) │ │└───────────┘ │