zoukankan      html  css  js  c++  java
  • 深入理解系统调用

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

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

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

    一、系统调用相关知识

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

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

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

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

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

    img

    1. Linux上的系统调用实现原理

    要想实现系统调用,主要实现以下几个方面:

    1. 通知内核调用一个哪个系统调用

    2. 用户程序把系统调用的参数传递给内核

    3. 用户程序获取内核返回的系统调用返回值

    下面看看Linux是如何实现上面3个功能的。

    • 通知内核调用一个哪个系统调用

    每个系统调用都有一个系统调用号,系统调用发生时,内核就是根据传入的系统调用号来知道是哪个系统调用的。

    在x86架构中,用户空间将系统调用号是放在eax中的,系统调用处理程序通过eax取得系统调用号。

    系统调用号定义在内核代码:arch/x86/include/asm/unistd.h 中,可以看出linux的系统调用不是很多。

    • 用户程序把系统调用的参数传递给内核

    系统调用的参数也是通过寄存器传给内核的,在x86系统上,系统调用的前5个参数放在ebx,ecx,edx,esi和edi中,如果参数多的话,还需要用个单独的寄存器存放指向所有参数在用户空间地址的指针。

    一般的系统调用都是通过C库(最常用的是glibc库)来访问的,Linux内核提供一个从用户程序直接访问系统调用的方法。

    参见内核代码:sysdeps/unix/sysv/linux/x86_64/sysdep.h

    arch/x86/include/asm/unistd.h :

    里面定义了6个宏,分别可以调用参数个数为0~6的系统调用

    syscall0(type,name)

    _syscall1(type,name,type1,arg1)

    _syscall2(type,name,type1,arg1,type2,arg2)

    _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)

    _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)

    _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)

    _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)

    超过6个参数的系统调用很罕见,所以这里只定义了6个。

    • 用户程序获取内核返回的系统调用返回值

    获取系统调用的返回值也是通过寄存器,在x86系统上,返回值放在eax中。

    2. 变量传递方式

    用户态数据栈-->寄存器-->内核态数据栈

    img

    二、环境准备

    1. 安装开发工具:

    sudo apt install build-essential 
    qemu qemu-system-x86 
    libncurses5-dev bison flex libssl-dev libelf-dev 
    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
    

    1

    4. 配置内核选项

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

    2016754-20200526190759980-1037628148

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

    2016754-20200526190811939-1483079509

    KASLR让加载到内核时是随机一个地址,如果加载内核一直同一个地址,容易被黑客攻击,这里为了实验方便,把它关闭。

    到这里,内核还不能够正常加载运⾏,因为没有⽂件系统,最终会kernel panic。

    5 编译内核

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

    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)    
    

    2016754-20200526190833626-1327607027

    然后编译安装,默认会安装到源码目录下的 _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 test-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 //在没有xwindow的情况下运行qemu 需要使用 -display curses 
    
    qemu-system-x86_64的参数比较多,这里简单说下:   
    -kernel是指定一个大内核文件,当仁不让的是bzImage。   
    -initrd是指定一个 initrd.img文件,这个文件就是我们使用busybox生成的initramfs.img。   
    -smp可以从名字猜想,它是给qemu指定几个处理器,或者是几个线程<嗯,大概意思就thread吧>。
    -gdb则是启动qemu的内嵌gdbserver,监听的是本地tcp端口1234—如果这样写: -gdb tcp:192.168.1.100:1234 ,似乎也是没问题的。 
    -S 就是挂起gdbserver,让gdb remote connect it。   
    -s 默认使用1234端口进行远程调试,和-gdb tcp::1234类似。   
    -m 2048指定内存大小为2048M
    

    此画面表示启动成功
    企业微信截图_16260806157273

    如果不成功参考:

    https://blog.csdn.net/baidu_31504167/article/details/93853921

    9. QEMU+GDB调试内核

    qemu-system-x86_64 -kernel /usr/src/linux-4.6.2/arch/x86/boot/bzImage -initrd …/initramfs.img -smp 2 -S -s  -display curses 
    
    gdb /usr/src/linux-4.6.2/vmlinux (修改成自己的vmlinux路径)
    target remote:1234 (默认端口是1234,进行远程连接)
    b start_kernel (设置断点)
    c (continue 运行到断点处)
    

    企业微信截图_20210712171054

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

    1. 首先查看系统调用表,选择setdomainname

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

    第一列的数字是系统调用号。

    第三列是系统调用的名字。

    第四列是系统调用在内核的实现函数。

    如上图,171是写调用: 系统调用 setdomainname,函数入口为 __x64_sys_setdomainname。

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

    系统调用

    write.c:

    #include <stdio.h>
    #include <unistd.h>
    int main (int argc, char *argv[])
    {
    	int a,b;
    	char buf[50]="test123";
    	int size=sizeof("test123");
    	a=setdomainname(buf,sizeof("test123"));
    	printf("a=%d
    ",a);
    	b=getdomainname(buf,sizeof(buf));
    	printf("new domainname is %s
    ",buf);
    	return 0;
    }
    

    内核调用方式

    write-asm.c:

    #include <stdio.h>
    #include <unistd.h>
    
    int main (int argc,char *argv[])
    {
            int a,b;
            char buf[50] = "abc444";
            int size = sizeof("abc444");
            asm volatile(
                    "movq %1,%%rdi
    	"            //EDI寄存器用于传递参数
                    "movq %2,%%rsi
    	"            //ESI寄存器用于传递参数
                    "movq $0xab,%%rax
    	"         //使用EAX传递系统调用号
                    "syscall
    	"                  //64位触发系统调用 ,32位使用int $x80 回去内存特定地址执行指定函数
                    "movq %%rax,%0
    	"      	   //保存返回值
                    : "=m"(a)
                    : "g" (buf),"g" (size)    /* input g mean Choose any one Regtster*/
                    );
    
            printf("a=%d
    ",a);
            b=getdomainname(buf,sizeof(buf));
            printf("new domainname is %s
    ",buf);
            return 0;
    }
    
    

    运行一下汇编程序:

    gcc -o write-asm write-asm.c -static
    //主要要静态编译
    ./write
    

    需要提前将write文件放入/home/uos/rootfs.cpio.gz中

    四、通过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:

    打断点

    b 函数名

    其中函数名查找arch/x86/entry/syscalls

    4. 设置断点跟踪内核:

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

    在gdb界面查看断点分析:

    5. gdb界面bt查看堆栈:

    (gdb) bt
    #0  __x64_sys_setdomainname (regs=0xffffc900001b7f58) at kernel/sys.c:1358
    #1  0xffffffff810025a3 in do_syscall_64 (nr=<optimized out>, regs=0xffffc900001b7f58) at arch/x86/entry/common.c:290
    #2  0xffffffff81c0007c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175
    #3  0x0000000000000000 in ?? ()
    

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

    • 第一层/ 顶层 __x64_sys_setdomainname 系统调用函数,开放给用户态使用的系统调用函数接口
    • 第二层 do_syscall_64 获取系统调用号, 前往系统调用函数
    • 第三层 entry_syscall_64 中断入口,做保存线程工作,调用 do_syscall_64
    • 第四层 操作系统

    五、分析系统调用在内核态的工作机制

    img

    1. 应用程序调用一个库函数xyz()
    2. 库函数内封装了一个系统调用SYSCALL,它将参数(系统调用号)传递给内核并触发中断
    3. 触发中断后,内核进行中断处理,执行系统调用处理函数 (5.0内核中是entry_INT80_32,不再是system_call了)
    4. 系统调用处理函数会根据系统调用号,选择相应的系统调用服务例程(在这里是sys_xyz),真正开始处理该系统调用

    其中 setdomainname函数由glibc实现

    源码位于glibc下的./sysdeps/unix/sysv/linux/x86_64/sysdep.h

    比如 syscall0的定义

    241 #undef internal_syscall0
    242 #define internal_syscall0(number, dummy...)             
    243 ({                                  
    244     unsigned long int resultvar;                    
    245     asm volatile (                          
    246     "syscall
    	"                           
    247     : "=a" (resultvar)                          
    248     : "0" (number)                          
    249     : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);            
    250     (long int) resultvar;                       
    251 })
    

    其中的

    246     "syscall
    	"
    

    为调用特殊模块寄存器(Model Specific Registers,简称 MSR)。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。 其值由人为定义.相当于手动配置的常量内存,由系统初始化时指定.

    在系统初始化的时候,trap_init 除了初始化上面的中断模式,这里面还会调用 cpu_init->syscall_init。*arch/x86/kernel/cpu/common.c 的 syscall_init 中初始化代码:

    1639 void syscall_init(void)
    1640 {
    1641     extern char _entry_trampoline[];
    1642     extern char entry_SYSCALL_64_trampoline[];
    1643 
    1644     int cpu = smp_processor_id();
    1645     unsigned long SYSCALL64_entry_trampoline =
    1646         (unsigned long)get_cpu_entry_area(cpu)->entry_trampoline +
    1647         (entry_SYSCALL_64_trampoline - _entry_trampoline);
    1648 
    1649     wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
    1650     if (static_cpu_has(X86_FEATURE_PTI))
    1651         wrmsrl(MSR_LSTAR, SYSCALL64_entry_trampoline);     
    1652     else
    1653         wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
    
    

    MSR_LSTAR 就是这样一个特殊的寄存器,当 syscall 指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用 entry_SYSCALL_64。

    这一步也就是堆栈信息中的 ??() 部分的动作,至此系统完成了以下动作:

    • 将调用号由用户态数据栈写入eax寄存器,将系统调用的参数依次写入寄存器rdi,rsi,rdx,r10,r8,r9(如果有)
    • 中断系统
    • 找到内存中系统调用内核态函数entry_SYSCALL_64的入口,进入内核态

    第三层

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

     ENTRY(entry_SYSCALL_64_trampoline)
         UNWIND_HINT_EMPTY
         swapgs
     
         /* Stash the user RSP. */
         movq    %rsp, RSP_SCRATCH
     
         /* Note: using %rsp as a scratch reg. */
         SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
     
         /* Load the top of the task stack into RSP */
         movq    CPU_ENTRY_AREA_tss + TSS_sp1 + CPU_ENTRY_AREA, %rsp
     
         /* Start building the simulated IRET frame. */
         pushq   $__USER_DS          /* pt_regs->ss 保存数据段其实位置*/ 
         pushq   RSP_SCRATCH         /* pt_regs->sp 保存函数栈栈顶*/ 
         pushq   %r11                /* pt_regs->flags 保存cpu标识*/
         pushq   $__USER_CS          /* pt_regs->cs 保存代码段起始位置*/
         pushq   %rcx                /* pt_regs->ip 保存指针指令寄存器*/
         pushq   %rdi
         movq    $entry_SYSCALL_64_stage2, %rdi                                                                                                                                          
         JMP_NOSPEC %rdi
     END(entry_SYSCALL_64_trampoline)
    
         
     ENTRY(entry_SYSCALL_64_stage2)
         UNWIND_HINT_EMPTY
         popq    %rdi
         jmp entry_SYSCALL_64_after_hwframe
     END(entry_SYSCALL_64_stage2)
         
         
     ENTRY(entry_SYSCALL_64)
    	UNWIND_HINT_EMPTY
         ...
    	/* Construct struct pt_regs on stack */
    	pushq	$__USER_DS			/* pt_regs->ss */
    	pushq	PER_CPU_VAR(rsp_scratch)	/* pt_regs->sp */
    	pushq	%r11				/* pt_regs->flags */
    	pushq	$__USER_CS			/* pt_regs->cs */
    	pushq	%rcx				/* pt_regs->ip */
         ...
     GLOBAL(entry_SYSCALL_64_after_hwframe)
         pushq   %rax                /* pt_regs->orig_ax */
     
         PUSH_AND_CLEAR_REGS rax=$-ENOSYS
     
         TRACE_IRQS_OFF
     
         /* IRQs are off. */
         movq    %rax, %rdi
         movq    %rsp, %rsi
         call    do_syscall_64       /* returns with IRQs disabled */
     	 ...
         TRACE_IRQS_IRETQ        /* we're about to change IF */
     
         /*
          * Try to use SYSRET instead of IRET if we're returning to
          * a completely clean 64-bit userspace context.  If we're not,
          * go to the slow exit path.
          */
         movq    RCX(%rsp), %rcx
         movq    RIP(%rsp), %r11
     
         cmpq    %rcx, %r11  /* SYSRET requires RCX == RIP */
         jne swapgs_restore_regs_and_return_to_usermode 
         //恢复进程信息
         ...
         popq	%rdi
    	 popq	%rsp
    	 USERGS_SYSRET64  //返回
         ...
    END(entry_SYSCALL_64)
    
    

    第二层

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

    __visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
    {
        struct thread_info *ti;
    
        enter_from_user_mode(); //
        local_irq_enable(); //
        ti = current_thread_info(); //记录进程信息
        if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
            nr = syscall_trace_enter(regs); //nr系统调用号 syscall_trace_enter检查用于trace服务 
        if (x32_enabled) {           
            nr &= ~__X32_SYSCALL_BIT;
            if (unlikely(nr >= NR_syscalls))
                goto bad;
            nr = array_index_nospec(nr, NR_syscalls);
            goto good;
        } else {
            nr &= ~0U;
            if (unlikely(nr >= NR_non_x32_syscalls))
                goto bad;
            nr = array_index_nospec(nr, NR_non_x32_syscalls); //NR_non_x32_syscalls 最大系统调用号
    good:
            regs->ax = sys_call_table[nr](regs); 
        }
    bad:
        syscall_return_slowpath(regs);
    }
    

    接下来,在编译的过程中,需要根据 syscall_64.tbl 生成 unistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。

    这里面会使用两个脚本,其中第一个脚本 arch/x86/entry/syscalls/syscallhdr.sh,会在文件中生成 #define __NR_open;

    第二个脚本 arch/x86/entry/syscalls/syscalltbl.sh,会在文件中生成 __SYSCALL(__NR_open, sys_open)。

    这样,unistd_64.h 是对应的系统调用号和系统调用实现函数之间的对应关系。

    在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件,这样所有的 sys_ 系统调用就都在这个表里面了

    其中sys_call_table()定义在arch/x86/entry/syscall_64.c :21

    asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
    #include <asm/syscalls_64.h>
    }; 
    

    通过引入<asm/syscalls_64.h> 来找到系统调用对应的入口

     851 #ifdef CONFIG_X86
     852 __SYSCALL_64(171, __x64_sys_setdomainname, ) 
     853 #else /* CONFIG_UML */
     854 __SYSCALL_64(171, sys_setdomainname, )
     855 #endif
    

    第一层

    (3)然后程序跳转到kernel/sys.c下的__x64_sys_setdomainname 函数,开始执行:

    SYSCALL_DEFINE2(setdomainname, char __user *, name, int, len)      
    {
        int errno;
        char tmp[__NEW_UTS_LEN];
    
        if (!ns_capable(current->nsproxy->uts_ns->user_ns, CAP_SYS_ADMIN)) //检查cap_sys_admin权限
            return -EPERM; //权限不足
        if (len < 0 || len > __NEW_UTS_LEN)
            return -EINVAL; // 参数不对
    
        errno = -EFAULT; //地址错
        if (!copy_from_user(tmp, name, len)) { //copy_from_user将name指向的字符串从用户空间拷贝到内核空间,失败返回没有被拷贝的字节数,成功返回0  来完成必须的检查以及内核空间与用户空间之间数据的来回拷贝
            struct new_utsname *u;
            down_write(&uts_sem);
            u = utsname();
            memcpy(u->domainname, tmp, len);
            memset(u->domainname + len, 0, sizeof(u->domainname) - len);
            errno = 0;
            uts_proc_notify(UTS_PROC_DOMAINNAME); //处理任务
            up_write(&uts_sem);
        }
    	return errno;
    }
    

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

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

    img

    附:相关知识-学习笔记

    汇编指令学习:

    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调试基础知识:

    r : run 运行程序

    b : break 设置断点

    c : continue

    bt : 查看堆栈状况

    n : next 执行下一条语句(不进入函数调用)

    s : step 执行下一条语句(若是函数调用,则进入)

    q : quit 结束调试

    l : list 显示多行源代码

    print 打印内部变量值

    附2 自定义syscall

    https://blog.csdn.net/qq_44222849/article/details/105754952

  • 相关阅读:
    面试题示例
    软件测试面试题(简答)
    278. 第一个错误的版本 领扣
    hbase搭建web项目 报500错误 HTTP Status 500
    java API连接虚拟机上的hbase
    java程序连接hive数据库遇到的问题
    java程序向hdfs中追加数据,异常以及解决方案
    创建一个简单的maven的web程序
    java连接hbase时出现....is accessible from more than one module:
    导师双选制系统
  • 原文地址:https://www.cnblogs.com/ggykx/p/15019987.html
Copyright © 2011-2022 走看看