zoukankan      html  css  js  c++  java
  • Linux实验二:深入理解系统调用

    实验要求:

    找一个系统调用,系统调用号为学号最后2位相同的系统调用

    • 通过汇编指令触发该系统调用
    • 通过gdb跟踪该系统调用的内核处理过程
    • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

    1 环境配置

    1.1安装开发工具

    1 sudo apt install build-essential
    2 sudo apt install qemu # install QEMU
    3 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
    4 sudo apt install axel

    1.2 下载内核源代码

    1 sudo apt install axel
    2 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
    3 xz -d linux-5.4.34.tar.xz
    4 tar -xvf linux-5.4.34.tar
    5 cd linux-5.4.34

    1.3 配置内核选项 

     1 make defconfig # Default configuration is based on 'x86_64_defconfig'
     2 make menuconfig
     3 # 打开debug相关选项
     4 Kernel hacking --->
     5     Compile-time checks and compiler options --->
     6         [*] Compile the kernel with debug info
     7         [*] Provide GDB scripts for kernel debugging
     8     [*] Kernel debugging
     9 # 关闭KASLR,否则会导致打断点失败
    10 Processor type and features ---->
    11     [] Randomize the address of the kernel image (KASLR)

          

    1.4 编译和运行内核

    1 make -j$(nproc) # nproc gives the number of CPU cores/threadsavailable //编译内核
    2 # 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统最终会kernelpanic
    3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage //启动qemu

    1.5 制作根文件系统

     

    电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具, bootloader加载根⽂件系统到内存中,内核会将其挂载到根目录下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统提供基本的⽤户态可执⾏程序。

    下载并解压安装包:

    1 axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
    2 tar -jxvf busybox-1.31.1.tar.bz2
    3 cd busybox-1.31.1

    编译:

    make menuconfig  #记得要编译成静态链接,不⽤动态链接库。
    Settings --->
        [*] Build static binary (no shared libs)
    make -j$(nproc) && make install  #编译安装,默认会安装到源码⽬录下的 _install ⽬录中。

    制作内存根文件系统镜像(rootfs文件夹放在linux-5.4.34文件夹中):

    1 mkdir rootfs
    2 cd rootfs
    3 cp ../busybox-1.31.1/_install/* ./ -rf
    4 mkdir dev proc sys home
    5 sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/

    rootfs文件夹中新建init文件,添加如下内容到init⽂件 :

    1 #!/bin/sh
    2 mount -t proc none /proc
    3 mount -t sysfs none /sys
    4 echo "Wellcome Wang LidoOS!"
    5 echo "--------------------"
    6 cd home
    7 /bin/sh

    给init脚本添加可执行权限:

    1 chmod +x init

    打包内存根文件系统镜像,打包文件存放在linux-5.4.34文件下

    1 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz#注意路径

    测试看内核启动后是否执行 init 脚本,在 linux-5.4.34目录下,启动qemu: 

    1 qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz

     看到显示“Welcom Wang LidoOS”,init脚本被执行。

    跟踪调试 Linux 内核

    2.1 系统调用semctl函数

    实验要求找一个系统调用号为学号最后2位相同的系统调用,通过汇编指令触发该系统调用。因此我找66号系统调用。

    在linux-5.4.34/arch/x86/entry/syscalls(有32与64位系统调用区别)路径,查到66号系统调用位semctl函数

    semctl函数介绍:

    在rootfs/home 目录下分别创建seml.c文件,写入semctl函数的源代码,其中将调用semctl函数处的代码改为汇编指令代码:

      1 #include <unistd.h>
      2 #include <sys/types.h>
      3 #include <sys/stat.h>
      4 #include <fcntl.h>
      5 #include <stdlib.h>
      6 #include <stdio.h>
      7 #include <string.h>
      8 #include <sys/sem.h>
      9  
     10 union semun
     11 {
     12   int val;
     13   struct semid_ds *buf;
     14   unsigned short *arry;
     15 };
     16  
     17 static int sem_id = 0;
     18 static int set_semvalue();
     19 static void del_semvalue();
     20 static int semaphore_p();
     21 static int semaphore_v();
     22  
     23 int main(int argc, char *argv[])
     24 {
     25     char message = 'X';
     26     int i = 0;
     27  
     28     // 创建信号量
     29     sem_id = semget((key_t) 1234, 1, 0666 | IPC_CREAT);
     30  
     31     if (argc > 1)
     32     {
     33         // 程序第一次被调用,初始化信号量
     34         if (!set_semvalue())
     35         {
     36             fprintf(stderr, "Failed to initialize semaphore
    ");
     37             exit(EXIT_FAILURE);
     38         }
     39  
     40         // 设置要输出到屏幕中的信息,即其参数的第一个字符
     41         message = argv[1][0];
     42         sleep(2);
     43     }
     44  
     45     for (i = 0; i < 10; ++i)
     46     {
     47         // 进入临界区
     48         if (!semaphore_p())
     49         {
     50             exit(EXIT_FAILURE);
     51         }
     52  
     53         // 向屏幕中输出数据
     54         printf("%c", message);
     55  
     56         // 清理缓冲区,然后休眠随机时间
     57         fflush(stdout);
     58         sleep(rand() % 3);
     59  
     60         // 离开临界区前再一次向屏幕输出数据
     61         printf("%c", message);
     62         fflush(stdout);
     63  
     64         // 离开临界区,休眠随机时间后继续循环
     65         if (!semaphore_v())
     66         {
     67             exit(EXIT_FAILURE);
     68         }
     69         sleep(rand() % 2);
     70     }
     71  
     72     sleep(10);
     73     printf("
    %d - finished
    ", getpid());
     74  
     75     if (argc > 1)
     76     {
     77         // 如果程序是第一次被调用,则在退出前删除信号量
     78         sleep(3);
     79         del_semvalue();
     80     }
     81     exit(EXIT_SUCCESS);
     82 }
     83  
     84 static int set_semvalue()
     85 {
     86     // 用于初始化信号量,在使用信号量前必须这样做
     87     union semun sem_union;
     88  
     89     sem_union.val = 1;
     90     //int res = semctl(sem_id, 0, SETVAL, sem_union);
     91     int res;
     92     asm volatile( 
     93         "mov %4, %%rdi
    	" 
     94         "mov %3, %%ecx
    	" 
     95         "mov %2, %%ebx
    	" 
     96         "mov %1, %%edi
    	" 
     97         "movl $0x42, %%eax
    	" //传递系统调用号 
     98     "syscall
    	" //系统调用 
     99     "movq %%rax, %0
    	" // 将函数处理结果返回给 res 变量中
    100      :"=m"(res) 
    101     :"a"(sem_id), "b"(0), "c"(SETVAL),"d"(sem_union)
    102     );
    103     if (res == -1)
    104     {
    105         return 0;
    106     }
    107     return 1;
    108 }
    109  
    110 static void del_semvalue()
    111 {
    112     // 删除信号量
    113     union semun sem_union;
    114  
    115     if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    116     {
    117         fprintf(stderr, "Failed to delete semaphore
    ");
    118     }
    119 }
    120  
    121 static int semaphore_p()
    122 {
    123     // 对信号量做减1操作,即等待P(sv)
    124     struct sembuf sem_b;
    125     sem_b.sem_num = 0;
    126     sem_b.sem_op = -1;//P()
    127     sem_b.sem_flg = SEM_UNDO;
    128     if (semop(sem_id, &sem_b, 1) == -1)
    129     {
    130         fprintf(stderr, "semaphore_p failed
    ");
    131         return 0;
    132     }
    133  
    134     return 1;
    135 }
    136  
    137 static int semaphore_v()
    138 {
    139     // 这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
    140     struct sembuf sem_b;
    141     sem_b.sem_num = 0;
    142     sem_b.sem_op = 1; // V()
    143     sem_b.sem_flg = SEM_UNDO;
    144     if (semop(sem_id, &sem_b, 1) == -1)
    145     {
    146         fprintf(stderr, "semaphore_v failed
    ");
    147         return 0;
    148     }

    使用 gcc 编译成可执行文件 seml:

    1 gcc -o server seml.c -static

    在rootfs文件夹重新打包内存根文件系统镜像:

    1 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

    2.2  使用 gdb 跟踪调试

    使用纯命令行启动qemu:

    1 qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

    使⽤gdb跟踪调试内核,加两个参数,⼀个是-s,在TCP 1234端⼝上创建了⼀个gdbserver;

    另外打开一个窗⼝,⽤gdb把带有符号表的内核镜像vmlinux加载进来,然后连接gdb server,设置断点跟踪内核。若不想使⽤1234端⼝,可以使⽤-gdb tcp:xxxx来替代-s选项),另⼀个是-S代表启动时暂停虚拟机,等待 gdb 执⾏ continue指令(可以简写为c)。

    再打开一个终端窗口, linux-5.4.34 目录下,加载内核镜像:

    1 gdb vmlinux

    连接 gdb  TCP1234端口:

    1 (gdb) target remote :1234

    为系统调用设置断点:

    1 (gdb)b __x64_sys_semctl   //(gdb) b 系统调用函数名

     输入 c 指令,打开第一个终端窗口显示:

    在第一个终端窗口输入:

    /home # ./seml 0 & ./seml

     

    打开第二个终端窗口:

     

     查看堆栈信息:

    1 (gdb) bt

    单步调试:

    1 (gdb) n

     

     

    3  分析

    分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

    3.1根据堆栈结果分析

     

    堆栈是自顶向下的,因此:

    #0   ksys_ semctl             系统调用函数接口

    #1   do_syscall_64           获取系统调用号,调用系统函数

    #2   entry_SYSCALL_64  保存现场工作,调用第二层的 do_SYSCALL_64

    #3   操作系统

    最初我们在semctl函数调用处设置了一个断点,定位于home/linux-5.4.34/ipc/sem.c 文件的1689行

     

     前往文件相应位置查看:

    1 cd linux-5.4.34/ipc
    2 cat -n sem.c

    1689行反回了ksys_semctl函数的调用结果,查看 ksys_semctl(semid,semnum,cmd,arg,IPC_64)函数:

     1 static long ksys_semctl(int semid, int semnum, int cmd, unsigned long arg, int version)
     2 {
     3     struct ipc_namespace *ns;
     4     void __user *p = (void __user *)arg;
     5     struct semid64_ds semid64;
     6     int err;
     7 
     8     if (semid < 0)
     9         return -EINVAL;
    10 
    11     ns = current->nsproxy->ipc_ns;
    12 
    13     switch (cmd) {
    14     case IPC_INFO:
    15     case SEM_INFO:
    16         return semctl_info(ns, semid, cmd, p);
    17     case IPC_STAT:
    18     case SEM_STAT:
    19     case SEM_STAT_ANY:
    20         err = semctl_stat(ns, semid, cmd, &semid64);
    21         if (err < 0)
    22             return err;
    23         if (copy_semid_to_user(p, &semid64, version))
    24             err = -EFAULT;
    25         return err;
    26     case GETALL:
    27     case GETVAL:
    28     case GETPID:
    29     case GETNCNT:
    30     case GETZCNT:
    31     case SETALL:
    32         return semctl_main(ns, semid, semnum, cmd, p);
    33     case SETVAL: {
    34         int val;
    35 #if defined(CONFIG_64BIT) && defined(__BIG_ENDIAN)
    36         /* big-endian 64bit */
    37         val = arg >> 32;
    38 #else
    39         /* 32bit or little-endian 64bit */
    40         val = arg;
    41 #endif
    42         return semctl_setval(ns, semid, semnum, val);
    43     }
    44     case IPC_SET:
    45         if (copy_semid_from_user(&semid64, p, version))
    46             return -EFAULT;
    47         /* fall through */
    48     case IPC_RMID:
    49         return semctl_down(ns, semid, cmd, &semid64);
    50     default:
    51         return -EINVAL;
    52     }
    53 }

    根据使用(gdb)bt查看堆栈调用结果:

    #0 调用ksys_semctl函数,定位在ipc/sem.c 文件第1633行,我们查找1633行,会发现就是上述ksys_semctl函数的代码

    #1  调用do_syscall_64 函数,定位在arch/x86/entry/common.c 文件290行,我们打开文件查找这一行:

     

    #2 entry_SYSCALL_64 函数,定位在arch/x86/entry/entry_64.S 175行

     

     CALL指令执行时,进行两步操作:(1)将程序前执行的位置IP压入堆栈中;(2)转移到调用的do_syscall_64

  • 相关阅读:
    鸡啄米vc++2010系列9(对话框控件的Tab顺序)
    鸡啄米vc++2010系列8(对话框:为控件添加消息处理函数)
    鸡啄米vc++2010系列7(对话框:创建对话框类和添加控件变量)
    鸡啄米vc++2010系列6(对话框添加控件)
    鸡啄米vc++2010系列5( 对话框:创建对话框模板和修改对话框属性)
    鸡啄米vc++2010系列4(消息机制)
    鸡啄米vc++2010系列3(MFC应用程序框架分析)
    鸡啄米vc++2010系列2(项目文件分析)
    鸡啄米VC++2010系列1(解决方案与工程)
    opencv2.3.1在vs2010上的配置
  • 原文地址:https://www.cnblogs.com/lidodo/p/12976565.html
Copyright © 2011-2022 走看看