Linux-0.11内核学习-添加系统调用

1、参考资料

2、概要

操作系统作为软件应用层和底层硬件之间的部分,向下提供服务,向上提供接口。系统调用便是操作系统向上层应用提供的操作底层硬件的和核心服务的接口,也就是API(Application Programming Interface)。我们知道一般API实现的方法是提供函数接口,只需要调用函数就可以实现相应的功能,底层的原理是程序地址的跳转。因为操作系统和用户程序同时存在于内存中,我们当然是不希望操作系统的数据被随意的篡改和访问,有可能造成十分严重的后果,所以操作系统对内存做了区分:

  • 内核态(0)
  • 服务态(1,2)
  • 用户态(3)

数值越小,级别越高,低级别进程无权访问高级别的内存区域,由此隔离了系统程序和用户程序,提高了操作系统的安全性。

因为CS:IP表示当前指令,所以用CS最低两位(CPL)来表示当前程序属于用户态还是核心态,访问的数据段DS最低两位(DPL)表示目标代码属于用户态还是核心态,在地址跳转时检查DPL和CPL,只有在CPL≤DPL时才允许跳转。但是对于系统调用来说,就需要找到一种方法穿透用户态和内核态的屏障,在x86处理器中,当用户代码想要调用内核代码时,硬件通过终端指令int将CPL改为0,从而穿透屏障实现调用。

3、系统调用write原理

选取write(…)指令进行分析,最典型调用write(…)的命令就是c中的printf(…)函数。write(…)定义在lib/write.c

首先看_syscall3的定义:

/*
 *  linux/lib/write.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>

_syscall3(int,write,int,fd,const char *,buf,off_t,count)

_syscall3是一个宏,定义在include/unistd.h中,可以发现其中还有多个相似的_syscallx宏,x表示可传入的参数个数

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

展开后可以看到是将传入宏的参数替换成了一个函数定义,主要内容是将传入的参数存入各个寄存器,之后调用了int 80的中断。其中__NR_##name被传入了eax,因为所有的调用都通过80号中断,显然这是区分不同函数的参数。同时发现在unistd.h中还定义了一系列类似的宏

#ifdef __LIBRARY__
...
#define __NR_write  4
...
#endif

所以可以看出这是对系统调用的索引,而真正起作用的函数定义在fs/read_write.c中

int sys_write(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    inode=file->f_inode;
    if (inode->i_pipe)
        return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode->i_mode))
        return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
    if (S_ISBLK(inode->i_mode))
        return block_write(inode->i_zone[0],&file->f_pos,buf,count);
    if (S_ISREG(inode->i_mode))
        return file_write(inode,file,buf,count);
    printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
    return -EINVAL;
}

可以看到sys_write(…)的函数原型与之前宏展开的一致,所以在调用write(…)后,实际是调用了sys_write(…),要弄清这个原理,就要去看int 80的实现。

在init/main.c中调用了sched_init(),sched_init()中有一句

set_system_gate(0x80,&system_call);

出现了80号中断,这是一句宏,在include/asm/system.h中,展开得

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \     //将偏移地址低字与选择符组合成低4字节(eax)
    "movw %0,%%dx\n\t" \          //将类型标识字与偏移高字组合成描述符高4字节
    "movl %%eax,%1\n\t" \         //分别设置门描述符的低4字节和高4字节
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))

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

在这里插入图片描述
idt是中断向量表基址,这里指定第80号中断,DPL被指定为3,同时在system_call为”处理函数入口点偏移“,之后进入kernel/system_call.s的system_call

...
nr_system_calls = 74
...
.globl system_call
...
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

nr_system_calls表示总系统调用数,所以最先比较eax中的调用号是否小于总数,之后将各个参数推入堆栈,之后调用地址为sys_call_table + %eax × 4处的函数。sys_call_table在include/linux/sys.h中里面有包括sys_write在内的72个系统调用函数的函数地址表。

    ...
    extern int sys_write();
    ...
    fn_ptr sys_call_table[] = { ...
sys_write, ...};

而sys_write()就是索引为4的函数。由此系统调用的过程结束,总结为
在这里插入图片描述

4、添加调用printval

首先在include/unistd.h中添加系统调用编号:

#define __NR_printval   72

include/linux/sys.h中添加:

extern int sys_printval();

fn_ptr sys_call_table[] = {..., sys_printval };

kernel/system_call.s中修改nr_system_calls的值 :

nr_system_calls = 73

kernel/sys.c中实现系统调用函数:

int sys_printval()
{
	printk("in sys_printval\n");
	return 0;
}

至此系统调用添加完成,重新编译,并启动内核,接下来我们在用户态编写测试代码。

5、用户态测试代码

修改系统头文件 /usr/include/unistd.h ,增加:

#define __NR_printval   72

编写用户态测试代码main.c

#define __LIBRARY__
#include <stdio.h>
#include <unistd.h>

_syscall0(int, printval);

int main()
{
	printf("Hello World!\n");
    printval(); 
    return 0;
}

编译运行:

# gcc main.c
# ./a.out
Hello World!
in sys_printval

参考:
Linux-0.11内核学习笔记【2】:添加系统调用
Linux-0.11源代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值