用BDI2000调试Linux内核和模块

用BDI2000调试Linux内核和模块

[email protected]
2007-12-22

BDI2000是性价比较高的JTAG调试器,通过装载不同的firmware就可以支持ARM、MIPS、XSCALE等多种嵌入式处理器。我所用的是
mips版本的bdiGDB,也就是能够仿真成为gdbserver,配合gdb进行源代码级调试。所用Linux内核为2.6.18.8版本。

1、BDI2000配置文件
如果目标板有bootloader,比如redboot或者u-boot,则可以先用bootloader初始化目标板,然后利用调试器直接下载程序到内存。
如果目标板上没有bootloader,则需要使用调试器配置文件来初始化目标板,包括CPU、内存等,我用的配置文件如下:

[INIT]

  WM32 0xB8000000 0xefbc8cd0 ;   ; 16bit ddr
 
  WM32 0xB8000004 0x8e7156a2 ;  ; 16 bitconservative twtr, twtr 19 trtw 21;
 
 WM32 0xB8000010 0x8   ;     
 WM32 0xB8000010 0x1   ;      write mode word
 WM32 0xB800000C 0x2   ;      enable dll (extened mode word)
 WM32 0xB8000010 0x2   ;      write extended mode word
 WM32 0xB8000010 0x8   ;      precharge enabled
 WM32 0xB8000008 0x61  ;      dll out of reset 16b
 WM32 0xB8000010 0x1   ;      write mode word
 WM32 0xB8000014 0x461b;
 WM32 0xB8000018 0xFFFF      ; 16 b
 
 ;pll/dividers
WM32 0xb8050000 0x800f3098        ;200/200/100
WM32 0xb8050008 0x1

 WM32 0xb8050004 0x50c0             ;gen 1Ghz
 WM32 0xb8050018 0x1313             ; ethernet 25Mhz (base)
 WM32 0xb805001c 0xee               ; 33Mhz PCI

 WM32 0xB800001C 0x07    
 WM32 0xB8000020 0x07
 WM32 0xB8000024 0x07
 WM32 0xB8000028 0x07


; Invalidate Caches
 IVIC 4 512 ;Invalidate IC, 4 way, 256 sets
 IVDC 4 256 ;Invalidate DC, 4 way, 256 sets

[TARGET]
JTAGCLOCK       2 ;
CPUTYPE         M24K ;the used target CPU type
ENDIAN          BIG ;target is big endian
RESET           HARD ; Reset is applied via the EJTAG reset pin
STARTUP         RESET ;STOP mode is used to let the monitor init the system
WORKSPACE       0xA0000000 ;workspace in target RAM for fast download
BDIMODE         AGENT ;the BDI working mode (LOADONLY | AGENT)
BREAKMODE       SOFT ;SOFT or HARD, HARD uses PPC hardware breakpoints
STEPMODE        SWBP ;JTAG, HWBP or SWBP
VECTOR          CATCH ;catch unhandled exceptions
SIO             8023 9600 ; TCP port for UART connection port 8023

[HOST]
IP          192.168.1.168
FILE       u-boot.bin
FORMAT     BIN 0x80060000
LOAD        MANUAL        ;load code MANUAL or AUTO after reset
PROMPT     MIPS>     ;used prompt
DEBUGPORT   2001
DUMP       dump.bin

[FLASH]
; WORKSPACE 0xa0000080
; CHIPTYPE AT49X16
; CHIPSIZE 0x200400
; BUSWIDTH 32
; FILE init.s19
; FORMAT SREC

[REGS]
FILE    BDI2000/reg24kf.def

2、调试Linux内核
1)编译内核,加入调试信息
#make menuconfig
在 Kernel Hacking -> 选中Compile the kernel with debug info,或者在.config文件中设置CONFIG_DEBUG_INFO=y,这样编译
选项CFLAGS中会带上-g参数,重新编译内核,生成的vmlinux大概有20多兆,这个内核是放在PC机上提供调试信息的,实际下载到
目标板的内核要用strip命令去除调试信息。
编译得到vmlinux.bin,该文件在arch/$(ARCH)/boot目录下。

#make vmlinux.bin

.了解内核的装入地址和入口地址:
利用readelf:
#mips-linux-uclibc-readelf -e vmlinux
............
Entry point address:               0x802bd000
...........
[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[ 0]                   NULL            00000000 000000 000000 00      0   0  0
[ 1] .text             PROGBITS        80060000 000800 1eb704 00  AX  0   0 2048
...........

可以看到.text段装入地址为0x80060000,内核入口地址为0x802bd000

使用objdump也可以获得这些信息,-d参数反汇编vmlinux可以得到装入地址,-f参数可以得到入口地址。
或者查看System.map文件:
#cat System.map | grep _text
#cat System.map | grep kernel_entry

.查看内核start_kernel的地址
由于要用BDI下断点,需要知道内核start_kernel的地址。
#cat System.map | grep start_kernel
802bd620 T start_kernel

2)下载内核到目标板
把strip之后的vmlinux.bin放到tftp server所在目录。
如果目标板上有bootloader,则复位目标板后先运行bootloader,等bootloader初始化目标板后,用BDI的调试器halt目标板,然后
在下载内核,注意要下载到内核的装入地址处。下载完内核后,在start_kernel处下断点,然后从内核入口地址处运行内核,很快
就会在start_kernel处触发断点,目标板重新进入调试状态,等待gdb连上目标板。
#telnet bdi
MIPS>halt
MIPS>load 0x80060000 vmlinux.bin bin
MIPS>bi 0x802bd620
MIPS>go 0x802bd000
- TARGET: target has entered debug mode

注意:bdi主机名的IP地址在/etc/hosts中设置。

3)用gdb调试内核
为了方便,可以在内核源码树目录下创建.gdbinit文件,内容为:
target remote bdi:2001
b panic
b sys_sync

注:调试程序可以使用target extended-remote bdi:2001替代.gdbinit的第一行,使用增强的连接模式。此时,gdbserver会在程序退出后自动再创建该程序的进程。

连接目标板
#mips-linux-gdbtui vmlinux
GNU gdb 6.7.1
Copyright (C) 2007 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 "--host=i686-pc-linux-gnu --target=mipseb-linux-uclibc"...
start_kernel () at init/main.c:457
Breakpoint 1 at 0x80085b68: file kernel/panic.c, line 78.
Breakpoint 2 at 0x800cb5e0: file fs/buffer.c, line 283.
(gdb)c

gdbtui是文本用户界面的gdb,可以显示命令行外带源代码、汇编代码、寄存器值窗口,使用简介参见附录,详细内容可以参考
<Debugging with gdb>文档。

在sys_sync处下断点的目的是为了在Linux命令行下用sync命令重新回到gdb控制。

这样就可以用gdb在源代码级调试Linux内核了。注意,在continue命令后,内核会停滞一段时间才能正常响应命令行,这是正常的,
不要认为内核down了。

3、调试Linux内核模块
步骤:正常启动内核(JTAG下载或者Flash启动)-> BDI2000 下halt命令 -> PC机上运行gdb,连上BDI2000仿真的gdbserver,下
断点 -> gdb continue命令 -> 获取内核模块装入地址 -> 在gdb中装入内核模块调试信息 -> 下断点调试内核模块

一般地,BDI2000中不下halt命令也可以,gdb一挂上目标板会自动停住CPU的执行,一般在Linux内核r4k_wait ()处。


1)编译内核模块
要在CFLAGS 上加入 -g 参数,以便生成调试信息。
获得模块入口地址:

Linux 2.4的内核:
目标板上
#insmod -m hello.o > map
#cat map 
.this           00000060  c88d8000  2**2
.text           00000035  c88d8060  2**2
.rodata         00000069  c88d80a0  2**5
……
.data           00000000  c88d833c  2**2
.bss            00000000  c88d833c  2**2
……

#sync
调试机上
(gdb)add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d833c -s .rodata 0xc88d80a0 -s .bss 0xc88d833c
(gdb)c

这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载
又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。
在目标板上用上述方法得到模块加载的地址信息,然后再用rmmod卸载模块。在调试机上将得到的模块地址信息导入到gdb环境中,
在内核代码的调用初始化代码之前设置断点。这样,在目标板上再次插入模块时,代码将在执行模块初始化之前停下来,这样就
可以使用gdb命令调试模块初始化代码了。

另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数sys_init_module(kernel/modle.c)执行对
内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下:
…… ……
 if (mod->init != NULL)
  ret = mod->init();
…… ……
 
在该语句上设置断点,也能在执行模块初始化之前停下来。

Linux 2.6内核:
需要在模块初始化代码处打印处模块的装入地址。.rodata段的地址可以通过执行命令readelf -e hello.ko,取得.rodata在文件
中的偏移量并加上段的align值得出。
为了方便,把打印装入地址的代码放在一个单独的库中,示例代码:
[root@root ~]#cat module_lib.h
#ifndef __MODULE_LIB_H__
#define __MODULE_LIB_H__
static int LIB_bss_indicator;

typedef int (*module_init_func)(void);
void print_module_addr(char * module_name, module_init_func init_func);

#endif /*__MODULE_LIB_H__*/

[root@root ~]# cat module_lib.c
#include <linux/module.h>
#include "module_lib.h"

void print_module_addr(char * module_name, module_init_func init_func)
{
static int data_indicator=0;
printk(KERN_ALERT "------------ module information ---------------/n");
printk(KERN_ALERT "%s: .text=0x%p/n",module_name, init_func);
printk(KERN_ALERT "%s: .data=0x%p/n",module_name, &data_indicator);
printk(KERN_ALERT "%s: .bss=0x%p/n",module_name, &LIB_bss_indicator);
printk(KERN_ALERT "-----------------------------------------------/n");
}

调用示例:
[root@root ~]# cat main.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include "module_lib.h"

static int __init test_init(void)
{
        printk("init module/n");
        print_module_addr("test", test_init);
        return 0;
}
static void __exit test_exit(void)
{
        printk("exit modules/n");
}

module_init(test_init);
module_exit(test_exit);

Makefile写法:
[root@root ~]# cat Makefile
PWD = $(shell pwd)
KERNEL_SRC = /lib/modules/`uname -r`/build

obj-m := hello.o
hello-y := module_lib.o main.o
KMAKE:= $(MAKE) 

all:
        $(KMAKE) -C $(KERNEL_SRC) M=$(PWD) modules

clean:
        $(KMAKE) -C $(KERNEL_SRC) M=$(PWD) clean
        $(RM) Module.symvers

注意:hello-y宏定义中要把module_lib.o放在第一位,这样在module_lib中定义的变量才会链接到ko文件的各段头部。
获得内核模块的各段地址后,调试方法和2.4内核下一样。

另外一种简单的办法:如果内核支持sysfs,则每一个安装的模块都会在/sys/module/下有对应的目录,该目录下的section目录
中可以查看所有section的地址,注意该处.text地址会和上面方法中看到的地址不一样,这种方法更精确。

#mount -t sysfs none /sys
#cd /sys/module/hello/sections
#cat .text
#cat .bss
#cat .data

如果Linux装入模块的地址是经过MMU之后的地址,则还需要设置BDI2000配置文件中的PTBASE和MMU选项,这样BDI2000才会去查找
linux的地址映射表,自动转换虚拟地址为物理地址,详细参加附录。

4、附录:
1)gdbtui的常用快捷键:
Ctrl + X , A 切换gdb模式/gdbtui模式 (先按Ctrl+X,然后再按A键,下同)
Ctrl + X , 1 改变布局模式为命令行窗口加另外一个窗口(源代码或汇编代码),如果为gdb模式会自动切换到gdbtui模式
Ctrl + X , 2 改变布局模式为命令行窗口加另外两个个窗口(源代码和汇编代码)
Ctrl + X , O 切换当前窗口
Ctrl + L     刷新屏幕内容

2)gdbtui的常用命令:
info win    显示窗口的大小
layout next  切换到下一个布局模式
layout prev  切换到上一个布局模式
layout src  只显示源代码
layout asm  只显示汇编代码
layout split  显示源代码和汇编代码
layout regs  增加寄存器内容显示
focus  cmd/src/asm/regs/next/prev  切换当前窗口
refresh    刷新所有窗口
tui reg next  显示下一组寄存器
tui reg system 显示系统寄存器
update     更新源代码窗口和当前执行点
winheight name +/- line 调整name窗口的高度
tabset nchar 设置tab为nchar个字符

3)gdbtui源代码窗口断点标识
第一个字符:
B   断点至少命中一次
b   没有命中的断点
H   硬件中断,至少被命中一次
h   没有命中的硬件中断
第二个字符:
+   断点被使能
-   断点被禁止

4)让BDI2000支持MMU
    BDI能够访问虚拟地址时,自动查找虚拟地址映射表并转换虚拟地址为物理地址。为了让BDI能够访问
MMU之后的虚拟地址,需要告诉BDI虚拟地址映射表的开始地址。BDI配置文件中的PTBASE参数指定了一个
物理地址,存放了指向Linux内核页表(swapper_pg_dir)和用户层页表(current_pgd),如果内核页表中没
有找到匹配项而且用户层页表不为0的情况下才会搜索用户层页表。

    PTBASE定义的地址应该是内核没有使用的内存地址,共8个字节,依次存放内核页表地址和用户层页
表地址。可以在内核装入地址之前找一个内存地址作为PTBASE。例如内核装入地址为0x80060000,则可以
选PTBASE地址为0x800002f0。

BDI配置文件修改:
[TARGET]
...
MMU        XLAT       ;MMU support enabled
PTBASE     0x800002f0 ;here are the page table pointers

    有两种方法可以把两个页表的地址写入PTBASE指定的地址,方法一是修改内核head.S文件,加入填写
PTBASE地址内容的代码;另外一种方法是在GDB或者BDI命令行下手动填写两个页表地址到PTBASE地址。

方法一:
在arch/mips/kernel/head.S文件的

j start_kernel
END(kernel_entry)
之前加入:

  /* Setup the PTE pointers for the Abatron bdiGDB.  */
  la t0, 0x800002f0    /* must match the bdiGDB config file */
  la t1, swapper_pg_dir
  sw t1, (t0)
  addiu t0, 4
  la t1, pgd_current
  sw t1, (t0)

方法二:
通过内核的System.map查找符号swapper_pg_dir和pgd_current的地址:
[root@root]#cat System.map | grep  swapper_pg_dir
80303000 B swapper_pg_dir
[root@root]# cat System.map | grep  pgd_current
80305018 B pgd_current

在GDB或者BDI的Telnet界面里填写这两个值到PTBASE指向的地址。
例如BDI中:
BDI>mm 0x800002f0 0x80303000
BDI>mm 0x800002f4 0x80305018

查看
BDI>md 0x800002f0

在GDB中:
(gdb) set *(int *)0x800002f0=0x80303000
(gdb) set *(int *)0x800002f4=0x80305018
查看:
(gdb) x /10 0x800002f0

可以把gdb的这两个命令放入.gdbinit中。


5、参考文献
[1]BDI2000 Application Notes# 02-001a:Using the Abatron BDI2000 to Debug a Linux Kernel
[2]bdiGDB EJTAG interface  for GNU Debugger MIPS32 User Manual (ManGDBR4K-2000C.pdf)
[3]Debugging with gdb
[4]Linux 系统内核的调试 http://www.ibm.com/developerworks/cn/linux/l-kdb/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章