• lab3_系统调用(下)


    注意,在进行实验3之前,
    需要将之前在实验2修改过的文件, 恢复成最初的没有修改的文件,最方便的方式是将 hit-oslab 重新解压;

    to do:
    实验中需要完成的事情:

    1. 在kernel 中,添加一个who.c 的模块文件, 在其中定义两个系统功能函数, sys_iam() 函数用于存储输入的字符串, sys_whoami() 用于输出已经存储的字符串。
    2. 增加该系统函数 所对应的 系统调用功能号, 方便通过接口函数, 通过功能号定位到我们新增加的系统函数,

    以上两个步骤, 属于系统内部的功能, 所以在添加和修改完成后,需要重新 make 重新生成新的系统内核;

    1. 编写应用程序进行测试, 即在用户模式下, 开始编写函数, 调用我们刚刚添加的两个系统函数, 从而测试是否成功添加了这两个新的系统功能。

    注意事项:

    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 语句之前,以免出现意想不到的错误。

    1. 增加系统功能函数

    添加新的系统功能函数, 在 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    所以只要自己创建一个文件:kernel/who.c,然后实现两个函数就万事大吉了。

    1.1 iam() 函数

    Linux 0.11 上添加两个系统功能函数,

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

    int sys_iam(const char* name)
    
    • 1

    该函数的作用:

    将字符串参数name 的内容, 从内存中的用户地址空间拷贝到内存中的内核地址空间保存起来;

    要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL

    kernal/who.c 中实现此系统调用。

    errno; errno 是一种传统的错误代码返回机制。

    当一个函数调用出错时,通常会返回 -1 给调用者。
    -1 只能说明出错,不能说明错是什么。
    为解决此问题,全局变量 errno 登场了。错误值被存放到 errno 中,于是调用者就可以通过判断 errno 来决定如何应对错误了。

    各种系统对 errno 的值的含义都有标准定义。
    Linux 下用“man errno”可以看到这些定义。

    1.2 whoami() 函数

    第二个系统函数是 sys_whoami(),同样在kernel/who.c 中添加该函数:

    int  sys_whoami(char* name, unsigned int size);
    
    • 1

    作用:
    将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,
    同时确保不会对name 越界访存, name 的大小由size 说明,

    返回值是拷贝的字符数,
    如果size 小于需要的空间, 返回-1, 并且置 errno 为 EINVAL

    1.3 参考代码

    这里需要注意 重要的内核函数, get_fs_byte(), put_fs_byte() 这两个函数 是实现 用户态的数据 和 内核态数据之间的 重要实现;
    get_fs_byte() : 输入一个地址, 该函数将返回该地址上一个字节的字符;

    tmp[i]  = get_fs_byte(name +i)
    
    • 1

    put_fs_byte() : 将一个字节的字符输出到指定的位置上,

    /*msg[i] 代表一个字符,  name +i 代表一个地址*/
    put_fs_byte(msg[i],  name +i)
    
    
    • 1
    • 2
    • 3
    /* 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;  /* 返回实际存储字符串的长度*/
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    2. 添加系统函数对应的调用功能号

    2.1 添加系统调用宏定义的编号

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2 修改可以系统调用的总数量

    kernel/system_call.s

    # offsets within sigaction
    sa_handler = 0
    sa_mask = 4
    sa_flags = 8
    sa_restorer = 12
    
    nr_system_calls = 74
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.3 添加系统调用名

    为新增的系统调用添加系统调用名并维护系统调用表,
    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 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3. 重新make, 生成新内核

    3.1 make

    修改 Makefile 文件,参见这里makefile 文件修改

    重新 make,

    /home/shiyanlou/os/oslab/linux-0.11 的路径下,重新执行 make clean && make .

    3.2 修改 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
    
    • 1
    • 2
    • 3

    修改为,添加了 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
    
    • 1
    • 2
    • 3

    第二处:

    ### 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    添加了 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    如果编译时提示 who.c 有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile 是对的。

    4. 编写应用程序,调用系统函数

    为了在 Linux 0.11 测试新添加的两个系统函数, 需要编写两个测试函数,

    在其环境下编写两个测试程序 iam.c 和 whoami.c。最终的运行结果是:
    在这里插入图片描述

    4.1 编写测试文件

    注意在, 在bochs 的环境 中使用 gcc-3.4 编译文件,
    宏定义必须放在 库的载入的前面,

    #define __LIBRARY__
    #include 
    
    • 1
    • 2

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.2 文件传输

    以上两个测试文件需要放到 linux-0.11操作系统上运行,
    才能验证新增的系统调用是否有效,

    • 那如何才能将这两个文件从宿主机转到稍后虚拟机中启动的linux-0.11操作系统上呢?

    这里我们采用挂载方式实现宿主机与虚拟机操作系统的文件共享,

    • 在 oslab 目录下执行以下命令挂载hdc目录到虚拟机操作系统上。
    :~/Hit_os-main/hit-oslab-linux-20110823/oslab$ sudo ./mount-hdc
    
    • 1

    挂载之后, 则虚拟机的操作系统的文件系统 可以显示:

    ~/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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    拷贝文件

    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
    
    • 1
    ~/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
    
    • 1
    • 2
    • 3

    注意,需要关闭该终端, 才能启动 bochs

    4.3 编译运行

    启动 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
    
    • 1

    gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:

    $ ./iam
    
    • 1

    如果如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运!

    5 调试分析

    5.1 用 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");
    //    ……
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    显然,printk() 首先 push %fs 保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()。查看 printf() 可以看到,它最终也要落实到这个函数上。

    • 汇编调试 ,
    # 确认在 oslab 目录下
    $ cd ~/oslab/
    # 运行脚本前确定已经关闭刚刚运行的 Bochs
    $ ./dbg-asm
    
    • 1
    • 2
    • 3
    • 4
    • c 语言调试:
    $ cd ~/oslab
    $ ./dbg-c
    
    • 1
    • 2

    注意:启动的顺序不能交换,否则 gdb 无法连接。

    然后再打开一个终端窗口,执行:

    $ cd ~/oslab
    $ ./rungdb
    
    • 1
    • 2

    此时, 再回到 之前打开的那个终端: 出现ed to 127.0.0.1
    才算连接成功。

  • 相关阅读:
    学习记录:js算法(三十五):K 个一组翻转链表
    熟练使用 Redis 的五大数据结构:Java 实战教程
    教你如何使用Nodejs搭建HTTP web服务器并发布上线公网
    软件测试|iOS 自动化测试——技术方案、环境配置
    Android系统修改驱动固定USB摄像头节点绑定前后置摄像头
    计算机毕业设计(附源码)python校园疫情管理系统
    2022杭电多校第二场
    MYSQL入门与进阶(十)
    基于java的高速公路收费系统 计算机毕业设计
    前端进击笔记第五节 JavaScript 如何实现继承?
  • 原文地址:https://blog.csdn.net/chumingqian/article/details/128209996