注意,在进行实验3之前,
需要将之前在实验2修改过的文件, 恢复成最初的没有修改的文件,最方便的方式是将hit-oslab
重新解压;
to do:
实验中需要完成的事情:
who.c
的模块文件, 在其中定义两个系统功能函数, sys_iam()
函数用于存储输入的字符串, sys_whoami()
用于输出已经存储的字符串。以上两个步骤, 属于系统内部的功能, 所以在添加和修改完成后,需要重新
make
重新生成新的系统内核;
注意事项:
make
: 每次修改过的文件之后, 都需要重新编译Makefile
,在oslab/linux-0.11
进行make
生成整个系统的新内核. 虽然kernel/
,lib/
目录下,都有对应的对应的Makefile
文件, 但都是编译该目录下的文件。
执行
sync
: 在Bochs
模拟环境中, 无论是修改了文件, 还是使用了gcc
生成了新的可执行文件, 此时修改的内容和 新生成的内容,都只是放在内存缓冲器, 需要执行sync
才能保存到存储器中;
宏定义
#define
需要写在#include
之前, 并且#include
包含的头文件后面不要有中文注释,其他地方的注释尽量使用/**/
的方式去注释,确保在bochs
中使用gcc 3.4 编译文件时可以通过。
这是因为 #include 语句会在预处理阶段将指定的头文件包含到当前文件中,而 #define 语句会在预处理阶段替换所有与宏定义同名的词。如果你把 #define 语句写在 #include 语句之后,那么在包含的头文件中可能会出现与宏定义同名的词,这样就会导致替换错误。因此,最好是把 #define 语句写在 #include 语句之前,以免出现意想不到的错误。
添加新的系统功能函数, 在 linux-0.11/kernel
内核目录下, 创建一个who.c
的文件模块, 在该模块中实现两个函数 sys_iam()
和 sys_whoami()
。
每个系统调用都有一个 sys_xxxxxx()
与之对应,它们都是我们学习和模仿的好对象。
比如在 fs/open.c
中的 sys_close(int fd)
,它没有什么特别的,都是实实在在地做 close()
该做的事情。:
int sys_close(unsigned int fd)
{
// ……
return (0);
}
所以只要自己创建一个文件:kernel/who.c
,然后实现两个函数就万事大吉了。
iam()
函数Linux 0.11 上添加两个系统功能函数,
第一个系统调用是 iam()
,其原型为:
int sys_iam(const char* name)
该函数的作用:
将字符串参数
name
的内容, 从内存中的用户地址空间拷贝到内存中的内核地址空间保存起来;
要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL
在 kernal/who.c
中实现此系统调用。
errno; errno 是一种传统的错误代码返回机制。
当一个函数调用出错时,通常会返回 -1
给调用者。
但 -1
只能说明出错,不能说明错是什么。
为解决此问题,全局变量 errno
登场了。错误值被存放到 errno 中,于是调用者就可以通过判断 errno 来决定如何应对错误了。
各种系统对 errno
的值的含义都有标准定义。
Linux 下用“man errno”可以看到这些定义。
whoami()
函数第二个系统函数是 sys_whoami()
,同样在kernel/who.c
中添加该函数:
int sys_whoami(char* name, unsigned int size);
作用:
将内核中由 iam()
保存的名字拷贝到 name
指向的用户地址空间中,
同时确保不会对name
越界访存, name 的大小由size
说明,
返回值是拷贝的字符数,
如果size 小于需要的空间, 返回-1
, 并且置 errno 为 EINVAL
这里需要注意 重要的内核函数, get_fs_byte()
, put_fs_byte()
这两个函数 是实现 用户态的数据 和 内核态数据之间的 重要实现;
get_fs_byte()
: 输入一个地址, 该函数将返回该地址上一个字节的字符;
tmp[i] = get_fs_byte(name +i)
put_fs_byte()
: 将一个字节的字符输出到指定的位置上,
/*msg[i] 代表一个字符, name +i 代表一个地址*/
put_fs_byte(msg[i], name +i)
/* error.h 用于 error 变量的使用*/
#include
/* string.h 库函数, 用于 errno 为 einval */
#include
/* 用于调用 get_fs_byte(), put_fs_byte() */
#include
#define maxSize 24
char msg[maxSize]; /* 设置全局变量msg, 用于存放字符 */
int sys_iam( char* name ){
/* 传入一个字符串的起始地址, 该地址是用户态中字符串的起始地址, 并将所有的字符保存到内核中*/
char tmp[maxSize]; /*用于 临时拷贝字符串到 tmp 中 */
int i ;
for (i = 0; i < maxSize; i++){ /*get_fs_byte 用于将用户态的字符 拷贝到内核态中, */
tmp[i] = get_fs_byte(name + i); /* use get_fs_byte() 使用该函数, 将 name 当前地址的一个字节的字符拷贝进去*/
printk("the current charater is %c \n", tmp[i]);
if ( tmp[i] == '\0') break; /* 如果当前的字符, 是结束字符 /0, 则终止拷贝*/
}
if (i == maxSize){
printk('the input string too long, please short for 23 chara \n');
return -EINVAL;
}
else { /* 如果没有超过 23个字符, 则正常将字符拷贝过去*/
strcpy(msg, tmp);
return i; /* 返回字符串的长度, */
}
}
int sys_whoami(char* name, unsigned int size ){
/* 传入一个地址,该地址是用户态中输入的地址, 即用户态中一个字符数值的起始地址,用一个数组名称可以表示; */
/* 传入一个,用户期待 输出字符的个数 */
int msg_size = 0;
/* 直接统计, 全局变量msg 字符串长度的大小 */
while ( msg[msg_size] != '\0' ) msg_size++;
/* 打印出, 保存在内核中, 字符串的长度和 字符串本身 */
printk("the following string output from the system kernl \n");
printk("the stored msg: %s \n", msg);
printk("the stored msg size: %d \n", msg_size);
/* 如果用户要求输出的长度 < 原始存储字符串的长度, 输出error*/
if ( size < msg_size ) return -EINVAL;
int i;
for (i=0; i< size; i++){
printk( "the current addres is %d \n", name + i ); /* 将传入的用户态的地址,逐个往后移动*/
printk("the current character is %c \n", msg[i] ); /* 如何将内核中的字符, 输出到用户态的地址中呢? */
/* put_fs_byte */
put_fs_byte(msg[i], name + i);
if( msg[i] == '\0') break;
}
return i; /* 返回实际存储字符串的长度*/
}
在 include/unistd.h
中添加 iam
,whoami
系统调用宏定义的编号;
#define __NR_sgetmask 68
#define __NR_ssetmask 69
#define __NR_setreuid 70
#define __NR_setregid 71
#define __NR_iam 72
#define __NR_whoami 73
kernel/system_call.s
# offsets within sigaction
sa_handler = 0
sa_mask = 4
sa_flags = 8
sa_restorer = 12
nr_system_calls = 74
为新增的系统调用添加系统调用名并维护系统调用表,
include/linux/sys.h
注意, 这里的系统调用表 sys_call_table
中各个调用函数的顺序需要与头文件中 include/unistd.h
中的定义系统调用编号(72, 73) 保持一致, 通常是从最后开始按照顺序添加。
extern int sys_setregid();
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 };
make
, 生成新内核make
修改 Makefile 文件,参见这里makefile 文件修改
重新 make,
到/home/shiyanlou/os/oslab/linux-0.11
的路径下,重新执行 make clean && make
.
makefile
要想让我们添加的kernel/who.c
可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。
Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 就可以编译整个代码树,是因为 make 完全按照 Makefile 里的指示工作。
Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile
。
需要修改两处。
第一处:
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
。
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:
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
。
### 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 是对的。
为了在 Linux 0.11 测试新添加的两个系统函数, 需要编写两个测试函数,
在其环境下编写两个测试程序 iam.c 和 whoami.c。最终的运行结果是:
注意在, 在bochs 的环境 中使用 gcc-3.4 编译文件,
宏定义必须放在 库的载入的前面,
#define __LIBRARY__
#include
error: #include expects “FILENAME”;
include 后面的注释中不能有中文出现 ;
分别编写iam.c
, whoami.c
的程序。
注意, 程序中的注释是为了说明, 在bochos 环境中使用Gcc 3.4 编译时,需要删除;
iam.c
代码:
#define __LIBRARY__
/* 使得系统调用syscallN 生效; */
#include // 该文件中这里定义了 syscallN 的宏定义, 并且编译器从中获取自定义的系统调用的编号;
#include // 全局变量,errno 用于返回错误值;
#include // 运行嵌入汇编
#inclcude <linux/kernel.h>
/* 接口函数 */
_syscall1(int, iam, const char*, name);
iam.c
代码:
/* iam.c */
/* 是的 syscalln 起作用*/
#define __LIBRARY__
/* 中文 注释*/
#include
_syscall1(int, iam, const char*, name);
int main(int argc, char *argv[])
{
/* argv 是一个指针数组,即该数组里面存放的都是地址,
默认,argv[0]: 输入执行程序的路径与名称;
argv[1]: 除了程序名称, 指向了第一个参数的首地址;
比方说 :
./test para_1 para_2
argv[0]: 代表了 test 的路径与名称;
argv[1]: 指向了参数 para_1 的首地址;
*/
/*调用系统调用iam()*/
iam(argv[1]);
return 0;
}
whoami.c
代码:
#define __LIBRARY__
#include /*donot include chinese */
#include
_syscall2(int,whoami,char*,name,unsigned int,size);
int main() {
char s[30] = {3,3,4,0,1,3}; /*如果打印,需要赋值, 建立的数组可以是任意长度,用于将 iam 的字符串赋值到其中;*/
whoami(s, 30); /*/ 但是,在系统调用的时候, 传入的长度不能超过 iam 中规定的字符串长度,即24;*/
printf("Hello, here output from the user mode : %s\n", s);
return 0;
}
以上两个测试文件需要放到 linux-0.11操作系统上运行,
才能验证新增的系统调用是否有效,
linux-0.11
操作系统上呢?这里我们采用挂载方式实现宿主机与虚拟机操作系统的文件共享,
:~/Hit_os-main/hit-oslab-linux-20110823/oslab$ sudo ./mount-hdc
挂载之后, 则虚拟机的操作系统的文件系统 可以显示:
~/Hit_os-main/hit-oslab-linux-20110823/oslab/hdc$ pwd
/home/shiyanlou/Hit_os-main/hit-oslab-linux-20110823/oslab/hdc
shiyanlou@Lenovo-Legion:~/Hit_os-main/hit-oslab-linux-20110823/oslab/hdc$ ls
bin dev etc image Image mnt shoelace tmp usr var
拷贝文件
cp /home/shiyanlou/Hit_os-main/hit-oslab-linux-20110823/lab3_system_call/iam.c /home/shiyanlou/Hit_os-main/hit-oslab-linux-20110823/lab3_system_call/whoami.c hdc/usr/root
~/Hit_os-main/hit-oslab-linux-20110823/oslab/hdc/usr/root$ ls
gcclib140 hello.c iam.c linux0.tgz README shoelace.tar.Z
hello hello.o linux-0.00 mtools.howto shoe whoami.c
注意,需要关闭该终端, 才能启动 bochs
启动 bochs
虚拟机, 在该虚拟环境下, 编译并运行这两个测试程序;
虽然, 之前在 include/unistd.h
中添加了新增的系统调用号,
但是这里的错误, 显示 虚拟机中的 /usr/include/unistd.h
文件中没有, 这里仍然添加进去, 使用 vi
打开并且编辑。
注意, vi
的使用, 稍微与 vim
不同
在bochs 虚拟环境中, 修改了文件时, 此时修改了保存在了内存缓冲区中
为了保证写入磁盘后,需要执行 sync
指令, 是否要在/usr/includel
路径下执行不确定;否则的话, 重启bochs 之后, 之前的修改会丢失掉;
激动地运行一下由你亲手修改过的 “Linux 0.11 pro++”!然后编写一个简单的应用程序进行测试。
可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:
$ gcc -o iam iam.c -Wall
gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:
$ ./iam
如果如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运!
printk()
调试内核:比如在 sys_iam()
中向终端 printk()
一些信息,让应用程序调用 iam()
,从结果可以看出系统调用是否被真的调用到了。
oslab 实验环境提供了基于 C 语言和汇编语言的两种调试手段。除此之外,适当地向屏幕输出一些程序运行状态的信息,也是一种很高效、便捷的调试方法,有时甚至是唯一的方法,被称为“printf 法”。
要知道到,printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()。
printk() 和 printf() 的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。
看一看 printk 的代码(在 kernel/printk.c 中)就知道了:
int printk(const char *fmt, ...)
{
// ……
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (i):"ax","cx","dx");
// ……
}
显然,printk() 首先 push %fs 保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()。查看 printf() 可以看到,它最终也要落实到这个函数上。
# 确认在 oslab 目录下
$ cd ~/oslab/
# 运行脚本前确定已经关闭刚刚运行的 Bochs
$ ./dbg-asm
$ cd ~/oslab
$ ./dbg-c
注意:启动的顺序不能交换,否则 gdb 无法连接。
然后再打开一个终端窗口,执行:
$ cd ~/oslab
$ ./rungdb
此时, 再回到 之前打开的那个终端: 出现ed to 127.0.0.1
才算连接成功。