实验二 深入理解系统调用

    

一、实验要求

  1. 找一个系统调用,系统调用号为学号最后2位相同的系统调用;
  2. 通过汇编指令触发该系统调用;
  3. 通过gdb跟踪该系统调用的内核处理过程;
  4. 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。

二、实验目的

  1. 理解Linux操作系统调用;
  2. 了解系统调用过程中内核堆栈状态的变化过程。

三、实验环境

  ubuntu-16.04.6(实验楼环境:https://www.shiyanlou.com/courses/195/learning/?id=725

四、实验相关

 1、用户态与内核态的切换

  

 

 

 

  内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

  用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

-------------------------------

  1)OS采用系统调用实现用户态进程与I/O进行交互,用户态下调用系统资源须采用系统调用。

  2)从用户态进入内核态有2种方式:系统调用(trap陷入)、中断。

  3)状态切换时会保存寄存器上下文,如用户态堆栈顶地址、当时的状态字、当时的cs:eip值。

  4)system_ call是linux中所有系统调用的入口点,系统调用的参数由eax传递

 

 五、实验步骤

 1、根据学号选择系统调用

  进入根目录下,通过  find . -name 'syscall_64.tbl'  查找系统调用表,我的学号尾号为82,因此查表之后,选择的系统调用为 rename  。

 

 

 

   此函数的系统调用函数的入口函数为sys_rename通过搜索,入口函数位置为/fs/namei.c。

   rename系统调用用于在同一个文件系统中做文件的rename操作。如果源和目的在不同mount点上,rename会返回错误EXDEV。

SYSCALL_DEFINE3(symlinkat, const char __user *, oldname,
        int, newdfd, const char __user *, newname)
{
    int error;
    struct filename *from;
    struct dentry *dentry;
    struct path path;
    unsigned int lookup_flags = 0;

    from = getname(oldname);
    if (IS_ERR(from))
        return PTR_ERR(from);
retry:
    dentry = user_path_create(newdfd, newname, &path, lookup_flags);
    error = PTR_ERR(dentry);
    if (IS_ERR(dentry))
        goto out_putname;

    error = security_path_symlink(&path, dentry, from->name);
    if (!error)
        error = vfs_symlink(path.dentry->d_inode, dentry, from->name);
    done_path_create(&path, dentry);
    if (retry_estale(error, lookup_flags)) {
        lookup_flags |= LOOKUP_REVAL;
        goto retry;
    }
out_putname:
    putname(from);
    return error;
}

SYSCALL_DEFINE2(symlink, const char __user *, oldname, const char __user *, newname)
{
    return sys_symlinkat(oldname, AT_FDCWD, newname);
}

  SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)
  {
   return sys_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0);
  }

  可以看到,它实际上是转调用了renameat系统调用。renameat系统调用的实现也在./fs/namei.c中,它的函数定义是:

SYSCALL_DEFINE4(renameat, int, olddfd, const char __user *, oldname,
                int, newdfd, const char __user *, newname);

  下面是它的具体实现:

  1、对oldname和newname分别做目录查找,得到它们对应的nameidata数据结构oldnd和newnd。这个过程会涉及到查找目录项缓存,如果目录不在目录项缓存中,需要将目录从磁盘读取到目录项缓存中,具体细节见这里:                                                           http://www.cnblogs.com/cobbliu/p/4888751.html。

  2、查看oldnd和newnd的mount点是否一样,不一样则返回EXDEV

  3、做一堆其他的验证和准备工作,这个过程中会找到oldname的old_dir的inode和old_dentry,newname的new_dir的inode和new_dentry

  4、调用VFS层的 error = vfs_rename(old_dir->d_inode, old_dentry, new_dir->d_inode, new_dentry) ;

 int vfs_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry) 的实现:

  1、如果目的和源的inode一样,则返回0

  2、查看是否需要删除old_dentry和是否需要新建new_dentry

  3、如果old_dentry是个目录则调用vfs_rename_dir,否则调用vfs_rename_other

 static int vfs_rename_other(struct inode *old_dir, struct dentry *old_dentry,struct inode *new_dir, struct dentry *new_dentry) 的实现:

  1、调用 dget(new_dentry) 

  2、调用 old_dir->i_op->rename(old_dir, old_dentry, new_dir, new_dentry); 做真正的rename操作

  3、调用 dput(new_dentry) 

  ext4_rename函数真正实现了rename过程,ext4_rename实际上是讲旧目录文件中的文件项的refcount递减,然后在新目录文件中加入新文件名的目录项,并不会移动实际的数据文件,也不会修改数据文件的inode号。

  /proc/sys/fs/dentry_state显示目录项高速缓存的一些信息:

  • nr_dentry - number of dentries currently allocated
  • nr_unused - nuber of unused dentries
  • age_limit - seconds after the entry may be reclaimed, when memory is short
  • remaining - reserved.

  通常linux文件系统中目录项高速缓存的age_limit是45s,也就是说该目录项在目录项高速缓存中停留45s还无访问,就将它换出。

2、触发系统调用

  使用下面的代码触发rename系统调用:

#include<stdio.h>
#include <fcntl.h>

int main(void)
{
        char oldname[100], newname[100];
        /* 触发rename文件重命名的系统调用 */
        // 传入需要修改文件名的完整路径
        printf("enter a entire file path: ");
        gets(oldname);
        // 传入新文件的名字
        printf("new path: ");
        gets(newname);
        // 调用rename
        if (rename(oldname, newname) == 0)
                printf("change %s to %s.\n", oldname, newname);
        else
                perror("rename");
        return 0;
}

  创建一个新文件命名为oldPath

 

   运行上述代码

gcc main.c -o main
./main

 

  对上述的程序进行修改,使用汇编来调用rename,其实就是使用汇编指令传递name的参数,并使用系统调用通过软中断0x80陷入内核

#include<stdio.h>
#include <fcntl.h>

int main(void)
{
        char oldname[100], newname[100];
        printf("enter a entire file path: ");
        gets(oldname);
        printf("new path: ");
        gets(newname);
        int flag;
        asm volatile(
            "movl $1,%%ebx\n\t" //系统调⽤传递第⼀个参数使⽤EBX寄存器
            "movl $2,%%ecx\n\t"
            "movl $0x52,%%eax\n\t"  // 0x52 号系统调用放入eax
            "int $0x80\n\t"
            "movl %%eax,%0\n\t"
            :"=m"(flag)  // 接收返回
            :"b"(oldname), "c"(newname)
        );
        if (flag == 0)
                printf("change %s to %s.\n", oldname, newname);
        else
                perror("rename");
        return 0;
}

  编译、运行

 

 

 3、通过gdb跟踪系统调用

重新打包根文件目录,纯命令⾏下启动虚拟机。

qemu-system-x86_64 -kernel shiyanlou/linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz,此时虚拟机会暂停在启动界面。在另一个terminal中开启gdb调试 gdb vmlinux,连接进行调试,target remote:1234。结果如下图所示:

 

 

 

 

 

 

  六、实验总结

 1 main:
 2 .LFB0:
 3 
 4     pushl    %ebp
 5 
 6     movl    %esp, %ebp
 7 
 8     andl    $-16, %esp
 9     subl    $224, %esp
10     movl    12(%ebp), %eax
11     movl    %eax, 12(%esp)
12     movl    %gs:20, %eax
13     movl    %eax, 220(%esp)
14     xorl    %eax, %eax
15     movl    $.LC0, (%esp)
16     call    printf
17     leal    20(%esp), %eax
18     movl    %eax, (%esp)
19     call    gets
20     movl    $.LC1, (%esp)
21     call    printf
22     leal    120(%esp), %eax
23     movl    %eax, (%esp)
24     call    gets
25     leal    120(%esp), %eax
26     movl    %eax, 4(%esp)
27     leal    20(%esp), %eax
28     movl    %eax, (%esp)
29     call    rename
30     testl    %eax, %eax
31     jne    .L2
32     leal    120(%esp), %eax
33     movl    %eax, 8(%esp)
34     leal    20(%esp), %eax
35     movl    %eax, 4(%esp)
36     movl    $.LC2, (%esp)
37     call    printf
38     jmp    .L3
39 .L2:
40     movl    $.LC3, (%esp)
41     call    perror
42 .L3:
43     movl    $0, %eax
44     movl    220(%esp), %edx
45     xorl    %gs:20, %edx
46     je    .L5
47     call    __stack_chk_fail
48 .L5:
49     leave
50     .cfi_restore 5
51     .cfi_def_cfa 4, 4
52     ret
53     .cfi_endproc

  用户空间->内核空间
    INT 0x80(封装在C库函数中) -->system_call(系统调用处理程序)-->系统调用服务例程 -->内核程序
    系统调用时通过软中断指令INT 0x80实现的,这条指令会让系统跳转到一个预先设置好的内核地址,指向系统调用处理程序system_call。

  1.执行int 0x80指令后系统从用户态进入内核态,跳到system_call()函数处执行相应服务进程。在此过程中内核先保存中断环境,然后执行系统调用函数。
  2.system_call()函数通过系统调用号查找系统调用表sys_cal_table来查找具体系统调用服务进程。
  3.执行完系统调用后,iret之前,内核会检查是否有新的中断产生、是否需要进程切换、是否学要处理其它进程发送过来的信号等。
  4.内核是处理各种系统调用的中断集合,通过中断机制实现进程上下文的切换,通过系统调用管理整个计算机软硬件资源。
  5.如没有新的中断,restore保存的中断环境并返回用户态完成一个系统调用过程。

七、参考:

  实验楼:https://www.shiyanlou.com/courses/195/learning/?id=725

  https://blog.csdn.net/bshcc/article/details/50950604

posted @ 2020-05-24 10:47  Simple_Code  阅读(482)  评论(0编辑  收藏  举报