zoukankan      html  css  js  c++  java
  • 深入理解Linux系统调用:write/writev

    实验要求:

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

     

    一、系统调用相关知识

    系统调用(system call)利用陷阱(trap),是异常(Exception)的一种,从用户态进⼊内核态。

    系统调用具有以下功能和特性:

    把用户从底层的硬件编程中解放出来。操作系统为我们管理硬件,⽤户态进程不用直接与硬件设备打交道。

    极⼤地提高系统的安全性。如果用户态进程直接与硬件设备打交道,会产⽣安全隐患,可能引起系统崩溃。

    使用户程序具有可移植性。用户程序与具体的硬件已经解耦合并用接⼝(api)代替了,不会有紧密的关系,便于在不同系统间移植。

     

    二、环境准备

    1. 安装开发工具:

    sudo apt install build-essential
    sudo apt install qemu
    sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev sudo apt install axel

    2. 下载内核源码:

    axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
    xz -d linux-5.4.34.tar.xz
    tar -xvf linux-5.4.34.tar 

    3. 编译menuOS调试工具

    cd linux-5.4.34
    make defconfig  #默认配置基于'x86_64_defconfig'
    make menuconfig

    4. 配置内核选项

    #打开debug相关选项
    Kernel hacking --->
        Compile-time checks and compiler options --->
            [*] Compile the kernel with debug info
            [*] Provide GDB scripts for kernel debugging [*] Kernel debugging

    #关闭KASLR,否则会导致打断点失败
    Processor type and features ---->
        [] Randomize the address of the kernel image (KASLR)

    5. 编译内核

    make -j$(nproc)  #编译内核,需要几分钟的时间
    #测试一下,不能正常加载运行
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage 

    6. 制作根文件系统

    电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具,bootloader加载根⽂件系统到内存中,内核会将其挂载到根⽬录/下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。

    我们这⾥为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序

    下载 busybox源代码解压:

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

    配置编译 并安装:

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

     

    7. 制作内存根文件系统镜像

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

     

    8. init脚本

    准备init脚本文件放在根文件系统目录下(rootfs/init),添加如下内容到init文件:

    #!/bin/sh
    mount -t proc none /proc 
    mount -t sysfs none /sys echo "Welcome to qingyang-OS!"
    echo "--------------------" cd home /bin/sh

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

    chmod +x init

    打包成内存根文件系统镜像:

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

    镜像文件在上一级目录:

     

     

    测试挂载根文件系统,看内核启动完成后是否执行init脚本:

    cd ../   #一定要返回到上一级,因为rootfs.cpio.gz在上一级
    qemu-system-x86_64 -kernel ~/linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

    QEMU界面如下,第一步系统配置完成:

     

     

    三、通过汇编指令触发该系统调用

    1. 首先查看系统调用表,我的学号末尾两位为01

    cat ~/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl

    如上图,01是写调用:  系统调用 write,函数入口为 __x64_sys_write

     

    2. 自己写一个简单C语言程序Write.c,通过这个程序触发系统调用write:

    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        char buffer[50] = "hello==>qingyang2199
    ";   //buffer里面写上String类型的内容
        int count;
        int fd = open ("abc.txt",O_RDWR);
        if (fd == -1)
        {
            fprintf(stderr,"can't open file:[%s]
    ","abc.txt");  //打不开文件
            exit(EXIT_FAILURE);
        }
        count = write(fd,buffer,strlen(buffer));  //在这里【write函数】将buffer里的内容,写入文件abc.txt
        if (count == -1)
        {
            fprintf(stderr,"write error
    ");  //写的时候出错
            exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
    }

     gcc编译(这里采用静态编译)后运行,输出结果:

    gcc -o Write Write.c -static

     

    生成可执行文件后,还需要一个abc.txt:

    然后执行可执行文件Write:

    可见,write的作用是将buffer里的内容写入文件。

     

    3.编写汇编程序Write-asm.c,触发write系统调用:

    写Write-asm.c之前,还需要从反汇编Write来获取一些信息:

    objdump -S Write >Write.S  #反汇编

     从Write.S汇编代码中得知,入口地址0x1:

    Write.c里面的write函数的那一行:

    count = write(fd,buffer,strlen(buffer));  //在这里【write函数】将buffer里的内容,写入文件abc.txt

    编写汇编程序Write-asm.c,只要把上面的Write.c里面的write函数的那一行,改写成汇编代码就可以了:

    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        char buffer[50] = "hello==>qingyang2199
    ";   //buffer里面写上String类型的内容
        int count;
        int fd = open ("abc.txt",O_RDWR);
        if (fd == -1)
        {
            fprintf(stderr,"can't open file:[%s]
    ","abc.txt");  //打不开文件
            exit(EXIT_FAILURE);
        }
    
        //count = write(fd,buffer,strlen(buffer));  //这行被下面的asm替换

    //------------------asm汇编代码-------------------// asm volatile( "movq %3, %%rdx " // 参数3 "movq %2, %%rsi " // 参数2 "movq %1, %%rdi " // 参数1 "movl $0x1,%%eax " // 传递系统调用号(入口地址0x1,从Write.S中得知,如下图:) "syscall " // 系统调用 "movq %%rax,%0 " // 结果存到%0 就是count中 :"=m"(count) //输出到count :"a"(fd),"b"(buffer),"c"(strlen(buffer)) //对应输入的三个参数 ); //------------------asm汇编代码-------------------//
    if (count == -1) { fprintf(stderr,"write error "); //写的时候出错 exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }

    编译新写的汇编程序:

    gcc -o Write-asm Write-asm.c -static

     

    然后运行汇编程序:

    ./Write-asm

     

    write汇编有问题,感谢热心同学指导,我替换为了writev,用来实现write相同的功能:

    write.c:

    #include <stdio.h>
    #include <sys/uio.h>
     
    /*
    struct iovec
    {
        void *iov_base;        //指向一个char数组
        size_t iov_len;        //大小
    };
    */
     
    int main(int argc,char *argv[])
    {
        struct iovec vec[2];
        char buf[]="qingyang2199";
        int str_len;
     
        vec[0].iov_base=buf;
        vec[0].iov_len=8;
    
         // 1 标准输出
        // vec 缓冲区
        // 1 缓冲区长度
        str_len=writev(1,vec,1);    //调用writev()函数
        puts("");
        printf("Write bytes: %d 
    ",str_len);
        return 0;
    }

    从write.S汇编代码中得知,入口地址0x14:

     

    write-asm.c:

    #include <stdio.h>
    #include <sys/uio.h>
     
    /*
    struct iovec
    {
        void *iov_base;        //指向一个char数组
        size_t iov_len;        //大小
    };
    */
     
    int main(int argc,char *argv[])
    {
        struct iovec vec[2];
        char buf[]="qingyang2199";
        int str_len;
     
        vec[0].iov_base=buf;
        vec[0].iov_len=8;
    
         // 1 标准输出
        // vec 缓冲区
        // 1 缓冲区长度
        //str_len=writev(1,vec,1);    //调用writev()函数
    
        asm volatile(
            "movq $0x1, %%rdx
    	"  // 参数3
            "movq %1, %%rsi
    	"   //  参数2
            "movq $0x1, %%rdi
    	"  //  参数1 
            "movl $0x14,%%eax
    	" //  传递系统调用号
            "syscall
    	"          //  系统调用
            "movq %%rax,%0
    	"    //  结果存到%0 就是str_len中
            :"=m"(str_len) // 输出
            :"g"(vec) // 输入
        );
            
        puts("");
        printf("Write bytes: %d 
    ",str_len);
        return 0;
    }

    运行一下汇编程序:

    ./write

     

     

    四、通过gdb跟踪该系统调用的内核处理过程

    gdb调试基础知识:

    • r : run 运行程序
    • q : quit
    • b : break 设置断点
    • c : continue
    • l : list 显示多行源代码
    • step 执行下一条语句(若是函数调用,则进入)
    • next 执行下一条语句(不进入函数调用)
    • print 打印内部变量值

    1.重新制作根文件系统:

    把编译好的 write-asm文件放在rootfs/syscall目录下:

    重新生成根文件系统(rootfs目录下):

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

     

    2. 纯命令行下启动虚拟机:

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

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

    然后发现这个窗口暂停等待(作为gdbserver,端口号TCP1234):

    3. 另外打开一个窗口,用gdb把带有符号表的内核镜像vmlinux加载进来:

    然后连接gdb server:

    4. 设置断点跟踪内核:

    在虚拟机中执行 write-asm,会卡住:

    在gdb界面查看断点分析:

     

    5. gdb界面bt查看堆栈:

     查看此时堆栈情况,有4层:

    • 第一层/ 顶层 __x64_sys_writev 系统调用函数
    • 第二层 do_syscall_64 获取系统调用号, 前往系统调用函数
    • 第三层 entry_syscall_64 中断入口,做保存线程工作,调用 do_syscall_64
    • 第四层 内部不可见

    6. 继续深入查看系统调用:

     

    断点定位为到 /home/qyf001/linux-5.4.34/fs/read_write.c 1128行:

    writev()函数内调用 do_writev():

    进入do_writev函数查看:

    可知,这里是完成程序内容的地方,前期的保存现场工作已经完成。

    执行完这个函数,发现回到了函数堆栈上一层的do_sys_call_64 中 ,接下来要执行的 syscall_return_slowpath 函数要为恢复现场做准备。

    继续执行(next),回到了函数堆栈的上一层,entry_SYSCALL_64:

    接下来执行的是用于恢复现场的汇编指令:

    最后伴随着两个pop指令,恢复了rdirsp寄存器。系统调用完成:

    实验操作部分到此结束,下面是工作机制的理论分析

     

    五、分析系统调用的工作机制

    writev函数从用户空间到内核空间的过程:

    • 第一层/ 顶层 __x64_sys_writev 系统调用函数
    • 第二层 do_syscall_64 获取系统调用号, 前往系统调用函数
    • 第三层 entry_syscall_64 中断入口,做保存线程工作,调用 do_syscall_64 
    • 第四层 内部不可见

    系统调用全部步骤:

    (1)汇编指令 syscall 触发系统调用,通过MSR寄存器找到了中断函数入口,此时,代码执行到/home/qyf001/linux-5.4.34/arch/x86/entry/entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口,然后开始通过swapgs 和压栈动作保存现场

    (2)然后跳转到了/linux-5.4.34/arch/x86/entry/common.c  目录下的 do_syscall_64  函数,在ax寄存器中获取到系统调用号,然后去执行系统调用内容:

    (3)然后程序跳转到/linux-5.4.34/fs/read_write.c 下的do_writev 函数,开始执行:

    (4)函数执行完后回到步骤(3)中的 syscall_return_slowpath(regs);  准备进行恢复现场:

    (5)接着程序再次回到arch/x86/entry/entry_64.S执行恢复现场,最后两句完成了堆栈的切换。

    过程分步骤截图:

    (1)汇编指令 syscall 触发系统调用,通过MSR寄存器找到了中断函数入口,此时,代码执行到/home/qyf001/linux-5.4.34/arch/x86/entry/entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口,然后开始通过swapgs 和压栈动作保存现场

    (2)然后跳转到了/linux-5.4.34/arch/x86/entry/common.c  目录下的 do_syscall_64  函数,在ax寄存器中获取到系统调用号,然后去执行系统调用内容:

     

    (3)然后程序跳转到/linux-5.4.34/fs/read_write.c 下的do_writev 函数,开始执行功能函数【这是本次系统调用最深的一层】

    (4)函数执行完后回到步骤(2)中的 syscall_return_slowpath(regs);  准备进行恢复现场:

     

    (5)接着程序再次回到arch/x86/entry/entry_64.S执行恢复现场,最后两句完成了堆栈的切换。

     


     附:相关知识-学习笔记

    汇编指令学习: 

    x86架构

    • Intel:Windows派系 -> vc编译器
    • AT&T:Linux/iOS派系 -> gcc编译器

    寄存器(16位):

    • ax bx cx dx 通用数据
    • sp 堆栈指针  bp 基址指针
    • ip 指令指针(下一条)
    • cs ds ss es 段     si di 变址    flag 标志

    16位:- -          push %ax

    32位:l e         pushl %eax

    64位:q r        pushq %rax

     

    8086常用指令(16位为例):

    mov ax,1122H     //将1122H存入寄存器ax

    jmp ax    //如果ax是1000H,那么IP将被改为1000H

    add ax,1111H    //将寄存器ax中的值加上1111H再赋值给ax   //sub类似

    ret    //栈顶值出栈,给IP

    lea dx,1111H   //把偏移地址存到dx

    cmp 比较

    inc 加一     dec减一

    mul 无符号乘法    div 无符号除法

    shl shr 逻辑左移/右移

    call 过程调用     ret 过程返回

    proc 定义过程   endp过程结束

    segment 定义段   ends段结束

    end程序结束

     

    大小端:

    • 大端模式(Big Endian):数据的低字节保存在内存的高地址。
    • 小端模式(Little Endian):数据的低字节保存在内存的低地址(从右到左保存)(8086、X86是小端)

     

    gcc-gdb使用方法学习:

    源文件123.c编译:gcc 123.c -o 123 得到123可执行文件

    然后 gdb 123  进行调试:b/c/s/...

    gdb调试基础知识:

    • : run 运行程序
    • b : break 设置断点
    • c : continue
    • bt : 查看堆栈状况
    • n : next 执行下一条语句(不进入函数调用)
    • s : step 执行下一条语句(若是函数调用,则进入)
    • q : quit 结束调试
    • l : list 显示多行源代码
    • print 打印内部变量值
  • 相关阅读:
    08 字体属性设置-font-family
    函数-函数进阶-生成器调用方法
    函数-函数进阶-斐波那契
    函数-函数进阶-列表生成式
    函数-函数进阶-装饰器带参数2
    函数-函数进阶-装饰带参数的函数
    函数-函数进阶-装饰器流程分析
    函数-函数进阶-装饰器
    函数-函数进阶-闭包
    函数-函数进阶-作用域的查找空间
  • 原文地址:https://www.cnblogs.com/qyf2199/p/12890688.html
Copyright © 2011-2022 走看看