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的执行原理,纯粹是探讨性内容,可能实际意义不大。
我大学物理学的不太好,特别是高等物理,这个概念是在很多科普性的读物中都可以见到,就像”罗素悖论“、哥德尔的”不完备理论“、爱因斯坦的”相对论“等,大家都是一知半解,然后根据这个概念大家自由发挥,所以就有千奇百怪的场景和理解了,最后以讹传讹,倒也不清楚这个东西原始真正意义,这种现象在很多成语中也经常出现,例如经典的、也是考试的时候出镜率很高的”差强人意“。
作为无数民间科学家中的一员,我对这个”测不准“的理解就是当你真正观察它的时候,它和它正常的行为是不同的。这一点可能在很多其他场合也是用,例如……(此处大家可以尽情发挥一下)。或者围城中胖诗人曹元朗说的”当你以为你理解了我的时候,你就误解了我“。
二、调试器依赖的手段
当调试器调试一个任务的时候,它同样会对被调试的任务产生一些微妙的影响,这些影响在一些实时系统中表现的比较突出,特别是那些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的执行原理,纯粹是探讨性内容,可能实际意义不大。