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

  • 相关阅读:
    CodeForces 510C Fox And Names (拓扑排序)
    Codeforces 1153D Serval and Rooted Tree (简单树形DP)
    HDU 6437 Problem L.Videos (最大费用)【费用流】
    Luogu P3381 (模板题) 最小费用最大流
    Codeforces 741B Arpa's weak amphitheater and Mehrdad's valuable Hoses (并查集+分组背包)
    Codeforces 1144F Graph Without Long Directed Paths (DFS染色+构造)
    HDU 2204 Eddy's 爱好 (容斥原理)
    Codeforces 939E Maximize! (三分 || 尺取)
    Codeforces 938D. Buy a Ticket (最短路+建图)
    CodeForces 959E Mahmoud and Ehab and the xor-MST (MST+找规律)
  • 原文地址:https://www.cnblogs.com/ggykx/p/15019987.html
Copyright © 2011-2022 走看看