本文章主要简介如何准备vmware的双机调试, 并示例了一些简单的驱动编写和调试信息

准备内核

linux kernel source

配置虚拟机

  1. 新建一个虚拟机
  2. 添加串口, 并做如下配置

    此虚拟机作为被调试机

克隆虚拟机

修改串口设备的配置,设定为”该端为客户端”, 此为调试机

在被调试机编译内核

  1. 解压内核

    1
    2
    tar jxvf linux-2.6.26.tar.bz2
    cd linux-*
  2. 安装组件

    1
    sudo apt-get install libncurses-dev gawk flex bison openssl libssl-dev dkms libelf-dev libudev-dev libpci-dev libiberty-dev autoconf
  3. 配置编译参数

    1
    make menuconfig

    可能的错误

    1
    2
    3
    4
    5
    6
    7
    8
    error: curses.h: No such file or directory
    基于Debian的发行版(如Debian、Ubuntu):sudo apt-get install libncurses5-dev
    基于Red Hat的发行版(如RHEL、CentOS):sudo yum install ncurses-devel

    如果出现找不到该包,尝试以下设置:
    vi /etc/apt/source.list
    添加其它系统的源[源列表地址](http://mirrors.163.com/.help/debian.html)
    比如kali,我添加的debian jessie的源

    如果提示窗口太小出错,说明你的命令行窗口小了,放大一点再试试.
    内核调试需要做如下设置:

    1
    2
    3
    4
    5
    6
    7
    Kernel Hacking –>
    compile-time checks and compiler options –>
    [*] Compile the kernel with debug info
    [*] Compile the kernel with frame pointers
    [*] kernel debugging
    [*] KGDB: kernel debugger–>
    <*> KGDB: use kgdb over the serial console

    完成上述选项后, 直接save后推出.

  4. 编译内核

    1
    2
    3
    4
    make -j 4 bzImage # -j 代表用多少线程, 不要超过cpu的最大线程数
    make modules
    make modules_install
    make install
  5. 修改启动表
    打开 /boot/grub/grub.conf (如果不存在, 就改grub.cfg文件)
    针对 grub.conf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     1 # grub.conf generated by anaconda
    2 #
    3 # Note that you do not have to rerun grub after making changes to this file
    4 # NOTICE: You have a /boot partition. This means that
    5 # all kernel and initrd paths are relative to /boot/, eg.
    6 # root (hd0,0)
    7 # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
    8 # initrd /initrd-version.img
    9 #boot=/dev/hda
    10 default=0
    11 timeout=5
    12 splashimage=(hd0,0)/grub/splash.xpm.gz
    13 hiddenmenu
    14 title CentOS (2.6.26)
    15 root (hd0,0)
    16 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00
    17 initrd /initrd-2.6.26.img
    18 title CentOS-4 i386 (2.6.9-67.ELsmp)
    19 root (hd0,0)
    20 kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00 <---------------------
    21 initrd /initrd-2.6.9-67.ELsmp.img

    在kernel那一行末尾添加 “kgdboc=ttyS0,115200 nokaslr”

    nokaslr是禁用内核的kaslr机制, 避免某些情况下内核地址随机化导致gdb没办法识别源码. 更多命令可以参考Using kgdb, kdb and the kernel debugger internals

如下图:

针对 grub.cfg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
menuentry 'Ubuntu, with Linux 3.8.0-19-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-3.8.0-19-generic-advanced-af5e68c6-4f1f-494e-8c35-fc0911ec3564' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 af5e68c6-4f1f-494e-8c35-fc0911ec3564
else
search --no-floppy --fs-uuid --set=root af5e68c6-4f1f-494e-8c35-fc0911ec3564
fi
echo 'Loading Linux 3.8.0-19-generic ...'
linux /boot/vmlinuz-3.8.0-19-generic root=UUID=af5e68c6-4f1f-494e-8c35-fc0911ec3564 ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US kgdboc=ttyS0,115200 kgdbwait quiet
^
|
-------------
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-3.8.0-19-generic
}

在新内核对应的内容下添加 “kgdboc=ttyS0,115200”

测试双机调试

开启两个虚拟机, 在被调试机上运行命令

1
echo hello >/dev/ttyS0

在调试机上运行

1
cat /dev/ttyS0

如果调试机没有收到消息, 就实时用/dev/ttyS1, 多试两下就行
如果成功了, 记得改grub的配置.

开始调试

如果grub没有配置”kgdbwait”参数, 就在被调试机开机后输入

1
echo g >/proc/sysrq-tirgger

在调试机里

1
2
3
4
5
6
7
8
9
10
11
$ gdb linux-3.18.34/vmlinux
.....
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0

Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/kgdb.c:1674
1674 wmb(); /*Sync point after breakpoint */
warning: shared library handler failed to enable breakpoint

(gdb)

如果”set remotebaud 115200”的时候出错,就换成”set serial baud 115200”

调试自定义驱动

一个简单的驱动文件可以是如下形式(test.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/module.h> 
#include <asm/io.h>
#include <linux/slab.h>
#include <linux/ioport.h>
MODULE_LICENSE("GPL");


int my_module_init(void){
printk("module init done\n");
return 0;
}

void my_module_exit(void){
printk("module exit\n");
return;
}

module_init( my_module_init );//声明初始化函数
module_exit( my_module_exit );

Makefile 文件内容如下:

1
2
3
4
5
6
7
obj-m += test.o
KDIR:=/lib/modules/$(shell uname -r)/build
MAKE:=make
default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean

注意, $(MAKE)前是tab, 不是空格! 高版本的gcc 需要把SUBDIRS替换成M

驱动的其它提示可以参考我的”Linux 使用技巧” 文章的关于linux驱动的部分

准备编译环境并编译安装驱动:

1
2
3
root@ubuntu:/home/vv# apt install bison flex gcc autoconf -y
root@ubuntu:/home/vv# make
root@ubuntu:/home/vv# insmod test.ko

如果想在加载驱动时拦截, 可以考虑在module_init里的入口函数处添加一个int3断点 asm(".byte 0xcc");实现.

如果想带符号, 假设我们要调试带符号的kvm.ko驱动, 首先在被调试机上通过cat /proc/modules|grep获取驱动基址:

1
2
3
root@ubuntu:/home/vv# cat /proc/modules |grep kvm
kvm_intel 294912 0 - Live 0xffffffffc066c000
kvm 843776 1 kvm_intel, Live 0xffffffffc055e000

此处可以看到, kvm的基址是0xffffffffc055e000.
然后通过echo g >/proc/sysrq-tirgger 使内核中断.
然后在调试器里输入以下命令添加符号:

1
2
3
4
5
gdb$ add-symbol-file arch/x86/kvm/kvm.ko 0xffffffffc055e000
add symbol table from file "arch/x86/kvm/kvm.ko" at
.text_addr = 0xffffffffc055e000
Reading symbols from arch/x86/kvm/kvm.ko...
gdb$

这代表我们添加成功了. 也可以索引到kvm的符号了.

简单的e1000网卡驱动示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <asm/dma.h>
#include <asm/page.h>
#include <linux/slab.h>
#include <linux/pci.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/pci_ids.h>
#include <linux/delay.h>

#define LOG "MYMOD:"
char e1000_driver_name[] = "t_e1000e";
static const struct pci_device_id e1000_pciid_table[] = {
{ PCI_VDEVICE(INTEL, 0x10D3), 3 },//E1000_DEV_ID_82574L) , board_82574
{0}
};

struct E1000_ADAPTER {
u8 *hw_addr0;
u8 *hw_addr1;
} *adapter=NULL;


#define LOGTAG "TEST: "
void klog(char *fmt, ...){
char textbuf[1024-32];
va_list args;
u32 len = 0;
char *tmp = fmt;
va_start(args, fmt);

while(*tmp){
len++;
tmp++;
}

memcpy(textbuf, LOGTAG, sizeof(LOGTAG)-1);
memcpy(textbuf + sizeof(LOGTAG) - 1, fmt, len);
memcpy(textbuf + sizeof(LOGTAG) + len - 1, "\n", 2);
vprintk(textbuf, args);
va_end(args);

return ;
}

static int e1000_probe_device(struct pci_dev *pdev,
const struct pci_device_id *id)
{// 3. 触发探测函数后, 操作因设备而异, 但是大同小异.
unsigned long mmio_start, mmio_len;
u64 dma;
int err = pci_enable_device_mem(pdev);// 4. enable mem

printk("probe device\n");
if(err){
printk(LOG"fail to enable device!\n");
return err;
}
err = pci_request_selected_regions_exclusive(pdev,
pci_select_bars(pdev, 0x200),
e1000_driver_name);//IORESOURCE_MEM 5. 激活设备的IOport/IOmem
if (err) {
printk(LOG"Failed to request region for adapter\n");
return err;
}

adapter=dma_zalloc_coherent(&pdev->dev, sizeof(struct E1000_ADAPTER), &dma,
GFP_KERNEL);;
if(!adapter){
printk(LOG"Failed to alloc adapter\n");
return -1;
}

pci_set_master(pdev);

mmio_start = pci_resource_start(pdev, 0);// 因为e1000网卡有两个bar,所以此处需要映射bar0和bar1
mmio_len = pci_resource_len(pdev, 0);

adapter->hw_addr0 = ioremap(mmio_start, mmio_len);// 6. 映射设备IOmem
if (!adapter->hw_addr0) {
printk("Map 0 fail\n");
return 0;
}

mmio_start = pci_resource_start(pdev, 1);
mmio_len = pci_resource_len(pdev, 1);

adapter->hw_addr1 = ioremap(mmio_start, mmio_len);
if (!adapter->hw_addr1) {
printk("Map 1 fail\n");
return 0;
}
// 7. 初始化好IO资源以后, 就可以开始其它初始化操作了
e1000_start(pdev);
return 0;
}

void e1000_remove_device(struct pci_dev *pdev){
printk("removed device!\n");
if(adapter){
if(adapter->hw_addr0)
iounmap(adapter->hw_addr0);
if(adapter->hw_addr1)
iounmap(adapter->hw_addr1);
}
pci_release_selected_regions(pdev,
pci_select_bars(pdev, 0x200));//IORESOURCE_MEM
pci_disable_device(pdev);
}

void e1000_shutdown_device(struct pci_dev *pdev){
//do nothing
return;
}

static struct pci_driver e1000_driver = {// 1. 准备驱动描述符
.name = e1000_driver_name,// 包括驱动名称
.id_table = e1000_pciid_table,// 硬件id
.probe = e1000_probe_device,// 硬件被识别后,第一次调用到的探测函数
.remove = e1000_remove_device,
.shutdown = e1000_shutdown_device,
};

int main_init(void){
int err = pci_register_driver(&e1000_driver);// 2. 注册驱动, 当识别到注册的硬件id后, 就会调用probe函数.
printk(LOG"mod init!\n");
return err;
}

void main_exit(void){
pci_unregister_driver(&e1000_driver);
printk(LOG"mod exit!\n");
if(adapter){
kfree(adapter);
}
return;
}

module_init(main_init);
module_exit(main_exit);