JOS 系统调用的过程
2015-09-14 20:15
162 查看
系统调用,是用户态进程转向内核态的一种安全机制,在保证了内核空间安全并且不被破坏的的前提下,让用户态程序可以实现一定的功能。
通过JOS系统,来看系统调用的具体过程。
系统调用,在语言层面来看,其实可以把系统调用看成是一种函数的调用。只是这种函数调用,不同于一般的用户态下的函数调用,用户态下,函数调用,只需要用栈来保存各种信息就可以了,因为调用前后,保存的信息的栈都是用户地址空间下的栈,所以只需要ESP和EBP两个寄存器相互配合,就可以很好的实现函数调用的功能了。在用户态下调用函数,栈是如何保存信息的,可以看看:/article/1822432.html
系统调用的过程,包括用户进程转到内核态,然后由内核态转回用户进程。和上面的函数调用不同的是,在进行系统调用的时候,需要的是内核地址下的栈来保存程序的相关信息,包括各个寄存器的值,地址空间的值等等。所以,在这个过程中,就存在栈的切换,进入内核后,用户栈需要切换为内核栈,而回到用户进程后,内核栈就需要切换为用户栈的状态。
之所以要用内核栈,个人理解,是因为在用户进程陷入内核中后,在内核中,也有函数的调用等操作,所以内核也需要栈来存放变量等。而之所以要把进程信息保存进内核栈中,个人感觉是因为,在内核中,很容易发生进程之间的切换,这样这些信息统一放在一个内核中中,这样比较好管理,可以很方便的调用提取这些信息。
下面来看一下具体的系统调用是如何实现的。
首先,看一下在JOS里面的进程描述符:
下面,跟踪一个具体的程序,来看看系统调用的过程是怎么样的:
hello:
上面这个是最简单的程序,就是输出一个hello world。
再上面的程序中,调用了一个系统进程sys_getenvid()——这些系统调用都是JOS系统里面的,和linux的可能有些不同。
直接跳过跳过前面的代码,直接来看系统调用的相关代码:
上面的sys_getenvid()是众多系统调用中的一个函数,在JOS中,所有的系统调用全部通过syscall()这个函数来完成相应的功能。通过syscall()中的第一个参数num,来确定到底使用的是那个系统调用。
在syscall()这个函数中,主要是一个参数的传递,可以看到,在函数里面,有内联汇编。这个内敛汇编的功能也比较简单。只有一句汇编代码:
int %1
%1即下面的 "i" (T_SYSCALL), 这句话,就是调用编号为T_SYSCALL(0x30)的中断,所有的系统中断,都采用0x30作为中断号,只是根据不同的num,来区分所调用的具体的系统调用。下面看下去,可以知道,这段汇编的功能就是把前面的syscall()中的各个参数,赋值给各个寄存器。由于系统调用,需要把栈从用户栈变换位内核栈,所以一般的函数调用,把参数存在栈中的方法无法满足要求,所以就需要用寄存器来传递参数。
参数传递完成之后,直接用调用int 0x30,
在这里,先看一下esp,此时的栈指针应该指向的是用户栈:
在JOS中,整个地址空间被分成了一下几个重要的部分:
1.从 虚拟地址UTOP(0xeec00000)开始,到 ULIM(0xef800000),这一段中间的地址对于用户进程来说,是一段只读进程,用户进程可以访问,但是不能写。
2.在UTOP以上的空间,是用户空间,用户可以访问,可写可读可执行。
3.在ULIM以上的空间,是内核空间,用户不可访问。
其中,用户栈初始位置是在 USTACKTOP (0xeebfe000), 内核栈的初始位置为:KSTACKTOP(0xf0000000),而内核栈的初始地址被保存在一个名为TSS的数据就够中,其里面有esp0来存储内核栈的初始位置,即KSTACKTOP(0xf0000000)。
从上面的途中,可以看出,现在esp指向的是用户栈的位置,下一步,进入中断,
在执行了int 0x30进入了相应的中断之后,可以看到,esp的指针已经指向了内核栈了,而且在内核栈中,已经有数据被压入内核栈。
可以看到,被压入内核栈的数据依次是: 0x23, 0xeebfdfc4, 0x86, 0x1b, 0x800bfc, 0x0
所以可以说,在JOS里面,int 0x30做了一下几件事情:
1.改变了esp指针,其指向了内核栈。
2.进入了相应的中断函数。
3.往内核栈中压入了一些值
通过查阅相关的文章,知道在linux中,通过int 0x80来进行系统调用,在这个过程中,int 0x80做了一下的几件事情:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
上面几点,综合起来就是得到首先内核栈的初始地址,然后把SS,ESP,EFLAGS,CS,EIP等进程寄存器的值压入内核栈中.
最后,加载中断函数的CS和EIP,执行中断函数。
回到int 0x30执行之前的状态,可以得到上述几个寄存器的值如下图:
和上面的内核栈里面的值一一对比,就可以看到,压入的前五个值就是相应的寄存器(eip不同是因为压入的是int 0x30的下一条语句的eip,这样iret之后就可以直接执行下一条语句)。
接下去,看进入中断之后的程序:
在内核栈的最后,压入了0x0,其实是压入了中断的错误号,有些中断有错误号,有些中断是没有错误号的,就用0代替。所以,最后会压入一个0来代替错误号(没有错误号还要压一个数字,是因为在中段描述符的寄存器变量里面有这一项,为了和有错误号的中断相统一)。
这段汇编比较简单,就是把寄存器和中断号压入内核栈中,这里最前面压入了一个0,是因为有些中断具有错误号,对这些中断,CPU会自动压入一个中断号,而有些中端是没有错误号的,此时,为了和前面的有错误号的中断相统一,就认为的压入一个0作为错误号,所以总共有两个预处理的宏,一个是上面的TRAPHANDLER_NOEC,这个由于CPU不会压入错误号,所以人为的压入。另一个是TRAPHANDLER,这个就没有pushl $0这一语句,因为CPU会自动压入错误号。
这里需要注意,在所有中断函数,各自执行相应的属于自己的函数之前,都需要经过以上两个步骤,即TRAPHANDLER/TRAPHANDLER_NOEC, 然后_alltraps。
这里这两个步骤的作用就是,把各种寄存器和错误号按照一定的顺序压入内核栈中,然后改变ds和es寄存器的值,使其指向内核的数据段。
接下来,看trap。
看一下这个结构体的具体的内容:
回忆一下int 0x30之后, 内核栈的一系列的行为:首先,系统硬件自动压入SS,ESP,EFLAGS,CS,EIP;然后接下去执行的TRAPHANDLER_NOEC,压入了数字0和中断号,接着在_alltraps里面,压入了ds,es,并且执行了pusha,
压入了所有的普通寄存器。
和上面的图片进行对比,可以看出,整个的变量入栈的过程,其实就是在创建一个Trapframe结构体,最后的esp栈指针,指向的就是Trapframe结构体的首地址,然后传给trap。这个结构体变量,包含了调用内核的用户进程的所有的寄存器信息,所以,在内核进入trap()函数之前,其实前面的所有的工作,就是生成这样的一张表,这张表里面记录了用户进程的寄存器信息。
在trap()函数中,接下去执行:
if ((tf->tf_cs & 3) == 3){
}
这里,主要是为了判断进入这个中断程序的是不是用户态程序。
cs是调用中断程序的进程的代码段选择子。其结构如下图所示:
可以看到,cs寄存器的最后两位是RPL,其表示程序段的特权级。在linux系统中,总共有4个特权级,依次为:0,1,2,3.其中,0位最高级,3位最低级。
在JOS系统里面,只用了其中的两个,0和3.0代表内核程序段,3代表的是用户程序段。
所以再看上面那段代码,就比较清楚了,如果cs的最后两位是11,则代表调用内核的是用户进程。如果不是,则表示调用内核的是内核进程,实现中断嵌套。
如果调用的是用户进程,则把tf存入curenv中,在文章的最开始,有
struct Env (进程描述符)的相关介绍,其中的env_tf,即存入的是进程寄存器的信息。
curenv表示的是最后一个执行的用户进程,所以这段代码的作用就是:如果调用内核的是用户进程,则调用的用户进程就是curenv这个进程,则把用户进程的相应寄存器信息存入,这样,以后在返回的时候就可以按照这些寄存器信息返回。
如果现在调用内核的是内核进程,则cs的最后两位必为0,则此时的tf里面存储的寄存器信息是内核的寄存器信息,也就不必要更新curenv的寄存器。因为是内核调用内核,所以栈指针没有改变,还是指向内核栈。这种中断调用,就和函数调用是一样的,最后可以通过栈esp和ebp来恢复,不必存入特定的结构体中。
接下去,就是执行相应的中断函数:trap_dispatch(tf)
这里需要注意一点,当是用户进程调用这段内核时,tf不在是前面刚刚进入trap时,而是指向curenv->env_tf的地址,这样,在中断执行的过程中,就可以通过改变tf里面的寄存器的值,来传递参数了。而如果是内核调用这段内核,则tf指向的其实还是内核栈里面的Trapframe,由于栈空间不变化,所以不需要用寄存器来传递参数,直接用栈,就可以传递输出参数,这一点,和函数调用是一样的。
接下来,看trap_dispatch()函数是如何执行的:
我们这里主要看系统调用,看一下syscal函数:
具体的系统调用实现比较简单,在执行了sys_getenvid()之后,会返回相应的进程id号。然后返回给上一层中的eax寄存器。注意,这个eax寄存器是用户进程的eax寄存器,并不是现在执行的内核中的eax寄存器。当返回用户进程之后,所有的寄存器都会通过env_tf恢复成系统调用之前的数值。所以通过这种,方法,又一次完成了从内核态到用户态的参数传递工作。
回到trap,可以看到,函数执行了env_run(curenv),即最后执行用户进程,完成用户进程的返回。
关于用户进程的返回,可以查看另外一份文档:/article/1822422.html,在这篇文章的第4点进程运行中,详细分析了env_run()函数.
总结:
1.系统调用的过程:
(1)首先,需要从TSS段中,找出内核栈的地址和,其存在esp0和ss0中.
(2)按照一定的格式(和进程描述符中的寄存器信息的存储方式有关),把寄存器压入内核栈中.
(3)给用户进程的进程描述符中的tf赋值,以便能够方便的回到用户进程调用系统之前的状态.
(4)执行对应的系统调用.
(5)返回用户进程.
2.从用户进程到内核进程的过程中,栈是会变化的,其会从用户栈转变为内核栈(int 0x30 硬件自动转换),所以参数的传递和一般的函数参数传递不一样.在用户进程和内核进程之间传递参数,只能通过寄存器.
3.在执行内核程序的过程中,也可能会出现中断,此时,这种中断就类似于函数调用,因为栈没有改变,一直是内核栈,理解这个,对很多机制都有帮助.
4.现在的内核基本就是纯分页的,不在用分段的机制了.要实现这点,只需要把段基址设为0, 段的范围设为0xffffffff,此时就可以达到纯分页的效果了.现在感觉,这个段的唯一作用,就是指明段的特权级,是在内核态中还是在用户态中了.通过这种保护,用户态的程序不能直接访问内核空间.
用户态的程序想要访问内核空间,方法就是陷入中断,采用 int 0x30(LINUX下是 int 0x80)来从用户态转到内核态.
而内核态,相对的,也使用iret,中断返回,来进入用户态.
通过JOS系统,来看系统调用的具体过程。
系统调用,在语言层面来看,其实可以把系统调用看成是一种函数的调用。只是这种函数调用,不同于一般的用户态下的函数调用,用户态下,函数调用,只需要用栈来保存各种信息就可以了,因为调用前后,保存的信息的栈都是用户地址空间下的栈,所以只需要ESP和EBP两个寄存器相互配合,就可以很好的实现函数调用的功能了。在用户态下调用函数,栈是如何保存信息的,可以看看:/article/1822432.html
系统调用的过程,包括用户进程转到内核态,然后由内核态转回用户进程。和上面的函数调用不同的是,在进行系统调用的时候,需要的是内核地址下的栈来保存程序的相关信息,包括各个寄存器的值,地址空间的值等等。所以,在这个过程中,就存在栈的切换,进入内核后,用户栈需要切换为内核栈,而回到用户进程后,内核栈就需要切换为用户栈的状态。
之所以要用内核栈,个人理解,是因为在用户进程陷入内核中后,在内核中,也有函数的调用等操作,所以内核也需要栈来存放变量等。而之所以要把进程信息保存进内核栈中,个人感觉是因为,在内核中,很容易发生进程之间的切换,这样这些信息统一放在一个内核中中,这样比较好管理,可以很方便的调用提取这些信息。
下面来看一下具体的系统调用是如何实现的。
首先,看一下在JOS里面的进程描述符:
struct Env { struct Trapframe env_tf; // Saved registers struct Env *env_link; // Next free Env envid_t env_id; // Unique environment identifier envid_t env_parent_id; // env_id of this env's parent enum EnvType env_type; // Indicates special system environments unsigned env_status; // Status of the environment uint32_t env_runs; // Number of times environment has run // Address space pde_t *env_pgdir; // Kernel virtual address of page dir }上面各项具体的内容,可以到另外一篇讲进程的文章里面去查看:/article/1822422.html
下面,跟踪一个具体的程序,来看看系统调用的过程是怎么样的:
hello:
// hello, world #include <inc/lib.h> void umain(int argc, char **argv) { envid_t envid = sys_getenvid(); cprintf("hello, world\n"); cprintf("i am environment %08x\n", envid); }
envid_t sys_getenvid(void) { return syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0); }
上面这个是最简单的程序,就是输出一个hello world。
再上面的程序中,调用了一个系统进程sys_getenvid()——这些系统调用都是JOS系统里面的,和linux的可能有些不同。
直接跳过跳过前面的代码,直接来看系统调用的相关代码:
static inline int32_t syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) { int32_t ret; asm volatile("int %1\n" : "=a" (ret) : "i" (T_SYSCALL), "a" (num), "d" (a1), "c" (a2), "b" (a3), "D" (a4), "S" (a5) : "cc", "memory"); if(check && ret > 0) panic("syscall %d returned %d (> 0)", num, ret); return ret; }
上面的sys_getenvid()是众多系统调用中的一个函数,在JOS中,所有的系统调用全部通过syscall()这个函数来完成相应的功能。通过syscall()中的第一个参数num,来确定到底使用的是那个系统调用。
在syscall()这个函数中,主要是一个参数的传递,可以看到,在函数里面,有内联汇编。这个内敛汇编的功能也比较简单。只有一句汇编代码:
int %1
%1即下面的 "i" (T_SYSCALL), 这句话,就是调用编号为T_SYSCALL(0x30)的中断,所有的系统中断,都采用0x30作为中断号,只是根据不同的num,来区分所调用的具体的系统调用。下面看下去,可以知道,这段汇编的功能就是把前面的syscall()中的各个参数,赋值给各个寄存器。由于系统调用,需要把栈从用户栈变换位内核栈,所以一般的函数调用,把参数存在栈中的方法无法满足要求,所以就需要用寄存器来传递参数。
参数传递完成之后,直接用调用int 0x30,
在这里,先看一下esp,此时的栈指针应该指向的是用户栈:
在JOS中,整个地址空间被分成了一下几个重要的部分:
1.从 虚拟地址UTOP(0xeec00000)开始,到 ULIM(0xef800000),这一段中间的地址对于用户进程来说,是一段只读进程,用户进程可以访问,但是不能写。
2.在UTOP以上的空间,是用户空间,用户可以访问,可写可读可执行。
3.在ULIM以上的空间,是内核空间,用户不可访问。
其中,用户栈初始位置是在 USTACKTOP (0xeebfe000), 内核栈的初始位置为:KSTACKTOP(0xf0000000),而内核栈的初始地址被保存在一个名为TSS的数据就够中,其里面有esp0来存储内核栈的初始位置,即KSTACKTOP(0xf0000000)。
从上面的途中,可以看出,现在esp指向的是用户栈的位置,下一步,进入中断,
在执行了int 0x30进入了相应的中断之后,可以看到,esp的指针已经指向了内核栈了,而且在内核栈中,已经有数据被压入内核栈。
可以看到,被压入内核栈的数据依次是: 0x23, 0xeebfdfc4, 0x86, 0x1b, 0x800bfc, 0x0
所以可以说,在JOS里面,int 0x30做了一下几件事情:
1.改变了esp指针,其指向了内核栈。
2.进入了相应的中断函数。
3.往内核栈中压入了一些值
通过查阅相关的文章,知道在linux中,通过int 0x80来进行系统调用,在这个过程中,int 0x80做了一下的几件事情:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
上面几点,综合起来就是得到首先内核栈的初始地址,然后把SS,ESP,EFLAGS,CS,EIP等进程寄存器的值压入内核栈中.
最后,加载中断函数的CS和EIP,执行中断函数。
回到int 0x30执行之前的状态,可以得到上述几个寄存器的值如下图:
和上面的内核栈里面的值一一对比,就可以看到,压入的前五个值就是相应的寄存器(eip不同是因为压入的是int 0x30的下一条语句的eip,这样iret之后就可以直接执行下一条语句)。
接下去,看进入中断之后的程序:
#define TRAPHANDLER_NOEC(name, num) \ .globl name; \ .type name, @function; \ .align 2; \ name: \ pushl $0; \ pushl $(num); \ jmp _alltraps .globl _alltraps _alltraps: pushl %ds pushl %es pushal movl $GD_KD, %eax movw %ax, %ds movw %ax, %es pushl %esp call trap
在内核栈的最后,压入了0x0,其实是压入了中断的错误号,有些中断有错误号,有些中断是没有错误号的,就用0代替。所以,最后会压入一个0来代替错误号(没有错误号还要压一个数字,是因为在中段描述符的寄存器变量里面有这一项,为了和有错误号的中断相统一)。
这段汇编比较简单,就是把寄存器和中断号压入内核栈中,这里最前面压入了一个0,是因为有些中断具有错误号,对这些中断,CPU会自动压入一个中断号,而有些中端是没有错误号的,此时,为了和前面的有错误号的中断相统一,就认为的压入一个0作为错误号,所以总共有两个预处理的宏,一个是上面的TRAPHANDLER_NOEC,这个由于CPU不会压入错误号,所以人为的压入。另一个是TRAPHANDLER,这个就没有pushl $0这一语句,因为CPU会自动压入错误号。
这里需要注意,在所有中断函数,各自执行相应的属于自己的函数之前,都需要经过以上两个步骤,即TRAPHANDLER/TRAPHANDLER_NOEC, 然后_alltraps。
这里这两个步骤的作用就是,把各种寄存器和错误号按照一定的顺序压入内核栈中,然后改变ds和es寄存器的值,使其指向内核的数据段。
接下来,看trap。
void trap(struct Trapframe *tf) { asm volatile("cld" ::: "cc"); assert(!(read_eflags() & FL_IF)); cprintf("Incoming TRAP frame at %p\n", tf); if ((tf->tf_cs & 3) == 3) { assert(curenv); curenv->env_tf = *tf; tf = &curenv->env_tf; } last_tf = tf; trap_dispatch(tf);<span style="white-space:pre"> </span>//执行相应的中断函数 assert(curenv && curenv->env_status == ENV_RUNNING); env_run(curenv); }trap函数,这里看一下函数的输入变量,是Trapframe型的一个结构体。
看一下这个结构体的具体的内容:
回忆一下int 0x30之后, 内核栈的一系列的行为:首先,系统硬件自动压入SS,ESP,EFLAGS,CS,EIP;然后接下去执行的TRAPHANDLER_NOEC,压入了数字0和中断号,接着在_alltraps里面,压入了ds,es,并且执行了pusha,
压入了所有的普通寄存器。
和上面的图片进行对比,可以看出,整个的变量入栈的过程,其实就是在创建一个Trapframe结构体,最后的esp栈指针,指向的就是Trapframe结构体的首地址,然后传给trap。这个结构体变量,包含了调用内核的用户进程的所有的寄存器信息,所以,在内核进入trap()函数之前,其实前面的所有的工作,就是生成这样的一张表,这张表里面记录了用户进程的寄存器信息。
在trap()函数中,接下去执行:
if ((tf->tf_cs & 3) == 3){
}
这里,主要是为了判断进入这个中断程序的是不是用户态程序。
cs是调用中断程序的进程的代码段选择子。其结构如下图所示:
可以看到,cs寄存器的最后两位是RPL,其表示程序段的特权级。在linux系统中,总共有4个特权级,依次为:0,1,2,3.其中,0位最高级,3位最低级。
在JOS系统里面,只用了其中的两个,0和3.0代表内核程序段,3代表的是用户程序段。
所以再看上面那段代码,就比较清楚了,如果cs的最后两位是11,则代表调用内核的是用户进程。如果不是,则表示调用内核的是内核进程,实现中断嵌套。
如果调用的是用户进程,则把tf存入curenv中,在文章的最开始,有
struct Env (进程描述符)的相关介绍,其中的env_tf,即存入的是进程寄存器的信息。
curenv表示的是最后一个执行的用户进程,所以这段代码的作用就是:如果调用内核的是用户进程,则调用的用户进程就是curenv这个进程,则把用户进程的相应寄存器信息存入,这样,以后在返回的时候就可以按照这些寄存器信息返回。
如果现在调用内核的是内核进程,则cs的最后两位必为0,则此时的tf里面存储的寄存器信息是内核的寄存器信息,也就不必要更新curenv的寄存器。因为是内核调用内核,所以栈指针没有改变,还是指向内核栈。这种中断调用,就和函数调用是一样的,最后可以通过栈esp和ebp来恢复,不必存入特定的结构体中。
接下去,就是执行相应的中断函数:trap_dispatch(tf)
这里需要注意一点,当是用户进程调用这段内核时,tf不在是前面刚刚进入trap时,而是指向curenv->env_tf的地址,这样,在中断执行的过程中,就可以通过改变tf里面的寄存器的值,来传递参数了。而如果是内核调用这段内核,则tf指向的其实还是内核栈里面的Trapframe,由于栈空间不变化,所以不需要用寄存器来传递参数,直接用栈,就可以传递输出参数,这一点,和函数调用是一样的。
接下来,看trap_dispatch()函数是如何执行的:
static void trap_dispatch(struct Trapframe *tf) { if(tf->tf_trapno == T_PGFLT){ page_fault_handler(tf); return; } if(tf->tf_trapno == T_BRKPT){ monitor(tf); return; } if(tf->tf_trapno == T_SYSCALL){ tf->tf_regs.reg_eax= syscall(tf->tf_regs.reg_eax, tf->tf_regs.reg_edx, tf->tf_regs.reg_ecx, tf->tf_regs.reg_ebx, tf->tf_regs.reg_edi, tf->tf_regs.reg_esi); return; } // Unexpected trap: The user process or the kernel has a bug. print_trapframe(tf); if (tf->tf_cs == GD_KT) panic("unhandled trap in kernel"); else { env_destroy(curenv); return; } }在JOS系统里面,总共只有三种中断类型,page fault, 中断,系统调用。
我们这里主要看系统调用,看一下syscal函数:
int32_t syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) { int ret = 0; switch(syscallno){ case SYS_cputs: sys_cputs( (const char *)a1, (size_t) a2); break; case SYS_cgetc: ret = sys_cgetc(); break; case SYS_getenvid: ret =sys_getenvid(); break; case SYS_env_destroy: ret= sys_env_destroy(a1); break; default: return -E_NO_SYS; } return ret; }首先看一下,输入参数。回顾一下,在最开始,用户进程执行系统调用的时候,是把系统调用的类型存入eax的,把其他参数存入相应的寄存器。而在这里,读取相应的寄存器的值,来进行syscall的调用。通过寄存器,实现了系统调用过程中,从用户态往内核态传参数。
具体的系统调用实现比较简单,在执行了sys_getenvid()之后,会返回相应的进程id号。然后返回给上一层中的eax寄存器。注意,这个eax寄存器是用户进程的eax寄存器,并不是现在执行的内核中的eax寄存器。当返回用户进程之后,所有的寄存器都会通过env_tf恢复成系统调用之前的数值。所以通过这种,方法,又一次完成了从内核态到用户态的参数传递工作。
回到trap,可以看到,函数执行了env_run(curenv),即最后执行用户进程,完成用户进程的返回。
关于用户进程的返回,可以查看另外一份文档:/article/1822422.html,在这篇文章的第4点进程运行中,详细分析了env_run()函数.
总结:
1.系统调用的过程:
(1)首先,需要从TSS段中,找出内核栈的地址和,其存在esp0和ss0中.
(2)按照一定的格式(和进程描述符中的寄存器信息的存储方式有关),把寄存器压入内核栈中.
(3)给用户进程的进程描述符中的tf赋值,以便能够方便的回到用户进程调用系统之前的状态.
(4)执行对应的系统调用.
(5)返回用户进程.
2.从用户进程到内核进程的过程中,栈是会变化的,其会从用户栈转变为内核栈(int 0x30 硬件自动转换),所以参数的传递和一般的函数参数传递不一样.在用户进程和内核进程之间传递参数,只能通过寄存器.
3.在执行内核程序的过程中,也可能会出现中断,此时,这种中断就类似于函数调用,因为栈没有改变,一直是内核栈,理解这个,对很多机制都有帮助.
4.现在的内核基本就是纯分页的,不在用分段的机制了.要实现这点,只需要把段基址设为0, 段的范围设为0xffffffff,此时就可以达到纯分页的效果了.现在感觉,这个段的唯一作用,就是指明段的特权级,是在内核态中还是在用户态中了.通过这种保护,用户态的程序不能直接访问内核空间.
用户态的程序想要访问内核空间,方法就是陷入中断,采用 int 0x30(LINUX下是 int 0x80)来从用户态转到内核态.
而内核态,相对的,也使用iret,中断返回,来进入用户态.
相关文章推荐
- IAP_支付本地验证
- Linq学习from let where子句
- Java中什么时候使用构造方法
- 磁盘管理和文件系统管理
- linux Shell学习笔记1
- 方法1
- 第十四篇:OC中block存储代码块的定义与应用
- 逻辑覆盖
- Android学习记录之Volley网络通信框架基础解析(1)
- Mapped Statements collection does not contain value for
- linux命令中 rpm –qa|grep softname的含义
- JQ与dom对象简单案例
- Java中的基本数据类型
- 欢迎你,这里是我的小世界~
- Spring下面的@Transactional注解标志的讲解
- java实现计算器
- 2015.9.13 VIM、权限和网络管理
- 怎样点关闭按钮的时候不关掉父窗口
- 波兰表达式问题
- 瀑布模型和敏捷方法的区别