文章目录
第二章、系统接口——通向操作系统内核的大门
一、什么是系统接口
操作系统提供的一些关键函数
而普通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。
四、小结
上述步骤一步一步都实现了以后,系统接口大致的轮廓就已经完成了,剩下的启动前的一些修改准备就不多赘述,想了解的可以在蓝桥云课上看一看哈工大的实验。