随笔-80  评论-24  文章-0  trackbacks-0
该文件是系统调用实现的主要文件。为了弄清楚系统调用,首先应该拿一个实例来看。
WinixJ系统仅仅实现了一个系统调用getpid(),但是其他系统调用的框架应是完全相同的,只在函数实现细节上有所不同,因此我们打算以getpid()作为案例讲解。
先看看getpid()呈现给用户的调用接口吧:
POSIX规范的getpid()声明如下:
pid_t getpid();等同于int getpid();为了简单起见我们就实现为int getpid();
它的代码也十分简单:

 1 int getpid()
 2 {
 3     int res;
 4     __asm__ __volatile__ (
 5             "movl $0x0, %%eax\n\t"
 6             "movl $0x0, %%ebx\n\t"
 7             "movl $0x0, %%ecx\n\t"
 8             "movl $0x0, %%edx\n\t"
 9             "int $0x30\n"
10             :"=a" (res)
11             :);
12     return res;
13 }
14 

虽然有嵌入汇编,但是也不难理解,看似是将eax、ebx、ecx、edx四个寄存器均传值为0,然后调用软中断int 0x30(实际上应该称之为陷阱,陷阱和中断还是有细微区别的,见此博文),最后将返回的结果由eax赋值给res变量。很简单,不是吗?
但是为何eax、ebx、ecx、edx都赋值了呢?调用int 0x30之后又发生什么了呢?
先说调用int 0x30之后会发生的事情。我们看下面这段代码,其实它在此博文已经出现过,就是sys_call函数的实现:

 1 ; 系统调用框架,系统调用采用0x30号中断向量,利用int 0x30指令产
 2 ; 生一个软中断,之后便进入sys_call函数,该函数先调用save_all框
 3 ; 架保存所有寄存器值,然后调用对应系统调用号的入口函数完成系统调用
 4 ; 注意!!!!!系统调用默认有三个参数,分别利用ebx、ecx、edx来
 5 ; 传递,其中eax保存系统调用号
 6 sys_call:
 7     save_all
 8 
 9     sti
10 
11     push ebx
12     push ecx
13     push edx
14     call [sys_call_table + eax * 4]
15     add esp, 4 * 3
16 
17     recover_from_sys_call
18 
19     cli
20 
21     iretd
22 

再看看save_all:

 1 ; 一个宏,因为所有的irq中断函数以及系统调用都是先保存现场并将数据段等堆栈段
 2 ; 切换到内核态,因此,该操作所有的irq中断入口函数均相同
 3 ; 故写成宏节省空间
 4 %macro save_all 0
 5     push eax
 6     push ecx
 7     push edx
 8     push ebx
 9     push ebp
10     push esi
11     push edi
12     push ds
13     push es
14     push fs
15     push gs
16     mov si, ss
17     mov ds, si
18     mov es, si
19     mov gs, si
20     mov fs, si
21 %endmacro
22 

有了这一段代码再说int 0x30后都发生了什么就比较简单了,首先和发生时钟中断等硬件中断类似,发生陷阱的时候CPU同样先从当前进程对应的TSS段中找到SS0和ESP0,然后将当前进程的SS/ESP/EFLAGS/CS/EIP五个寄存器值压入SS0:ESP0,然后再去执行save_all的代码,save_all同样是将所有的常规寄存器压入堆栈,这样一个过程之后就完成了从用户态到内核态的切换,之后再看sys_call代码,压入三个寄存器作为形参,然后调用sys_call_table[eax * 4]函数。到这儿肯定就明白了,原来getpid()函数是通过四个通用寄存器来向内核态的系统调用函数传递参数的,这样做是为了快捷。
还有一点就是install_sys_call_handler和install_sys_call两个函数功能是不同的,说白了,WinixJ就只有一个系统调用,sys_call,它是所有POSIX规定的系统调用的框架,所有系统调用的实现都在sys_call_table[]函数数组里。这样,将getpid()作为第0号系统调用的意思就是将sys_call_table[0] = sys_call_getpid();然后getpid()函数在发生陷阱的时候会通过eax传入系统调用号0,这样在sys_call中就会调用sys_call_table[eax * 4],即sys_call_table[0],即sys_call_getpid();  而install_sys_call_handler函数的作用是将IDT中0x30这一中断描述符项设置成DPL = 3的陷阱门,该陷阱门的入口为sys_call,这样,当用户程序调用int 0x30的时候就会调用sys_call;而install_sys_call的作用是将getpid()函数与第0号系统调用对应起来,即完成这一工作:sys_call_table[0] = sys_call_getpid();这样,当通过eax = 0传入sys_call中时,sys_call知道需要调用sys_call_table[]中的第几个函数指针。
到此基本就明白了,还有最后一个细节:为什么在系统调用完成后返回的时候不使用所有中断返回时都使用的宏recover_all,而使用recover_from_sys_call?我们看看代码:

 1 ; 一个宏,恢复现场
 2 %macro recover_all 0
 3     pop gs
 4     pop fs
 5     pop es
 6     pop ds
 7     pop edi
 8     pop esi
 9     pop ebp
10     pop ebx
11     pop edx
12     pop ecx
13     pop eax
14 %endmacro
15 
16 ; 注意从系统调用返回时不需要从栈中弹出eax的值,因为eax保存着调用
17 ; 对应系统调用之后的返回值
18 %macro recover_from_sys_call 0
19     pop gs
20     pop fs
21     pop es
22     pop ds
23     pop edi
24     pop esi
25     pop ebp
26     pop ebx
27     pop edx
28     pop ecx
29     add esp, 4 * 1
30 %endmacro

仔细研究代码应该已经明白了,其实getpid()是需要返回值的,恰好我们就使用eax作为返回值,这样,在从堆栈中恢复现场的时候就不能将eax的值恢复,因为eax还需要传递返回值。
至此,所有的系统调用细节应该明白了,这里只是讲述的一种系统调用的实现,而有些操作系统的系统调用采用完全不同的风格实现的,它没有sys_call_table[]这样的函数数组,而是将每个系统调用都设置成一个陷阱门,在IDT中都占有一个描述符项,至于优劣,其实没有大的差别。WinixJ使用的方法正是Linux的方法。在此向Linus和Linux致敬。
posted on 2012-02-14 15:41 myjfm 阅读(502) 评论(0)  编辑 收藏 引用 所属分类: 操作系统

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理