系统相关
首页 > 系统相关> > 系统调用linux0.11源码详细剖析

系统调用linux0.11源码详细剖析

作者:互联网

总流程:

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
实现系统调用
在 Linux 0.11 上添加两个系统调用
(1)iam()

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

int iam(const char * name);

完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。

kernal/who.c 中实现此系统调用。

1、应用程序如何调用系统调用

在这里插入图片描述

调用系统调用,过程是:

把系统调用的编号存入 EAX;
把函数参数存入其它通用寄存器;
触发 0x80 号中断(int 0x80)。

我们不妨看看 lib/close.c,研究一下 close() 的 API:

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)

其中 _syscall1 是一个宏,在 include/unistd.h 中定义。


#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; \
}

将 _syscall1(int,close,int,fd) 进行宏展开,可以得到:


int close(int fd)
{
    long __res;
    __asm__ volatile ("int $0x80"
        : "=a" (__res)
        : "0" (__NR_close),"b" ((long)(fd)));
    if (__res >= 0)
        return (int) __res;
    errno = -__res;
    return -1;
}

这就是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。

其中 __NR_close 就是系统调用的编号,在 include/unistd.h 中定义:

#define __NR_close    6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/
/*
而在应用程序中,要有:
*/

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

我们首先就需要添加自己实现的编号:
在这里插入图片描述

2、从“int 0x80”进入内核函数

int 0x80 触发后,接下来就是内核的中断处理了。先了解一下 0.11 处理 0x80 号中断的过程。

在内核初始化时,主函数(在 init/main.c 中,Linux 实验环境下是 main())调用了 sched_init() 初始化函数:

void main(void)
{
//    ……
    time_init();
    sched_init();
    buffer_init(buffer_memory_end);
//    ……
}

sched_init() 在 kernel/sched.c 中定义为:


void sched_init(void)
{
//    ……
    set_system_gate(0x80,&system_call);
}

set_system_gate 是个宏,在 include/asm/system.h 中定义为:

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

_set_gate 的定义是:

#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))

虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。具体细节请参考《注释》的第 4 章。

看不懂的同学可以看下图:
这是IDT表过程如下,将变量逐个赋值:

在这里插入图片描述

在这里插入图片描述

接下来看 system_call。该函数纯汇编打造,定义在 kernel/system_call.s 中:


!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……

.globl system_call
.align 2
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,是传递给系统调用的参数
    pushl %ebx

! # 让ds, es指向GDT,内核地址空间
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
    mov %dx,%fs
    call sys_call_table(,%eax,4)
    pushl %eax
    movl current,%eax
    cmpl $0,state(%eax)
    jne reschedule
    cmpl $0,counter(%eax)
    je reschedule

system_call 用 .globl 修饰为其他函数可见。

call sys_call_table(,%eax,4) 之前是一些压栈保护,修改段选择子为内核段,call sys_call_table(,%eax,4) 之后是看看是否需要重新调度,这些都没有直接关系,此处只关心 call sys_call_table(,%eax,4) 这一句。

在这里插入图片描述

根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx。

显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:

extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();
 
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。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上:

extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();
extern int sys_whoami();
extern int sys_iam();

不然,编译会出错的。

3、实现 sys_iam() 和 sys_whoami()

添加系统调用的最后一步,是在内核中实现函数 sys_iam() 和 sys_whoami()。

每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。

比如在 fs/open.c 中的 sys_close(int fd):

int sys_close(unsigned int fd)
{
//    ……
    return (0);
}

它没有什么特别的,都是实实在在地做 close() 该做的事情。

所以只要自己创建一个文件:kernel/who.c,然后实现两个函数就万事大吉了。

#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
 
 
char temp[64]={0};
 
 
int sys_iam(const char* name)
{
   int i=0;
   while(get_fs_byte(name+i)!='\0')
        i++;
   if(i>23){   
        return -EINVAL;
   } 
   printk("%d\n",i);
   i=0;
   while((temp[i]=get_fs_byte(name+i))!='\0'){
i++;
    }   
    return i;
}
 
 
int sys_whoami(char* name,unsigned int size)
{
    int i=0;
    while (temp[i]!='\0')
i++;
    if (size<i)
    return -1;
    i=0;
    while(temp[i]!='\0'){
put_fs_byte(temp[i],(name+i));
i++;
    }
    return i;
}

写完后把 who.c 放到linux-0.01/kernel 目录下 执行make all who.c 就会被编译到内核里了

4、修改 Makefile

要想让我们添加的kernel/who.c可以和其它Linux代码编译链接到一起,必须要修改Makefile文件。Makefile里记录的是所有源程序文件的编译、链接规则,《注释》3.6节有简略介绍。我们之所以简单地运行make就可以编译整个代码树,是因为make完全按照Makefile里的指示工作。

Makefile在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是kernel/Makefile。需要修改两处。一处是:

改为:

另一处:

### 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

Makefile修改后,和往常一样“make all”就能自动把who.c加入到内核中了。如果编译时提示who.c有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明Makefile是对的。

标签:__,sys,..,int,剖析,源码,call,linux0.11,include
来源: https://blog.csdn.net/qq_37857224/article/details/119055676