为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

system_call

2011-11-09 8页 pdf 47KB 23阅读

用户头像

is_465901

暂无简介

举报
system_call System Call 1 System Call 操作系统的重要任务之一就是为应用程序提供服务,而提供服务的最直接手段就是允许 应用程序能够调用操作系统的相关服务代码。作为程序员,我们知道,一个程序如果想调用 某段代码,它必须知道这段代码的入口地址。对于 DOS这种非保护模式操作系统,如果我 们知道其内核某段代码的入口地址,我们的应用程序只需要执行一条 CALL 指令,就可以 执行这段内核代码,以获取服务。 对于这一点,操作系统的设计原则有两种:即对应用程序是信任的,或是不信任的。如 果操作系统对于...
system_call
System Call 1 System Call 操作系统的重要任务之一就是为应用程序提供服务,而提供服务的最直接手段就是允许 应用程序能够调用操作系统的相关服务代码。作为程序员,我们知道,一个程序如果想调用 某段代码,它必须知道这段代码的入口地址。对于 DOS这种非保护模式操作系统,如果我 们知道其内核某段代码的入口地址,我们的应用程序只需要执行一条 CALL 指令,就可以 执行这段内核代码,以获取服务。 对于这一点,操作系统的原则有两种:即对应用程序是信任的,或是不信任的。如 果操作系统对于应用程序总是信任的,那就意味着操作系统认为应用程序总是按照它们之间 的约定或协议在做事情,应用程序永远不会违反这些协议,无论是有意的还是无意的;但很 明显,即使一个程序员总是在善意的写程序,他也无法保证其程序没有错误,无论其水平有 多高,经验如何丰富;而对于恶意的程序,由于这种操作系统几乎不设防,一个水平不用太 高的程序员就可以很轻松的写一个程序让系统崩溃。这正是 DOS时代病毒肆虐的原因。对 于一个单任务(Single-Task)的操作系统来说,当问程序运行引起系统崩溃时,只会影 响当前存在问题的任务。但对于多任务操作系统来说,一个问题程序引起其它健康程序无法 运行,无疑是一种非常不公平的结果。 所以,今天运行在 PC 机或更高平台(包括工作站,小型机,大型机)的操作系统对应 用程序几乎都是采取不信任原则,也就是说,操作系统假定运行在其上的任何应用程序都可 能是不安全的,这种类型的操作系统非常保守,尽其所能对内核以及其它应用程序给予保护, 任何不遵守协议的应用程序都无法正常运行,却又不会影响操作系统自身的安全,也不会影 响其它正常应用程序的运行。一个安全的社会总需要完善的法制来保障,这就是生活。 在具有保护模式的操作系统中,操作系统内核运行在特权级别(内核态),而用户应用 程序运行在非特权级别(用户态)。应用程序无法直接调用任何操作系统的代码,即使应用 程序非常清楚这些代码的入口地址,但由于硬件给予的保护,应用程序对于这些入口地址的 调用,会引起异常。操作系统会捕获到这个异常,并在这个异常的处理中将进行非法调用的 应用程序杀掉。 但另一方面,操作系统必须向应用程序提供服务,否则,应用程序几乎无法作任何有价 值的事情,甚至无法运行。既然直接调用的会导致系统的不安全,那么只能进行间接调 用,那就是使用中断机制。 即使对于非保护模式的 DOS来说,它也是通过中断的方法向应用程序提供服务,这就 是 DOS程序员熟悉的 INT 21H。事实上,通过中断的方法,应用程序无需知道相应的操作 系统服务例程的入口地址。因为中断服务程序知道它们,而这一切都是由操作系统设定和维 护的。应用程序只需要设定好相应的寄存器,然后执行一条 INT 指令就可以对这些操作系 统服务例程进行调用,并通过寄存器获取执行的结果。 2 Developing Your Own Unix-Like OS on IBM PC 对于保护机制的操作系统来说,中断机制本身也是受保护的,在 IBM PC 上,Intel 规 定多达 255个中断号,但只有授权给应用程序保护等级的中断号才是可以被应用程序调用 的,对于未被授权的中断号,如果应用程序进行调用,同样会引起保护异常,而导致自己被 操作系统杀死。比如,Linux仅仅给应用程序授权了 4个中断号— — 3,4,5,以及 80h。 前三个中断号是为了提供给应用程序调试(单步跟踪)的手段,而 80h正是我们本节讨论 的系统调用(system call)的中断号。 应用程序在执行用户态代码时,被称为运行在用户态;在通过系统调用执行内核服务代 码时,被称为运行在内核态— —这是绝大多数基于整体内核实现的各种 Unix的实现模型。 在这种模型里,系统调用形象的被称作应用程序的陷阱(Trap)。应用程序通过系统调用从用 户态陷入内核态,就像一个人从路面行走,突然掉入了陷阱,而应用程序重返用户态,就像 这个人从陷阱中爬了出来,非常有趣!某些硬件系统有专门的陷阱指令,其机制仍然是中断 机制,或类似于中断机制。 对于基于消息机制的微内核操作系统来说,当应用程序需要申请内核服务的时候,是通 过系统调用通知内核向某个内核服务进程发送一条消息,然后等待一条来自于目标进程的应 答消息。这是一种 Client/Server 模式。如 Figure 1所示。 1. Mechanism 前面我们已经讨论,系统调用是通过中断机制实现的,并且一个操作系统的所有系统调 用都通过同一个中断来实现。如下的一些例子都是 Unix系统的系统调用函数: Call User Kernel User Kernel Send Receive Receive Send A: 整体内核模型 B: 基于消息机制的微内核模型 Figure 1 – System Call Model Ÿ int fork(); Ÿ void exit(int status); Ÿ int open(char* path, int flags); Ÿ int lseek(int fildes, off_t offset, int wherence); System Call 3 从这些例子可以看出,它们有 3个特点: Ÿ 参数个数不固定,可以没有参数,也可以有多个参数; Ÿ 函数的返回值要么为 void,要么为 32-bit 类型(integer); Ÿ 函数的形式参数都为 32-bit 类型; DOS程序员都知道如何通过 INT 21H 来进行 DOS功能调用,应用程序只需要将功能 号装入%ax寄存器,将参数(如果存在的话)按照需要装入%bx,%cx,%dx,%si,%di 寄存器,然后调用 INT 21H 指令。等中断服务程序完成后,%ax 寄存器用来存放返回值(如 果有的话),其它寄存器用来存放执行结果(如果存在的话)。依赖于 Intel 80x86芯片的 通用寄存器只有%eax,%ebx,%ecx,%edx,%esi,%edi等 6个寄存器,其中%eax要用 来存放功能号,使用这种手段最多只能允许有 5个参数。 1. 设置相应的通用寄存器内容; 2. 调用中断指令“int $0x80”,陷入操作系统内核,调用相应的 ISR; 3. ISR将各个寄存器按照一定顺序压栈; 4. 随后,ISR以%eax寄存器的值为索引,到系统调用函数入口表中进程查找并调用; 5. 从 Stack中恢复系统调用前寄存器的值; 6. 通过 iret指令返回用户态; 7. 通过%eax寄存器取得系统调用返回值。 Figure 2 – Linux System Call Handling Procedure Function Code Parameter 1 Parameter 2 Parameter 3 Parameter 4 Parameter 5 %eax %ebx %ecx %edx %esi %edi (1) (2) User Kernel function 1 entry function 2 entry function 3 entry ⋯⋯ Function n-1 entry function n entry (4) 系统调用函数入口查找表 内核栈 (3) %ebp %edi %esi %edx %ecx %ebx %eax Other Registers %esp (5) (6) (7) 4 Developing Your Own Unix-Like OS on IBM PC 保护模式的操作系统也可以使用相同的手段来进行系统调用的处理——通过通用寄存 器来传递系统调用所需要的参数。 我们以 Linux为例,如 Figure 2所示,一个 Application在进行系统调用时,它使用 %eax存放系统调用功能号,在内核中每一个功能号对应一个系统调用功能,比如0对应exit, 1对应 fork,2对应 read。下面列出的就是部分 Linux系统调用号的定义。 如果对应的系统调用函数需要参数,那么应用程序需要按照此函数要求的参数个数分别 设置%ebx,%ecx,%edx,%esi,%edi。这种方法所允许的参数也最多为 5个。为了能够 定义6个参数的系统调用功能函数,Linux使用了%ebp,但%ebp对于函数来说是一个非常 重要的寄存器,所以使用前必须进行压栈保存。由于这些寄存器都是 32-bit 宽度的,所以要 求参数也都应该为 32-bit 类型的。当所需寄存器设置完成之后,通过调用指令”int $0x80”, 系统陷入内核。 下面是 Linux带有 3个参数的系统调用的宏定义: 这个宏被定义之后,可以使用它来定义一个被 Application使用的 3参数的系统调 用函数,下面就是一个系统调用 write的定义。 Linux捕获到这个中断后,将执行其对应的中断服务程序(system_call),system_call 首先将必要的寄存器(包括这 6个通用寄存器)push到栈中。然后以%eax中的值作为索引 #define __NR_exit 1 /* exit 的系统调用号 */ #define __NR_fork 2 /* fork 的系统调用号 */ #define __NR_read 3 /* read 的系统调用号 */ #define __NR_write 4 /* write 的系统调用号 */ #define __NR_open 5 /* open 的系统调用号 */ #define __NR_close 6 /* close 的系统调用号 */ #define syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1,type2 arg2,type3 arg3) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name), \ "b" ((long)(arg1)), \ "c" ((long)(arg2)), \ "d" ((long)(arg3))); \ __syscall_return(type,__res); \ } static inline syscall3(int,write,int,fd, \ const char *,buf,off_t,count) System Call 5 到其所维护的系统调用功能函数入口表(sys_call_table)中查找并调用相应函数。等系统调用 完成后,Linux内核将返回值装入%eax寄存器(如果不是 void 的话),但不用其它寄存器 来返回执行结果。 Linux通过汇编语言使用静态的方法定义了系统调用函数入口表 sys_call_table,下面 就是相关的代码,为了节约篇幅,这里省去了绝大多数入口定义。 下面是 Linux 2.4代码中用来保存相关寄存器内容的 Macro。 由于函数参数传递的方式是将参数 Push到 Stack中,被调用函数执行时到 Stack 中访 问相关参数,所以 SAVE_ALL 中将各个寄存器压栈的顺序是经过精心安排的,不能够随意 更改。我们看栈顶的6个寄存器依次为%ebx,%ecx,%edx,%esi,%edi,%ebp。对于不 需要参数的系统调用功能函数来说,它不需要访问任何保存在栈中的寄存器的值;对于需要 1个参数的寄存器来说,栈顶%ebx寄存器的内容就是它所需的参数的值;对于需要2个参 /* 此 Macro用来保存和设置所有相关寄存器*/ #define SAVE_ALL \ cld; \ pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__KERNEL_DS),%edx; \ movl %edx,%ds; \ movl %edx,%es; .data ENTRY(sys_call_table) .long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) /* 为了节约篇幅,这里省略的其它函数入口数据 */ .rept NR_syscalls-(.-sys_call_table)/4 .long SYMBOL_NAME(sys_ni_syscall) .endr 6 Developing Your Own Unix-Like OS on IBM PC 数的寄存器来说,栈顶的%ebx,%ecx 寄存器的内容就是它所需的2个参数的值,其中%ebx 对应第一个参数,%ecx对应第2个参数… … 依次类推,对于需要6个参数的寄存器来说, 栈顶的6个寄存器的内容就依次是它所需的6个参数的值。 下面是 Linux 2.4中 80h对应的中断服务例程 system_call的实现。 假如系统只提供了 100个系统调用功能函数(0至 99),而应用程序通过%eax 寄存器 传入的系统调用功能号为 100以上的值,这很显然是一个非法调用,直接返回-ENOSYS错 误。 当系统调用表(sys_call_table)中的某个系统调用功能函数被执行后,由于此函数运行在 内核态,内核当然应该信任自身的任何代码,所以它可以访问任何资源,可以调用任何操作 系统内核中的函数。此时我们认为用户应用程序运行在内核态。 当内核执行了相应的系统调用函数之后,使用%eax保存相应的返回值,然后从 Stack 中 pop出各个之前压栈的寄存器的内容,最后执行指令 iret,返回到用户态。下面是 Linux 恢复寄存器内容的 Macro——RESTORE_ALL的定义。 /* 为了便于理解,system_call中几条与此关键机制 * 无关的指令被删去 */ ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL # %eax中存放的系统调用号必须为一个存在的号 cmpl $(NR_syscalls),%eax jae badsys # 否则返回-ENOSYS错误 # 以%eax为索引,调用相应系统调用函数 call *SYMBOL_NAME(sys_call_table)(,%eax,4) movl %eax,EAX(%esp) # save the return value badsys: movl $-ENOSYS,EAX(%esp) jmp ret_from_sys_call #define RESTORE_ALL \ popl %ebx; \ popl %ecx; \ popl %edx; \ popl %esi; \ popl %edi; \ popl %ebp; \ popl %eax; \ popl %ds; \ popl %es; \ addl $4,%esp; \ iret; System Call 7 在入口 ret_from_sys_call处,上面定义的宏 RESTORE_ALL 被调用。下面就是入口 ret_from_sys_call的代码。 上面代码中,第 2,3行是关于进程调度的相关代码,第 4,5行是信号处理的相关代码, 这些会在相关章节中讨论,这里我们就不再赘述。 最后,当 iret 指令执行后,系统返回用户态,由于系统调用返回前,曾经将系统调用的 返回值保存在%eax寄存器中,所以此时用户进程可以通过%eax获取到返回值,并保存到 全局变量 errno 中。比如带有一个参数的系统调用 Macro 定义为: 其中,__syscall_return定义为: 0 ENTRY(ret_from_sys_call) 1 cli # need_resched and signals atomic test 2 cmpl $0,need_resched(%ebx) 3 jne reschedule 4 cmpl $0,sigpending(%ebx) 5 jne signal_return 6 restore_all: 7 RESTORE_ALL #define _syscall1(type,name,type1,arg1) \ type name(type1 arg1) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1))); __syscall_return(type,__res); \ } #define __syscall_return(type, res) \ do { \ if ((unsigned long)(res) >= (unsigned long)(-125)) { \ errno = -(res); \ res = -1; \ } \ return (type) (res); \ } while (0) 8 Developing Your Own Unix-Like OS on IBM PC 2. Conclusion 本章我们结合 Linux 2.4的实现,讨论了 System Call的实现原理和机制。在不同的平 台上,System Call的实现会有一些细节上的差别,但基本原理是一致的。 搞清楚 System Call 的原理,对我们进一步了解保护机制的操作系统原理非常重要。
/
本文档为【system_call】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索