哈工大操作系统--第二章、系统接口——通向操作系统内核的大门

第二章、系统接口——通向操作系统内核的大门

一、什么是系统接口

操作系统提供的一些关键函数

而普通c代码+关键函数,就构成了连接应用程序和操作系统的关键所在

二、从应用程序到系统调用的过程

在这里插入图片描述

三、自底向上

1.sys_iam的实现

(1)接口get_fs_byte(const char *addr)

sys_iam()函数的目的是什么呢,是捕获键盘的输入,将输入存放到字符数组(也可以叫应用程序内存缓冲区)中等待输出

操作系统为我们提供了一个很重要的内核函数——get_fs_byte(),来实现捕获键盘键入的接口,我们可以直接使用他

extern inline unsigned char get_fs_byte(const char * addr)
{
    unsigned register char _v;
    __asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
    return _v;
}

movb——》每次传送一个字节

%%fs:%1是什么意思呢?fs作为段选择子指向的是LDT表的一个表项,他从LDT的表项中去除用户态应用程序代码段等基地址,然后%1指的是当前需要的部分的偏移地址,找到以后,将一个字节赋给了_v,然后将 _v返回

(2)sys_iam实现
char mesg[24];
int sys_iam(const char* name){
    int i;
    char temp[26];
    for(i=0;i<26;i++){
        temp[i]=get_fs_byte(name+i);
        if(temp[i]=='\0')
            break;
    }
    if(i>23)	return -(EINVAL);
    strcpy(mesg,temp);
    return i;
}

get_fs_byte(name+i)返回了一个字符,返回的是name+i位置的字符,把他赋给中转字符数组。

2.sys_whoami的实现

(1)接口put_fs_byte(char val,char * addr)

有了输入的系统调用后,我们需要编写输出的系统调用

这里就要介绍一下**put_fs_byte(char val,char *addr)**接口函数了

显而易见,他与上面的接口函数是相对的

extern inline void put_fs_byte(char val,char *addr)
{
    __asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

不过多赘述

(2)sys_whoami实现
int sys_whoami(char *name,unsighed int size){
    int len=0;
    for(;mesg[len]!='\0';len++);
    if(len>size){
        return -(EINVAL);
    }
    int i=0;
    for(i=0;i<size;i++){
        put_fs_byte(mesg[i],name+i);
        if(mesg[i]=='\0')
            break;
    }
    return i;
}

这两个系统调用函数全部实现,接下来需要考虑系统应该怎么调用这两个函数

3.系统函数表sys_call_table

可以看作是一个数组,它通过索引来查找对应的系统调用,所以我们需要将这两个新的调用添加到函数表中

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, sys_iam, sys_whoami };

注册函数

extern int sys_whoami();
extern int sys_iam();

完成这两项之后,我们的table就搞定了

4.中断处理代码——system_call

system_call主要做五件事

(1)将段寄存器DS\ES\FS保存在栈中,因为这三个寄存器存储着用户态程序指向的位置,而在内核态需要重新设置

(2)调用sys_call_table中的函数

(3)将重要参数告诉调用的重要函数

(4)设置%fs=0x17

目的是为了在操作系统内核中访问用户态内存,用户态数据段的段选择符为0x17

system_call:
cmpl $nr_system_call-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %ebx
pushl %ecx
pushl %edx
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
mov %dx,%fs
call sys_call_table(,%eax,4)
pop %ds
pop %es
pop %fs
iret

cmpl语句将函数表中的函数总数与eax中保存的系统中断号比较,如果大于,就说明不存在这个函数,跳转到bad_sys_call函数执行

由于ds,es,fs仍然指向的是用户态数据段的内容,而system_call在操作系统内核中,接下来要在操作系统内核中执行,所以需要重新设置寄存器,原来的DS,ES,FS要压栈保存,在回到用户态之前再弹栈恢复寄存器的值。

0x10对应内核态数据段的段选择符

call sys_call_table(,%eax,4)

——>也就是跳转到 sys_call_table+4*%eax位置的函数执行

fs=0x17是怎么访问用户态的呢?

17转为二进制就是10111,可见最后三位2进制数为11和1,前两者转换成十进制就是3,而这个位置刚好是fs的CPL所在的位置,CPL为3,说明段的特权级是3(用户态段),后者TI=1说明要查找的段描述符在LDT表中(用户态应用程序的代码段、数据段内存区域)。这样就可以利用fs在操作系统内核中找到当前进程(即调用系统调用的进程)的用户态内存,并实现用户态内存和内核态内存的信息交换

5.int 0x80

32位机器在每执行完一条指令后都要查看INTR的CPU寄存器,如果发现它的某一位被设置为1,就根据1所在的位去查IDT表中对应的表象。0x80将INTR的0x80位设置为了1,转换为2进制就是第8位,接着就去查第8位对应的表项。

0x80号中断会作为段选择子,在IDT表中查询出来信息设置CS=0x0008,为什么是这个地址呢,第一章已经提到过了,CS=0x8在GDT表中对应的是操作系统内核代码段,而我们的目的就是为了调用system_call代码段,所以这也就说的通了

我们还再IDT表中取出了EIP=system_call函数的入口地址,GDT表获得基址,加上EIP偏移,就跳到了system_call 函数的位置。由于此时CS=0x8最后两位都是0,说明从此刻开始CPL=0,接下来的指令就具有了内核态的特权,任何区域都可以访问了,现在已经通过操作系统接口进入到了操作系统内核。

IDT表(中断向量表)初始化&int 0x80对应表项初始化
void sched_init(){
    …………
    set_system_gate(0x80,&system_call);
}

系统初始化的其中一个函数,IDT表初始化函数

#define set_system_gate(n,addr) set_gate(&idt[n],15,3,addr)

15对应着type,type区分终端门、陷阱门、任务门等……

3就对应的是dpl

设置IDT表的宏代码

#define set_gate(gate_addr,type,dpl,addr)
__asm__("movw %%dx,%%ax" "movw %0,%%dx" "movl %%eax,%1" "movl %%edx,%2": :"i" ((short)(0x8000+(dpl<<13)+type<<8))),"o" (*((char*)(gate_addr))),"o" (*(4+(char*)(gate_addr))),"d" ((char*)(addr)),"a" (0x00080000))

这里的执行效果是,在内嵌汇编之前,先把addr的地址(system_call的首地址)放进EDX中,再把0x00080000放进EAX中,接着执行内嵌汇编,将EDX的低16位放在EAX的低十六位中,EAX的低16位变成了system_call的低16位,第二条指令使EDX的高16位为system_call的高16位地址,低16位变成了**(0x8000+(dpl<<13)+type<<8)** (这个就是把dpl=3和type=15和IDT表项相应的位置对应上)

在这里插入图片描述

观察IDT表结构并结合EDX、EAX高/低16位的意义,可以知道,IDT表项的前四个字节是EAX的内容,后四个字节是EDX的内容

后面的两个内嵌汇编指令,就是把EAX和EDX分别写到对应的位置

6.syscall1系统接口的实现

名字后面的1代表这个接口对应的函数只有一个参数

#define syscall1(type,name,atype a)
type name(atype a){
	long __res;
	__asm__("int 0x80":"=a"(__res):" "(__NR_##name))
	if(__res>=0)
		return (type)__res;
	errno=-__res;
	return -1;
}

就提一下输入部分的" "空的引号部分,引号内为空,默认代表着寄存器EAX。

四、小结

上述步骤一步一步都实现了以后,系统接口大致的轮廓就已经完成了,剩下的启动前的一些修改准备就不多赘述,想了解的可以在蓝桥云课上看一看哈工大的实验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值