实验3 系统调用

提醒

这次实验涉及的宏过于复杂,加上本人能力有限,我也没有花大量时间去研究每一段代码,只是理解到每一段代码做了什么这一程度。

实验目的

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

  1. iam()

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

    int iam(const char * name);
    

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

  2. whoami()

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

    int whoami(char* name, unsigned int size);
    

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

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

在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。

调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。

而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

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

linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。

我们不妨看看 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);

在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。

该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami__NR_iam 两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。

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

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

在内核初始化时,主函数在 init/main.c 中,调用了 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

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

!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!…….globl system_call
.align 2
system_call:! # 检查系统调用编号是否在合法范围内cmpl \$nr_system_calls-1,%eaxja bad_sys_callpush %dspush %espush %fspushl %edxpushl %ecx! # push %ebx,%ecx,%edx,是传递给系统调用的参数pushl %ebx! # 让ds, es指向GDT,内核地址空间movl $0x10,%edxmov %dx,%dsmov %dx,%esmovl $0x17,%edx
! # 让fs指向LDT,用户地址空间mov %dx,%fscall sys_call_table(,%eax,4)pushl %eaxmovl current,%eaxcmpl $0,state(%eax)jne reschedulecmpl $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 中:

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

增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iamsys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。

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

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

不然,编译会出错的。

实现 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,然后实现两个函数就万事大吉了。

按照上述逻辑修改相应文件

通过上文描述,我们已经理清楚了要修改的地方在哪里

  1. 添加iam和whoami系统调用编号的宏定义(_NR_xxxxxx),文件:include/unistd.h

  2. 修改系统调用总数, 文件:kernel/system_call.s

  3. 为新增的系统调用添加系统调用名并维护系统调用表,文件:include/linux/sys.h

  4. 为新增的系统调用编写代码实现,在linux-0.11/kernel目录下,创建一个文件 who.c

    #include <asm/segment.h>
    #include <errno.h>
    #include <string.h>char _myname[24];int sys_iam(const char *name)
    {char str[25];int i = 0;do{// get char from user inputstr[i] = get_fs_byte(name + i);} while (i <= 25 && str[i++] != '\0');if (i > 24){errno = EINVAL;i = -1;}else{// copy from user mode to kernel modestrcpy(_myname, str);}return i;
    }int sys_whoami(char *name, unsigned int size)
    {int length = strlen(_myname);printk("%s\n", _myname);if (size < length){errno = EINVAL;length = -1;}else{int i = 0;for (i = 0; i < length; i++){// copy from kernel mode to user modeput_fs_byte(_myname[i], name + i);}}return length;
    }
    

修改 Makefile

要想让我们添加的 kernel/who.c 可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。

Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 就可以编译整个代码树,是因为 make 完全按照 Makefile 里的指示工作。

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

(1)第一处

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

添加了 who.o

(2)第二处

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

添加了 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h

Makefile 修改后,和往常一样 make all 就能自动把 who.c 加入到内核中了。

编写测试程序

到此为止,内核中需要修改的部分已经完成,接下来需要编写测试程序来验证新增的系统调用是否已经被编译到linux-0.11内核可供调用。首先在oslab目录下编写iam.c,whoami.c

/* iam.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);int main(int argc, char *argv[])
{/*调用系统调用iam()*/iam(argv[1]);return 0;
}
/* whoami.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
#include <stdio.h>_syscall2(int, whoami,char *,name,unsigned int,size);int main(int argc, char *argv[])
{char username[64] = {0};/*调用系统调用whoami()*/whoami(username, 24);printf("%s\n", username);return 0;
}

以上两个文件需要放到启动后的linux-0.11操作系统上运行,验证新增的系统调用是否有效,那如何才能将这两个文件从宿主机转到稍后虚拟机中启动的linux-0.11操作系统上呢?这里我们采用挂载方式实现宿主机与虚拟机操作系统的文件共享,在 oslab 目录下执行以下命令挂载hdc目录到虚拟机操作系统上。

sudo ./mount-hdc

再通过以下命令将上述两个文件拷贝到虚拟机linux-0.11操作系统/usr/root/目录下,命令在oslab/目录下执行:

cp iam.c whoami.c hdc/usr/root

如果目标目录下存在对应的两个文件则可启动虚拟机进行测试了。

  • 编译

    [/usr/root]# gcc -o iam iam.c
    [/usr/root]# gcc -o whoami whoami.c
    
  • 运行测试

    [/usr/root]# ./iam wcf
    [/usr/root]# ./whoami
    

命令执行后,很可能会报以下错误:

这代表虚拟机操作系统中/usr/include/unistd.h文件中没有新增的系统调用调用号

为新增系统调用设置调用号

#define __NR_whoami      72
#define __NR_iam        73

再次执行:

实验成功


  • 为什么这里会打印2次?

  • 因为在系统内核中执行了 printk() 函数,在用户模式下又执行了一次 printf() 函数。

要知道到,printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()。

printk()printf() 的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。

天道酬勤

实验三总共花费7小时,看的不是特别仔细,没有特别深入的学习宏展开和内联汇编。但基本理解了系统调用的目的和方式,Linus永远的神!

超详细!操作系统实验三 系统调用(哈工大李治军)相关推荐

  1. 【操作系统】实验楼操作系统实验三——系统调用

    操作系统实验3--系统调用 环境配置 ​ 首先同样是配置环境 cd /home/shiyanlou/oslab tar -zxvf hit-oslab-linux-20110823.tar.gz -C ...

  2. 操作系统实验一到实验九合集(哈工大李治军)

    操作系统实验 作者寄语 操作系统实验的学习是一个循序渐进的过程,初次看linux-0.11中的代码,看着满屏的汇编语言,确实头疼.但通过学习赵炯博士的Linux内核0.11完全注释,结合着王爽老师的汇 ...

  3. Linux0.11操作系统(哈工大李治军老师)实验楼实验2-系统调用

    Linux0.11操作系统(哈工大李治军老师)实验楼实验2-系统调用 在 Linux 0.11 上添加两个系统调用iam()和whoami(),并编写两个简单的应用程序测试它们. 原理 1. 应用程序 ...

  4. Linux0.11操作系统(哈工大李治军老师)实验楼实验1-引导

    Linux0.11操作系统(哈工大李治军老师)实验楼实验1-引导 实验源地址: https://www.lanqiao.cn/courses/115/learning/ 1.完成bootsect.s屏 ...

  5. 【K8S实战】-超详细教程(三)

    [K8S实战]-超详细教程(三) 1.存储 1.1.nfs默认存储 我这里只演示nfs作为K8S的默认存储,其他的可以看这里[存储类]. 1.1.1.安装nfs服务 所有机器都安装nfs工具 所有机器 ...

  6. 操作系统实验三、进程通信

    文章目录 操作系统实验三.进程通信 一.实验目的 二.实验内容 三.设计原理(或方案)及相关算法 四.结果分析 五.源代码 操作系统实验三.进程通信 一.实验目的 ​ 1.了解和熟悉Linux支持的消 ...

  7. WebRTC VideoEngine超详细教程(三)——集成X264编码和ffmpeg解码

    转自:http://blog.csdn.net/nonmarking/article/details/47958395 本系列目前共三篇文章,后续还会更新 WebRTC VideoEngine超详细教 ...

  8. linux中关于ssh实验,操作系统实验三linux的telnetftpssh的相关配置及验证

    操作系统实验三linux的telnetftpssh的相关配置及验证 -1-昆明理工大学信息工程与自动化学院学生实验报告( 2010 -2011 学年第 二 学期 )课程名称:操作系统 开课实验室:信自 ...

  9. 数据结构--链栈的c语言实现(超详细注释/实验报告)

    数据结构–链栈的c语言实现(超详细注释/实验报告) 知识小回顾 栈(Stack)作为一种限定性线性表,是将线性表的插入和删除操作限制为仅在表的一端进行,通常将表中允许进行插入.删除操作的一端成为栈顶( ...

最新文章

  1. 【LeetCode】Recursion(共11题)
  2. 认识 linux sysfs文件系统
  3. python基础: String类型
  4. 用Canvas为网页加入动态背景
  5. java ThreadLocal理解和使用
  6. 网站登陆页面设计灵感,UI设计得有这个范儿
  7. FILESTREAM data cannot be placed on an empty filegroup 解决办法
  8. 晚上无聊象征性收取了网友100元辛苦费,实现支持多语言功能的XML语言包版的C#的ASP.NET多语言支持例子程序...
  9. 产品迭代的节奏怎样适应需求的变化?
  10. 转载:AD的授权还原和主还原:深入浅出Active Directory系列(六)
  11. python获取session里的_python 怎么取sessionid-问答-阿里云开发者社区-阿里云
  12. mysql join 组合索引,图文详解MySQL中两表关联的连接表如何创建索引
  13. 运维定位服务故障时,前5分钟都在忙啥?
  14. 设计原则 里氏替换原则
  15. 苹果电脑 默认安装jdk位置_CH01_JDK安装和配置(含macOS)
  16. 基于Python的视频解析器
  17. 网工必备交换机原理与配置
  18. 如何压缩动态图片大小?gif图太大了怎么压缩?
  19. 浏览器端转盘抽奖策略实现
  20. 让整个页面从iframe中跳出来

热门文章

  1. 基于BufPay的php支付平台,用于discuz积分充值。(不用跳转到bufpay)
  2. 云原生周报 | K8s 树内存储向 CSI 卷迁移进展;百度混部技术解析;BFE 通过工信部开源成熟度评估
  3. 使用linux系统的手机有哪些内容吗,为什么工作选LINUX,我只听说过手机有这个系统...
  4. swift associatedtype和typealias
  5. QCustomplot常用设置
  6. 盛世昊通企业微信板块打造数字生态共同体
  7. *** glibc detected *** ./Simple_Sound_Recording: free(): corrupted unsorted chunks: 0x0001c8a0 ***
  8. Redis基本命令速查表
  9. 七年程序老鸟理解的互联网3.0时代
  10. 机器学习(六)树模型详解