linux0.11添加系统调用的步骤

假设添加一个系统调用foo()

1.修改include/linux/sys.h

  添加声明

extern int foo();

  同时在sys_call_table数组的最后添加一个元素

...,sys_foo

2.修改include/unistd.h

  增加一个调用号宏

#define __NR_iam     72

  再声明供用户调用的函数

int foo();

3.修改kernel/sys_call.s:

  增加调用个数

nr_system_calls = 

4.新增文件kernel/foo.c,里面包含foo函数,实现系统调用

5.修改kernel/Makefile

  补充和foo.c相关的规则

OBJS  = ... who.o
......
### Dependencies:
foo.s foo.o: foo.c ../include/unistd.h \
../include/asm/segment.h ../include/errno.h
......

到此为止代码的修改已经完成了,下面就是进行make重新生成os镜像:

  到linux-0.11目录下make一次计可,生成的Image就可以作为新系统来驱动计算机了

然后需要挂载hdc,把修改过的sys.h和unistd.h覆盖hdc里面那个系统的usr/include/unistd.h和ust/include/linux/sys.h,最后卸载hdc

然后在oslab里运行./run启动新系统,在里面编写测试文件进行测试新的系统调用就可以啦!

  

系统调用:系统库中为系统调用编写了许多接口函数(API),不同的API对应了不同的真正的(OS内核中)系统调用

系统调用的三个基本步骤:

  1.把系统调用编号存到 寄存器eax中

  2.把函数参数传到其他通用寄存器中:第一个参数:ebx,第二个:ecx ...

  3.触发0x80号中断

在内核加载完毕,切换到用户模式下时,会做一些初始化工作(如main.c里一大堆_init()函数),最后一步启动shell,

用户在shell中可以进行进程调度

  

int 0x80的触发过程和实现细节

1.一些宏定义和头文件include/unistd,h的细节

先看一个系统调用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) \
: "" (__NR_##name),"b" ((long)(a))); \
if (__res >= ) \
return (type) __res; \
errno = -__res; \
return -; \
}

close()用该宏展开后就是

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

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

__NR_close就是系统调用close的对应的编号,该宏在include/unistd.h中定义

__NR_name其实就是系统调用name的对应的编号

2.从中断进入内核

OS内核初始化时(init/main.c函数进行初始化),调用了一个sched_init()函数

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

sched_init()在kernel/sched.c中定义。作用是将各种编号的中断和中断处理程序的地址进行绑定

而0x80中断则和system_call函数进行绑定

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],,,addr)

_set_gate()函数的第一个参数是中断描述符表,表中的每个指针指向一个中断描述符,n=80时对应的描述符就是int80的描述符

3表示将DPL修改成3,因为用户态的CPL是3,这么修改使CPL>=DPL,从而得以进入内核(只有int80的DPL是3,所以用户态只能通过int80进入内核)

addr是中断处理程序,这里将system_call填到int80的中断描述符中

事实上,_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<<)+(type<<))), \
"o" (*((char *) (gate_addr))), \
"o" (*(+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))

这段代码做的就是上述的事

接下去看int80的中断处理函数system_call,定义在 kernel/system_call.s 中,是纯汇编代码

 !……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls =
!…… .globl system_call
.align
system_call: ! # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls-,%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,)
pushl %eax
movl current,%eax
cmpl $,state(%eax)
jne reschedule
cmpl $,counter(%eax)
je reschedule

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

call sys_call_table(,%eax,4):根据寻址方式 展开成 call sys_call_table + 4 * %eax

  其中eax是系统调用号,

    sys_call_table 是一个指针数组,每个指针指向一个系统调用函数,其定义在 include/linux/sys.h 中

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...

  所以4*%eax的意思其实是eax对应的函数指针在表中的地址

至此已经找到了内核中系统调用函数的那个指针,所以直接调用该指针对应的函数就可以啦!

3.用户态和内核态之间数据的传递过程

指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据

例如,调用open(char *filename, ……),

int open(const char * filename, int flag, ...)
{
// ……
__asm__("int $0x80"
:"=a" (res)
:"" (__NR_open),"b" (filename),"c" (flag),
"d" (va_arg(arg,int)));
// ……
}

eax里存了open的调用号,ebx里存了filename字符串指针,即指向了用户态的一串字符串,ecx存了flag...

现在的问题是,内核态要找ebx里的地址对应的字符串时,是在内核态的地址里找的,而ebx里的地址对应的是用户态的

所以要进行一些转换,在system_call函数里:

system_call: //所有的系统调用都从system_call开始
! ……
pushl %edx
pushl %ecx
pushl %ebx # push %ebx,%ecx,%edx,这是传递给系统调用的参数
movl $0x10,%edx # 让ds,es指向GDT,指向核心地址空间
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # 让fs指向的是LDT,指向用户地址空间
mov %dx,%fs
call sys_call_table(,%eax,) # 即call sys_open

可见,在call sys_open前,system对一些寄存器做了处理,

重点来看fs寄存器!

  首先ds,es指向了gdt,即指向了内核的gdt,

  但是fs指向了ldt,ldt对应的是用户态的地址空间

所以通过fs指向用户态地址,ds,es指向内核态地址来进行用户空间和内核空间的信息传递

不了解gdt和ldt的话看下图,来源:https://www.cnblogs.com/chenwb89/p/operating_system_003.html

其实cs的选择子作用在用户态是体现在ldt上的,即可以通过ldt来寻找某个段的地址

总之,依靠fs可以进行用户态的寻址并获取数据,下面看sys_open,在 fs/open.c 文件中定义

int sys_open(const char * filename,int flag,int mode)  //filename这些参数从哪里来?
/*是否记得上面的pushl %edx, pushl %ecx, pushl %ebx?
实际上一个C语言函数调用另一个C语言函数时,编译时就是将要
传递的参数压入栈中(第一个参数最后压,…),然后call …,
所以汇编程序调用C函数时,需要自己编写这些参数压栈的代码…*/
{
……
if ((i=open_namei(filename,flag,mode,&inode))<) {
……
}
……
}

sys_open将最重要的功能交给open_namei(),然后open_namei()再交给dir_namei(),get_dir()

最后get_dir()中调用了一个get_fs_byte()

static struct m_inode * get_dir(const char * pathname)
{
……
if ((c=get_fs_byte(pathname))=='/') {
……
}
……
}

显然,get_fs_byte(pathname)获得了一个用户空间的fs寄存器指向的字节数据

那么同理,也有put_fs_byte函数向fs指向的用户空间输出函数

这两个函数被定义在include/asm/segment.h :

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;
}
extern inline void put_fs_byte(char val,char *addr)
{
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

事实上,他俩以及所有 put_fs_xxx() 和 get_fs_xxx() 都是用户空间和内核空间之间的桥梁

实验部分:增加iam和whoami两个系统调用

思路:

  iam的api里应该有一段包含int80指令的宏_syscall(),把iam对应的系统调用号赋值给eax,其余参数赋值给其余通用寄存器

  通过int80切入到内核中,首先要在unist.h里新增系统调用宏_NR_iam 对应的编号

  因为int80是通过system_call处理的,system_call里的系统调用宏个数应该+1

  sys.h里应该加上sys_iam的系统调用函数声明,同在sys.h里的 sys_call_table应该加上iam个指针,这样才能通过system_call找到这两个函数的地址

  然后在kernel里建立who.c,里面实现函数sys_iam

  然后还要再修改makefile里的数据

实验做起来还有很多细节的东西,我是参考这篇博客的 https://www.shiyanlou.com/courses/reports/1264940/

linux0.11源码内核——系统调用,int80的实现细节的更多相关文章

  1. 鸿蒙内核源码分析(系统调用篇) | 开发者永远的口头禅 | 百篇博客分析OpenHarmony源码 | v37.03

    百篇博客系列篇.本篇为: v37.xx 鸿蒙内核源码分析(系统调用篇) | 开发者永远的口头禅 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁 ...

  2. Linux 0.11源码阅读笔记-中断过程

    Linux 0.11源码阅读笔记-中断过程 是什么中断 中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行.中断包括硬件中断和软件中断,硬中断是由外设自动产 ...

  3. Linux 0.11源码阅读笔记-文件管理

    Linux 0.11源码阅读笔记-文件管理 文件系统 生磁盘 未安装文件系统的磁盘称之为生磁盘,生磁盘也可以作为文件读写,linux中一切皆文件. 磁盘分区 生磁盘可以被分区,分区中可以安装文件系统, ...

  4. Linux 0.11源码阅读笔记-总览

    Linux 0.11源码阅读笔记-总览 阅读源码的目的 加深对Linux操作系统的了解,了解Linux操作系统基本架构,熟悉进程管理.内存管理等主要模块知识. 通过阅读教复杂的代码,锻炼自己复杂项目代 ...

  5. Linux 0.11源码阅读笔记-文件IO流程

    文件IO流程 用户进程read.write在高速缓冲块上读写数据,高速缓冲块和块设备交换数据. 什么时机将磁盘块数据读到缓冲块? 什么时机将缓冲块数据刷到磁盘块? 函数调用关系 read/write( ...

  6. jQuery1.11源码分析(1)-----Sizzle源码概览[原创]

    最近在啃jQuery1.11源码,上来就遇到Sizzle这个jQuery的大核心,虽然已经清楚了Sizzle的用途,先绕过去也没事,但明知山有虎偏向虎山行才是我们要做的. 本文面向的阅读对象:正在学习 ...

  7. Linux0.11内核源码——内核态进程切换的改进

    本来想自己写的,但是发现了一篇十分优秀的博客 https://www.cnblogs.com/tradoff/p/5734582.html system_call的源码解析:https://blog. ...

  8. Linux0.11内核源码——内核态线程(进程)切换的实现

    以fork()函数为例,分析内核态进程切换的实现 首先在用户态的某个进程中执行了fork()函数 fork引发中断,切入内核,内核栈绑定用户栈 首先分析五段论中的第一段: 中断入口:先把相关寄存器压栈 ...

  9. jQuery1.11源码分析(7)-----jQuery一些基本的API

    这篇文章比较繁杂,主要就是把jQuery源码从上到下列出来,看我的注释就好了. jQuery源码对各种加载器做了处理. //阅读这个源码是请先了解一下概念,即时函数,工厂模式 (function( g ...

随机推荐

  1. Hive 窗口函数之 lead() over(partition by ) 和 lag() over(partition by )

    lead函数用于提取当前行前某行的数据 lag函数用于提取当前行后某行的数据 语法如下: lead(expression,offset,default) over(partition by ... o ...

  2. socketpair

    与pipe的区别 pipe产生的文件描述符是半双工的,需要pipe两次才能实现全双工,产生的两个描述符是一个读,一个写 socketpair直接就可以全双工,产生的两个文件描述符的任何一个都可读可写 ...

  3. [Linux] 008 文件处理命令

    1. 文件处理命令:touch 命令名称:touch 命令所在路径:/bin/touch 执行权限:所有用户 语法:touch [文件名] 功能描述:创建空文件 范例: 文件名不包含空格 touch ...

  4. Java_1.Java符号体系

    Java符号包含五类:标识符.关键字.常量及字面量.运算符.分隔符 1.标识符 定义:用于标明程序中元素的名字,如类.方法和变量 命名规则: ·由字母.数字.下划线(_)和美元符号($)构成的字母序列 ...

  5. [fw]Linux下tty/pty/pts/ptmx详解

    基本概念: 1> tty(终端设备的统称):tty一词源于Teletypes,或者teletypewriters,原来指的是电传打字机,是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东 ...

  6. proxool连接池配置方法

    proxool.properties: jdbc-1.proxool.alias=test #jdbc-1.proxool.driver-class=com.mysql.jdbc.Driver #jd ...

  7. CodeChef Gcd Queries

    Gcd Queries   Problem code: GCDQ   Submit All Submissions   All submissions for this problem are ava ...

  8. 合并石子 (区间dp+前缀和)

    [题目描述] N堆石子.现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分.计算出将N堆石子合并成一堆的最小得分. [题目链接] http: ...

  9. SpringBoot-技术专区-详细打印启动时异常堆栈信息

    SpringBoot在项目启动时如果遇到异常并不能友好的打印出具体的堆栈错误信息,我们只能查看到简单的错误消息,以致于并不能及时解决发生的问题,针对这个问题SpringBoot提供了故障分析仪的概念( ...

  10. Go语言_流程控制语句:for、if、else、switch 和 defer

    流程控制语句:for.if.else.switch 和 defer 学习如何使用条件.循环.分支和推迟语句来控制代码的流程. Go 作者组编写,Go-zh 小组翻译. https://go-zh.or ...