make clean;make all;make modules;make modules_install;make install
将其编译成名为getpid的执行文件”gcc –o getpid <路径>/getpid.c”, 我们使用KDB来产看它进入内核后的执行路径。
l 激活KDB (按下pause键,当然你必须已经给内核打了KDB补丁);设置内核断点 “bp sys_getpid” ;退出kdb “go”;然后执行./getpid 。瞬间,进入内核调试状态,执行路径停止在断点sys_getpid处。
l 在KDB>提示符下,执行bt命令观察堆栈,发现调用的嵌套路径,可以看到在sys_getpid是在内核函数system_call中被嵌套调用的。
l 在KDB>提示符下,执行rd命令查看寄存器中的数值,可以看到eax中存放的getpid调用号——0x00000014(=20).
l 在KDB>提示符下,执行ssb(或ss)命令跟踪内核代码执行路径,可以发现sys_getpid执行后,会返回system_call函数,然后接者转入ret_from_sys_call例程。(再往后还有些和调度有关其他例程,我们这里不说了它们了。)
1 UML调试(2.6.18)
II)到上述目录
make clean
make mrproper (这两个操作对于以前编译过其他arch的情况比较重要)
make xconfig ARCH=um
make all ARCH=um
现在目录下有个linux的可执行文件。
III)不幸,运行uml还需要一个rootfs,就是uml的‘硬盘’啦。当然
下载
一个
比较快,http://user-mode-linux.sourceforge.net/dl-sf.html
选择了Debian-3.0r0.ext2.bz2,解压缩后改名为root_fs
IV)./linux ubd0=root_fs 这样就搞定了。
怎么运行UML?
一个命令就好了
./linux ubd0=Debian-3.0r0.ext2
如果不想敲这么长,那就把Debian-3.0r0.ext2重命名为'root_fs',UML启动会自动挂载这个文件
如果是SMP内核需要传个参数给内核,不然它还是一个CPU
./linux ncpus=2 ubd0=Debian-3.0r0.ext2
怎么调试UML?
用gdb调试就行了,没什么特别的,但是要屏蔽两个信号
linux #gdb ./linux
(gdb) handle SIGSEGV pass nostop noprint
(gdb) handle SIGUSR1 pass nostop noprint
(gdb) b main
(gdb) run ubd0=Debian-3.0r0.ext2
编译UML调试环境
在linux源代码目录下make menuconfig ARCH=um 生成可执行文件linux和vmlinux
下载根文件系统rootfs
复制linux和rootfs在一个目录中
./linux rw init=/bin/bash
真是没有用过
linux,很多事情搞不清楚呢还。读mm/shmem.c 的时候看到mmap 的私有共享影射使用了tmpfs的文件,但是
ls /dev/shm并没有看到那个dev/zero名字的文件。于是想搞清楚。(mm/shmem.c shmem_zero_setup)
先是考虑ls使用什么
系统调用读取文件夹内的文件名列表,开始没有经验,就是
搜索到了sysv_readdir这个系统调用.顺着看下去,觉得
vfs_readdir是个关键的函数。并通过查看tmpfs的相关代码确认了这一点。sys_open->dentry_open->fops_get(inode->i_fop)然后看tmpfs
的inode,shmem_get_inode 使用的是simple_dir_operations。
然后看dcache_readdir,开始没有仔细看,只是注意到有如下的一句:
(本文实际上都是采用
kernel2.6.14)
if (d_unhashed(next) || !next->d_inode)
continue;
直接注释掉,编译内核,测试。不幸,什么都没有。
如此反复了好几次,后来又改shemem_setup_zero,初看觉得inode->i_link被置位0了,去掉(注意,这里已经走入歧途了),还是
没有结果。这样改来改去没有任何结果编译了多次内核。倒是体会到,这样的小改动只需要
make
即可,无须make clean等操作。
过了几天,感觉需要先找个什么方法看看是不是这样行的通。于是直接将shmem_zero_setup的shmem_file_setup换成相应的filp_open
(注意需要创建,并且名字不能相同)。居然可以了。(后来意识到这样不过是饮鸩止渴)于是认定只要走一个完整的filp_open就可以将问
题找出来(没想到这样是在错误的方向上越走越远)。于是将filp_open拆分,改动到shmem.c.经历了‘成功’后,这次痛苦的时间更长了。
下面是相关的改动(注意这不能工作,老是死机):
int shmem_do_open_namei(const char * pathname, int flag, int mode, struct nameidata *nd)
{
struct dentry *dentry;
struct dentry *dir;
struct qstr *this;
/*prepare ND */
/* Fill in the open() intent data */
nd->intent.open.flags = flag;
nd->intent.open.create_mode = mode;
/*
* Create - we need to know the parent.
*/
/*Get parent nd*/
#if 0
error = path_lookup(pathname, LOOKUP_PARENT|LOOKUP_OPEN|LOOKUP_CREATE, nd);
if (error)
return error;
#endif
nd->dentry = shm_mnt->mnt_root;
nd->mnt = mntget(shm_mnt);
nd->last_type= LAST_NORM;
{ /*prepare last compnet hash*/
this= &nd->last;
unsigned long hash;
unsigned char c;
this->name = pathname;
c = *(const unsigned char *)pathname;
hash = init_name_hash();
do {
pathname++;
hash = partial_name_hash(c, hash);
c = *(const unsigned char *)pathname;
} while (c && (c != '/'));
this->len = pathname - (const char *) this->name;
this->hash = end_name_hash(hash);
}
dir = shm_mnt->mnt_root;
nd->flags &= ~LOOKUP_PARENT;
{
/*dentry = shmem__lookup_hash(&nd->last, nd->dentry, nd);*/
dentry = d_alloc(dir, &nd->last);
if (!dentry)
return 0;
}
/* dentry = inode->i_op->lookup(inode, new, nd); */
d_add(dentry, NULL);
/* Negative dentry, just create the file */
/*error = shmem_create(dir->d_inode, dentry, mode, nd);*/
struct inode *inode = shmem_get_inode(dir->d_sb, S_IFREG | S_IRWXUGO, 0);;
if (inode) {
dir->d_inode->i_size += BOGO_DIRENT_SIZE;
dir->d_inode->i_ctime = dir->d_inode->i_mtime = CURRENT_TIME;
d_instantiate(dentry, inode);
}else
return -ENOSPC;
nd->dentry = dentry;
return 0;
}
struct file *shmem_filp_open(const char * filename, int flags, int mode)
{
int error;
struct nameidata nd;
error = shmem_do_open_namei(filename, flags, mode, &nd);
{
/*
if (!error)
return dentry_open(nd.dentry, nd.mnt, flags);*/
struct file * f;
struct inode *inode;
int error;
error = -ENFILE;
f = get_empty_filp();
if (!f)
goto cleanup_dentry;
f->f_flags = flags;
f->f_mode = FMODE_WRITE | FMODE_READ;;
inode = nd.dentry->d_inode;
file_ra_state_init(&f->f_ra, inode->i_mapping);
f->f_dentry = nd.dentry;
f->f_vfsmnt =nd.mnt;
f->f_pos = 0;
f->f_op = &shmem_file_operations;
file_move(f, &inode->i_sb->s_files);
return f;
cleanup_dentry:
dput(nd.dentry);
mntput(nd.mnt);
return ERR_PTR(error);
}
}
/*
* shmem_zero_setup - setup a shared anonymous mapping
*
* @vma: the vma to be mmapped is prepared by do_mmap_pgoff
*/
void itoax(char *str, int num)
{
int i=32;
while(i)
{
if(num&0x1) *str++ = '1';
else *str++='0';
num>>=1;
i--;
if(num==0) break;
}
*str=0;
}
int shmem_zero_setup(struct vm_area_struct *vma)
{
struct file *file;
loff_t size = vma->vm_end - vma->vm_start;
/*file = shmem_file_setup("zero", size, vma->vm_flags);*/
{ static int i=0;
char name[128], num[33];
strcpy(name,"zerov");
itoax(num,i);
strcat(name,num);
/*file = shmem_filp_open(name,FMODE_READ|FMODE_WRITE,0700);*/
filp_open(name,O_WRONLY|O_CREAT,0700);
file = shmem_file_setup(name, size, vma->vm_flags);
file->f_dentry->d_inode->i_size=size;
i++;
}
if (IS_ERR(file))
return PTR_ERR(file);
if (vma->vm_file)
fput(vma->vm_file);
vma->vm_file = file;
vma->vm_ops = &shmem_vm_ops;
return 0;
}
痛苦的久了就想到了‘万能’的
调试,实在是增很printk(何况还不知道如何查看输出信息,ft)。只是知道printk,kgdb,qmenu
(?)可以工作,但是我感觉User mode linux对付这个问题才是王道。于是查找uml的使用指南,并逐步实施。
I) 首先是找一个合适的内核版本来编译ARCH=um的内核。我选择了2.6.14,后
来证明这个内核比较稳定,编译没有问题。
解压缩到/usr/src/linux2.6.14-uml。
II)到上述目录
make clean
make mrproper (这两个操作对于以前编译过其他arch的情况比较重要)
make xconfig ARCH=um
make all ARCH=um
现在目录下有个linux的可执行文件。
III)不幸,运行uml还需要一个rootfs,就是uml的‘硬盘’啦。当然
下载一个
比较快,http://user-mode-linux.sourceforge.net/dl-sf.html
选择了Debian-3.0r0.ext2.bz2,解压缩后改名为root_fs
IV)./linux ubd0=root_fs 这样就搞定了。
V)其实还有要注意的事情:make xconfig ARCH=um的时候注意
1.如果有如下提示:
VFS: Cannot open root device "ubd0" or unknown-block(0,0)
则大概问题如下
1)需要支持rootfs的文件类型,选上ext2,ext3支持
2)选上Block devices/Virtual block device[*](ubd支持)
2.如果提示: Cannot open init console
则你的rootfs 的/dev/console文件不存在,需要
mount -o loop root_fs /mnt
#cp -dpR /dev/tty[0-9] /mnt/dev
#cp -dpR /dev/ram* /mnt/dev
#cp -dpR /dev/console /mnt/dev
3.如果提示:cannot setup thead-local storage: cannot setup LDT for
thread-local storage, 那么证明你的rootfs使用了NPTL,不幸现在uml
还不能很好支持这个特性。换个简单点的rootfs吧。
VI)具体到我推荐的这个rootfs,还是有些问题的:
1。竟然没有启动bash, 在相应的runlevel中加上启动bash的命令即可
2。没有安装shm,编辑fstab加上相应条目即可
3。fstab文件中 / 安装在 /dev/ubd/0,改为/dev/ubd0
4。编译uml的时候当然要选上proc支持
VII)我使用的host linux也是linux2.6.14,挺好用,如果你么有发现更好的
用这个就可以了。并且我使用的是FC4,不推荐FC5。
这样,搞定了uml,可以测试了。我写了一个小程序,只有一行:
#include
#include
#include
#include
main(int argc, char** argv)
{
int i;
char *p_map;
char temp;
p_map=(char*)mmap(NULL,10*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
if(p_map==NULL)
printf("mmap error!\n");
}
在host linux下编译,拷贝到uml rootfs即可。测试的时候执行 shm&,让他后
台运行。
至于调试,就是:(直接在/usr/src/linux2.6.14-uml下执行)
#gdb
# (gdb) set args ubd0=root_fs
# (gdb) file linux
# (gdb) run
# (gdb) c (不断回车直到uml启动完成)
# (gdb) break dcache_readdir
# (gdb) c
进入到uml
#(none):shm&
#(none):ls /dev/shm
好,会在一个奇怪的地方断住, c,然后就到了dcache_readdir.不断的用n,s,finish等命令调试你的uml内核即可. 可以用p命令打印相应变量.
info break 可以看断点,clear 可以清楚断点.好像我就用了这几个命令.bt都没有用过.
调试的结果很令人不快.问题依然.要注意的是内核编译有-O2优化,代码执行起来怪怪的,如果你想加一段程序,gcc看起来没有用就会不
执行到(甚至都没有,优化掉了),解决的方法是:
var=....;
pirntk("",var);
这样可以骗过gcc,保留你的代码.
如果你改动了几个文件,只需要
make ARCH=um即可,编译很快的.
不再详述调试过程了,最后的转机是在2006.8.1号,昨天,突然注意到,dcache_read
dir中filp->f_dentry(/dev/shm->root of tmpfs)的地址并不是shm_mnt->root的地址,
终于明白了,原来不是一棵树,赫赫.剩下的就简单了,只要修改dcache_readdir将
struct dentry *dentry = filp->f_dentry;
换成
struct dentry *dentry = shm_mnt->root;/*注意,需要将shm_mnt声明为非static*/
然后删除:
if (d_unhashed(next) || !next->d_inode)
continue;
就可以在ls /dev/shm的时候看到一堆 dev/zero文件了.
int dcache_readdir(struct file * filp, void * dirent, filldir_t filldir)
{
struct dentry *dentry = filp->f_dentry;
struct dentry *cursor = filp->private_data; /*dcache_dir_open 将cursor 设置为dentry ''.''*/
/* cursor d_child 挂入了filp->f_dentry->d_subdirs */
struct list_head *p, *q = &cursor->d_child;
ino_t ino;
int i = filp->f_pos;
switch (i) {
case 0:
ino = dentry->d_inode->i_ino;
if (filldir(dirent, ".", 1, i, ino, DT_DIR) < 0)
break;
filp->f_pos++;
i++;
/* fallthrough */
case 1:
ino = parent_ino(dentry);
if (filldir(dirent, "..", 2, i, ino, DT_DIR) < 0)
break;
filp->f_pos++;
i++;
/* fallthrough */
default:
spin_lock(&dcache_lock);
if (filp->f_pos == 2) { /* pos=2 意味从头开始 */
list_del(q);/*将cursor从f_dentry->d_subdirs 删除*/
list_add(q, &dentry->d_subdirs);/*又加回来,从而位于subdir之首*/
}
/*从cursor 的下一个subdir 开始*/
for (p=q->next; p != &dentry->d_subdirs; p=p->next) {
struct dentry *next;
next = list_entry(p, struct dentry, d_child);
if (d_unhashed(next) || !next->d_inode)
continue;
spin_unlock(&dcache_lock);
if (filldir(dirent, next->d_name.name, next->d_name.len, filp->f_pos,
next->d_inode->i_ino, dt_type(next->d_inode)) < 0)
return 0;
spin_lock(&dcache_lock);
/* next is still alive */
list_del(q); /*q 是cursor, 后移一个位置 */
list_add(q, p);
p = q; /*从cursor 取下一个subdir*/
filp->f_pos++;
}
spin_unlock(&dcache_lock);
}
return 0;
}
最后, ls 使用的函数是:sys_getdents64.
2006.8.2 by hyl.
http://blog.chinaunix.net/u/17564/showart_199223.html 看看这个
Oops是内核编程中比较容易遇到的问题,为了跟多的了解Oops来便于调试,我对Oops提供的信息进行一个总结,以及如何调试Oops。
一个完整的Oops:
BUG: unable to handle kernel paging request at 00316b01
IP: [] netif_receive_skb+0x335/0x377
*pde = 00000000
Thread overran stack, or stack corrupted
Oops: 0000 [#1] SMP
last sysfs file: /sys/block/hda/size
Modules linked in: mymod ipv6 autofs4 nls_utf8 cifs lockd sunrpc dm_multipath
scsi_dh video output sbs sbshc battery lp sg snd_ens1371 gameport ide_cd_mod
snd_rawmidi cdrom snd_ac97_codec ac97_bus snd_seq_dummy snd_seq_oss
snd_seq_midi_event snd_seq snd_seq_device snd_pcm_oss snd_mixer_oss
parport_pc ac floppy serio_raw snd_pcm button parport rtc_cmos rtc_core
rtc_lib snd_timer snd pcnet32 mii soundcore snd_page_alloc i2c_piix4 i2c_core
pcspkr dm_snapshot dm_zero dm_mirror dm_region_hash dm_log dm_mod ata_piix
libata mptspi mptscsih mptbase scsi_transport_spi sd_mod scsi_mod ext3 jbd
uhci_hcd ohci_hcd ehci_hcd [last unloaded: mymod]
Pid: 0, comm: swapper Not tainted (2.6.30.9 #1) VMware Virtual Platform
EIP: 0060:[] EFLAGS: 00010206 CPU: 0
EIP is at netif_receive_skb+0x335/0x377
EAX: 00316ae1 EBX: deb7d600 ECX: 00316ae1 EDX: e2f357c0
ESI: 00000008 EDI: de9a4800 EBP: c9403f40 ESP: c9403f10
DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
Process swapper (pid: 0, ti=c9403000 task=c0737320 task.ti=c0779000)
Stack:
00316ae1 c07777a0 e2f787c0 00000000 00000001 00000008 00000010 deb7d600
c9403f40 deb7d600 00000000 df5acc58 c9403fb0 e0e61db0 00000000 00000010
de9a4bb8 de9a4b40 de9a4800 00002000 00000001 00000000 1ea2c822 deb7d600
Call Trace:
[] ? pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] ? net_rx_action+0x6a/0xf4
[] ? __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
<0> [] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
Code: 74 14 f0 ff 83 a8 00 00 00 8b 4d d8 89 d8 8b 53 14 57 ff 51 08 58 8b 45
d0 89 45 d8 8b 55 d0 8b 42 20 83 e8 20 89 45 d0 8b 4d d0 <8b> 41 20 0f 18 00
90 89 c8 83 c0 20 3b 45 d4 75 a4 83 7d d8 00
EIP: [] netif_receive_skb+0x335/0x377 SS:ESP 0068:c9403f10
CR2: 0000000000316b01
---[ end trace 0330855ac41edfb5 ]---
Kernel panic - not syncing: Fatal exception in interrupt
Pid: 0, comm: swapper Tainted: G D 2.6.30.9 #1
Call Trace:
[] panic+0x3f/0xdf
[] oops_end+0x8c/0x9b
[] no_context+0x10c/0x116
[] __bad_area_nosemaphore+0xe0/0xe8
[] bad_area_nosemaphore+0xd/0x10
[] do_page_fault+0xde/0x1e3
[] ? do_page_fault+0x0/0x1e3
[] error_code+0x6d/0x74
[] ? tcp_v4_rcv+0x55b/0x600
[] ? do_page_fault+0x0/0x1e3
[] ? netif_receive_skb+0x335/0x377
[] pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] net_rx_action+0x6a/0xf4
[] __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
[] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
解析Oops的具体含义:
BUG: unable to handle kernel paging request at 00316b01
IP: [] netif_receive_skb+0x335/0x377
*pde = 00000000
Thread overran stack, or stack corrupted
Oops: 0000 [#1] SMP
last sysfs file: /sys/block/hda/size
Modules linked in: mymod ipv6 autofs4 nls_utf8 cifs lockd sunrpc dm_multipath
scsi_dh video output sbs sbshc battery lp sg snd_ens1371 gameport ide_cd_mod
snd_rawmidi cdrom snd_ac97_codec ac97_bus snd_seq_dummy snd_seq_oss
snd_seq_midi_event snd_seq snd_seq_device snd_pcm_oss snd_mixer_oss
parport_pc ac floppy serio_raw snd_pcm button parport rtc_cmos rtc_core
rtc_lib snd_timer snd pcnet32 mii soundcore snd_page_alloc i2c_piix4 i2c_core
pcspkr dm_snapshot dm_zero dm_mirror dm_region_hash dm_log dm_mod ata_piix
libata mptspi mptscsih mptbase scsi_transport_spi sd_mod scsi_mod ext3 jbd
uhci_hcd ohci_hcd ehci_hcd [last unloaded: mymod]
上面这段这个是载入的模块信息
Pid: 0, comm: swapper Not tainted (2.6.30.9 #1) VMware Virtual Platform
EIP: 0060:[] EFLAGS: 00010206 CPU: 0
EIP is at netif_receive_skb+0x335/0x377
EIP这行指明发生Oops的具体位置,我们可以通过这个来找到出现Oops的源代码的具体行。
具体方法如下:
通过使用objdump -S反汇编netif_receice_skb所在的目标文件,然后找到偏移量为0x355的行,看看这行是有什么代码汇编来的,再结合寄存器的值就能分析这个Oops的原因了。
EAX: 00316ae1 EBX: deb7d600 ECX: 00316ae1 EDX: e2f357c0
ESI: 00000008 EDI: de9a4800 EBP: c9403f40 ESP: c9403f10
DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
Process swapper (pid: 0, ti=c9403000 task=c0737320 task.ti=c0779000)
Stack:
00316ae1 c07777a0 e2f787c0 00000000 00000001 00000008 00000010 deb7d600
c9403f40 deb7d600 00000000 df5acc58 c9403fb0 e0e61db0 00000000 00000010
de9a4bb8 de9a4b40 de9a4800 00002000 00000001 00000000 1ea2c822 deb7d600
上面这段是寄存器和栈的信息。
Call Trace:
[] ? pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] ? net_rx_action+0x6a/0xf4
[] ? __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
<0> [] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
发生Oops的内核栈信息。
Code: 74 14 f0 ff 83 a8 00 00 00 8b 4d d8 89 d8 8b 53 14 57 ff 51 08 58 8b 45
d0 89 45 d8 8b 55 d0 8b 42 20 83 e8 20 89 45 d0 8b 4d d0 <8b> 41 20 0f 18 00
90 89 c8 83 c0 20 3b 45 d4 75 a4 83 7d d8 00
EIP: [] netif_receive_skb+0x335/0x377 SS:ESP 0068:c9403f10
CR2: 0000000000316b01
---[ end trace 0330855ac41edfb5 ]---
Kernel panic - not syncing: Fatal exception in interrupt
Pid: 0, comm: swapper Tainted: G D 2.6.30.9 #1
如果kernel报告Tainted,说明kernel被损坏了,在“Trainted:”后面最多会有10个字符的提示信息来表示具体的信息。每一位上使用一个字母来表示,如下:
1: 'G': 所有的模块都是GPL的License。如果有模块缺少MODULE_LICENSE()或者声明是Proprietary的,则为'P'。
2: 'F': 如果有模块是使用 insmod -f 强制载入的。否则为空。
3: 'S': 如果Oops发生在SMP的CPU上,但这个型号的CPU还没有被认为是SMP安全的。
4: 'R': 如果有模块是使用 rmmod -f 强制卸载的。否则为空。
5: 'M': 有CPU报告了Machine Check Exception,否则为空。
6: 'B': 如果有page-release函数发现一个错误的page或未知的page标志。
7: 'U': 来自用户空间的程序设置的这个标志位。
8: 'D': 内核刚刚死掉,比如Oops或者是bug。
9: 'A': ACPI表被覆盖。
10: 'W': 之前kernel已经产生过警告。
Tainted字符串主要的目的是告诉调试器这个kernel已经不是一个干净的kernel了。如果一个模块在加载了之后又卸载了,Tainted仍然会保持。
Call Trace:
[] panic+0x3f/0xdf
[] oops_end+0x8c/0x9b
[] no_context+0x10c/0x116
[] __bad_area_nosemaphore+0xe0/0xe8
[] bad_area_nosemaphore+0xd/0x10
[] do_page_fault+0xde/0x1e3
[] ? do_page_fault+0x0/0x1e3
[] error_code+0x6d/0x74
[] ? tcp_v4_rcv+0x55b/0x600
[] ? do_page_fault+0x0/0x1e3
[] ? netif_receive_skb+0x335/0x377
[] pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] net_rx_action+0x6a/0xf4
[] __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
[] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
最后发现一篇调试Oops的专题:
paper on debugging kernel oops or hang 虽然是针对2.4的,但还是值得一读。
4.4 调试系统故障
即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就会产生系统故障。在出现这种情况时,获取尽可能多的信息对解决问题是至关重要的。
注
意,“故障”不意味着“panic”。Linux
代码非常健壮(用术语讲即为鲁棒,robust),可以很好地响应大部分错误:故障通常会导致当前进程崩溃,而系统仍会继续运行。如果在进程上下文之外发
生故障,或是系统的重要组成被损害时,系统才有可能
panic。但如果问题出在驱动程序中时,通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时,为进程上下文分配的一
些内存可能会丢失;例如,由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而,由于内核在进程中止时会对已打开的设备调用 close
操作,驱动程序仍可以释放由 open 方法分配的资源。
我们已经说过,当内核行为异常时,会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息。尽管它们对于初学者来说相当晦涩,不过处理器在出错时转储出的这些数据包含了许多值得关注的信息,通常足以查明程序错误,而无需额外的测试。
4.4.1 oops消息
大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。
由
处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13
章中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失
效”的信号。如果地址非法,内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。
值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理。
oops
显示发生错误时处理器的状态,包括 CPU
寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的
printk 语句产生,就象前面“printk”一节所介绍的那样分发出来。
让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[]
EFLAGS: 00010286
eax: ffffffea ebx: c2281a20 ecx: c48370c0 edx: c2281a40
esi: 4000c000 edi: 4000c000 ebp: c38adf8c esp: c38adf8c
ds: 0018 es: 0018 ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \
0000010e
4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \
4000c000
0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \
00000004
Call Trace: [] []
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
这个消息是通过对 faulty 模块的一个设备进行写操作而产生的,faulty 这个模块专为演示出错而编写。faulty.c 中 write 方法的实现很简单:
ssize_t faulty_write (struct file *filp, const char *buf, size_t count,
loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
正如读者所见,我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值,所以错误发生,内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中,faulty 模块还有更多有意思的错误状态。
char faulty_buf[1024];
ssize_t faulty_read (struct file *filp, char *buf, size_t count,
loff_t *pos)
{
int ret, ret2;
char stack_buf[4];
printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count);
/* the next line oopses with 2.0, but not with 2.2 and later */
ret = copy_to_user(buf, faulty_buf, count);
if (!ret) return count; /* we survived */
printk(KERN_DEBUG "didn't fail: retry\n");
/* For 2.2 and 2.4, let's try a buffer overflow */
sprintf(stack_buf, "1234567\n");
if (count > 8) count = 8; /* copy 8 bytes to the user */
ret2 = copy_to_user(buf, stack_buf, count);
if (!ret2) return count;
return ret2;
}
这
段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0
内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return
指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:
EIP: 0010:[]
[...]
Call Trace: []
Code: Bad EIP value.
用
户处理 oops
消息的主要问题在于,我们很难从十六进制数值中看出什么内在的意义;为了使这些数据对程序员更有意义,需要把它们解析为符号。有两个工具可用来为开发人员
完成这样的解析:klogd 和 ksymoops。前者只要运行就会自行进行符号解码;后者则需要用户有目的地调用。下面的讨论,使用了在我们第一个
oops 例子中通过使用NULL 指针而产生的出错信息。
使用 klogd
klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下,klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助。
当 faulty 的一个oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[faulty:faulty_write+3/576]
EFLAGS: 00010286
eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00
esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c
ds: 0018 es: 0018 ss: 0018
Process cat (pid: 23413, stackpage=c2337000)
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
00000001
0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \
0804d038
00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \
00000004
Call Trace: [sys_write+214/256] [system_call+52/56]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
klogd
提供了大多数必要信息用于发现问题。在这个例子中,我们看到指令指针(EIP)正执行于函数 faulty_write
中,因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数的第3个字节上,而函数整体长度为 576
个字节。注意这些数值都是十进制的,而非十六进制。
然而,当错误发生在可
装载模块中时,为了获取错误相关的有用信息,开发者还必须注意一些情况。klogd 在开始运行时装入所有可用符号,并随后使用这些符号。如果在
klogd 已经对自身初始化之后(一般在系统启动时),装载某个模块,那 klogd 将不会有这个模块的符号信息。强制
klogd取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd
进程,这种操作在时间顺序上,必须是在模块已经装入(或重新装载)之后,而在进行任何可能引起 oops 的处理之前。
还可以在运行 klogd 时加上 -p 选项,这会使它在任何发现 oops 消息的时刻重新读入符号信息。不过,klogd 的man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息。而发生错误之后,所获得的信息可能是完全错误的了。
为
了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本。通常这个文件在 /boot
中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot,或告知 klogd
到什么位置查看。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。
使用 ksymoops
有
些时候,klogd
对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址,又要获得对应的符号,而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外,往
往还需要更多的信息。对 klogd 来说,在出错期间被杀掉,也是常用的事情。在这些情况下,可以调用一个更为强大的 oops
分析器,ksymoops 就是这样的一个工具。
在 2.3 开发系列之前,ksymoops 是随内核源码一起发布的,位于 scripts 目录之下。它现在则在自己的FTP 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许还可以从
ftp://ftp.ocs.com.au/pub/ksymoops
站点上获取这个工具的升级版本。
为了取得最佳的工作状态,除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项:
System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。
模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块,以便获得它们的符号信息。如果未提供这个列表,ksymoops 会查看 /proc/modules。
在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。
当
前正运行的内核映像的复本注意,ksymoops 需要的是一个直接的内核映像,而不是象 vmlinuz、zImage 或 bzImage
这样被大多数系统所使用的压缩版本。默认是不使用内核映像,因为大多数人都不会保存这样的一个内核。如果手边就有这样一个符合要求的内核的话,就应该采用
-v 选项告知 ksymoops 它的位置。
已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块,不过在开发中,几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。
虽
然 ksymoops 会访问 /proc 中的文件来取得它所需的信息,但这样获得的结果是不可靠的。在 oops 发生和 ksymoops
运行的时间间隙中,系统几乎一定会重新启动,这样取自 /proc 的信息就可能与故障发生时的实际状态不符合。只要有可能,最好在引起 oops
发生之前,保存 /proc/modules 和 /proc/ksyms 的复本。
我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页,这是一个很好的资料文档。
这
个工具命令行中的最后一个参数是 oops 消息的位置;如果缺少这个参数,ksymoops 会按Unix
的惯例去读取标准输入设备。运气好的话,消息可以从系统日志中重新恢复;在发生很严重的崩溃情况时,我们可能不得不将这些消息从屏幕上抄下来,然后再敲进
去(除非用的是串口控制台,这对内核开发人员来说,是非常棒的工具)。
注意,当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱。如果 klogd 已经运行,而且 oops 发生后系统仍在运行,那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。
如果没有明确地提供全部的上述信息,ksymoops 会发出警告。对于载入模块未作符号定义这类的情况,它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。
ksymoops 的输出类似如下:
>>EIP; c48370c3
Trace; c010b860
Code; c48370c3
00000000 :
Code; c48370c3
5: 00 00 00 00 00
Code; c48370cd
a: 31 c0 xorl %eax,%eax
Code; c48370cf
c: 89 ec movl %ebp,%esp
Code; c48370d1
e: 5d popl %ebp
Code; c48370d2
f: c3 ret
Code; c48370d3
10: 8d b6 00 00 00 leal 0x0(%esi),%esi
Code; c48370d8
15: 00
正
如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd
所做的很相似,不过要更为准确,而且是十六进制形式的。可以注意到,faulty_write 函数的长度被正确地报告为 0x20个字节。这是因为
ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息。
而且在这个例子中,还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确切地判断发生了些什么事情;这里很明显,错误在于一个向 0 地址写入数据 0 的指令。
ksymoops
的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上,而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出
PC 的世界,我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了):
Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
\/ ____ \/
"@'/ .. \`@"
/_| \_ _/ |_\
\_ _ _/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 \
30680005 01000000 01000000 01000000 01000000
请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。
下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息:
>>TPC; 0000000001000128 >O7; 0000000000457fb4
>>I7; 0000000000410114
Trace; 0000000000410114
Trace; 000000007007cba4
Code; 000000000100011c
0000000000000000 :
Code; 000000000100011c
0: 01 00 00 00 nop
Code; 0000000001000120
4: 90 10 20 00 clr %o0 ! 0
Code; 0000000001000124
8: 81 c3 e0 08 retl
Code; 0000000001000128
10: 30 68 00 05 b,a %xcc, 24 \
0000000001000140
Code; 0000000001000130
14: 01 00 00 00 nop
Code; 0000000001000134
18: 01 00 00 00 nop
Code; 0000000001000138
1c: 01 00 00 00 nop
Code; 000000000100013c
20: 01 00 00 00 nop
要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要这些信息,是因为 SPARC64 用户空间的本地结构是32位的)。本例中,使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。
读
者可能会抱怨对调用的跟踪并没带回什么值得注意的信息;然而,SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中:07 和 I7
寄存器保存了最后调用的两个函数的指令指针,这就是它们出现在调用跟踪记录边上的原因。在这个例子中,我们可以看到,故障指令位于一个由
sys_write 调用的函数中。
要注意的是,无论平台/结构是怎样的
一种配合情况,用来显示反汇编代码的格式与 objdump 程序所使用的格式是一样的。objdump
是个很强大的工具;如果想查看发生故障的完整函数,可以调用命令: objdump -d faulty.o(再次重申,对于 SPARC64
平台,需要使用特殊选项:--target elf64-sparc-architecture sparc:v9)。
关于 objdump 和它的命令行选项的更多信息,可以参阅这个命令的手册页帮助。
学
习对 oops
消息进行解码,需要一定的实践经验,并且了解所使用的目标处理器,以及汇编语言的表达习惯等。这样的准备是值得的,因为花费在学习上的时间很快会得到回
报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识,仍有必要花些时间对此进行学习,因为Unix 的语法与 Intel
的语法并不一样。(在 as 命令 infor 页的“i386-specific”一章中,对这种差异进行了很好的描述。)
4.4.2 系统挂起
尽
管内核代码中的大多数错误仅会导致一个oops
消息,但有时它们则会将系统完全挂起。如果系统挂起了,任何消息都无法打印。例如,如果代码进入一个死循环,内核就会停止进行调度,系统不会再响应任何动
作,包括 Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然;要么就是亡羊补牢,在发生挂起后调试代码。
通
过在一些关键点上插入 schedule 调用可以防止死循环。schedule
函数(正如读者猜到的)会调用调度器,并因此允许其他进程“偷取”当然进程的CPU时间。如果该进程因驱动程序的错误而在内核空间陷入死循环,则可以在跟
踪到这种情况之后,借助 schedule 调用杀掉这个进程。
当然,应
该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule
允许其他进程开始运行。假设驱动程序进行了合适的锁定,这种重入通常还并不致于带来问题。不过,一定不要在驱动程序持有spinlock
的任何时候调用 schedule。
如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule 调用时,最好的方法是加入一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)。
有
时系统看起来象挂起了,但其实并没有。例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况。运行专为探明此种情况而设计的程序,通过查看它的输出情
况,可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新,就说明 scheduler
正在工作。如果没有使用图形显示,则可以运行一个程序让键盘LED闪烁,或不时地开关软驱马达,或不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免;
可改为寻求 ioctl 命令 KDMKTONE ),来检查 scheduler 是否工作正常。O’Reilly
FTP站点上可以找到一个例子(misc-progs/heartbeat.c),它会使键盘LED不断闪烁。
如
果键盘不接收输入,最佳的处理方法是从网络登录到系统中,杀掉任何违例的进程,或是重新设置键盘(用 kdb_mode
-a)。然而,如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起是由键盘死锁造成的也没有用了。如果是这样的情况,就应该配置一种可替代的输入设
备,以便至少可以正常地重启系统。比起去按所谓的“大红钮”,在你的计算机上,通过替代的输入设备来关机或重启系统要更为容易些,而且它可以免去fsck
对磁盘的长时间扫描。
例如,这种替代输入设备可以是鼠标。1.10或更新
版本的 gpm
鼠标服务器可以通过命令行选项支持类似的功能,不过仅限于文本模式。如果没有网络连接,并且以图形方式运行,则建议采用某些自定义的解决方案,比如,设置
一个与串口线 DCD 针脚相连的开关,并编写一个查询 DCD 信号状态变化的脚本,用于从外界干预键盘已被死锁的系统。
对
于上述情形,一个不可缺少的工具是“magic SysRq key”,2.2 和后期版本内核中,在其它体系结构上也可利用得到它。SysRq
魔法键是通过PC键盘上的 ALT 和 SysRq 组合键来激活的,在 SPARC 键盘上则是 ALT 和 Stop
组合键。连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种,这些动作如下:
r
在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式。
k
激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程,留下一个干净的终端。
s
对所有磁盘进行紧急同步。
u
尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着 s 动作之后被调用,它可以在系统处于严重故障状态时节省很多检查文件系统的时间。
b
立即重启系统。注意先要同步并重新挂装磁盘。
p
打印当前的寄存器信息。
t
打印当前的任务列表。
m
打印内存信息。
还
有其它的一些 SysRq 功能;要获得完整列表,可参阅内核源码 Documentation 目录下的sysrq.txt 文件。注意,SysRq
功能必须明确地在内核配置中被开启,出于安全原因,大多数发行系统并未开启它。不过,对于一个用于驱动程序开发的系统来说,为开启 SysRq
功能而带来的重新编译新内核的麻烦是值得的。SysRq 必须在运行时通过下面的命令启动:
echo 1 > /proc/sys/kernel/sysrq
在
复现系统的挂起故障时,另一个要采取的预防措施是,把所有的磁盘都以只读的方式挂装在系统上(或干脆就卸装它们)。如果磁盘是只读的或者并未挂装,就不会
发生破坏文件系统或致使文件系统处于不一致状态的危险。另一个可行方法是,使用通过 NFS
(网络文件系统)将其所有文件系统挂装入系统的计算机。这个方法要求内核具有“NFS-Root”的能力,而且在引导时还需传入一些特定参数。如果采用这
种方法,即使我们不借助于 SysRq,也能避免任何文件系统的崩溃,因为NFS 服务器管理文件系统的一致性,而它并不受驱动程序的影响。
http://blog.chinaunix.net/u/17564/showart_199223.html
例如这样的一个Oops:
Oops: 0000 [#1] PREEMPT SMP
Modules linked in: capidrv kernelcapi isdn slhc ipv6 loop dm_multipath snd_ens1371 gameport snd_rawmidi snd_ac97_codec ac97_bus snd_seq_dummy snd_seq_oss snd_seq_midi_event snd_seq snd_seq_device snd_pcm_oss snd_mixer_oss snd_pcm snd_timer snd parport_pc floppy parport pcnet32 soundcore mii pcspkr snd_page_alloc ac i2c_piix4 i2c_core button power_supply sr_mod sg cdrom ata_piix libata dm_snapshot dm_zero dm_mirror dm_mod BusLogic sd_mod scsi_mod ext3 jbd mbcache uhci_hcd ohci_hcd ehci_hcd
Pid: 1726, comm: kstopmachine Not tainted (2.6.24-rc3-module #2)
EIP: 0060:[] EFLAGS: 00010092 CPU: 0
EIP is at list_del+0xa/0x61
EAX: e0c3cc04 EBX: 00000020 ECX: 0000000e EDX: dec62000
ESI: df6e8f08 EDI: 000006bf EBP: dec62fb4 ESP: dec62fa4
DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
Process kstopmachine (pid: 1726, ti=dec62000 task=df8d2d40 task.ti=dec62000)
Stack: 000006bf dec62fb4 c04276c7 00000020 dec62fbc c044ab4c dec62fd0 c045336c
df6e8f08 c04532b4 00000000 dec62fe0 c043deb0 c043de75 00000000 00000000
c0405cdf df6e8eb4 00000000 00000000 00000000 00000000 00000000
Call Trace:
[] show_trace_log_lvl+0x1a/0x2f
[] show_stack_log_lvl+0x9b/0xa3
[] show_registers+0xa3/0x1df
[] die+0x11f/0x200
[] do_page_fault+0x533/0x61a
[] error_code+0x72/0x78
[] __unlink_module+0xb/0xf
[] do_stop+0xb8/0x108
[] kthread+0x3b/0x63
[] kernel_thread_helper+0x7/0x10
=======================
Code: 6b c0 e8 2e 7e f6 ff e8 d1 16 f2 ff b8 01 00 00 00 e8 aa 1c f4 ff 89 d8 83 c4 10 5b 5d c3 90 90 90 55 89 e5 53 83 ec 0c 8b 48 04 <8b> 11 39 c2 74 18 89 54 24 08 89 44 24 04 c7 04 24 be 32 6b c0
EIP: [] list_del+0xa/0x61 SS:ESP 0068:dec62fa4
note: kstopmachine[1726] exited with preempt_count 1
1, 有自己编译的vmlinux: 使用gdb
编译时打开complie with debug info选项。
注意这行:
EIP is at list_del+0xa/0x61
这告诉我们,list_del函数有0x61这么大,而Oops发生在0xa处。 那么我们先看一下list_del从哪里开始:
# grep list_del /boot/System.map-2.6.24-rc3-module
c10e5234 T plist_del
c10e53cc T list_del
c120feb6 T klist_del
c12d6d34 r __ksymtab_list_del
c12dadfc r __ksymtab_klist_del
c12e1abd r __kstrtab_list_del
c12e9d03 r __kstrtab_klist_del
于是我们知道,发生Oops时的EIP值是:
c10e53cc + 0xa == c10e53d6
然后用gdb查看:
# gdb /home/arc/build/linux-2.6/vmlinux
(gdb) b *0xc10e53d6
Breakpoint 1 at 0xc10e53d6: file /usr/src/linux-2.6.24-rc3/lib/list_debug.c, line 64.
看,gdb直接就告诉你在哪个文件、哪一行了。
gdb中还可以这样:
# gdb Sources/linux-2.6.24/vmlinux
(gdb) l *do_fork+0x1f
0xc102b7ac is in do_fork (kernel/fork.c:1385).
1380
1381 static int fork_traceflag(unsigned clone_flags)
1382 {
1383 if (clone_flags & CLONE_UNTRACED)
1384 return 0;
1385 else if (clone_flags & CLONE_VFORK) {
1386 if (current->ptrace & PT_TRACE_VFORK)
1387 return PTRACE_EVENT_VFORK;
1388 } else if ((clone_flags & CSIGNAL) != SIGCHLD) {
1389 if (current->ptrace & PT_TRACE_CLONE)
(gdb)
也可以直接知道line number。
或者:
(gdb) l *(0xffffffff8023eaf0 + 0xff) /* 出错函数的地址加上偏移 */
2, 没有自己编译的vmlinux: TIPS
如果在lkml或bugzilla上看到一个Oops,而自己不能重现,那就只能反汇编以"Code:"开始的行。 这样可以尝试定位到
源代码中。
注意,Oops中的Code:行,会把导致Oops的第一条指令,也就是EIP的值的第一个字节, 用尖括号<>括起来。 但是,有些
体系结构(例如常见的x86)指令是不等长的(不一样的指令可能有不一样的长度),所以要不断的尝试(trial-and-error)。
Linus通常使用一个小程序,类似这样:
const char array[] = "\xnn\xnn\xnn...";
int main(int argc, char *argv[])
{
printf("%p\n", array);
*(int *)0 = 0;
}
e.g. /*{{{*/ /* 注意, array一共有从array[0]到array[64]这65个元素, 其中出错的那个操作码<8b> == arry[43] */
#include
#include
const char array[] ="\x6b\xc0\xe8\x2e\x7e\xf6\xff\xe8\xd1\x16\xf2\xff\xb8\x01\x00\x00\x00\xe8\xaa\x1c\xf4\xff\x89\xd8\x83\xc4\x10\x5b\x5d\xc3\x90\x90\x90\x55\x89\xe5\x53\x83\xec\x0c\x8b\x48\x04\x8b\x11\x39\xc2\x74\x18\x89\x54\x24\x08\x89\x44\x24\x04\xc7\x04\x24\xbe\x32\x6b\xc0";
int main(int argc, char *argv[])
{
printf("%p\n", array);
*(int *)0 = 0;
}
/*}}}*/
用gcc -g编译,在gdb里运行它:
[arc@dhcp-cbjs05-218-251 ~]$ gdb hello
GNU gdb Fedora (6.8-1.fc9)
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <[url]http://gnu.org/licenses/gpl.html[/url]>
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-redhat-linux-gnu"...
(no debugging symbols found)
(gdb) r
Starting program: /home/arc/hello
0x80484e0
Program received signal SIGSEGV, Segmentation fault.
注意,这时候就可以反汇编0x80484e0这个地址:
(gdb) disassemble 0x80484e0
Dump of assembler code for function array:
0x080484e0
: imul $0xffffffe8,%eax,%eax
0x080484e3
: jle,pn 0x80484dc <__dso_handle+20>
0x080484ec
: mov $0x1,%eax
0x080484f1
: call 0x7f8a1a0
0x080484f6
: mov %ebx,%eax
0x080484f8
: add $0x10,%esp
0x08048502
: mov %esp,%ebp
0x08048505
: sub $0xc,%esp
0x08048508
: mov 0x4(%eax),%ecx
0x0804850b
: mov (%ecx),%edx
0x0804850d
: cmp %eax,%edx
0x0804850f
: je 0x8048529
0x08048511
: mov %edx,0x8(%esp)
0x08048515
: mov %eax,0x4(%esp)
0x08048519
: movl $0xc06b32be,(%esp)
0x08048520
: add %ah,0xa70
End of assembler dump.
(gdb)
OK, 现在你知道出错的那条指令是array[43],也就是mov (%ecx),%edx,也就是说,(%ecx)指向了一个错误内存地址。
补充:
为了使汇编代码和C代码更好的对应起来, Linux内核的Kbuild子系统提供了这样一个功能: 任何一个C文件都可以单独编译成汇编文件,例如:
make path/to/the/sourcefile.s
例如我想把kernel/sched.c编译成汇编,那么:
make kernel/sched.s V=1
或者:
make kernel/sched.lst V=1
编译出*.s文件
有时侯需要对*.s文件进行分析,以确定BUG所在的位置。 对任何一个内核build目录下的*.c文件,都可以
直接编译出*.s文件。
# make drivers/net/e100.s V=1
而对于自己写的module,就需要在Makefile中有一个灵活的target写法:
# cat Makefile
obj-m := usb-skel.o
KDIR := /lib/modules/`uname -r`/build
traget := modules
default:
make -C $(KDIR) M=$(shell pwd) $(target)
clean:
rm -f *.o *.ko .*.cmd *.symvers *.mod.c
rm -rf .tmp_versions
# make target=usb-skel.s V=1
这样,kbuild系统才知道你要make的目标不是modules,而是usb-skel.s。
另外, 内核源代码目录的./scripts/decodecode文件是用来解码Oops的:
./scripts/decodecode < Oops.txt
第二篇:定位可动态加载的内核模块的OOPS代码行 作者:Godbach (To be 千里马!)
最近又仔细学习了albcamus版主提供的《定位Oops的具体代码行》(链接:http://linux.chinaunix.net/bbs/viewthread.php?tid=1008573),并且进行了实践。因此这里简单总结一下,并且以实例的方式给出定位可动态加载模块Oops信息的方法。
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
Godbach
Apr 19, 2009
1. 从vmlinux获取具体的代码行
文章中albcamus版主也提到了,需要有自己编译的vmlinux,而且编译时打开compile with debug info. 这个选项打开之后会使vmlinux文件比不加调试信息大一些。我这里代调试信息的是49M。建议如果学习的时候,想使用gdb的方式获取出错代码行的话,就加上这个编译条件。
然后就可以按照具体的方法去操作,可以定位到具体的C 代码行。
2. 从自己编译的内核模块出错信息中获取代码行
以ldd3中提供的misc-modules/faulty.c为例。主要以faulty_write函数作分析。
(1)由于作者提供的函数代码就一样,过于简单,我这里简单加上一些代码(也就是判断和赋值),如下:
ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count,
loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
if(count > 0x100)
count = 0x100;
*(int *)0 = 0;
return count;
}
(2)编译该模块,并且mknod /dev/faulty
(3)向该模块写入数据:echo 1 > /dev/faulty, 内核OOPS,信息如下:
<1>BUG: unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
f8e8000e
*pde = 00000000
Oops: 0002 [#3]
SMP
Modules linked in: faulty autofs4 hidp rfcomm l2cap ...... //此处省略若干字符
CPU: 1
EIP: 0060:[] Not tainted VLI
EFLAGS: 00010283 (2.6.18.3 #2)
EIP is at faulty_write+0xe/0x19 [faulty]
eax: 00000001 ebx: f4f6ca40 ecx: 00000001 edx: b7c2d000
esi: f8e80000 edi: b7c2d000 ebp: 00000001 esp: f4dc5f84
ds: 007b es: 007b ss: 0068
Process bash (pid: 6084, ti=f4dc5000 task=f7c8d4d0 task.ti=f4dc5000)
Stack: c1065914 f4dc5fa4 f4f6ca40 fffffff7 b7c2d000 f4dc5000 c1065f06 f4dc5fa4
00000000 00000000 00000000 00000001 00000001 c1003d0b 00000001 b7c2d000
00000001 00000001 b7c2d000 bfd40aa8 ffffffda 0000007b c100007b 00000004
Call Trace:
[] vfs_write+0xa1/0x143
[] sys_write+0x3c/0x63
[] syscall_call+0x7/0xb
Code: Bad EIP value.
EIP: [] faulty_write+0xe/0x19 [faulty] SS:ESP 0068:f4dc5f84
其中,我们应该关注的信息是第一行红色标出部分:告诉我们操作了NULL指针。其次,就是第二行红色部分:EIP is at faulty_write+0xe/0x19。这个出错信息告诉我们EIP指针出现问题的地方时faulty_write函数,而且指出了是faulty模块。
同时,faulty_write+0xe/0x19的后半部分0xe/0x19,说明该函数的大小时0x019,出错位置是在0x0e。这两个值应该值得都是汇编代码的值。
(4)将faulty模块反汇编出汇编代码:
objdump -d faulty.ko > faulty.s
或
objdump -d faulty.o > faulty.s
然后,我们打开faulty.s文件。由于我们需要关注的部分正好在文件的前面,因此我这里只贴出文件的前面一部分内容:
faulty.o: file format elf32-i386
Disassembly of section .text:
00000000 :
0: 81 f9 00 01 00 00 cmp $0x100,%ecx
6: b8 00 01 00 00 mov $0x100,%eax
b: 0f 46 c1 cmovbe %ecx,%eax
e: c7 05 00 00 00 00 00 movl $0x0,0x0
15: 00 00 00
18: c3 ret
00000019 :
19: a1 00 00 00 00 mov 0x0,%eax
1e: ba 00 00 00 00 mov $0x0,%edx
23: e9 fc ff ff ff jmp 24
00000028 :
28: a1 00 00 00 00 mov 0x0,%eax
2d: b9 00 00 00 00 mov $0x0,%ecx
32: ba 00 00 00 00 mov $0x0,%edx
37: e8 fc ff ff ff call 38
3c: 85 c0 test %eax,%eax
3e: 78 13 js 53
40: 83 3d 00 00 00 00 00 cmpl $0x0,0x0
47: 74 03 je 4c
49: 31 c0 xor %eax,%eax
4b: c3 ret
4c: a3 00 00 00 00 mov %eax,0x0
51: 31 c0 xor %eax,%eax
53: c3 ret
由以上汇编代码可以看出,faulty_write函数的大小确实是0x18 -0x00 +1 = 0x19. 那么EIP指针出问题的地方是0x0e处,代码为:
e: c7 05 00 00 00 00 00 movl $0x0,0x0
这行汇编代码就是将0值保存到0地址的位置。那么很显然是非法的。这一行对应的C 代码应该就是:
*(int *)0 = 0;
(5)以上是对模块出错信息的分析。不过也有一定的局限。
首先就是EIP出错的位置正好在本模块内部,这样可以在本模块定位问题;
其次,要求一定的汇编基础,特别是当一个函数的代码比较多时,对应的汇编代码也比较大,如何准确定位到C代码行需要一定的经验和时间。
实际运用中,可以将内核代码行的定位和可动态加载的内核模块代码行的定位结合起来使用,应该可以较快的定位问题。
分析中有纰漏或者不妥的地方希望大家指出,也希望有网友分享更有效的方法。
Linux的Kernel在产生Oops后会默认情况下把Oops的相关信息打印在控制台上,只有通过控制台才能看到Oops的信息,而且因为受到控制台行数限制,不能完整的看到Oops的信息,这样对调试Oops很麻烦,一种方法使用虚拟机,把串口输出指定到文件,然后再的Linux的控制台消息重定向到串口,这样可以很方便的捕获串口输出,方便调试Oops。
第一步,在VMware中设置串口输出:
Settings -> Hardware -> Add... 添加一个新的串口设备,指定使用文件输出。
第二步,在Linux中对串口进行重定向。修改 /etc/grub.conf 的kernel 行,在行尾加入如下参数:
console=ttyS0,115200 console=tty0
重启,然后测试一下产生一个Oops,看看串口文件,如下,已经有完整的Oops的信息了:
sd 0:0:0:0: [sda] Assuming drive cache: write through
sd 0:0:0:0: [sda] Assuming drive cache: write through
BUG: unable to handle kernel paging request at 00316b01
IP: [] netif_receive_skb+0x335/0x377
*pde = 00000000
Thread overran stack, or stack corrupted
Oops: 0000 [#1] SMP
last sysfs file: /sys/block/hda/size
Modules linked in: mymod ipv6 autofs4 nls_utf8 cifs lockd sunrpc dm_multipath scsi_dh video output sbs sbshc battery lp sg snd_ens1371 gameport ide_cd_mod snd_rawmidi cdrom snd_ac97_codec ac97_bus snd_seq_dummy snd_seq_oss snd_seq_midi_event snd_seq snd_seq_device snd_pcm_oss snd_mixer_oss parport_pc ac floppy serio_raw snd_pcm button parport rtc_cmos rtc_core rtc_lib snd_timer snd pcnet32 mii soundcore snd_page_alloc i2c_piix4 i2c_core pcspkr dm_snapshot dm_zero dm_mirror dm_region_hash dm_log dm_mod ata_piix libata mptspi mptscsih mptbase scsi_transport_spi sd_mod scsi_mod ext3 jbd uhci_hcd ohci_hcd ehci_hcd [last unloaded: mymod]
Pid: 0, comm: swapper Not tainted (2.6.30.9 #1) VMware Virtual Platform
EIP: 0060:[] EFLAGS: 00010206 CPU: 0
EIP is at netif_receive_skb+0x335/0x377
EAX: 00316ae1 EBX: deb7d600 ECX: 00316ae1 EDX: e2f357c0
ESI: 00000008 EDI: de9a4800 EBP: c9403f40 ESP: c9403f10
DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
Process swapper (pid: 0, ti=c9403000 task=c0737320 task.ti=c0779000)
Stack:
00316ae1 c07777a0 e2f787c0 00000000 00000001 00000008 00000010 deb7d600
c9403f40 deb7d600 00000000 df5acc58 c9403fb0 e0e61db0 00000000 00000010
de9a4bb8 de9a4b40 de9a4800 00002000 00000001 00000000 1ea2c822 deb7d600
Call Trace:
[] ? pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] ? net_rx_action+0x6a/0xf4
[] ? __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
<0> [] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
Code: 74 14 f0 ff 83 a8 00 00 00 8b 4d d8 89 d8 8b 53 14 57 ff 51 08 58 8b 45 d0 89 45 d8 8b 55 d0 8b 42 20 83 e8 20 89 45 d0 8b 4d d0 <8b> 41 20 0f 18 00 90 89 c8 83 c0 20 3b 45 d4 75 a4 83 7d d8 00
EIP: [] netif_receive_skb+0x335/0x377 SS:ESP 0068:c9403f10
CR2: 0000000000316b01
---[ end trace 0330855ac41edfb5 ]---
Kernel panic - not syncing: Fatal exception in interrupt
Pid: 0, comm: swapper Tainted: G D 2.6.30.9 #1
Call Trace:
[] panic+0x3f/0xdf
[] oops_end+0x8c/0x9b
[] no_context+0x10c/0x116
[] __bad_area_nosemaphore+0xe0/0xe8
[] bad_area_nosemaphore+0xd/0x10
[] do_page_fault+0xde/0x1e3
[] ? do_page_fault+0x0/0x1e3
[] error_code+0x6d/0x74
[] ? tcp_v4_rcv+0x55b/0x600
[] ? do_page_fault+0x0/0x1e3
[] ? netif_receive_skb+0x335/0x377
[] pcnet32_poll+0x347/0x66a [pcnet32]
[] ? run_rebalance_domains+0x13d/0x3ed
[] net_rx_action+0x6a/0xf4
[] __do_softirq+0x94/0x138
[] ? __do_softirq+0x0/0x138
[] ? irq_exit+0x29/0x2b
[] ? do_IRQ+0x6d/0x83
[] ? common_interrupt+0x29/0x30
[] ? default_idle+0x5b/0x92
[] ? cpu_idle+0x3a/0x4e
[] ? rest_init+0x53/0x55
[] ? start_kernel+0x293/0x298
[] ? i386_start_kernel+0x6a/0x6f
paper on debugging kernel oops or hang
--------------------------------------------------------------------------------
Subject: paper on debugging kernel oops or hang
From: "HABBINGA,ERIK (HP-Loveland,ex1)"
Date: Mon, 25 Aug 2003 14:30:10 -0700
List-archive:
List-help:
List-owner:
List-post:
List-software: Listar version 1.0.0
List-subscribe:
List-unsubscribe:
--------------------------------------------------------------------------------
Hello all,
I recently wrote this paper for some co-workers of mine, and thought it
might be of interest to the kernelnewbies crowd. I didn't see anything like
it on the kernelnewbies.org home page. Forgive me if this information is
common knowledge. The paper talks about using ksymoops and objdump to debug
kernel oops and hang problems. I'm not planning on updating the document
again, but feel free to use it as you see fit (i.e. I don't want to maintain
this document, but someone else sure can). This paper deals with the 2.4.x
series of kernels. Debugging might be different on 2.6.x, I haven't spent
much time with that series of kernels.
Enjoy,
Erik Habbinga
Debugging a kernel oops or hang
Erik Habbinga
August 20, 2003
Step 1: capture the kernel state
Kernel oops
-----------
If the kernel has oopsed or BUG'd, there will be crash information left on
the
console. If you're debugging over a serial port, cut and paste the oops data
into a file. If the system was stable enough during the crash, the oops
data
will be in /var/log/messages upon reboot. You can even use a digital
camera to take a picture of the console and transcribe the oops data later.
In any case, you should end up with data that looks like this:
Unable to handle kernel NULL pointer dereference at virtual address 00000004
80289912
*pde = 591be001
Oops: 0002
deadman multipath md bonding1 bonding cpqci cpqhealth cpqrom e100 lpfcdd
CPU: 1
EIP: 0010:[<80289912>] Not tainted
EFLAGS: 00010202
eax: 00000000 ebx: f8f37138 ecx: f8b36d98 edx: 00000000
esi: 00619380 edi: f504ec00 ebp: 00000900 esp: d91bdcf8
ds: 0018 es: 0018 ss: 0018
Process spew (pid: 1230, stackpage=d91bd000)
Stack: f504ec00 f504ed70 f587f370 00bb8000 00000080 00000001 00000050
00000000
80286341 d91bdd56 d91bdd58 00610180 f504ec00 00003a01 daa9c320
00609250
00bb8000 00000020 00000200 f587f200 f5a2e000 00610180 00000600
09001000
Call Trace: [<80286341>] [<80286427>] [<80219fdc>] [<8021a051>]
[<8013b78c>]
[<8013b94b>] [<80139b70>] [<8012fd22>] [<80130020>] [<8013008c>]
[<80130b
1292ff>] [<8012919c>] [<801d8425>] [<801d34d9>] [<80137717>]
[<80106f27>]
Code: 89 42 04 89 10 c7 01 00 00 00 00 c7 41 04 00 00 00 00 8b 03
This data contains a register and stack dump for the process that caused the
oops, along with a explanation of what caused the oops. In this case, the
spew process caused a kernel NULL pointer dereference at virtual address
0x00000004. All addresses below PAGE_SIZE are trapped by the CPU's MMU as
probable NULL pointer dereferences, and this is what happened here.
Sometimes, if the oops makes it into /var/log/messages, the syslogd daemon
might disassemble the oops automatically. This is usually handy, unless you
don't have the correct System.map file on the system. The wrong System.map
file will give wrong stack trace results, requiring you to generate the
system
addresses by hand into something resembling above by referring to that
incorrect System.map file. Hopefully this won't happen to you.
Here's a quick description of a kernel BUG(). Sprinkled throughout the
kernel
code are calls to the BUG() macro. This code is only executed at times
where
the kernel knows something is horribly wrong and the best course of action
is
to stop and report status. On i386, the BUG() macro decodes to the "ud2a"
instruction (0x0f 0x0b). This is an invalid instruction on i386, and the
CPU
will throw an "invalid opcode" exception upon execution. The kernel traps
the
invalid opcode exception, prints "kernel BUG at __FILE__:__LINE!", and then
dumps the CPU state just like any other fault/trap/exception handler that
halts the machine. So, if you see code like the following on i386, you know
you're looking at the BUG() macro:
Code; 00000000 Before first symbol
0: 0f 0b ud2a
kernel hang
-----------
If the kernel has hung, you'll have to use the magic sysrq functionality to
get the stack trace for all currently running processes. Compile the kernel
with the CONFIG_MAGIC_SYSRQ option enabled, found in the "kernel hacking"
section of your favorite "make *config" display. From the console, doing
the
key sequence "Alt" -> "PrintScreen/SysRq" -> "t" will start the stack trace
procedure. Hold down the keys as you type them on the console's keyboard.
If
you're running from a serial port, enable the "serial console" option under
"character devices" in "make *config" and put the appropriate options on the
kernel boot parameter line for your boot loader. For example, the
following would be added to the "append" line in lilo.conf:
console=ttyS0,9600 console=tty0
This tells the kernel to send all console messages to serial port 0 at 9600
baud as well as the VGA console.
In the latest 2.4.x kernels, a file shows up in /proc when
CONFIG_MAGIC_SYSRQ
is configured. Doing:
echo t > /proc/sysrq-trigger
will start the stack trace process. Of course, if the system is hung to the
point you can't type, this won't be of help. The serial port + console will
be your best option for getting the stack trace information.
The syslogd daemon will send the sysrq information to the /var/log/messages
file, disassembling it with the currently installed System.map file. Once
again, if the wrong System.map file is on the system, you're in for some
serious pain. The console might also show disassembled stack traces.
Here's the start of output from a raw SysRq-T stack trace:
SysRq : Show State
^M
^M free sibling
^M task PC stack pid father child younger older
^Minit R 82831F2C 3780 1 0 1213 (NOTLB)
^MCall Trace: [<801138fa>] [<80113820>] [<801470ee>] [<80147490>]
[<80106f27
>]
^Mkeventd R 00010000 6068 2 1 3 (L-TLB)
^MCall Trace: [<801235d5>] [<80105694>]
^Mksoftirqd_CPU R F7BFE000 5184 3 1 4 2 (L-TLB)
^MCall Trace: [<8011b62f>] [<8011bb3c>] [<80105694>]
^Mksoftirqd_CPU S F7BFC000 5924 4 1 5 3 (L-TLB)
^MCall Trace: [<8011b62f>] [<8011bb3c>] [<80105694>]
^Mksoftirqd_CPU R F7BFA000 6000 5 1 6 4 (L-TLB)
^MCall Trace: [<8011b62f>] [<8011bb3c>] [<80105694>]
^Mksoftirqd_CPU S F7BF8000 5404 6 1 7 5 (L-TLB)
^MCall Trace: [<8011b62f>] [<8011bb3c>] [<80105694>]
^Mkswapd S F7BD8000 6356 7 1 8 6 (L-TLB)
^MCall Trace: [<80119cae>] [<8012ffa6>] [<80105694>]
^Mbdflush R 00000286 6308 8 1 9 7 (L-TLB)
^MCall Trace: [<8011429e>] [<8013bac3>] [<80105694>]
< lots of lines deleted >
^Msmbd R F435BF2C 5540 1513 612 1511 (NOTLB)
^MCall Trace: [<80146e7c>] [<801138fa>] [<80113820>] [<801470ee>]
[<80147490
>] [<80106f27>]
The call trace for every program running on the system is listed. Note the
embedded "^M"'s. This trace was captured off of a windows serial port
terminal program. I've written a perl script called "mapread" (see the end
of this
document) that will take a file containing the SysRq-T listing, strip out
the
"^M"'s, and translate the addresses using the System.map file located in the
current directory. See the next section for details on mapread.
If you look at /var/log/messages after a SysRq-T call trace procedure,
you'll
see something like this in /var/log/messages. In this case, syslogd managed
to do the addresss translation:
May 6 22:37:15 catdog4 kernel: SysRq : Show State
May 6 22:37:15 catdog4 kernel:
May 6 22:37:15 catdog4 kernel: free
sibling
May 6 22:37:15 catdog4 kernel: task PC stack pid father
chil
d younger older
May 6 22:37:16 catdog4 kernel: init S 82825F2C 3780 1 0
309
4 (NOTLB)
May 6 22:37:16 catdog4 kernel: Call Trace: [schedule_timeout+122/156]
[process
_timeout+0/96] [do_select+458/516] [sys_select+832/1148]
[system_call+51/56]
May 6 22:37:16 catdog4 kernel: keventd S 00010000 4992 2 1
3 (L-TLB)
May 6 22:37:16 catdog4 kernel: Call Trace: [context_thread+277/464]
[arch_kern
el_thread+40/56]
May 6 22:37:16 catdog4 kernel: ksoftirqd_CPU S 82872000 5064 3 1
4 2 (L-TLB)
May 6 22:37:16 catdog4 kernel: Call Trace: [ksoftirqd+144/196]
[arch_kernel_th
read+40/56]
I'd always run /var/log/messages through a script like the following to
strip
out the date, time, server name, and kernel stuff:
#!/usr/bin/perl
@lines=<>;
for (@lines)
{
print substr($_, 32); # highly dependent on the length of your system
name
}
Of course, there are other ways to do this. In any case, if syslogd didn't
do
the address translation, I'd take /var/log/messages, grab the "SysRq: Show
State" lines, run them through the script above, save to a file, and run
them
through mapread (again, see the next section for details on mapread).
Step 2: disassemble the kernel state, translate the addresses
Kernel oops
-----------
For kernel oops data, the ksymoops tool will dissassemble the code into
something understandable. You'll need the System.map generated by the
kernel
compilation process. System.map will be in the top level of the kernel
directory after a build.
To run ksymoops, do the following:
% ksymoops -K -L -O -m System.map < oops.txt > oops.out
where the oops data above is in a file oops.txt and the corresponding
dissassembled oops will be written to the file oops.out. Here's what you'll
get:
ksymoops 2.4.5 on i686 2.4.18-14. Options used
-V (default)
-K (specified)
-L (specified)
-O (specified)
-m System.map-andrew (specified)
Unable to handle kernel NULL pointer dereference at virtual address 00000004
80289912
*pde = 591be001
Oops: 0002
deadman multipath md bonding1 bonding cpqci cpqhealth cpqrom e100 lpfcdd
CPU: 1
EIP: 0010:[<80289912>] Not tainted
Using defaults from ksymoops -t elf32-i386 -a i386
EFLAGS: 00010202
eax: 00000000 ebx: f8f37138 ecx: f8b36d98 edx: 00000000
esi: 00619380 edi: f504ec00 ebp: 00000900 esp: d91bdcf8
ds: 0018 es: 0018 ss: 0018
Process spew (pid: 1230, stackpage=d91bd000)
Stack: f504ec00 f504ed70 f587f370 00bb8000 00000080 00000001 00000050
00000000
80286341 d91bdd56 d91bdd58 00610180 f504ec00 00003a01 daa9c320
00609250
00bb8000 00000020 00000200 f587f200 f5a2e000 00610180 00000600
09001000
Call Trace: [<80286341>] [<80286427>] [<80219fdc>] [<8021a051>]
[<8013b78c>]
[<8013b94b>] [<80139b70>] [<8012fd22>] [<80130020>] [<8013008c>]
[<80130b
1292ff>] [<8012919c>] [<801d8425>] [<801d34d9>] [<80137717>]
[<80106f27>]
Code: 89 42 04 89 10 c7 01 00 00 00 00 c7 41 04 00 00 00 00 8b 03
>>EIP; 80289912 <=====
>>ebx; f8f37138
>>ecx; f8b36d98
>>esi; 00619380 Before first symbol
>>edi; f504ec00
>>ebp; 00000900 Before first symbol
>>esp; d91bdcf8
Trace; 80286341
Trace; 80286427
Trace; 80219fdc
Trace; 8021a051
Trace; 8013b78c
Trace; 8013b94b
Trace; 80139b70
Trace; 8012fd22
Trace; 80130020
Trace; 8013008c
Code; 80289912
00000000 <_EIP>:
Code; 80289912 <=====
0: 89 42 04 mov %eax,0x4(%edx) <=====
Code; 80289915
3: 89 10 mov %edx,(%eax)
Code; 80289917
5: c7 01 00 00 00 00 movl $0x0,(%ecx)
Code; 8028991d
b: c7 41 04 00 00 00 00 movl $0x0,0x4(%ecx)
Code; 80289924
12: 8b 03 mov (%ebx),%eax
ksymoops has translated the addresses for most of the call trace section
into
the corresponding function names. Note that 10 traces are disassembled, but
16
were listed in the call trace section. The translated addresses are given
in
"function_name + hexOffset/totalAddressesInFunction" form. For example:
>>EIP; 80289912 <=====
Trace; 80286341
means that the instruction pointer held address 0x80289912 at the time of
the
oops, which was 0xa6 bytes past the start of the lvm_snapshot_remap_block
function, which is 0xf8 bytes long. Similarly, one of the addresses in the
call trace was 0x3b9 bytes past the lvm_map function which is 0x490 bytes
long.
ksymoops has also disassembled the first few lines of code starting at EIP.
This helps in finding the corresponding code when tracking down the problem.
kernel hang
-----------
Mapread is a tool I wrote that does two things: translate single addresses
or
translate an entire sysrq-T call trace listing. Mapread uses the
System.map file that exists in the current directory. For example:
% mapread 801138fa
801138fa: 80113880 T schedule_timeout (schedule_timeout+0x7a/122d)
The address 801138fa is in the schedule_timeout function, 0x7a or 122 bytes
from the function start.
For sysrq-T listings:
% mapread sysrq.txt > sysrq.out
sysrq.out will contain:
task: init (pid: 1)
801138fa: 80113880 T schedule_timeout (schedule_timeout+0x7a/122d)
80113820: 80113820 t process_timeout (process_timeout+0x0/0d)
801470ee: 80146f24 T do_select (do_select+0x1ca/458d)
80147490: 80147150 T sys_select (sys_select+0x340/832d)
80106f27: 80106ef4 T system_call (system_call+0x33/51d)
task: keventd (pid: 2)
801235d5: 801234c0 t context_thread (context_thread+0x115/277d)
80105694: 8010566c T kernel_thread (kernel_thread+0x28/40d)
task: ksoftirqd_CPU (pid: 3)
8011b62f: 8011b5c0 T do_softirq (do_softirq+0x6f/111d)
8011bb3c: 8011baac t ksoftirqd (ksoftirqd+0x90/144d)
80105694: 8010566c T kernel_thread (kernel_thread+0x28/40d)
task: ksoftirqd_CPU (pid: 4)
8011b62f: 8011b5c0 T do_softirq (do_softirq+0x6f/111d)
8011bb3c: 8011baac t ksoftirqd (ksoftirqd+0x90/144d)
80105694: 8010566c T kernel_thread (kernel_thread+0x28/40d)
task: ksoftirqd_CPU (pid: 5)
8011b62f: 8011b5c0 T do_softirq (do_softirq+0x6f/111d)
8011bb3c: 8011baac t ksoftirqd (ksoftirqd+0x90/144d)
80105694: 8010566c T kernel_thread (kernel_thread+0x28/40d)
< lots of lines deleted >
task: smbd (pid: 1511)
80146e7c: 80146df4 T __pollwait (__pollwait+0x88/136d)
801138fa: 80113880 T schedule_timeout (schedule_timeout+0x7a/122d)
80113820: 80113820 t process_timeout (process_timeout+0x0/0d)
801470ee: 80146f24 T do_select (do_select+0x1ca/458d)
80147490: 80147150 T sys_select (sys_select+0x340/832d)
80106f27: 80106ef4 T system_call (system_call+0x33/51d)
The addresses have been translated into function calls based on the
System.map
file, and the offsets from the start of the function have been given in both
hex and decimal. For example,
801470ee: 80146f24 T do_select (do_select+0x1ca/458d)
means that, in the call trace for smbd (pid: 1511), do_select is in the call
stack, stopping at address 0x801470ee. That address is 0x1ca, or 458 bytes
from the beginning of the do_select function.
NOTE: syslogd, when translating oopses and sysrq-T call traces, gives all
offset
results in decimal, not hex! Look at the first entry for init:
May 6 22:37:15 catdog4 kernel: SysRq : Show State
May 6 22:37:15 catdog4 kernel:
May 6 22:37:15 catdog4 kernel: free
sibling
May 6 22:37:15 catdog4 kernel: task PC stack pid father
chil
d younger older
May 6 22:37:16 catdog4 kernel: init S 82825F2C 3780 1 0
309
4 (NOTLB)
May 6 22:37:16 catdog4 kernel: Call Trace: [schedule_timeout+122/156]
[process
_timeout+0/96] [do_select+458/516] [sys_select+832/1148]
[system_call+51/56]
[schedule_timeout+122/156]
This says that the address 122 bytes past the start of schedule_timeout,
which
is 156 bytes long. When digging through the dissassembled code, you'll want
everything in hex. I don't know how to make syslogd to do stuff in hex, but
it sure would save me a lot of debugging time. ksymoops gives results in
hex, gdb
gives results in decimal, objdump gives results in hex, mapread gives
results
in both decimal and hex. Decimal sucks.
Step 3: walk the call stack, verify function chain
Now that you have a translated call trace for a particular process, you'll
need to verify the call trace. Unfortunately, the call trace isn't always
accurate. Somehow phantom function calls get inserted in the trace, yet
there
is no code path to those functions. To follow a call trace, you'll need a
disassembled kernel. The kernel build process will generate a uncompressed
kernel image called "vmlinux", and it will be in the top level kernel
directory after the compilation process. To disassemble the vmlinux file:
% objdump -s -d vmlinux > vmlinux.disas
It is usually very helpful to re-compile the kernel with symbols and
generate
a new vmlinux file with line number information. Change the following in
the
top level kernel Makefile to add symbols:
chroot_env (1.9-BR_NFSPERF)# diff -Nur Makefile Makefile.symbols
--- Makefile Wed Aug 20 16:16:16 2003
+++ Makefile.symbols Wed Aug 20 16:16:08 2003
@@ -92,7 +92,7 @@
CPPFLAGS := -D__KERNEL__ -I$(HPATH)
CFLAGS := $(CPPFLAGS) -Wall -Wstrict-prototypes -Wno-trigraphs -O2 \
- -fno-strict-aliasing -fno-common
+ -fno-strict-aliasing -fno-common -g
ifndef CONFIG_FRAME_POINTER
CFLAGS += -fomit-frame-pointer
endif
The objdump for disassembling symboled vmlinux files is as follows:
% objdump -s -l --source vmlinux > vmlinux.disas
Note that there will be address offsets between these two kernels due to the
debugging information. For example, in a non-symboled vmlinux file,
kupdate will be at address 8013bd68. In the symboled vmlinux file, it will
be
at address 8013bd58. Here's the chunk of the vmlinux file where kupdate
starts, both without and with symbols:
no symbols:
8013bd68 :
8013bd68: 57 push %edi
8013bd69: 56 push %esi
8013bd6a: 53 push %ebx
8013bd6b: bb 00 e0 ff ff mov $0xffffe000,%ebx
8013bd70: 21 e3 and %esp,%ebx
8013bd72: c7 83 88 00 00 00 01 movl $0x1,0x88(%ebx)
8013bd79: 00 00 00
8013bd7c: c7 83 80 00 00 00 01 movl $0x1,0x80(%ebx)
8013bd83: 00 00 00
8013bd86: 8d bb 3a 03 00 00 lea 0x33a(%ebx),%edi
8013bd8c: be ce 94 2e 80 mov $0x802e94ce,%esi
8013bd91: ac lods %ds:(%esi),%al
symbols:
8013bd58 :
kupdate():
8013bd58: 57 push %edi
8013bd59: 56 push %esi
8013bd5a: 53 push %ebx
/src/kernel/linux/include/asm/current.h:9
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
8013bd5b: bb 00 e0 ff ff mov $0xffffe000,%ebx
8013bd60: 21 e3 and %esp,%ebx
/src/kernel/linux/fs/buffer.c:3112
/*
* This is the kernel update daemon. It was used to live in userspace
* but since it's need to run safely we want it unkillable by mistake.
* You don't need to change your userspace configuration since
* the userspace `update` will do_exit(0) at the first sys_bdflush().
*/
int kupdate(void *startup)
{
struct task_struct * tsk = current;
int interval;
tsk->session = 1;
8013bd62: c7 83 88 00 00 00 01 movl $0x1,0x88(%ebx)
8013bd69: 00 00 00
/src/kernel/linux/fs/buffer.c:3113
tsk->pgrp = 1;
8013bd6c: c7 83 80 00 00 00 01 movl $0x1,0x80(%ebx)
8013bd73: 00 00 00
/src/kernel/linux/include/asm/string.h:34
*/
#define __HAVE_ARCH_STRCPY
static inline char * strcpy(char * dest,const char *src)
{
8013bd76: 8d bb 3a 03 00 00 lea 0x33a(%ebx),%edi
8013bd7c: be 4e 96 2e 80 mov $0x802e964e,%esi
/src/kernel/linux/include/asm/string.h:36
int d0, d1, d2;
__asm__ __volatile__(
8013bd81: ac lods %ds:(%esi),%al
Here's an example of a call trace taken from mapread. This call trace isn't
accurate, and we'll see why:
task: kupdated (pid: 9)
40105be4: 40105b78 T __down
40105d80: 40105d78 T __down_failed
401d6a9f: 401d6a90 t .text.lock.page_buf_locking
401c6599: 401c6570 T xfs_getsb
401ca9f7: 401ca9b8 T xfs_trans_getsb
401c95bd: 401c95a4 t xfs_trans_apply_sb_deltas
401c9c2a: 401c9b08 T xfs_trans_commit
401db19e: 401dac08 T xfs_strategy
401db1cd: 401dac08 T xfs_strategy
401db1ee: 401dac08 T xfs_strategy
401d5ac3: 401d5980 t _page_buf_page_apply
401db445: 401db3a4 T xfs_bmap
401d7078: 401d7038 t map_blocks
401d76da: 401d7550 t delalloc_convert
401d7cc8: 401d7c6c t linvfs_writepage
40138b2d: 40138a90 t write_some_buffers
401d2250: 401d20a0 t xfs_inode_flush
401ccf11: 401ccefc t xfs_sync
401df6b3: 401df6a8 t linvfs_statfs
4013cc9d: 4013cba8 T sync_supers
4013bf07: 4013beec t sync_old_buffers
4013c1cf: 4013c0c8 T kupdate
40105694: 4010566c T kernel_thread
The call trace starts at the bottom and moves upward. From the listing
above,
we expect the following call chain:
kernel_thread
kupdate
sync_old_buffers
sync_supers
linvfs_statfs
xfs_sync
xfs_inode_flush
write_some_buffers
...etc...
Here's what really happens, according to the disassembled vmlinux file.
First, I search for the first address, 40105694. The line immediately
preceding the address will be the call to the next function.
40105692: ff d2 call *%edx
40105694: b8 01 00 00 00 mov $0x1,%eax
Figuring out what happens through function pointers (as in address 40105692)
requires a visit to the source code. Let's assume that we're actually going
into kupdate at this point. Code like XFS that is function pointer crazy
requires more debugging. Trying the next address:
4013c1ca: e8 1d fd ff ff call 4013beec
4013c1cf: 81 3d 00 28 3f 40 00 cmpl $0x403f2800,0x403f2800
So far so good. Next address:
4013bf02: e8 89 cb ff ff call 40138a90
4013bf07: 83 c4 04 add $0x4,%esp
Whoa! We actually don't go to sync_supers as the call trace says, but we go
to
write_some_buffers instead. I've run into this case more often than not, so
it always pays to verify the call trace. I don't know why the call trace is
screwed up in linux.
Once you have a verified call trace, you can start figuring out what
happened
during the crash. Let's take the example oops I've shown above, where the
spew process caused a page fault:
Unable to handle kernel NULL pointer dereference at virtual address 00000004
80289912
*pde = 591be001
Oops: 0002
deadman multipath md bonding1 bonding cpqci cpqhealth cpqrom e100 lpfcdd
CPU: 1
EIP: 0010:[<80289912>] Not tainted
Using defaults from ksymoops -t elf32-i386 -a i386
EFLAGS: 00010202
eax: 00000000 ebx: f8f37138 ecx: f8b36d98 edx: 00000000
esi: 00619380 edi: f504ec00 ebp: 00000900 esp: d91bdcf8
ds: 0018 es: 0018 ss: 0018
Process spew (pid: 1230, stackpage=d91bd000)
Stack: f504ec00 f504ed70 f587f370 00bb8000 00000080 00000001 00000050
00000000
80286341 d91bdd56 d91bdd58 00610180 f504ec00 00003a01 daa9c320
00609250
00bb8000 00000020 00000200 f587f200 f5a2e000 00610180 00000600
09001000
Call Trace: [<80286341>] [<80286427>] [<80219fdc>] [<8021a051>]
[<8013b78c>]
[<8013b94b>] [<80139b70>] [<8012fd22>] [<80130020>] [<8013008c>]
[<80130b
1292ff>] [<8012919c>] [<801d8425>] [<801d34d9>] [<80137717>]
[<80106f27>]
Code: 89 42 04 89 10 c7 01 00 00 00 00 c7 41 04 00 00 00 00 8b 03
>>EIP; 80289912 <=====
>>ebx; f8f37138
>>ecx; f8b36d98
>>esi; 00619380 Before first symbol
>>edi; f504ec00
>>ebp; 00000900 Before first symbol
>>esp; d91bdcf8
Trace; 80286341
Trace; 80286427
Trace; 80219fdc
Trace; 8021a051
Trace; 8013b78c
Trace; 8013b94b
Trace; 80139b70
Trace; 8012fd22
Trace; 80130020
Trace; 8013008c
Code; 80289912
00000000 <_EIP>:
Code; 80289912 <=====
0: 89 42 04 mov %eax,0x4(%edx) <=====
Code; 80289915
3: 89 10 mov %edx,(%eax)
Code; 80289917
5: c7 01 00 00 00 00 movl $0x0,(%ecx)
Code; 8028991d
b: c7 41 04 00 00 00 00 movl $0x0,0x4(%ecx)
Code; 80289924
12: 8b 03 mov (%ebx),%eax
The instruction at address 80289912 generated a page fault. This code is
moving the value in eax into the memory addresss generated by adding 0x4
to the contents of edx. The oops says edx contains 0, and the page fault
happens when we try to access memory address 0x00000004. Some pointer is
NULL
and it shouldn't be. Looking at the dissambled vmlinux file containing
symbols, I searched for the beginning of lvm_snapshot_remap_block, added
0xa6
to it, and found the following code:
/src/kernel/linux/drivers/md/lvm-snap.c:131
80289a7b: 39 71 08 cmp %esi,0x8(%ecx)
80289a7e: 75 f1 jne 80289a71
80289a80: 66 39 69 0c cmp %bp,0xc(%ecx)
80289a84: 75 eb jne 80289a71
/src/kernel/linux/drivers/md/lvm-snap.c:133
80289a86: 83 7c 24 14 00 cmpl $0x0,0x14(%esp,1)
80289a8b: 74 23 je 80289ab0
/src/kernel/linux/include/linux/list.h:81
80289a8d: 8b 41 04 mov 0x4(%ecx),%eax
80289a90: 8b 11 mov (%ecx),%edx
/src/kernel/linux/include/linux/list.h:82
80289a92: 89 42 04 mov %eax,0x4(%edx)
/src/kernel/linux/include/linux/list.h:83
80289a95: 89 10 mov %edx,(%eax) <=== oops occurs
here
/src/kernel/linux/include/linux/list.h:94
80289a97: c7 01 00 00 00 00 movl $0x0,(%ecx)
/src/kernel/linux/include/linux/list.h:95
80289a9d: c7 41 04 00 00 00 00 movl $0x0,0x4(%ecx)
/src/kernel/linux/include/linux/list.h:40
80289aa4: 8b 03 mov (%ebx),%eax
/src/kernel/linux/include/linux/list.h:41
80289aa6: 89 48 04 mov %ecx,0x4(%eax)
Because I'm looking at a vmlinux file compiled with symbols, but a non
symboled kernel was the one that crashed, my addresses will be slightly
different. Thankfully ksymoops decodes some instructions to help us
synchronize the addresses. The oops address 80289912 with code "move
%eax,0x4(%edx)" becomes address 80289a95 in the symboled vmlinux file. So,
something at line 83 in list.h is crashing. Here's the relevant code from
list.h and lvm-snap.c
static inline lv_block_exception_t *lvm_find_exception_table(kdev_t
org_dev,
unsigned long
org_start,
lv_t * lv)
{
117 struct list_head *hash_table = lv->lv_snapshot_hash_table,
*next;
< code removed >
126 for (next = hash_table->next; next != hash_table;
127 next = next->next) {
128 lv_block_exception_t *exception;
129
130 exception = list_entry(next, lv_block_exception_t, hash)
131 if (exception->rsector_org == org_start &&
132 exception->rdev_org == org_dev) {
133 if (i) {
134 /* fun, isn't it? :) */
135 list_del(next); <=== oops here
136 list_add(next, hash_table);
137 }
138 ret = exception;
139 break;
140 }
include/linux/list.h
18 struct list_head {
19 struct list_head *next, *prev;
20 };
80 static inline void __list_del(struct list_head *prev, struct list_head
*next)
81 {
82 next->prev = prev;
83 prev->next = next;
84 }
91 static inline void list_del(struct list_head *entry)
92 {
93 __list_del(entry->prev, entry->next);
94 entry->next = (void *) 0;
95 entry->prev = (void *) 0;
96 }
The last line in lvm-snap.c listed in the vmlinux file is #133. This code
is
comparing some memory address to zero and jumping somewhere if true. That's
the "if (i) {" line. The next code is the "list_del(next)" code. Some code
inspection reveals that next is put into ecx before the list_del call is
made.
list_del(next) translates to:
__list_del(next->prev, next->next)
which becomes:
(next->next)->prev = (next->prev);
(next->prev)->next = (next->next);
Here's the annotated assembly for __list_del:
/src/kernel/linux/include/linux/list.h:81
80289a8d: 8b 41 04 mov 0x4(%ecx),%eax
- move next->prev into eax
80289a90: 8b 11 mov (%ecx),%edx
- move next->next into edx
/src/kernel/linux/include/linux/list.h:82
80289a92: 89 42 04 mov %eax,0x4(%edx)
- move next->prev into next->next->prev
/src/kernel/linux/include/linux/list.h:83
80289a95: 89 10 mov %edx,(%eax) <=== oops occurs
here
- move next->next into next->prev->next (or just next->prev, which is the
same
address as next->prev->next).
Note that eax and edx are equal to 0x00000000 in the oops. This means that
next->prev == 0x00000000 and next->next = 0x00000000. This is an invalid
state for the linux list.h code, which is a doubly linked list where the
prev
and next pointers are always expected to be valid. Only list nodes that
have
been deleted have next and prev pointers of zero, as the last two lines of
list_del() do. Somehow we're trying to delete a list node that looks like
it's already been deleted, and we page fault while doing it.
Appendix A: mapread perl script
#!/usr/bin/perl
open(MAPFILE, "System.map") || die "Couldn't open System.map: $!\n";
@Map = ;
close(MAPFILE);
if (-r $ARGV[0])
{
# we're reading a magic sysrq trace
&mapread_sysrq($ARGV[0]);
} else {
# we're just looking up one address
$addr = $ARGV[0];
$result = &mapread($addr);
print "$result\n";
}
sub mapread
{
local($addr) = shift(@_);
local($lastAddr);
local(@fields);
local($i);
local($offset);
local($start);
local($function);
local($str);
$lastAddr = "not in file";
for ($i = 0; $i < $#Map; $i++)
{
@fields = split(' ', $Map[$i]);
last if ($addr lt $fields[0]);
$lastAddr = $Map[$i];
}
# try to figure out the offset into the function
# lines in mapfile are startAddr, something, functionName
@fields = split(' ', $lastAddr);
$start = $fields[0];
$function = $fields[2];
$offset = hex($addr) - hex($start);
chop($lastAddr);
$str = sprintf("%s: %s (%s+0x%x/%dd)", $addr, $lastAddr, $function,
$offset, $offset);
return $str;
}
sub mapread_sysrq
{
local($sysrq_file) = shift(@_);
local(@sysrq);
local($state);
local($i);
local($j);
local(@fields);
local($task);
local(@traces);
local($pid);
# write sysrq file to another file, just in case there are line
# feed issues
open(SYSRQ_FILE, $sysrq_file) || die "Couldn't open $sysrq_file: $!\n";
@sysrq = ;
close(SYSRQ_FILE);
open(SYSRQ_FILE_TMP, ">" . $sysrq_file . ".tmp") || die "Couldn't open
$sysrq_file.tmp: $!\n";
for ($i = 0; $i <= $#sysrq; $i++)
{
$sysrq[$i] =~ s/^M//; # enter ctrl-V ctrl-M to get a true ^M in the
code
print SYSRQ_FILE_TMP "$sysrq[$i]";
}
close(SYSRQ_FILE_TMP);
open(SYSRQ_FILE, $sysrq_file . ".tmp") || die "Couldn't open
$sysrq_file.tmp: $!\n";
@sysrq = ;
close(SYSRQ_FILE);
system("rm $sysrq_file.tmp");
# state: 0 = start of file
# 1 = getting task name
# 2 = getting call traces
$state = 0;
for ($i = 0; $i <= $#sysrq; $i++)
{
chop($sysrq[$i]);
@fields = split(' ', $sysrq[$i]);
if ($state == 0)
{
# look for line with task
if ($fields[0] eq "task")
{
$state = 1;
}
}
elsif ($state == 1)
{
# first field is task
$task = $fields[0];
$pid = $fields[4];
@traces = ();
$state = 2;
}
elsif ($state == 2)
{
# if first field is "Call", skip to third field
$j = 0;
if (($fields[0] eq "Call") && ($fields[1] eq "Trace:"))
{
$j = 2;
}
# clean up the traces and put them in the array
for (; $j <= $#fields; $j++)
{
$fields[$j] =~ s/\[//;
$fields[$j] =~ s/
$fields[$j] =~ s/>//;
$fields[$j] =~ s/\]//;
push(@traces, &mapread($fields[$j]));
}
# if next line doesn't start with a [<, it's a task
if ($i < $#sysrq)
{
@fields = split(' ', $sysrq[$i+1]);
if (substr($fields[0], 0, 2) ne "[<")
{
# dump our trace list
print "task: $task (pid: $pid)\n";
for ($j = 0; $j <= $#traces; $j++)
{
print "\t$traces[$j]\n";
}
$state = 1;
}
}
}
}
}
--
Kernelnewbies: Help each other learn about the Linux kernel.
Archive: http://mail.nl.linux.org/kernelnewbies/
FAQ: http://kernelnewbies.org/faq/
2010-02-03
--------------------------------------------------------------------------------
jincm
Linux内核调试工具:Kdb应用指南 (1)
发布时间:2002.09.13 10:30 来源:开放系统世界——赛迪网 作者:胡风华
kdb是一个Linux系统的内核调试器,它是由SGI公司开发的遵循GPL许可证的开放源码调试工具。kdb嵌入在Linux内核中,为内核程序员提供调试手段。它适合于调试内核空间的程序代码,譬如进行设备驱动程序调试,内核模块的调试等。目前kdb支持包括x86(IA32)、IA64和MIPS在内的体系结构。
安装kdb
官方发布的Linux内核并不包含kdb。kdb是一个内核源程序的补充。kdb通过修改内核源程序将调试器的源代码嵌入到内核中从而提供方便的调试手段。因此要使用kdb进行调试,需要重新编译内核。编译后的内核中包含kdb的调试器代码。安装kdb的步骤如下。
1.获得kdb源代码
kdb的源代码是由SGI提供的,网上也有许多站点提供kdb源代码包。在下载源代码包之前,需要知道所使用的Linux内核的版本。针对不同的内核版本,kdb有不同的源码包。这里假定我们使用的是2.4.7的内核。在如下地址 http://oss.sgi.com/projects/kdb可以找到关于kdb的简短介绍。SGI提供ftp下载,地址为:ftp: //oss.sgi.com/www/projects/kdb/download/ix86 ,在此目录下,找到相应版本的的kdb源码包。源码包有两种格式,一种是.gz格式压缩,一种是.bz2格式压缩,文件名后缀分别为.gz和.bz2,用户可以根据自己的情况选择相应格式的文件下载。源码包以如下格式进行命名:kdb-vX.X-Y.Y.Y.bz2(.gz),其中X.X为kdb的版本号,而Y.Y.Y为所对应的Linux内核的版本号。根据所使用的内核版本,选择相应的Y.Y.Y后缀的文件下载。注意:kdb仅仅提供对Linux官方发布的内核版本的支持,如果使用发行商修改的内核版本,譬如Redhat 7.2的内核版本为2.4.7-10,这是经过Red Hat修改的内核版本,kdb没有相应的版本,如果选择为2.4.7而写的kdb版本,在对内核重编译时将会失败。因此,如果要使用kdb,必须使用官方发布的内核版本。目前kdb提供从2.2.3到2.4.19之间的所有内核版本的相应源代码包。
2.安装kdb源代码
下载kdb源码包后,将源码包解压缩,拷贝到内核源代码目录下,然后执行如下命令:
#cd /usr/src/Linux-2.4.7
#patch p1 /proc/sys/kernel/kdb。
然后就可以按“pause”键进入调试环境了。按“pause”键后,出现提示符kdb>,同时键盘上Caps和Scroll两指示灯不停闪烁,提示现在处于kdb调试环境中。
kdb提供丰富的命令实现运行控制、内存操纵、寄存器操纵、断点设置、堆栈跟踪等许多功能,总共有33条命令,下面分别进行介绍。
运行控制类
包括go、ss和ssb三个命令,提供对程序执行的控制。具体用法如下:
go:继续程序执行
格式:go
该命令使内核继续执行,直到遇到一个断点才停止。如果没有设置断点,该命令将离开kdb调试器,系统回到正常运行状态。Caps和Scroll指示灯恢复到原来的状态。
ss:单步执行程序
格式:ss
该命令仅仅执行下一条指令,执行完后停止。这在进行跟踪时是必不可少的。
ssb:执行到分支或者函数调用时停止
格式:ssb
该命令与ss的区别是,ss只执行一条语句,而ssb执行一组语句,它使指令继续执行,在遇到一个分支语句,或者遇到一个函数调用语句时停止。
断点类
kdb提供强大的断点功能,包括设置断点、清除断点、激活断点、使断点失效,kdb也可以设置硬件断点。断点指令包括bp、bl、bpa、bph、bpha、bc、be和bd。
bp:设置或者显示断点
格式:bp []
该命令设置一个新的断点,其中vaddr是要设置的断点的地址。如果不带参数,运行bp将显示当前设置的所有断点。
bl:设置或者显示断点
格式:bl []
该命令的操作与bp命令相同。
bpa:设置或者显示全局断点
格式:bpa []
该命令设置一个全局断点,或者显示所有全局断点,用法同上。
bph:设置硬件断点或者显示所有断点
格式:bph [vaddr [datar|dataw|io [length]]]
如果不带参数,则显示所有断点。如果带参数,那么设置断点。其中vaddr为要设置硬件断点的地址,datar表示对该内存区进行读操作,dataw表示写操作,io表示对该内存区进行io输入输出操作。length指明读写io操作的数据长度。
bpha:设置硬件断点或者显示所有断点
格式和用法同bph。
bc:清除断点
格式:bc
清除标号为bpnum的断点。如果断点号为“*”,将清除所有断点。
bd:使断点无效
格式:bd
使标号为bpnum的断点无效,如果标号为“*”,表示使所有断点无效。
be:激活断点
格式:be
激活标号为bpnum的断点。如果标号为“*”,将激活所有无效的断点。
内存操作类
内存操作类命令包括对内存进行显示和修改的md、mdr、mds、mm四条命令。
md:显示内存内容
格式1:md [vaddr [line-count [output-radix]] ]
显示地址为vaddr的内存的内容。line-count为要显示的内存的行数,output-radix指定以8进制、10进制或者16进制显示。如果省略line-count和output-radix,那么将以设置的环境变量MDCOUNT和RADIX方式显示。如果不带任何参数,md命令将接着上次md命令的后续地址显示内存内容。
============================================================================================================
Linux内核调试工具:Kdb应用指南 (3)
发布时间:2002.09.13 10:30 来源:开放系统世界——赛迪网 作者:胡风华
格式2:mdWcn
在缺省情况下,md以当前环境变量BYTESPERWORD的值读取数据,在读取硬件寄存器的时候,需要指定数据的宽度。这是可以使用mdWcn来进行读取,W是读取的宽度,单位是字节,cn为要读取的数目。
mdr:显示原始内存的内容
格式:mdr
从指定地址vaddr开始显示count长度的内存,它打印一连串的内存数据。这个命令是留给外部的调试器使用的,一般很少使用。
mds:以符号的方式显示内存的内容
格式:mds [vaddr [line-count [output-radix]]]
从指定地址vaddr开始显示内存的内容,与md的区别是每行仅显示一个字,并且它试图将该地址与符号表进行匹配,如果找到,那么它将显示相应的符号名以及偏移值。如果不带参数,它将从上次mds的末尾开始显示。
mm:修改内存内容
格式1:mm
将指定地址vaddr开始的数据修改为新的数据。修改的数据的长度为一个机器字。
格式2:mmW
意义同上,区别在于它改变W字节的内容。
堆栈跟踪类
该类指令实现对堆栈的跟踪,包括bt、btp和bta三条命令。
bt:显示调用堆栈
格式:bt []
如果不指定参数,它根据当前寄存器的内容显示堆栈,提供当前活动线程的完整的堆栈跟踪。如果指定stack-frame addr参数,它将从该地址开始跟踪。
btp:显示进程的堆栈
格式:btp
显示由pid指定的进程的堆栈。
bta:显示所有进程的堆栈
格式:bta
寄存器类
寄存器类命令包括对寄存器内容进行显示和修改的rd和rm指令,以及异常帧显示指令ef。
rd:显示寄存器内容
格式:rd [c|d|u]
如果不带任何参数,rd显示所有进入kdb调试器时该点所设置的所有通用寄存器的值。如果带c参数,它将显示控制寄存器cr0、cr1、cr2、cr4寄存器的内容。如果带d参数,它显示调试寄存器的内容。如果带u参数,它显示当进入kdb调试器时当前任务的所有寄存器。
rm:修改寄存器的内容
格式:rm
该命令修改register-name指定的寄存器的内容为register- content。其中register-name为%eax、%ebx、%ecx、%edx、%esi、%edi、%esp、%eip或%ebp。如果参数为%%,由rd u指定的寄存器将被修改。当前rm命令不允许修改控制寄存器,也不允许显示和修改Pentium和Pentium Pro系列的特定寄存器。
ef:显示异常帧
格式: ef
显示vaddr地址处的异常帧。
环境变量类
这类指令对kdb调试器环境变量进行显示和设置。包括set和env命令。
set:设置环境变量
格式:set
将环境变量env-var的值设置为value。最多有33个环境变量,每个环境变量最大512字节。kdb的主要环境变量有:
PROMPT:kdb调试器提示符,缺省为kdb>。
MOREPROMPT:在一屏显示不下的情况下,系统的提示符,缺省为more>。
RADIX:显示数据时所使用的数制,缺省为16进制。
LINES:kdb调试器显示行数。缺省为24行。
COLUMNS:kdb调试器显示的列数。缺省为80列。
MDCOUNT:执行md指令时显示的内存行数,缺省为8行。
BTARGS:执行bt跟踪时,指定任一函数在打印时所使用参数最大个数。
SSCOUNT:该环境变量规定在执行ssb命令时,如果显示超过此数,执行将停止。缺省为20。
IDMODE:反汇编时所使用的指令格式。缺省为x86。
BYTESPERWORD:指定字的长度,缺省为4个字节。
IDCOUNT:反汇编时,一次反汇编的指令长度,缺省为16条指令。
env:显示环境变量
格式:env
显示所有环境变量的值。
============================================================================================================
Linux内核调试工具:Kdb应用指南 (4)
发布时间:2002.09.13 10:30 来源:开放系统世界——赛迪网 作者:胡风华
杂项
id:指令反汇编
格式:id
从vaddr开始的地址反汇编指令。
cpu:切换到另一个CPU
格式:cpu
这条命令仅仅在SMP结构下有用,它切换到由cpunum指定的CPU。
ps:显示所有活动的进程
格式:ps
显示当前的活动的进程。包括pid、父进程pid、CPU号、当前状态,以及对应的线程。
reboot:重新启动机器
格式:reboot
在某些情况下,内核无法返回到正常工作状态,这时可以利用reboot重新启动机器。注意在重启机器前,它不进行任何状态保存的工作。
sections:列出内核中所有已知的段的信息
格式:sections
列出模块和内核的所有已知的段的信息。首先是模块信息,最后是内核信息。包括模块名和一个或者多个段的信息。段信息包括段名、段起始地址、段结束地址和段标识。本命令仅仅是为外部调试器而设立的。
sr:激活SysRq代码,也就是调用MAGIC_SYSRQ函数
格式:sr
将sysrq key字符作为参数传递给SysRq函数进行处理,就像你已经键入了SysRq键和该字符一样。如果要使用这个命令,需要在配置内核时,选择Magic SysRq Key。然后在新内核启动后,使用如下命令激活SysRq功能。
#echo “1” > /proc/sys/kernel/sysrq
这是一个功能强大的命令,它使得在kdb中可以使用操作系统提供的SysRq处理函数。
lsmod:列出内核中加载的所有模块
格式:lsmod
显示所有模块的信息。包括模块名、模块大小、模块结构地址、引用计数,以及被哪个模块所引用。
rmmod:卸载一个模块
格式:rmmod
将由modname指定的模块从内核中卸载。
ll:对链表中的每个元素重复执行命令
格式:ll
它对以地址addr开头的链表的头link-offset个元素,重复执行cmd命令。
help和?:显示帮助信息。
格式:help 或者?
显示kdb的命令以及简单的用法。
提高调试效率
kdb是一个强大的内核调试工具,gdb需要两台机器通过串口才能进行调试,而kdb只需要一台机器即可进行调试,对于普通用户来说,是非常方便的。对于编写内核程序(譬如可加载模块)的程序员来说,kdb提供的这些命令使得调试工作难度大大降低,使得调试效率得以提高。另外对于内核感兴趣的人可以使用kdb来查看内核的数据结构和运行状态,从而加深对内核的理解。不足之处是kdb无法提供源码级的调试,要求程序员有一定的汇编程序基础。但总的来说,kdb提供了一种强有力的内核调试手段,笔者在开发内核模块时,使用kdb进行调试,在较短的时间内完成了调试任务。
(责任编辑 Sunny)