Gdb远程调试Linux内核遇到的Bug
本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/gdb-bug
本博客同步在http://www.cnblogs.com/papertree/p/6298774.html
在用qemu + gdb 调试linux内核时,遇到一个gdb的bug:“Remote 'g' packet reply is too long” ,记录一下。
(声明:不敢保证这是bug,有相关争论认为不应当在gdb client打patch,而应当在gdb server打patch,但是该博客提及的patch能够解决遇到的问题并且暂未发现其他问题。如果要在gdb server打patch,那么博主遇到的场景中,应当修改qemu内置的gdb server,并且编译qemu的源码。)
1. 实验环境
1. qemu 版本:
luzeshu@localhost:~$ qemu-system-x86_64 --version
QEMU emulator version 2.1.2 (Debian 1:2.1+dfsg-12+deb8u6), Copyright (c) 2003-2008 Fabrice Bellard
2. gdb版本:
luzeshu@localhost:~$ gdb --version
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Copyright (C) 2014 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
3. 装有linux内核与grub的镜像 fd.img
Linux内核版本:3.0.0
grub版本:grub-2.02~beta3
2. 目标指令: lret
出现这次bug的指令是在linux内核启动时,从32位兼容模式进入64位长模式时的一条指令。
源代码在 linux-3.0.0/arch/x86/boot/compressed/head_64.S 里面:
37 __HEAD
38 .code32
39 ENTRY(startup_32)
40 +---111 lines: cld-------------------------------------------------------------------------------------------------
151 /* Enable Long mode in EFER (Extended Feature Enable Register) */
152 movl $MSR_EFER, %ecx
153 rdmsr
154 btsl $_EFER_LME, %eax
155 wrmsr
156
157 /*
158 * Setup for the jump to 64bit mode
159 *
160 * When the jump is performend we will be in long mode but
161 * in 32bit compatibility mode with EFER.LME = 1, CS.L = 0, CS.D = 1
162 * (and in turn EFER.LMA = 1). To jump into 64bit mode we use
163 * the new gdt/idt that has __KERNEL_CS with CS.L = 1.
164 * We place all of the values on our mini stack so lret can
165 * used to perform that far jump.
166 */
167 pushl $__KERNEL_CS
168 leal startup_64(%ebp), %eax
169 pushl %eax
170
171 /* Enter paged protected Mode, activating Long Mode */
172 movl $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode */
173 movl %eax, %cr0
174
175 /* Jump from 32bit compatibility mode into 64bit mode. */
176 lret
177 ENDPROC(startup_32)
首先把EFER寄存器的MSR值(0xC0000080)放到ecx,给EFER设置LME,启用长模式。
但此时CS.L = 0,CS.D = 1(读取自GDT的CS表项,装载到CS寄存器16位以上的隐藏位中,这些隐藏位只作为CPU内部使用,包括GDT表项的base地址其实都装在这里面。参考 《segment 寄存器的真实结构》,CPU 处于32位的兼容模式。
接下来把新的CS值和EIP值压栈(模拟lcall指令,以成功执行lret指令)。
此时,如果执行了lret指令,那么CPU就会从栈顶取出CS和EIP,跳转到新的指令位置。同时因为新的CS.L = 1,CPU会从32位兼容模式进入64位模式。
那么gdb出现的bug就是在该指令执行之后。
3. 实验步骤
step1. export DISPLAY=:0.0
设置 X server
step2. “qemu-system-x86_64 fd.img -s -S” (-s 等同于 -gdb tcp::1234)
此时会把qemu界面(作为一个X client)发送到X server对应的桌面环境。
启动qemu虚拟机,并挂起在CPU复位后的状态,停在F000:FFF0这一条指令,仍未进入BIOS芯片初始程序。
开启gdb tcp远程调试的监听端口,等待gdb连接进行远程调试。
step3. 启动gdb,输入“target remote :1234”
此时,如图3-1,gdb 远程连接上qemu,并且停在CPU的初始状态(F000:FFF0)
step4. 打断点 “break *0x10000ed”
这是上面lret指令的地址。
(声明:grub可以用linux16(从16位实模式启动内核)、linux(从32位保护模式启动内核)两条命令装载内核,这里只考虑从保护模式启动的情况,而且linux内核版本不同该指令装载地址可能都会出现偏差。)
step5. gdb 按“c” 继续执行。
此时qemu界面会停在grub shell,依次输入“linux /boot/bzImage”、“boot”启动linux内核。启动内核后,执行到0x10000ed 这一行中断。
step6. gdb “按ni” 下一条指令
出现图3-2的错误。
4. 原因与方案
通过搜索,原因是gdb在远程调试时,因为寄存器大小的切换,导致gdb出现的bug。
那么对上面的解释可能就是,lret指令,执行后,CPU从32位兼容模式进入长模式,导致传输报文中的寄存器大小发生了变化。
(声明:这里博主未深入探究GDB源码、也未探究GDB远程调试协议,原因来自搜索,解释来自联系)
到gdb的官方站点:https://www.gnu.org/software/gdb/current/
(gdb代码托管在这里:git clone git://sourceware.org/git/binutils-gdb.git )
找到它的Bug database:https://sourceware.org/bugzilla/
搜索到一个相关的patch:https://sourceware.org/bugzilla/show_bug.cgi?id=13984
--- remote.c 2015-02-20 19:11:44.000000000 +0200
+++ remote-fixed.c 2015-08-12 20:00:14.966043900 +0300
@@ -6154,8 +6154,20 @@
buf_len = strlen (rs->buf);
/* Further sanity checks, with knowledge of the architecture. */
- if (buf_len > 2 * rsa->sizeof_g_packet)
- error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+ //if (buf_len > 2 * rsa->sizeof_g_packet)
+ // error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+
+ if(buf_len > 2 * rsa->sizeof_g_packet) {
+ rsa->sizeof_g_packet = buf_len;
+ for(i = 0; i < gdbarch_num_regs(gdbarch); i++){
+ if(rsa->regs->pnum == -1)
+ continue;
+ if(rsa->regs->offset >= rsa->sizeof_g_packet)
+ rsa->regs->in_g_packet = 0;
+ else
+ rsa->regs->in_g_packet = 1;
+ }
+ }
/* Save the size of the packet sent to us by the target. It is used
as a heuristic when determining the max size of packets that the
5. 解决
1. 下载gdb 7.12版本的源码
2. 验证7.12是否存在该问题
为了不与原本安装的gdb冲突,configure时指定make install的路径
“./configure --prefix=/home/luzeshu/tools/gdb-7.12”
“make”
“make install”
重复上面几个step,发现7.12一样有该问题。
3. 解决
方法1:
按照上面的patch,修改源码。
方法2(版本7.9):
如果下载了7.9的源码,可以把patch保存成“fix-remote.patch”文件,直接用“patch < fix-remote.patch” 打补丁。
如果出现下面错误,或许是空格的问题,给patch命令加上 --ignore-whitespace
patching file remote.c
Hunk #1 FAILED at 6154.
1 out of 1 hunk FAILED -- saving rejects to file remote.c.rej
方法2(版本7.12):
用7.12版本的,同样可以用上面的patch文件,不过line number要把6154改成7.12版对应的line number。
改完代码,再重新编译安装。再重复上面几个step,解决了。
6. gdb set architecture
最后一点,执行完lret切换到长模式之后,需要通过“set architecture i386:x86-64:intel”给gdb设置成64位。如果CPU进入长模式,而GDB没有跟着设置,显示的信息都是错乱的。
这里猜想是gdb所处的模式(32位或64位)对报文数据解读出的差错。