Linux 内核0.11 系统调用详解(下)
阅读原文时间:2021年04月20日阅读:1

备注:上讲中,博猪讲到了操作系统是如何让用户程序调用系统函数的,这讲继续接上讲的话题,从一个系统内核系统函数创建的小实验来学习系统内核具体做了些什么。理清下系统调用的整体过程。

实验:在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。

  • iam()

第一个系统调用是iam(),其原型为:

int iam(const char * name); 完成的功能是将字符串参数name的内容拷贝到内核中保存下来。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。在kernal/who.c中实现此系统调用。

  • whoami()

二个系统调用是whoami(),其原型为:

int whoami(char* name, unsigned int size); 它将内核中由iam()保存的名字拷贝到name指向的用户地址空间中,同时确保不会对name越界访存(name的大小由size说明)。返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errno为EINVAL。也是在kernal/who.c中实现。

Let‘s go!

等等,linux 0.11内核源码的编写与编译,需要在虚拟机模拟x86环境的情况下进行,这在我的Windows下用Bochs编译运行Linux-0.11有详细阐述,不再赘述。

1、编写内核态下,系统函数具体实现iam()以及whoami()。

目录:/linux/kernel/who.c(创建)

#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segement.h>
char usnm[64]={0};
int sys_iam(const char* name){
  int result=0;
  int cnt;
  while(get_fs_byte(name+result)!='\0'&&result<64) result++;//统计字符串个数
  if(result>23) return -EINVAL; 
  else{
    for(cnt =0;cnt <=result;cnt++) usnm[cnt]=get_fs_byte(name+cnt); //将字符串写入核心态内存中
    return result;
  }
}
int sys_whoami(char* name,unsigned int size){
  int result=0;
  int cnt;
  while(usnm[result]!='\0'&&result<64) result++;
  if(return >size) return -1;
  else{
    for(cnt =0;cnt<=result;cnt++) put_fs_byte(usnm[cnt],(name+cnt)); 
    return result;
  }
}

2、那操作系统如何调用到who.c中的代码呢?

在上一讲中,我们知道了用户程序将通过int 0x80中断进入核心态,并且,会跳转到system_call函数地址处去执行。接下来我们就来看看system_call源代码。

目录:/linux/kernel/system_call.s(修改),找到nr_system_calls = 86,将系统调用的个数增加两个。

#### int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
.align 2  
_system_call:  
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax 中置-1 并退出。  
ja bad_sys_call  
push %ds # 保存原段寄存器值。  
push %es  
push %fs  
pushl %edx # ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。  
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 # ds,es 指向内核数据段(全局描述符表中数据段描述符)。  
mov %dx,%es  
movl $0x17,%edx # fs points to local data space  
mov %dx,%fs # fs 指向局部数据段(局部描述符表中数据段描述符)。  
# 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。  
# 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个  
# 系统调用C 处理函数的地址数组表。  
call _sys_call_table(,%eax,4)  
pushl %eax # 把系统调用号入栈。  
movl _current,%eax # 取当前任务(进程)数据结构地址??eax。  
# 下面97-100 行查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。  
# 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。  
cmpl $0,state(%eax) # state  
jne reschedule  
cmpl $0,counter(%eax) # counter  
je reschedule  
# 以下这段代码执行从系统调用C 函数返回后,对信号量进行识别处理。  
ret_from_sys_call:  
# 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回。  
# 103 行上的_task 对应C 程序中的task[]数组,直接引用task 相当于引用task[0]。  
movl _current,%eax # task[0] cannot have signals  
cmpl _task,%eax  
je 3f # 向前(forward)跳转到标号3。  
# 通过对原调用程序代码选择符的检查来判断调用程序是否是超级用户。如果是超级用户就直接  
# 退出中断,否则需进行信号量的处理。这里比较选择符是否为普通用户代码段的选择符0x000f  
# (RPL=3,局部表,第1 个段(代码段)),如果不是则跳转退出中断程序。  
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?  
jne 3f  
# 如果原堆栈段选择符不为0x17(也即原堆栈不在用户数据段中),则也退出。  
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?  
jne 3f  
# 下面这段代码(109-120)的用途是首先取当前任务结构中的信号位图(32 位,每位代表1 种信号),  
# 然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,再把  
# 原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal()。  
# do_signal()在(kernel/signal.c,82)中,其参数包括13 个入栈的信息。  
movl signal(%eax),%ebx # 取信号位图??ebx,每1 位代表1 种信号,共32 个信号。  
movl blocked(%eax),%ecx # 取阻塞(屏蔽)信号位图??ecx。  
notl %ecx # 每位取反。  
andl %ebx,%ecx # 获得许可的信号位图。  
bsfl %ecx,%ecx # 从低位(位0)开始扫描位图,看是否有1 的位,  
# 若有,则ecx 保留该位的偏移值(即第几位0-31)。  
je 3f # 如果没有信号则向前跳转退出。  
btrl %ecx,%ebx # 复位该信号(ebx 含有原signal 位图)。  
movl %ebx,signal(%eax) # 重新保存signal 位图信息??current->signal。  
incl %ecx # 将信号调整为从1 开始的数(1-32)。  
pushl %ecx # 信号值入栈作为调用do_signal 的参数之一。  
call _do_signal # 调用C 函数信号处理程序(kernel/signal.c,82)  
popl %eax # 弹出信号值。  
3: popl %eax  
popl %ebx  
popl %ecx  
popl %edx  
pop %fs  
pop %es  
pop %ds  
iret

且看第20行代码:call _sys_call_table(,%eax,4) ,执行完一系列的保护现场措施后,程序将根据(_sys_call_table的初始地址)+4*%eax 进行跳转执行,%eax就是我们的系统调用中断号。因此,想要实现系统调用,需要在_sys_call_table 下添加iam()和whoami()的函数指针。

3、在sys_call_table中添加iam()和whoami()的函数指针。

目录:/linux/include/linux/sys.h(修改)
添加:

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

且在sys_call_table中的最后加入:

fn_ptr sys_call_table={....,....,....,sys_iam,sys_whoami};

刚才谈到系统函数调用中断号%eax,大家一定很含糊这是个什么都东西。先把这个疑问暂存心中,我们继续往下走。

4、假设现在已经把内核态代码编写完成,开始准备用户程序的编写。

目录:/root/iam.c 和/root/whoami.c(创建),注意我引用的头文件哦。

#include <usname.h>  
int main(int argc,char * argv[]){

  if(argc>1){
    if(iam(argv[1]<0)) return -1;
    else printf("Input ok!");
  }
  else return -1;
}

#include <usname.h>
#include <stdio.h>

int main(void){
  char str[128];
  if(whoami(str,24)<0) return -1;
  else printf("%s\n",str);
  return 0;
}

usname.h为我们自定义的系统库函数。

5、创建usname.h系统库函数。

目录:/linux/include/usname.h(创建)

#include <unistd.h>

_syscall1(int,iam,const char*,name)
_syscall2(int,whoami,char*,name,unsigned int,size)

6、重点来了,继续注意该程序下的头文件,unist.h。

目录:/linux/include/unist.h(修改)

// 以下是内核实现的系统调用符号常数,用于作为系统调用函数表中的索引值。( include/linux/sys.h )  
#define __NR_setup 0        /* used only by init, to get system going */  
/* __NR_setup 仅用于初始化,以启动系统 */  
#define __NR_exit 1  
#define __NR_fork 2  
...
#define __NR_iam    87 
#define __NR_whoami 88
...
// 有1 个参数的系统调用宏函数。type name(atype a)  
// %0 - eax(__res),%1 - eax(__NR_name),%2 - ebx(a)。  
#define _syscall1(type,name,atype,a) /  
type name(atype a) /  
{ /  
long __res; /  
__asm__ volatile ( "int $0x80" /  
: "=a" (__res) /  
: "" (__NR_##name), "b" ((long)(a))); /  
if (__res >= 0) /  
return (type) __res; /  
errno = -__res; /  
return -1; /  
}

第7、8行为我们增加的代码,我还贴出了系统函数调用的宏定义。跟我们自己创建的usname.h一结合,我们就翻译出了如下代码:

int iam(char* name)
{
  long __res;
  __asm__ volatile ( "int $0x80" 
  : "=a" (__res)
  : "0" (__NR_iam), "b" ((long)(a)));
  if (__res >= 0)
  return (type) __res; 
  errno = -__res;
  return -1; 
}  

在执行中断调用前,NR_iam的系统调用号传给了eax,由此可见,执行中断后,eax保存的就是系统调用号,而系统调用号配合sys_call_table,最终找到了我们的who.c程序下实现的两个系统函数,故事结束了。。。o(∩_∩)o

7、最后一步,修改makefile文件。重新编译下Linux内核。

目录:/linux/kernel/Makefile(修改)

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o
改为:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o who.o

另一处:

\### Dependencies:exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h
  改为:

\### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

8、编译用户程序。输出看结果吧,好运!o(∩_∩)o