gdb调试器之"测不准原则"

一、测不准原则
我大学物理学的不太好,特别是高等物理,这个概念是在很多科普性的读物中都可以见到,就像”罗素悖论“、哥德尔的”不完备理论“、爱因斯坦的”相对论“等,大家都是一知半解,然后根据这个概念大家自由发挥,所以就有千奇百怪的场景和理解了,最后以讹传讹,倒也不清楚这个东西原始真正意义,这种现象在很多成语中也经常出现,例如经典的、也是考试的时候出镜率很高的”差强人意“。
作为无数民间科学家中的一员,我对这个”测不准“的理解就是当你真正观察它的时候,它和它正常的行为是不同的。这一点可能在很多其他场合也是用,例如……(此处大家可以尽情发挥一下)。或者围城中胖诗人曹元朗说的”当你以为你理解了我的时候,你就误解了我“。
二、调试器依赖的手段
当调试器调试一个任务的时候,它同样会对被调试的任务产生一些微妙的影响,这些影响在一些实时系统中表现的比较突出,特别是那些FIFO类型的实时调度任务,因为当任务被调试的时候,它的很多重要事件都要由内核代劳首先通知给调试器,在调试器发出下一个指示之前,被调试任务(线程)不能继续运行,这一点对于很多对初始化顺序有严格要求的系统来说是不能容忍的,所以调试器在很多时候并不是完成的。
对于非实时的系统,调试器同样可能会影响调度,原因同上,但是现象可能不尽相同。因为非实时系统对于任务的调度顺序没有依赖和假设,它本来就是可以以任意顺序运行的,如果需要排序可能使用各种锁来同步。
调试器主要是通过SIGSTOP来主动要求一个线程暂时冷静下来,而内核则通过ptrace_stop来强制,大家可以看一下内核在哪些地方调用了这个函数:
static void ptrace_stop(int exit_code, int nostop_code, siginfo_t *info)
{
    /* Let the debugger run.  */
    set_current_state(TASK_TRACED);该状态不可运行,并且不接收信号。
……
if (may_ptrace_stop()) {
        do_notify_parent_cldstop(current, CLD_TRAPPED);通知父进程,
        read_unlock(&tasklist_lock);
        schedule();让出调度权。
    } else {
        /*
         * By the time we got the lock, our tracer went away.
         * Don‘t stop here.
         */
        read_unlock(&tasklist_lock);
        set_current_state(TASK_RUNNING);
        current->exit_code = nostop_code;
    }
}
当进程附加的时候,内核也不拿自己当外人,也是毫不客气的发送了一个SIGSTOP信号过去。我们知道,很多时候,线程都是信号敏感的,也就是系统调用是可以被信号唤醒的,这明显会影响系统任务的执行,我们看一下调试器附加的代码:
sys_ptrace--->>>ptrace_attach
    force_sig_specific(SIGSTOP, task);
这里向被附加任务发送了一个SIGSTOP信号,之后将会看到,这个调用将会对被调试进程的运行产生影响。
三、附加被调试任务
1、测试代码
之前的代码已经看到,它会发送SIGSTOP给被附加线程,我们测试一下最为简单的read系统调用,测试程序为:
[tsecer@Harry TracerInter]$ cat tracersense.c 
#include <fcntl.h>
#include <stdio.h>

int main()
{
    char buf[10];
    int readin = read(0,buf,sizeof buf);
    printf("readin is %d\n",readin);
}
[tsecer@Harry TracerInter]$ gcc tracersense.c -g
[tsecer@Harry TracerInter]$ sleep 1000 | ./a.out
然后到另一个终端中使用调试器附加a.out对应的进程,看它是否会被从read系统调用唤醒。
[root@Harry ~]# cat /proc/18587/status
Name:    a.out
State:    S (sleeping)
……
voluntary_ctxt_switches:    3 注意这个调度次数,在调试器附加之后,被调试线程的调度次数将会增加
nonvoluntary_ctxt_switches:    2
[root@Harry ~]# gdb -p 18587
……
(gdb) shell cat /proc/18587/status
Name:    a.out
State:    T (tracing stop)
……
voluntary_ctxt_switches:    4 调试器附加之后,被调试任务执行次数加一,说明被调试线程从read系统调用返回了,但是程序没有退出运行。
nonvoluntary_ctxt_switches:    2
(gdb) quit
A debugging session is active.

Inferior 1 [process 18587] will be detached.

Quit anyway? (y or n) y
Detaching from program: /home/tsecer/CodeTest/TracerInter/a.out, process 18587
[root@Harry ~]# cat /proc/18587/status
Name:    a.out
State:    S (sleeping)
Tgid:    18587
……
voluntary_ctxt_switches:    5调试器退出附加之后,被调试线程调度次数再次增加。但是奇怪的是被调试任务并没有从read系统调用返回到用户态空间(否则进程会直接退出)。
nonvoluntary_ctxt_switches:    2
2、管道read被唤醒
linux-2.6.21\fs\pipe.c
pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t pos)
        if (signal_pending(current)) {
            if (!ret)
                ret = -ERESTARTSYS;
            break;
        }
注意这个返回错误码。当调试器收到一个子进程上报的信号之后,如果是自己发送的SIGSTOP,那么会对被调试任务透明的取消这个信号,取消的方法就是通过ptrace的PTRACE_CONT请求实现,看其实现非常简单linux-2.6.21\arch\i386\kernel\ptrace.c
        child->exit_code = data;
然后子进程继续运行,执行信号获取函数
linux-2.6.21\kernel\signal.c:   int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka, struct pt_regs *regs, void *cookie)
            /* Let the debugger run.  */
            ptrace_stop(signr, signr, info);

/* We‘re back.  Did the debugger cancel the sig?  */
            signr = current->exit_code;
            if (signr == 0)
                continue;
对于我们测试的例子,它刚好满足这个条件(调试器通过PTRACE_CONT清空了这个信号值),所以直接返回,然后进入信号处理函数
linux-2.6.21\arch\i386\kernel\signal.c:static void fastcall do_signal(struct pt_regs *regs)
if (signr > 0) {不满足该条件,执行下面分支。
……
        return;
    }

/* Did we come from a system call? */
    if (regs->orig_eax >= 0) {
        /* Restart the system call - no handlers present */
        switch (regs->eax) {
        case -ERESTARTNOHAND:
        case -ERESTARTSYS:
        case -ERESTARTNOINTR:
            regs->eax = regs->orig_eax;
            regs->eip -= 2;
            break;

case -ERESTART_RESTARTBLOCK:
            regs->eax = __NR_restart_syscall;
            regs->eip -= 2;这是整个机制的实现核心:将用户态指针减去两个字节,也就是386体系结构中linux下系统调用int 0x80指令占用的两个字节,这样被中断的系统调用(read)就可以再次执行而不真正对APP可见这次唤醒。
            break;
        }
    }
我看了一下另一个常用的可以测试的系统调用sys_pause,它被唤醒的时候也是设置
asmlinkage long
sys_pause(void)
{
    current->state = TASK_INTERRUPTIBLE;
    schedule();
    return -ERESTARTNOHAND;
}
所以使用pause测试应用程序可感知唤醒也不行、select也不行,所以这个现象只是作为一个理论存在,但是工程中应该比价少出现的情况,暂且不说。
四、对于不可唤醒睡眠任务的附加失败例子
[tsecer@Harry TracerInter]$ cat NonInt.c
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
    pid_t foker;
    if (0 == (foker = vfork())) 执行vfork,从而使父进程进入不可唤醒休眠。
    {
        sleep (1000);
    }
    printf("Father side This sentense should never been seen\n");
}
[tsecer@Harry TracerInter]$ gcc NonInt.c -o NonInt.c.exe -g
[tsecer@Harry TracerInter]$ ./NonInt.c.exe 
另一个终端中调试器附加父进程:
[root@Harry ~]# ps aux
……
tsecer   18841  0.0  0.0   1740   272 pts/0    D+   22:17   0:00 ./NonInt.c.exe
tsecer   18842  0.0  0.0   1740   272 pts/0    S+   22:17   0:00 ./NonInt.c.exe
root     18843  1.0  0.0   4688   992 pts/6    R+   22:17   0:00 ps aux
[root@Harry ~]# cat /proc/18841/status
Name:    NonInt.c.exe
State:    D (disk sleep)
Tgid:    18841
Pid:    18841
PPid:    12127
……
[root@Harry ~]# gdb -p 18841 附加父进程,
GNU gdb (GDB) Fedora (7.0-3.fc12)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Attaching to process 18841

这个显示将会一直持续,也就是说调试器将会一直无法从这里返回,这说明调试器没有收到内核通知的子进程收到SIGSTOP的事件,调试器在此一直等待。
五、sum up
这里没有分析gdb的实现代码,只是结合了内核的相关接口猜测和观察了一下gdb的执行原理,纯粹是探讨性内容,可能实际意义不大。

原文地址:https://www.cnblogs.com/tsecer/p/10486361.html

时间: 2024-08-29 01:07:40

gdb调试器之"测不准原则"的相关文章

gdb调试 print打印不出变量值或者不准确

编译选项加了 -O,即便是-O0,也不能正常显示,需要加上-gstabs+这个编译选项,-gdwarf-2这个编译选项会与-gstabs+冲突,去掉-gstabs+,只保留-gdwarf-2选项可以解决gdb调试的时候print变量不准和有些变量显示不出来的问题. 参考 http://bbs.csdn.net/topics/390708777?page=1#post-398696875 另外,编译的时候加 -Wall,调试程序之前,进gdb,输入list会显示main()函数前后10行代码,如果

为DEV C++/CodeBlock配置gdb调试遇到的问题

DEV C++和CodeBlock都只是一个IDE,不能编译调试,需要自己配置MINGW和gdb调试 1.MINGW 在这下载mingw-get-setup.exe安装即可. https://sourceforge.net/projects/mingw/files/MinGW/ 配置MINGW很简单,安装包就好了,可以只安装gcc和g++的. 选中前面的方块,然后installation->ApplyChanges即可.这个安装过程...很慢,也可能是我的网不好,他慢慢的安装了快一个小时. 2.

gdb调试

[前言]使用gdb调试前,在编译程序时,要加 -g 选项,否则你将看不见程序的函数名.变量名,所代替的全是运行时的内存地址. 1.开始调试 a.  gdb <program> program也就是你的执行文件,一般在当前目录下. b. gdb <program> core 用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件. 2.[列出源码],从第n行开始(编译时要加 -g 选项) l n 3.[设置断点]在第N行加断点 break

gdb调试命令

本篇摘自互联网,纯属自己学习笔记,然分享给看到我的博客的人们. 用GDB调试程序 GDB是一个强大的命令行调试工具.大家知道命令行的强大就是在于,其可以形成执行序列,形成脚本.UNIX下的软件全是命令行的,这给程序开发提代供了极大的便利,命令行软件的优势在于,它们可以非常容易的集成在一起,使用几个简单的已有工具的命令,就可以做出一个非常强大的功能. 于是UNIX下的软件比Windows下的软件更能有机地结合,各自发挥各自的长处,组合成更为强劲的功能.而Windows下的图形软件基本上是各自为营,

Go语言gdb调试踩坑

整个是一个docker环境 docker版本: 1.12.1,镜像是我自己做的基于ubuntu:14.04.05. 容器操作系统版本: Ubuntu 14.04.5 LTS go版本: 1.6.3 在gdb中执行run命令出错! 错误输出: warning:Error disabling address space randomization: Operation not permitted 环境:docker 解决办法: warning:Error disabling address spac

GDB调试汇编堆栈

GDB调试汇编堆栈 准备工作 终端编译工具: 编译64位Linux版本32位的二进制文件,需要安装一个库,使用指令sudo apt-get install libc6-dev-i386 测试代码: test.c 分析过程 1.生成汇编代码:gcc -g gdbtest.c -o gdbtest -m32 2.调试:gdb test 3.设置断点,因为目的是分析而不是调试bug,所以我们将断点设置在main函数 4.开始gdb调试:r(un),如若想获取此时的汇编代码,可用指令:disassemb

GDB调试汇编分析

GDB调试汇编分析 代码 本次实践我参照了许多先做了的同学的博客,有卢肖明,高其,张梓靖同学.代码借用的是卢肖明同学的代码进行调试运行. GCC编译 使用gcc -g gdbtest.c -o gdbtest -m32命令在64位的机器上产生32位汇编代码 在使用gdb进行调试运行时,有cgdb和gdb两种工具,我建议大家使用张梓靖同学使用的cgdb工具,因为使用时可以随时看到自己的源代码,看到我们的断点在哪里,每一步返回值到了哪行,更加直观. 分析过程 使用b main指令在main函数处设置

20145234黄斐《信息安全系统设计基础》GDB调试汇编堆栈过程分析(1)

堆栈跟踪 首先编辑一个程序 用gcc编译,再使用gdb调试,发现gdb尚未下载 下载后重新运行gdb 设置断点:b+行号或者"main" 运行:r frame:打印出的信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句. info frame:打印出的信息:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的.函数参数地址及值.局部变量的地址等等. 输入命令disassemble:显示出该代码(main())的汇编形式 info

[转]GDB调试基础

一.gdb常用命令: 命令 描述 backtrace(或bt) 查看各级函数调用及参数 finish 连续运行到当前函数返回为止,然后停下来等待命令 frame(或f) 帧编号 选择栈帧 info(或i) locals 查看当前栈帧局部变量的值 list(或l) 列出源代码,接着上次的位置往下列,每次列10行 list 行号 列出从第几行开始的源代码 list 函数名 列出某个函数的源代码 next(或n) 执行下一行语句 print(或p) 打印表达式的值,通过表达式可以修改变量的值或者调用函