用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/

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