此篇文章主要为了解决哈工大操作系统实验3——系统调用,从一点都不了解一步步深入探究,最终完成实验的整个过程。所以篇幅较长,可耐心看,会有收获。
linux0.11怎么实现的系统调用
Linux0.11中,include/unistd.h 中定义了72个系统调用号(太多此处不贴出)以及几个宏函数(如下):
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
#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; \
}
#endif /* __LIBRARY__ */
这4个 _syscall函数可以展开成为系统调用函数,后面的数字表示函数有几个参数。所以可以知道_syscall宏展开的系统调用最多3个参数。
type | name | atype | a | btype | b | ctype | c |
函数返回类型 | 函数名 | 第一个参数类型 | 第一个参数 | 第二个参数类型 | 第二个参数 | 第三个参数类型 | 第三个参数 |
使用_syscall
以write为例,在unistd.h中,write函数声明如下:
int write(int fd, const char * buf, off_t count);
只要按照syscall参数格式填上去
type | name | atype | a | btype | b | ctype | c |
函数返回类型 | 函数名 | 第一个参数类型 | 第一个参数 | 第二个参数类型 | 第二个参数 | 第三个参数类型 | 第三个参数 |
int | write | int | fd | const char * | buf | off_t | count |
lib/write.c中是他的实现
#define __LIBRARY__
#include <unistd.h>
_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;
}
这是一个内嵌汇编,通过int 0x80 中断来实现系统调用
第一个冒号后面是输出,输出的寄存器前面要有等号,"=a"表示 输出存放在eax寄存器中,最终汇编结束时会赋值给变量__res
第二个冒号后面是输入,”0″ 表示的是第一个寄存器,也就是输出用的eax,它的值被赋值为__NR_write, 也就是系统调用号。后面”b”, “c”, “d”表示寄存器ebx,ecx和edx,用来存放write的三个参数。
int 0x80
int 0x80是一个中断号,在boot/head.s中创建了中段描述符表(IDT):
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long _idt
_idt: .fill 256,8,0 # idt is uninitialized
lidt指令为6字节操作数,它将_idt的地址加载进idtr寄存器,IDT被设置为包含256个8字节表项的描述符表。
中断描述符表的初始化工作主要通过宏_set_get来完成,它定义于include/asm/system.h中,如下:
#define _set_gate(gate_addr,type,dpl,addr) /
__asm__ ("movw %%dx,%%ax/n/t" /
"movw %0,%%dx/n/t" /
"movl %%eax,%1/n/t" /
"movl %%edx,%2" /
: /
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), /
"o" (*((char *) (gate_addr))), /
"o" (*(4+(char *) (gate_addr))), /
"d" ((char *) (addr)),"a" (0x00080000))
/*设置中断门函数,特权级0,类型386中断门*/
#define set_intr_gate(n,addr) /
_set_gate(&idt[n],14,0,addr)
/*设置陷阱门函数,特权级0,类型386陷阱门*/
#define set_trap_gate(n,addr) /
_set_gate(&idt[n],15,0,addr)
/*设置系统调用函数,特权级3,类型386陷阱门*/
#define set_system_gate(n,addr) /
_set_gate(&idt[n],15,3,addr)
内核用这些宏初始化IDT表:
//在初始化的main函数中的sched_init中,有下面的代码
void sched_init(void)
{
...
set_system_gate(0x80,&system_call);
}
这句话将0x80中断号和sys_tem_call函数进行绑定,调用 int 0x80时,就会执行system_call函数。
在include/asm/system.h 有两个宏(__asm__是一个GCC的内联汇编,后面讲解这段代码的含义)
//include/asm/system.h
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
因此这个set_system_gate(0x80,&system_call); 语句展开后变成了:
_set_gate(&idt[0x80],15,3,&system_call);
第一个参数表示描述符的地址,0x80号中断的描述符地址就是&idt[0x80] , 第二个参数描述符类型,第三个参数是描述符特权级dpl,第四个参数是偏移地址。
再展开,变成
__asm__ (
"movw %%dx,%%ax\n\t"
"movw %0,%%dx\n\t"
"movl %%eax,%1\n\t"
"movl %%edx,%2"
:
: "i" ((short) (0x8000+(3<<13)+(15<<8))),
"o" (*((char *) (&idt[0x80]))),
"o" (*(4+(char *) (&idt[0x80]))),
"d" ((char *) (&system_call)),
"a" (0x00080000)
);
首先,对这段内联汇编进行解释:
__asm__是GCC关键字asm的宏定义:
#define __asm__ asm
它用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以他开头的,是必不可少的。
movw,movl 后面的w l 表示操作数的长度, b(byte,8-bit),w(word,16-bit),l(long,32-bit)。例如movb %al,%bl 就是移动8bit的数据,从al寄存器到bl寄存器。(这个语法上面寄存器都要加个百分号)。而%%是因为GCC在编译时会将%视为特殊字符,拥有特殊意义,%%仅仅是为了汇编的不被GCC全部转译掉。
\n\t是嵌入式汇编一种书写格式
%0 ,%1,%2,%3:0,1,2,3可以看做变量, 这些变量在程序的 ':' 之后,程序的两个 ':' 是定义输入、输出的。针对这段程序这些变量的前面都加了明确的限定,例如"i"(输入项)、"o"(输出项),剩下的"d"(edx的初始值),"a"(eax的初始值)。而0、1、2、3的概念就是指第几个变量,这里输入项、输出向、寄存器初始混合编号;相应的0("i"((short)(0x8000+(dpl<<13)+(type<<8)))));1((*((char *)(gate_addr))));2((*(4+(char *)(gate_addr))));3("d"((char *)(addr)));4("a"(0x00080000))。
经过解释,现在就能看懂这段代码了,我们针对IDT的表项,对其进行计算:
IDT表的表项
代码中两个”o” 分别是表项的低4个字节和高4个字节,序号是%1和%2
i:第一个输入(short) (0x8000+(3<<13)+(15<<8)) 的二进制是
1110 1111 0000 0000
放到dx中,也就是edx的低2个字节,赋值给%2 ,也就是设置了
而edx的高2个字节是system_call偏移地址的高2个字节
dex的低2个字节是system_call偏移地址的低2个字节,复制给ax(eax的低2个字节)
eax的高2个字节是0x0008
表项赋值为:
system_call
kernel/system
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
注意从pushl %edx开始的三句代码,是前面第3点提到的三个参数依次从右向左入栈。重点是call _sys_call_table(,%eax,4)这句代码,翻译过来就是call [eax*4 + _sys_call_table],4表示每个函数地址是4个字节,根据第3点,eax存的是_NR_write的值也就是4,因为_sys_call_table是sys.h中的一个int (*)()类型的数组,里面存的是所有的系统调用函数地址,所以再翻译一下就是访问sys_call_table[4]也就是sys_write函数:
sys_call_table是一个函数表,在 include/linux/sys.h中定义
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };
write 的 调用号是4,和这里对应的。
sys_write在fs下的read_write.c:
int
sys_write (unsigned int fd, char *buf, int count)
{
struct file *file;
struct m_inode *inode;
...
}
到这里为止才明白调用的就是这个sys_write函数。
总结一下:linux0.11系统调用的执行过程:
(1)当执行系统调用函数时,系统调用函数会执行int 0x80中断命令,同时将系统调用号放入eax寄存器中,并将要传递给系统的参数放入ebx,ecx,edx中。中断处理程序会执行system_call()函数。
(2) system_call()函数首先保存原段寄存器,在将调用参数压入栈中。然后将ds(在保护模式下,ds装的是段选择符)、es指向内核数据段,cs段会在中断产生时由中断门的段选择符赋值为内核代码段,并将原段选择符保存到栈中。然后调用对应的功能函数。当从功能函数返回时,内核会查看当前任务运行状态,如果不在就绪态就去执行调度程序。如果在就绪态,但其时间片用完,则也去执行调度程序。当任务继续执行时则继续对信号进行处理,然后退回到系统调用函数。
参考资料:
http://ju.outofmemory.cn/entry/374099 Linux0.11 _syscall分析
http://www.cnblogs.com/hongzg1982/articles/2120993.html 关于嵌入式汇编的解释