哈工大 操作系统 lab2解答

2024-01-07 22:59

本文主要是介绍哈工大 操作系统 lab2解答,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

实验目的

  • 建立对系统调用接口的深入认识
  • 掌握系统调用的基本过程
  • 能完成系统调用的全面控制
  • 为后续实验做准备

实验内容

此次实验的基本内容是:在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中实现。

测试程序

运行添加过新系统调用的Linux 0.11,在其环境下编写两个测试程序iam.c和whoami.c。最终的运行结果是:

$ ./iam lizhijun
$ ./whoami
lizhijun

实验原理

操作系统实现系统调用的基本过程是:

  1. 应用程序调用库函数(API);
  2. API将系统调用号存入EAX,然后通过中断调用使系统进入内核态;
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  4. 系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数;
  5. 中断处理函数返回到API中;
  6. API将EAX返回给应用程序。

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

在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。调用自定义函数是通过call指令直接跳转到该函数的地址,继续运行。而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫API(Application Programming Interface)。API并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

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

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中定义。将_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。而在应用程序中,要有:

#define __LIBRARY__                    /* 有它,_syscall1等才有效。详见unistd.h */
#include <unistd.h>                /* 有它,编译器才能获知自定义的系统调用的编号 */
_syscall1(int, iam, const char*, name);        /* iam()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);    /* whoami()在用户空间的接口函数 */

在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中,Linux实验环境下是main(),Windows下因编译器兼容性问题被换名为start())调用了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章。

接下来看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_callpush %dspush %espush %fspushl %edxpushl %ecx        pushl %ebx        # push %ebx,%ecx,%edx,是传递给系统调用的参数movl $0x10,%edx        # 让ds,es指向GDT,内核地址空间mov %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修饰为其他函数可见。Windows实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。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_iam和sys_whoami。当然该函数在sys_call_table数组中的位置必须和__NR_xxxxxx的值对应上。同时还要仿照此文件中前面各个系统调用的写法,加上:

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

不然,编译会出错的。

实验过程

添加系统调用的流程

添加一个系统调用的流程如下:

  1. 修改 include/unistd.h, 添加#define __NR_foo num,num为接下来使用的系统调用号

  2. 修改 include/linux/sys.h , 添加extern rettype sys_foo();, 在sys_call_table数组对应位置加入sys_foo

  3. 修改 kernel/system_call.s,修改 nr_system_calls = num (num为系统调用总数目)

  4. kernel 中添加 foo.c (若需要支持内核态与用户态数据交互,则包含 include/asm/segment.h,其中有 put_fs_XXXget_fs_XXX函数)

  5. foo.c 实现系统调用sys_foo()

  6. 修改 kernel 的Makefile,将 foo.c 与内核其它代码编译链接到一起

  7. 系统调用用户需要使用

#define __LIBRARY__
#include <unistd.h>
_syscallN宏展开系统调用,提供用户态的系统调用接口(参数数目确定具体宏)

添加whoami和iam两个系统调用:

  1. 修改 include/unistd.h, 添加#define __NR_foo num,num为接下来使用的系统调用号
#define __NR_iam        72
#define __NR_whoami     73
  1. 修改 include/linux/sys.h , 添加extern rettype sys_foo();, 在sys_call_table数组对应位置加入sys_foo
extern int sys_iam();
extern int sys_whoami();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};
  1. 修改 kernel/system_call.s,修改 nr_system_calls = num (num为系统调用总数目)
nr_system_calls = 74
  1. kernel 中添加 foo.c (若需要支持内核态与用户态数据交互,则包含 include/asm/segment.h,其中有 put_fs_XXXget_fs_XXX函数)
  2. foo.c 实现系统调用sys_foo()

who.c内容如下:

#include <string.h>
#include <errno.h>
#include <asm/segment.h>char username[24];int sys_iam(const char * name) {char tmp[26];short break_flag = 0, i = 0;for (i = 0; i < 26; ++i) {tmp[i] = get_fs_byte(name + i);if (tmp[i] == '\0') {break_flag = 1;break;}}if (!break_flag || i > 23) {return -(EINVAL);}char* dest = username;strcpy(dest, tmp);return i;
}int sys_whoami(char* name, unsigned int size) {short length = strlen(username);if (length > size) {return -(EINVAL);}short i = 0;for (i; i < size; ++i) {put_fs_byte(username[i], name + i);if (username[i] == '\0') {break;}}return i;
}
  1. 修改 kernel 的Makefile,将 foo.c 与内核其它代码编译链接到一起
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:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
  1. 用户调用系统调用:(在运行的linux0.11上编写编译运行)

whoami.c:

#include <errno.h>
#define __LIBRARY__
#include <unistd.h>_syscall2(int, whoami,char*,name,unsigned int,size);int main()
{char s[30];whoami(s,30);printf("%s",s);return 0;
}

iam.c:

#include <errno.h>
#define __LIBRARY__
#include <unistd.h>
#include <stdio.h>_syscall1(int, iam, const char*, name);int main(int argc,char ** argv)
{iam(argv[1]);return 0;
}

如编译错误,说__NR_whoami和__NR_iam未定义,则是下面的问题:

在0.11环境下编译C程序,包含的头文件都在/usr/include目录下。该目录下的unistd.h是标准头文件(它和0.11源码树中的unistd.h并不是同一个文件,虽然内容可能相同),没有__NR_whoami和__NR_iam两个宏,需要手工加上它们,也可以直接从修改过的0.11源码树中拷贝新的unistd.h过来。

实验结果

可以发现可以很好地执行!证明此次实验是成功的!

80bd2d73ecd6a250643a2ab96748c98

image-20211206163543977

reference

[1] 实验指导书

[2] 现成代码

这篇关于哈工大 操作系统 lab2解答的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/581525

相关文章

Linux操作系统 初识

在认识操作系统之前,我们首先来了解一下计算机的发展: 计算机的发展 世界上第一台计算机名叫埃尼阿克,诞生在1945年2月14日,用于军事用途。 后来因为计算机的优势和潜力巨大,计算机开始飞速发展,并产生了一个当时一直有效的定律:摩尔定律--当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。 那么相应的,计算机就会变得越来越快,越来越小型化。

1、简述linux操作系统启动流程

1、简述linux操作系统启动流程 启动第一步--加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它。这是因为BIOS中包含了CPU的相关信息、设备启动顺序信息、硬盘信息、内存信息、时钟信息、PnP特性等等。开机时将ROM中的指令映射到RAM的低地址空间,CPU读取到这些指令,硬件的健康状况进行检查,按照BIOS中设置的启

操作系统是怎么为不同的程序分配所需的内存空间的

操作系统为不同的程序分配内存空间的过程涉及多个关键步骤,确保每个程序都有其所需的内存资源,同时避免程序之间的冲突。以下是操作系统如何为程序分配内存空间的详细过程: 1. 内存管理的基础概念 虚拟内存:现代操作系统使用虚拟内存机制来为程序提供隔离的内存空间。每个程序运行在其独立的虚拟地址空间中,这使得程序间的内存互不干扰。物理内存:实际的 RAM(随机存取存储器),由操作系统和硬件共同管理。虚拟

操作系统安全保护

操作系统安全概述 概念:满足安全策略要求,具有响应安全机制及安全功符合特定安全标准,在一定约束条件下 能抵御常见网络安全威胁,保障自身安全运行及资源安全 安全等级:根据安全功能和安全保障要求分为 用户自主保护级  系统审计保护级 安全标记保护级 结构化保护级 访问验证保护级 操作系统作用: 负责计算系统的资源管理、支撑和控制各种应用程序运行,为用户提供计算机系统管理接口 是构成网络信息

Linux操作系统命令集(一)

最近开了操作系统的课,弄着虚拟机的linux系统命令学学 文件和目录操作命令: ls:列出目录内容 示例:ls -l 以长格式列出目录内容cd:切换目录 示例:cd /home/user 切换到 /home/user 目录mkdir:创建目录 示例:mkdir new_directory 创建名为 new_directory 的目录rmdir:删除空目录touch:创建空文件或更新文件的时间戳

操作系统分页式存储管理

每次输入地址后,计算出页号,若页号越界,则给出错误提示。否则依次调用FIFO和LRU算法,这里值得注意的是,由于我们的FIFO算法先于LRU算法被调用,那么当在处理FIFO算法时,我们暂且不将位视图相应位置做变化,留到处理LRU算法再做处理。 对于FIFO、LRU算法的缺页,我们分两种情况考虑,第一种是模拟栈内还有空间,那么直接将其入栈。第二种是模拟栈内无空间,要发生置换。发生置换时把模拟栈最底

linux定时监听ssh服务是否启动-------麒麟操作系统永久关闭swap

linux监听ssh服务是否启动 1、监听脚本2、定时任务3、麒麟操作系统,永久关闭swap 1、监听脚本 #在/usr/local/bin目录下新建脚本文件 cd /usr/local/bintouch check_sshd.sh#给可执行权限chmod +x /usr/local/bin/check_sshd.sh 脚本内容如下: #!/bin/bashs

【银河麒麟高级服务器操作系统实例】虚拟化平台系统服务中断现象分析及处理建议

服务器环境以及配置 【机型】虚机 处理器: Kunpeng-920 内存: 40G 【内核版本】 4.19.90-23.8.v2101.ky10.aarch64 【OS镜像版本】 银河麒麟操作系统 Kylin-Server-10-SP1-Release-Build20-20210518-arm64 【第三方软件】 智能运维系统、mysql数据集群 现象描述 环境描

VMware17 虚拟机下载以及 CentOS8 操作系统安装配置 一条龙全教程

目录 一、安装 vmware workstation 虚拟机  二、安装 CentOS8 操作系统 三、安装 FinalShell 远程连接 一、安装 vmware workstation 虚拟机     安装中...(耐心等待)  到此安装完成,点击启动运行  激活码如下:  MC60H-DWHD5-H80U9-6V85M-8280D

操作系统之虚拟机

虚拟机(Virtual Machine, VM)是一种通过虚拟化技术将一台物理计算机虚拟化为多个独立的虚拟计算机的技术。每个虚拟机都可以运行自己的操作系统,仿佛它们是独立的物理计算机。虚拟机的引入大大提高了计算资源的利用率,并提供了更大的灵活性。 虚拟机的定义 虚拟机是通过虚拟化技术在一台物理计算机上创建的多个虚拟计算环境。每个虚拟环境(虚拟机)都可以运行一个独立的操作系统和应用程序,仿