BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / soft-design / #43967同步于 2013/9/15
该镜像源已超过 30 天没有更新,可能在源站已被删除。
SoftDesign机器人发帖

发一个内核丢失unix signal的debug报告吧

zxsword
2013/9/15镜像同步2 回复
整理简历和过往工作经历,将曾经写过的一个内核调试报告给发出来吧。 这大概是之前比较开心的时候之一吧,理所当然的加班,加班也开心。 具体问题是这样的:当进程间频繁的互发unix signal,signal会丢。 当这个bug发现时,距离芯片的投片的已经没有多少时间了,如果真的是硬件的bug,那么芯片还能不能投?老大找我开会说,接下来每天都加班,然后debug工作就开始了。 这是在ex公司工作时写的有限的几个报告之一,当初不懂的写报告的重要性,现如今这个报告就是我曾经为中国芯而工作的纪念吧。 报告如下: r4k_switch.S bug Bug 代码示例: arch/mips/kernel/r4k_switch.S /* * task_struct *resume(task_struct *prev, task_struct *next, * struct thread_info *next_ti) */ /* * check if we need to save FPU registers */ PTR_L t3, TASK_THREAD_INFO(a0) LONG_L t0, TI_FLAGS(t3) li t1, _TIF_USEDFPU and t2, t0, t1 beqz t2, 1f nor t1, zero, t1 and t0, t0, t1 LONG_S t0, TI_FLAGS(t3) 触发bug的代码示例: void signal_wake_up(struct task_struct *t, int resume) { unsigned int mask; set_tsk_thread_flag(t, TIF_SIGPENDING); 触发bug的现场说明: set_tsk_thread_flag(t, TIF_SIGPENDING);将会对进程的flag置SIGPENDING位,而此函数是对用llsc实现set_bit函数的简单包裹,也就是说当置位结束函数返回时,flag的SIGPENDING位理应被置位。但如果发生下示的情况时,置的位将会丢失。 core 0 core 1 LONG_L t0, TI_FLAGS(t3) li t1, _TIF_USEDFPU and t2, t0, t1 beqz t2, 1f nor t1, zero, t1 set_tsk_thread_flag(t, TIF_SIGPENDING); and t0, t0, t1 LONG_S t0, TI_FLAGS(t3) 如上所示,当CPU1将该位置位后,紧接着会被CPU0清位。从而造成用户空间的进程丢失一个unix signal。另外,必须要说的是,CPU0并不会在每次执行上下文切换都会存储浮点寄存器,LONG_S t0, TI_FLAGS(t3)指令并不是每次都会执行。只有当beqz t2, 1f不跳转时,才会被执行,而不跳转的先决条件是进程使用了FPU。当有一条浮点指令时,即会触发浮点异常并将flag中的TIF_USEDFPU位置位,于是操作系统不能使用lazyFPU模式,而必须保存浮点寄存器,此时隐藏的那个bug就有可能触发。 此项测试也在模拟器上进行过大量的测试,但因为模拟器的bug,beqz t2, 1f每次均会发生跳转,所以该bug没能在模拟器重现,并因为模拟器测试通过而一度怀疑硬件问题。 追踪bug的过程: 首先简单描述发现bug的测试程序,该测试程序的主要内容为父子进程互相发送信号。父进程kill信号给子进程,从而子进程执行信号处理函数,而信号处理函数给父进程回发信号,然后退出,如此交错发送信号。当触发bug时,该程序的表现现象为丢失信号。代码如下: #include "unpipc.h" static int counter, nloop; static pid_t childpid, parentpid; void sig_usr1(int signo) { Kill(parentpid, SIGUSR2); /* child receives USR1, sends USR2 */ return; } void sig_usr2(int signo) { if (++counter < nloop) Kill(childpid, SIGUSR1); /* parent receives USR2, sends USR1 */ else Kill(parentpid, SIGTERM); /* parent terminates below */ return; } void sig_term(int signo) { printf("latency: %.3f usec\n", Stop_time() / nloop); Kill(childpid, SIGTERM); exit(0); } int main(int argc, char **argv) { if (argc != 2) err_quit("usage: lat_signal <#loops>"); nloop = atoi(argv[1]); counter = 0; parentpid = getpid(); Signal(SIGUSR1, sig_usr1); /* for child */ Signal(SIGUSR2, sig_usr2); /* for parent */ if ( (childpid = Fork()) == 0) { for ( ; ; ) { /* child */ pause(); } exit(0); /* never reached */ } /* 4parent */ Signal(SIGTERM, sig_term); /* for parent only */ Start_time(); Kill(childpid, SIGUSR1); for ( ; ; ) pause(); } 在追踪的过程中,对该测试代码进行了各种改写,并生成了多种变体,例如更换程序中使用的不可靠信号为可靠信号,又例如当信号丢失之后,发送sigint(通过ctrl – c发送给前台会话组进程,已为测试程序增写sigint 信号处理程序,故不会简单的终止进程),发现测试又能继续运行,所以猜测信号可能并没有丢失。 为了更好的追踪错误,追踪出信号究竟是如何在内核中丢失的,只能更改内核代码。更改调试内核代码的过程个人很有学习意义,过程如下: 增添打记的函数: //for fpga debug xzhao #define DBG_ARR_SIZE 512 extern unsigned int dbg_arr[DBG_ARR_SIZE]; extern unsigned int dbg_pt; /* tmp bit 16-20 where point */ #include <asm/atomic.h> static inline void push_mark(struct task_struct *p, int mark) { unsigned int pos,tmp; if (current->pid < 50) return; pos = atomic_add_return(1,(atomic_t *)&dbg_pt); mark = (current->pid << 8) | mark; tmp = raw_smp_processor_id() << 8; tmp |= task_thread_info(p)->flags & 0xff; dbg_arr[pos%DBG_ARR_SIZE] = (mark << 16) | tmp; } static inline void dbg_push(unsigned int tmp) { int pos = atomic_add_return(1,(atomic_t *)&dbg_pt); dbg_arr[pos%DBG_ARR_SIZE] = tmp; } //end xzhao 通过push_mark函数,我们可以记录kernel运行过程中一些关键的时间点,某个变量的值。例如我们使用该函数监测了进程的flag的sigpending位。我们的编码规则为:31-24位为进程ID,23-16位为标记位置编码,16-8位为CPU ID,7-0位保存监测的flag。记录的数据存放在dbg_arr中。 通过下面的代码可以将dbg_arr中的数据导出来: /** * sys_kill - send a signal to a process * @pid: the PID of the process * @sig: signal to be sent */ SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) { struct siginfo info; //xzhao // printk("pid:%d sig:%08x\n",pid ,sig); if( unlikely( (sig == 2) && (pid < 45 && pid > 40)) ){ int i, pos = dbg_pt; for(i=0; i<DBG_ARR_SIZE; i++){ printk("\t%08x\n",dbg_arr[(pos--)%DBG_ARR_SIZE]); } } //end Pid为shell的pid,每次启动约为42或者43,故简单的做个判断。Sigint(ctrl – c)信号等于2。故键盘按ctrl –c 即可打印出我们标记的数据。 打标记也是一个渐进的过程,通过不断打标读出测试数据,并不断的猜测,最终贴进了错误的发生点。贴近的数据记录如下: 3f5b0100 3f310100 3f6b0100 3f2f0100 3e620000 3e610000 3e560000 3e640000 3e630000 3f230100 3e620000 3f210100 3e610000 3f200100 3f450100 3e550000 3e6a0000 3f410100 3f400102 62 21 Switch_to() set_bit() 63 23 标记62是切换之前,标记63是切换之后。标记21是置sigpending位之前,23是置pending位之后。 经过多次测试,发现出现错误的模式均如上表所示。也就是说,当CPU0正在进程切换的时候,CPU1给CPU0发送的信号,会“丢失”。贴近到此处,范围已经较小了,通过逻辑分析仪跟踪PC流,抓出出错时的PC流,并根据kernel dump分析,找当CPU1正在置位时,CPU0在干什么,最终找到错误代码:r4k_switch.S 中的resume函数。
订阅后,新回复会通过你的通知中心匿名送达。
2 条回复
zxsword机器人#1 · 2013/9/15
见识过浮点数计算错误,见识过各式各样的内存错,见识过信号量错误,我想能遇到这些错误也是件不容易的事儿吧
iFadeToBlack机器人#2 · 2013/9/17
赞一个,虽然感觉几乎看不懂了……