﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>IT博客-1，2，3。。。 大道以多岐路亡羊，学者以多方丧生。-随笔分类-linux 内核驱动</title><link>http://www.cnitblog.com/schkui/category/2839.html</link><description>政府为什么敢控制物价，不敢控制房价，---- 因为为房，是(多或少)数官员和大商人的一起的利益。。。怕影响自己的政绩...    </description><language>zh-cn</language><lastBuildDate>Mon, 26 Sep 2011 06:14:46 GMT</lastBuildDate><pubDate>Mon, 26 Sep 2011 06:14:46 GMT</pubDate><ttl>60</ttl><item><title>Linux2.6内核驱动移植参考 </title><link>http://www.cnitblog.com/schkui/archive/2006/11/02/18768.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Thu, 02 Nov 2006 07:11:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/02/18768.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18768.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/02/18768.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18768.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18768.html</trackback:ping><description><![CDATA[
		<p>作者：晏渭川 <br />随着Linux2.6的发布，由于2.6内核做了教的改动，各个设备的驱动程序在不同程度上要 <br />进行改写。为了方便各位Linux爱好者我把自己整理的这分文档share出来。该文当列举 <br />了2.6内核同以前版本的绝大多数变化，可惜的是由于时间和精力有限没有详细列出各个 <br />函数的用法。 <br />特别声明：该文档中的内容来自http://lwn.net，该网也上也有各个函数的较为详细的 <br />说明可供各位参考。如果需要该文档的word版的朋友， 请mail到weiriver@sohu.com索 <br />取。 <br /><br /><strong>1、 使用新的入口 <br /></strong>必须包含 &lt;linux/init.h&gt; <br />module_init(your_init_func); <br />module_exit(your_exit_func); <br />老版本：int init_module(void); <br />void cleanup_module(voi); <br />2.4中两种都可以用，对如后面的入口函数不必要显示包含任何头文件。 <br /><strong>2、 GPL</strong><br />MODULE_LICENSE("Dual BSD/GPL"); <br />老版本：MODULE_LICENSE("GPL"); <br /><strong>3、 模块参数</strong><br />必须显式包含&lt;linux/moduleparam.h&gt; <br />module_param(name, type, perm); <br />module_param_named(name, value, type, perm); <br />参数定义 <br />module_param_string(name, string, len, perm); <br />module_param_array(name, type, num, perm); <br />老版本：MODULE_PARM(variable,type); <br />MODULE_PARM_DESC(variable,type); <br /><strong>4、 模块别名</strong><br />MODULE_ALIAS("alias-name"); <br />这是新增的，在老版本中需在/etc/modules.conf配置，现在在代码中就可以实现。 <br /><strong>5、 模块计数 </strong><br />int try_module_get(&amp;module); <br />module_put(); <br />老版本：MOD_INC_USE_COUNT 和 MOD_DEC_USE_COUNT <br /><strong>6、 符号导出</strong><br />只有显示的导出符号才能被其他模块使用，默认不导出所有的符号，不必使用EXPORT_NO <br />_SYMBOLS <br />老板本：默认导出所有的符号，除非使用EXPORT_NO_SYMBOLS <br /><strong>7、 内核版本检查</strong><br />需要在多个文件中包含&lt;linux/module.h&gt;时，不必定义__NO_VERSION__ <br />老版本：在多个文件中包含&lt;linux/module.h&gt;时，除在主文件外的其他文件中必须定义_ <br />_NO_VERSION__，防止版本重复定义。 <br /><strong>8、 设备号</strong><br />kdev_t被废除不可用，新的dev_t拓展到了32位，12位主设备号，20位次设备号。 <br />unsigned int <font style="background-color: rgb(255, 255, 0);">iminor</font>(struct inode *inode); <br />unsigned int imajor(struct inode *inode); <br />老版本：8位主设备号，8位次设备号 <br />int MAJOR(kdev_t dev); <br />int MINOR(kdev_t dev); <br /><strong>9、 内存分配头文件变更</strong><br />所有的内存分配函数包含在头文件&lt;linux/slab.h&gt;，而原来的&lt;linux/malloc.h&gt;不存在 <br />老版本：内存分配函数包含在头文件&lt;linux/malloc.h&gt; <br /><strong>10、 结构体的初试化</strong><br />gcc开始采用ANSI C的struct结构体的初始化形式： <br />static struct some_structure = { <br />.field1 = value, <br />.field2 = value, <br />.. <br />}; <br />老版本：非标准的初试化形式 <br />static struct some_structure = { <br />field1: value, <br />field2: value, <br />.. <br />}; <br /><strong>11、 用户模式帮助器</strong><br />int call_usermodehelper(char *path, char **argv, char **envp, <br />int wait); <br />新增wait参数 <br /><strong>12、 request_module() <br /></strong>request_module("foo-device-%d", number); <br />老版本： <br />char module_name[32]; <br />printf(module_name, "foo-device-%d", number); <br />request_module(module_name); <br /><strong>13、 dev_t引发的字符设备的变化 <br /></strong>1、取主次设备号为 <br />unsigned <font style="background-color: rgb(255, 255, 0);">iminor</font>(struct inode *inode); <br />unsigned imajor(struct inode *inode); <br />2、老的register_chrdev()用法没变，保持向后兼容，但不能访问设备号大于256的设备 <br />。 <br />3、新的接口为 <br />a)注册字符设备范围 <br />int register_chrdev_region(dev_t from, unsigned count, char *name); <br />b)动态申请主设备号 <br />int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char <br />*name); <br />看了这两个函数郁闷吧^_^！怎么和file_operations结构联系起来啊？别急！ <br />c)包含 &lt;linux/cdev.h&gt;，利用struct cdev和file_operations连接 <br />struct cdev *cdev_alloc(void); <br />void cdev_init(struct cdev *cdev, struct file_operations *fops); <br />int cdev_add(struct cdev *cdev, dev_t dev, unsigned count); <br />（分别为，申请cdev结构，和fops连接，将设备加入到系统中！好复杂啊！） <br />d)void cdev_del(struct cdev *cdev); <br />只有在cdev_add执行成功才可运行。 <br />e)辅助函数 <br />kobject_put(&amp;cdev-&gt;kobj); <br />struct kobject *cdev_get(struct cdev *cdev); <br />void cdev_put(struct cdev *cdev); <br />这一部分变化和新增的/sys/dev有一定的关联。 <br /><strong>14、 新增对/proc的访问操作</strong><br />&lt;linux/seq_file.h&gt; <br />以前的/proc中只能得到string, seq_file操作能得到如long等多种数据。 <br />相关函数： <br />static struct seq_operations 必须实现这个类似file_operations得数据中得各个成 <br />员函数。 <br />seq_printf()； <br />int seq_putc(struct seq_file *m, char c); <br />int seq_puts(struct seq_file *m, const char *s); <br />int seq_escape(struct seq_file *m, const char *s, const char *esc); <br />int seq_path(struct seq_file *m, struct vfsmount *mnt, <br />struct dentry *dentry, char *esc); <br />seq_open(file, &amp;ct_seq_ops); <br />等等 <br /><strong>15、 底层内存分配</strong><br />1、&lt;linux/malloc.h&gt;头文件改为&lt;linux/slab.h&gt; <br />2、分配标志GFP_BUFFER被取消，取而代之的是GFP_NOIO 和 GFP_NOFS <br />3、新增__GFP_REPEAT，__GFP_NOFAIL，__GFP_NORETRY分配标志 <br />4、页面分配函数alloc_pages()，get_free_page()被包含在&lt;linux/gfp.h&gt;中 <br />5、对NUMA系统新增了几个函数： <br />a) struct page *alloc_pages_node(int node_id, <br />unsigned int gfp_mask, <br />unsigned int order); <br />b) void free_hot_page(struct page *page); <br />c) void free_cold_page(struct page *page); <br />6、 新增Memory pools <br />&lt;linux/mempool.h&gt; <br />mempool_t *mempool_create(int min_nr, <br />mempool_alloc_t *alloc_fn, <br />mempool_free_t *free_fn, <br />void *pool_data); <br />void *mempool_alloc(mempool_t *pool, int gfp_mask); <br />void mempool_free(void *element, mempool_t *pool); <br />int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask); <br /><strong>16、 per-CPU变量</strong><br />get_cpu_var(); <br />put_cpu_var(); <br />void *alloc_percpu(type); <br />void free_percpu(const void *); <br />per_cpu_ptr(void *ptr, int cpu) <br />get_cpu_ptr(ptr) <br />put_cpu_ptr(ptr) <br />老版本使用 <br />DEFINE_PER_CPU(type, name); <br />EXPORT_PER_CPU_SYMBOL(name); <br />EXPORT_PER_CPU_SYMBOL_GPL(name); <br />DECLARE_PER_CPU(type, name); <br />DEFINE_PER_CPU(int, mypcint); <br />2.6内核采用了可剥夺得调度方式这些宏都不安全。 <br /><strong>17、 内核时间变化</strong><br />1、现在的各个平台的HZ为 <br />Alpha: 1024/1200; ARM: 100/128/200/1000; CRIS: 100; i386: 1000; IA-64: <br />1024; M68K: 100; M68K-nommu: 50-1000; MIPS: 100/128/1000; MIPS64: 100; <br />PA-RISC: 100/1000; PowerPC32: 100; PowerPC64: 1000; S/390: 100; SPARC32: <br />100; SPARC64: 100; SuperH: 100/1000; UML: 100; v850: 24-100; x86-64: 1000. <br />2、由于HZ的变化，原来的jiffies计数器很快就溢出了，引入了新的计数器jiffies_64 <br />3、#include &lt;linux/jiffies.h&gt; <br />u64 my_time = get_jiffies_64(); <br />4、新的时间结构增加了纳秒成员变量 <br />struct timespec current_kernel_time(void); <br />5、他的timer函数没变，新增 <br />void add_timer_on(struct timer_list *timer, int cpu); <br />6、新增纳秒级延时函数 <br />ndelay()； <br />7、POSIX clocks 参考kernel/posix-timers.c <br /><strong>18、 工作队列（workqueue） <br /></strong>1、任务队列（task queue ）接口函数都被取消，新增了workqueue接口函数 <br />struct workqueue_struct *create_workqueue(const char *name); <br />DECLARE_WORK(name, void (*function)(void *), void *data); <br />INIT_WORK(struct work_struct *work, <br />void (*function)(void *), void *data); <br />PREPARE_WORK(struct work_struct *work, <br />void (*function)(void *), void *data); <br />2、申明struct work_struct结构 <br />int queue_work(struct workqueue_struct *queue, <br />struct work_struct *work); <br />int queue_delayed_work(struct workqueue_struct *queue, <br />struct work_struct *work, <br />unsigned long delay); <br />int cancel_delayed_work(struct work_struct *work); <br />void flush_workqueue(struct workqueue_struct *queue); <br />void destroy_workqueue(struct workqueue_struct *queue); <br />int schedule_work(struct work_struct *work); <br />int schedule_delayed_work(struct work_struct *work, unsigned long <br />delay); <br /><strong>19、 新增创建VFS的"libfs" <br /></strong>libfs给创建一个新的文件系统提供了大量的API. <br />主要是对struct file_system_type的实现。 <br />参考源代码： <br />drivers/hotplug/pci_hotplug_core.c <br />drivers/usb/core/inode.c <br />drivers/oprofile/oprofilefs.c <br />fs/ramfs/inode.c <br />fs/nfsd/nfsctl.c (simple_fill_super() example) <br /><strong>20、 DMA的变化</strong><br />未变化的有： <br />void *pci_alloc_consistent(struct pci_dev *dev, size_t size, <br />dma_addr_t *dma_handle); <br />void pci_free_consistent(struct pci_dev *dev, size_t size, <br />void *cpu_addr, dma_addr_t dma_handle); <br />变化的有： <br />1、 void *dma_alloc_coherent(struct device *dev, size_t size, <br />dma_addr_t *dma_handle, int flag); <br />void dma_free_coherent(struct device *dev, size_t size, <br />void *cpu_addr, dma_addr_t dma_handle); <br />2、列举了映射方向： <br />enum dma_data_direction { <br />DMA_BIDIRECTIONAL = 0, <br />DMA_TO_DEVICE = 1, <br />DMA_FROM_DEVICE = 2, <br />DMA_NONE = 3, <br />}; <br />3、单映射 <br />dma_addr_t dma_map_single(struct device *dev, void *addr, <br />size_t size, <br />enum dma_data_direction direction); <br />void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, <br />size_t size, <br />enum dma_data_direction direction); <br />4、页面映射 <br />dma_addr_t dma_map_page(struct device *dev, struct page *page, <br />unsigned long offset, size_t size, <br />enum dma_data_direction direction); <br />void dma_unmap_page(struct device *dev, dma_addr_t dma_addr, <br />size_t size, <br />enum dma_data_direction direction); <br />5、有关scatter/gather的函数： <br />int dma_map_sg(struct device *dev, struct scatterlist *sg, <br />int nents, enum dma_data_direction direction); <br />void dma_unmap_sg(struct device *dev, struct scatterlist *sg, <br />int nhwentries, enum dma_data_direction direction); <br />6、非一致性映射（Noncoherent DMA mappings） <br />void *dma_alloc_noncoherent(struct device *dev, size_t size, <br />dma_addr_t *dma_handle, int flag); <br />void dma_sync_single_range(struct device *dev, dma_addr_t dma_handle, <br />unsigned long offset, size_t size, <br />enum dma_data_direction direction); <br />void dma_free_noncoherent(struct device *dev, size_t size, <br />void *cpu_addr, dma_addr_t dma_handle); <br />7、DAC (double address cycle) <br />int pci_dac_set_dma_mask(struct pci_dev *dev, u64 mask); <br />void pci_dac_dma_sync_single(struct pci_dev *dev, <br />dma64_addr_t dma_addr, <br />size_t len, int direction); <br /><strong>21、 互斥</strong><br />新增seqlock主要用于： <br />1、少量的数据保护 <br />2、数据比较简单(没有指针)，并且使用频率很高 <br />3、对不产生任何副<font style="background-color: rgb(0, 255, 255);">作用</font>的数据的访问 <br />4、访问时写者不被饿死 <br />&lt;linux/seqlock.h&gt; <br />初始化 <br />seqlock_t lock1 = SEQLOCK_UNLOCKED; <br />或seqlock_t lock2; seqlock_init(&amp;lock2); <br />void write_seqlock(seqlock_t *sl); <br />void write_sequnlock(seqlock_t *sl); <br />int write_tryseqlock(seqlock_t *sl); <br />void write_seqlock_irqsave(seqlock_t *sl, long flags); <br />void write_sequnlock_irqrestore(seqlock_t *sl, long flags); <br />void write_seqlock_irq(seqlock_t *sl); <br />void write_sequnlock_irq(seqlock_t *sl); <br />void write_seqlock_bh(seqlock_t *sl); <br />void write_sequnlock_bh(seqlock_t *sl); <br />unsigned int read_seqbegin(seqlock_t *sl); <br />int read_seqretry(seqlock_t *sl, unsigned int iv); <br />unsigned int read_seqbegin_irqsave(seqlock_t *sl, long flags); <br />int read_seqretry_irqrestore(seqlock_t *sl, unsigned int iv, long <br />flags); <br /><strong>22、 内核可剥夺</strong><br />&lt;linux/preempt.h&gt; <br />preempt_disable()； <br />preempt_enable_no_resched()； <br />preempt_enable_noresched()； <br />preempt_check_resched()； <br /><strong>23、 眠和唤醒</strong><br />1、原来的函数可用，新增下列函数： <br />prepare_to_wait_exclusive()； <br />prepare_to_wait()； <br />2、等待队列的变化 <br />typedef int (*wait_queue_func_t)(wait_queue_t *wait, <br />unsigned mode, int sync); <br />void init_waitqueue_func_entry(wait_queue_t *queue, <br />wait_queue_func_t func); <br /><strong>24、 新增完成事件（completion events）</strong><br />&lt;linux/completion.h&gt; <br />init_completion(&amp;my_comp); <br />void wait_for_completion(struct completion *comp); <br />void complete(struct completion *comp); <br />void complete_all(struct completion *comp); <br /><strong>25、 RCU（Read-copy-update） <br /></strong>rcu_read_lock(); <br />void call_rcu(struct rcu_head *head, void (*func)(void *arg), <br />void *arg); <br /><strong>26、 中断处理</strong><br />1、中断处理有返回值了。 <br />IRQ_RETVAL(handled)； <br />2、cli(), sti(), save_flags(), 和 restore_flags()不再有效，应该使用local_save <br />_flags() 或local_irq_disable()。 <br />3、synchronize_irq()函数有改动 <br />4、新增int can_request_irq(unsigned int irq, unsigned long flags); <br />5、 request_irq() 和free_irq() 从 &lt;linux/sched.h&gt;改到了 &lt;linux/interrupt.h&gt; <br /><strong>27、 异步I/O(AIO)</strong><br />&lt;linux/aio.h&gt; <br />ssize_t (*aio_read) (struct kiocb *iocb, char __user *buffer, <br />size_t count, loff_t pos); <br />ssize_t (*aio_write) (struct kiocb *iocb, const char __user *buffer, <br />size_t count, loff_t pos); <br />int (*aio_fsync) (struct kiocb *, int datasync); <br />新增到了file_operation结构中。 <br />is_sync_kiocb(struct kiocb *iocb)； <br />int aio_complete(struct kiocb *iocb, long res, long res2); <br /><strong>28、 网络驱动</strong><br />1、struct net_device *alloc_netdev(int sizeof_priv, const char *name, <br />void (*setup)(struct net_device *)); <br />struct net_device *alloc_etherdev(int sizeof_priv); <br />2、新增NAPI(New API) <br />void netif_rx_schedule(struct net_device *dev); <br />void netif_rx_complete(struct net_device *dev); <br />int netif_rx_ni(struct sk_buff *skb); <br />(老版本为netif_rx()) <br /><strong>29、 USB驱动</strong><br />老版本struct usb_driver取消了，新的结构体为 <br />struct usb_class_driver { <br />char *name; <br />struct file_operations *fops; <br />mode_t mode; <br />int minor_base; <br />}; <br />int usb_submit_urb(struct urb *urb, int mem_flags); <br />int (*probe) (struct usb_interface *intf, <br />const struct usb_device_id *id); <br /><strong>30、 block I/O 层 <br /></strong>这一部分做的改动最大。不祥叙。 <br /><strong>31、 mmap()</strong><br />int remap_page_range(struct vm_area_struct *vma, unsigned long from, <br />unsigned long to, unsigned long size, <br />pgprot_t prot); <br />int io_remap_page_range(struct vm_area_struct *vma, unsigned long from, <br />unsigned long to, unsigned long size, <br />pgprot_t prot); <br />struct page *(*nopage)(struct vm_area_struct *area, <br />unsigned long address, <br />int *type); <br />int (*populate)(struct vm_area_struct *area, unsigned long address, <br />unsigned long len, pgprot_t prot, unsigned long pgoff, <br />int nonblock); <br />int install_page(struct mm_struct *mm, struct vm_area_struct *vma, <br />unsigned long addr, struct page *page, <br />pgprot_t prot); <br />struct page *vmalloc_to_page(void *address); <br /><strong>32、 零拷贝块I/O(Zero-copy block I/O) <br /></strong>struct bio *bio_map_user(struct block_device *bdev, <br />unsigned long uaddr, <br />unsigned int len, <br />int write_to_vm); <br />void bio_unmap_user(struct bio *bio, int write_to_vm); <br />int get_user_pages(struct task_struct *task, <br />struct mm_struct *mm, <br />unsigned long start, <br />int len, <br />int write, <br />int force, <br />struct page **pages, <br />struct vm_area_struct **vmas); <br /><strong>33、 高端内存操作kmaps <br /></strong>void *kmap_atomic(struct page *page, enum km_type type); <br />void kunmap_atomic(void *address, enum km_type type); <br />struct page *kmap_atomic_to_page(void *address); <br />老版本：kmap() 和 kunmap()。 <br /><strong>34、 驱动模型 <br /></strong>主要用于设备管理。 <br />1、 sysfs <br />2、 Kobjects </p>
		<p>推荐文章： <br /><a href="http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kernel26/index.shtml" target="_blank">http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kernel26/index.shtml</a><br /><a href="http://www-900.ibm.com/developerWorks/cn/linux/l-inside/index.shtml" target="_blank">http://www-900.ibm.com/developerWorks/cn/linux/l-inside/index.shtml</a><span class="postbody"><br /></span></p>
		<p>
				<span class="postbody">2.6里不需要再定义“__KERNEL__”和“MODULE”了。 <br />用下面的Makefile文件编译： <br /><br /></span>
		</p>
		<table align="center" border="0" cellpadding="3" cellspacing="1" width="90%">
				<tbody>
						<tr>
								<td>
										<span class="genmed">
												<b>代码:</b>
										</span>
								</td>
						</tr>
						<tr>
								<td class="code">
										<br />    obj-m   := hello.o <br /><br />    KDIR   := /lib/modules/$(shell uname -r)/build <br />    PWD      := $(shell pwd) <br />    default: <br />              $(MAKE) -C $(KDIR) M=$(PWD) modules </td>
						</tr>
				</tbody>
		</table>
<img src ="http://www.cnitblog.com/schkui/aggbug/18768.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-02 15:11 <a href="http://www.cnitblog.com/schkui/archive/2006/11/02/18768.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>The Linux Kernel Module Programming Guide</title><link>http://www.cnitblog.com/schkui/archive/2006/11/01/18739.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Wed, 01 Nov 2006 09:55:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/01/18739.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18739.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/01/18739.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18739.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18739.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: The Linux Kernel Module Programming Guide																				Peter Jay Salzman																												Michael Burian																												Ori Pomerantz													...&nbsp;&nbsp;<a href='http://www.cnitblog.com/schkui/archive/2006/11/01/18739.html'>阅读全文</a><img src ="http://www.cnitblog.com/schkui/aggbug/18739.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-01 17:55 <a href="http://www.cnitblog.com/schkui/archive/2006/11/01/18739.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 内核调试器内幕</title><link>http://www.cnitblog.com/schkui/archive/2006/11/01/18737.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Wed, 01 Nov 2006 09:45:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/01/18737.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18737.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/01/18737.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18737.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18737.html</trackback:ping><description><![CDATA[
		<img alt="" src="http://www.ibm.com/i/c.gif" height="1" width="10" />
		<table class="no-print" align="right" border="0" cellpadding="0" cellspacing="0" width="160">
				<tbody>
						<tr>
								<td width="10">
										<img alt="" src="http://www.ibm.com/i/c.gif" height="1" width="10" />
								</td>
								<td>
										<table border="0" cellpadding="0" cellspacing="0" width="150">
												<tbody>
														<tr>
																<td class="v14-header-1-small">文档选项</td>
														</tr>
												</tbody>
										</table>
										<table class="v14-gray-table-border" border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td class="no-padding" width="150">
																		<img alt="" src="http://www.ibm.com/i/c.gif" height="1" width="8" />
																		<input value="调试内核问题时，能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB 提供了这种功能。在本文中您将了解如何使用 KDB 所提供的功能，以及如何在 Linux 机器上安装和设置 KDB。您还将熟悉 KDB 中可以使用的命令以及设置和显示选项。" name="body" type="hidden" />
																		<input name="subject" value="Linux 内核调试器内幕" type="hidden" />
																		<input name="lang" value="cn" type="hidden" />
																		<noscript>
																				<tr valign="top">
																						<td width="8">
																								<img alt="" height="1" width="8" src="//www.ibm.com/i/c.gif" />
																						</td>
																						<td width="16">
																								<img alt="" width="16" height="16" src="//www.ibm.com/i/c.gif" />
																						</td>
																						<td class="small" width="122">
																								<p>
																										<span class="ast">未显示需要 JavaScript
的文档选项</span>
																								</p>
																						</td>
																				</tr>
																		</noscript>
																		<table border="0" cellpadding="0" cellspacing="0" width="143">
																				<form action="https://www-128.ibm.com/developerworks/secure/email-it.jsp" name="email">
																				</form>
																				<script language="JavaScript" type="text/javascript">
																						<!--
document.write('<tr valign="top"><td width="8"><img src="//www.ibm.com/i/c.gif" width="8" height="1" alt=""/></td><td width="16"><img src="//www.ibm.com/i/v14/icons/em.gif" height="16" width="16" vspace="3" alt="将此页作为电子邮件发送" /></td><td width="122"><p><a class="smallplainlink" href="javascript:document.email.submit();"><b>将此页作为电子邮件发送</b></a></p></td></tr>');
//-->
																				</script>
																				<tbody>
																						<tr valign="top">
																								<td width="8">
																										<img src="http://www.ibm.com/i/c.gif" alt="" height="1" width="8" />
																								</td>
																								<td width="16">
																										<img src="http://www.ibm.com/i/v14/icons/em.gif" alt="将此页作为电子邮件发送" height="16" vspace="3" width="16" />
																								</td>
																								<td width="122">
																										<p>
																												<a class="smallplainlink" href="javascript:document.email.submit();">
																														<b>将此页作为电子邮件发送</b>
																												</a>
																										</p>
																								</td>
																						</tr>
																				</tbody>
																		</table>
																</td>
														</tr>
												</tbody>
										</table>
										<!--START RESERVED FOR FUTURE USE INCLUDE FILES-->
										<!-- 03/20/06 updated by gretchen -->
										<br />
										<table border="0" cellpadding="0" cellspacing="0" width="150">
												<tbody>
														<tr>
																<td class="v14-header-2-small">拓展 Tomcat 应用</td>
														</tr>
												</tbody>
										</table>
										<table class="v14-gray-table-border" border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td class="no-padding" width="150">
																		<table border="0" cellpadding="0" cellspacing="0" width="143">
																				<tbody>
																						<tr valign="top">
																								<td width="8">
																										<img src="http://www.ibm.com/i/c.gif" alt="" height="1" width="8" />
																								</td>
																								<td>
																										<img src="http://www.ibm.com/i/v14/icons/fw_bold.gif" alt="" border="0" height="16" vspace="3" width="16" />
																								</td>
																								<td width="125">
																										<p>
																												<a href="http://www-128.ibm.com/developerworks/cn/kickstart/webserver.html?S_TACT=105AGX52&amp;S_CMP=simpleart" class="smallplainlink">下载 IBM 开源 J2EE 应用服务器 WAS CE 新版本 V1.1</a>
																										</p>
																								</td>
																						</tr>
																				</tbody>
																		</table>
																</td>
														</tr>
												</tbody>
										</table>
										<!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
										<br />
								</td>
						</tr>
				</tbody>
		</table>
		<p>级别: 初级</p>
		<p>
				<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#author">Hariprasad Nellitheertha</a>, 软件工程师, IBM<br /></p>
		<p>2003 年  9 月  01 日</p>
		<blockquote>调
试内核问题时，能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB
提供了这种功能。在本文中您将了解如何使用 KDB 所提供的功能，以及如何在 Linux 机器上安装和设置 KDB。您还将熟悉 KDB
中可以使用的命令以及设置和显示选项。</blockquote>
		<!--START RESERVED FOR FUTURE USE INCLUDE FILES-->
		<!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters -->
		<!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
		<p>Linux 内核调试器（KDB）允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁，它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试：您可以调试正在运行的内核。</p>
		<p>设置一台用于 KDB 的机器需要花费一些工作，因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译（在一定程度上还要熟悉内核内部机理），但是如果您需要编译内核方面的帮助，请参阅本文结尾处的
        <a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#resources">参考资料</a>一节。
      </p>
		<p>在本文中，我们将从有关下载 KDB 补丁、打补丁、（重新）编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后，我们将研究一下有关设置和显示选项方面的一些详细信息。</p>
		<p>
				<a name="1">
						<span class="atitle">入门</span>
				</a>
		</p>
		<p>
KDB 项目是由 Silicon Graphics 维护的（请参阅
        <a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#resources">参考资料</a>以获取链接），您需要从它的
        <a href="ftp://oss.sgi.com/www/projects/kdb/download">FTP 站点</a>下
载与内核版本有关的补丁。（在编写本文时）可用的最新 KDB 版本是
4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁，包含了对通用内核代码的更改，另一个是特定于体系结构的补丁。补丁可作为 bz2
文件获取。例如，在运行 2.4.20 内核的 x86 机器上，您会需要 kdb-v4.2-2.4.20-common-1.bz2 和
kdb-v4.2-2.4.20-i386-1.bz2。 </p>
		<p>这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。</p>
		<p>将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件：</p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2<br /><br /><br /><br /><br />#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。</p>
		<p>现在，应用这些补丁：</p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />#patch -p1 &lt;kdb-v4.2-2.4.20-common-1<br /><br /><br /><br /><br />#patch -p1 &lt;kdb-v4.2-2.4.20-i386-1<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>这些补丁应该干净利落地加以应用。查找任何以 .rej 结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题，那么补丁的应用就不会有任何问题。</p>
		<p>接下来，需要构建内核以支持 KDB。第一步是设置 
        <code>CONFIG_KDB</code> 选项。使用您喜欢的配置机制（xconfig 和 menuconfig 等）来完成这一步。转到结尾处的“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项。
      </p>
		<p>您还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项（如果有的话）则设置 
        <code>CONFIG_FRAME_POINTER</code> 标志。这将产生更好的堆栈回溯，因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDB off by default”选项。这将设置 
        <code>CONFIG_KDB_OFF</code> 标志，并且在缺省情况下将关闭 KDB。我们将在后面一节中对此进行详细介绍。
      </p>
		<p>保存配置，然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。</p>
		<br />
		<table border="0" cellpadding="0" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<img src="http://www.ibm.com/i/v14/rules/blue_rule.gif" alt="" height="1" width="100%" />
										<br />
										<img alt="" src="http://www.ibm.com/i/c.gif" border="0" height="6" width="8" />
								</td>
						</tr>
				</tbody>
		</table>
		<table class="no-print" align="right" cellpadding="0" cellspacing="0">
				<tbody>
						<tr align="right">
								<td>
										<img src="http://www.ibm.com/i/c.gif" alt="" height="4" width="100%" />
										<br />
										<table border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td valign="middle">
																		<img src="http://www.ibm.com/i/v14/icons/u_bold.gif" alt="" border="0" height="16" width="16" />
																		<br />
																</td>
																<td align="right" valign="top">
																		<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#main" class="fbox">
																				<b>回页首</b>
																		</a>
																</td>
														</tr>
												</tbody>
										</table>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<p>
				<a name="IDAIVPSE">
						<span class="atitle">初始化并设置环境变量</span>
				</a>
		</p>
		<p>
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令，该文件位于 Linux
源代码树（当然是在打了补丁之后）的 KDB
目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是，在您更改了文件之后需要重
新构建并重新安装内核。</p>
		<br />
		<table border="0" cellpadding="0" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<img src="http://www.ibm.com/i/v14/rules/blue_rule.gif" alt="" height="1" width="100%" />
										<br />
										<img alt="" src="http://www.ibm.com/i/c.gif" border="0" height="6" width="8" />
								</td>
						</tr>
				</tbody>
		</table>
		<table class="no-print" align="right" cellpadding="0" cellspacing="0">
				<tbody>
						<tr align="right">
								<td>
										<img src="http://www.ibm.com/i/c.gif" alt="" height="4" width="100%" />
										<br />
										<table border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td valign="middle">
																		<img src="http://www.ibm.com/i/v14/icons/u_bold.gif" alt="" border="0" height="16" width="16" />
																		<br />
																</td>
																<td align="right" valign="top">
																		<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#main" class="fbox">
																				<b>回页首</b>
																		</a>
																</td>
														</tr>
												</tbody>
										</table>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<p>
				<a name="IDAQVPSE">
						<span class="atitle">激活 KDB</span>
				</a>
		</p>
		<p>
如果编译期间没有选中 
        <code>CONFIG_KDB_OFF</code> ，那么在缺省情况下 KDB 是活动的。否则，您需要显式地激活它 － 通过在引导期间将 
        <code>kdb=on</code> 标志传递给内核或者通过在挂装了 /proc 之后执行该工作：
      </p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />#echo "1" &gt;/proc/sys/kernel/kdb<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>倒过来执行上述步骤则会取消激活 KDB。也就是说，如果缺省情况下 KDB 是打开的，那么将 
        <code>kdb=off</code> 标志传递给内核或者执行下面这个操作将会取消激活 KDB：
      </p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />#echo "0" &gt;/proc/sys/kernel/kdb<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>在引导期间还可以将另一个标志传递给内核。 
        <code>kdb=early</code> 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试，那么这将有所帮助。
      </p>
		<p>调用 KDB 的方式有很多。如果 KDB 处于打开状态，那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然，要做到这一点，需要设置串行控制台（请参阅
        <a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#resources">参考资料</a>以获取这方面的帮助）并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。
      </p>
		<br />
		<table border="0" cellpadding="0" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<img src="http://www.ibm.com/i/v14/rules/blue_rule.gif" alt="" height="1" width="100%" />
										<br />
										<img alt="" src="http://www.ibm.com/i/c.gif" border="0" height="6" width="8" />
								</td>
						</tr>
				</tbody>
		</table>
		<table class="no-print" align="right" cellpadding="0" cellspacing="0">
				<tbody>
						<tr align="right">
								<td>
										<img src="http://www.ibm.com/i/c.gif" alt="" height="4" width="100%" />
										<br />
										<table border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td valign="middle">
																		<img src="http://www.ibm.com/i/v14/icons/u_bold.gif" alt="" border="0" height="16" width="16" />
																		<br />
																</td>
																<td align="right" valign="top">
																		<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#main" class="fbox">
																				<b>回页首</b>
																		</a>
																</td>
														</tr>
												</tbody>
										</table>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<p>
				<a name="IDAXWPSE">
						<span class="atitle">KDB 命令</span>
				</a>
		</p>
		<p>
KDB 是一个功能非常强大的工具，它允许进行几个操作，比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些，可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。</p>
		<p>
				<a name="N100CD">
						<span class="smalltitle">内存显示和修改</span>
				</a>
		</p>
		<p>
这一类别中最常用的命令是 
        <code>md</code> 、 
        <code>mdr</code> 、 
        <code>mm</code> 和 
        <code>mmW</code> 。
      </p>
		<p>
				<code>md</code> 命令以一个地址／符号和行计数为参数，显示从该地址开始的 
        <code>line-count</code> 行的内存。如果没有指定 
        <code>line-count</code> ，那么就使用环境变量所指定的缺省值。如果没有指定地址，那么 
        <code>md</code> 就从上一次打印的地址继续。地址打印在开头，字符转换打印在结尾。
      </p>
		<p>
				<code>mdr</code> 命令带有地址／符号以及字节计数，显示从指定的地址开始的 
        <code>byte-count</code> 字节数的初始内存内容。它本质上和 
        <code>md</code> 一样，但是它不显示起始地址并且不在结尾显示字符转换。 
        <code>mdr</code> 命令较少使用。
      </p>
		<p>
				<code>mm</code> 命令修改内存内容。它以地址／符号和新内容作为参数，用 
        <code>new-contents</code> 替换地址处的内容。
      </p>
		<p>
				<code>mmW</code> 命令更改从地址开始的 
        <code>W</code> 个字节。请注意， 
        <code>mm</code> 更改一个机器字。
      </p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N1012C">
				<b>显示从 0xc000000 开始的 15 行内存：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; md 0xc000000 15<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N10136">
				<b>将内存位置为 0xc000000 上的内容更改为 0x10：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; mm 0xc000000 0x10<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>
				<a name="N10140">
						<span class="smalltitle">寄存器显示和修改</span>
				</a>
		</p>
		<p>
这一类别中的命令有 
        <code>rd</code> 、 
        <code>rm</code> 和 
        <code>ef</code> 。
      </p>
		<p>
				<code>rd</code> 命令（不带任何参数）显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 
        <code>c</code> 参数，则 
        <code>rd</code> 显示处理器的控制寄存器；如果带有 
        <code>d</code> 参数，那么它就显示调试寄存器；如果带有 
        <code>u</code> 参数，则显示上一次进入内核的当前任务的寄存器组。
      </p>
		<p>
				<code>rm</code> 命令修改寄存器的内容。它以寄存器名称和 
        <code>new-contents</code> 作为参数，用 
        <code>new-contents</code> 修改寄存器。寄存器名称与特定的体系结构有关。目前，不能修改控制寄存器。
      </p>
		<p>
				<code>ef</code> 命令以一个地址作为参数，它显示指定地址处的异常帧。
      </p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N10188">
				<b>显示通用寄存器组：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; rd<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N10192">
				<b>
				</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; rm %ebx 0x25<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>
				<a name="N1019C">
						<span class="smalltitle">断点</span>
				</a>
		</p>
		<p>
常用的断点命令有 
        <code>bp</code> 、 
        <code>bc</code> 、 
        <code>bd</code> 、 
        <code>be</code> 和 
        <code>bl</code> 。
      </p>
		<p>
				<code>bp</code> 命令以一个地址／符号作为参数，它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。 
        <code>bpa</code> 命令对 SMP 系统中的所有处理器应用断点。 
        <code>bph</code> 命令强制在支持硬件寄存器的系统上使用它。 
        <code>bpha</code> 命令类似于 
        <code>bpa</code> 命令，差别在于它强制使用硬件寄存器。
      </p>
		<p>
				<code>bd</code> 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点，而只是禁用它。断点号从 0 开始，根据可用性顺序分配给断点。
      </p>
		<p>
				<code>be</code> 命令启用断点。该命令的参数也是断点号。
      </p>
		<p>
				<code>bl</code> 命令列出当前的断点集。它包含了启用的和禁用的断点。
      </p>
		<p>
				<code>bc</code> 命令从断点表中除去断点。它以具体的断点号或 
        <code>*</code> 作为参数，在后一种情况下它将除去所有断点。
      </p>
		<br />
		<a name="N101F0">
				<b>示例</b>
		</a>
		<br />
		<p>
				<b>对函数 
          <code>sys_write()</code> 设置断点：
        </b>
		</p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bp sys_write<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N10204">
				<b>列出断点表中的所有断点：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bl<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N1020E">
				<b>清除断点号 1：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bc 1<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>
				<a name="N10218">
						<span class="smalltitle">&gt;堆栈跟踪</span>
				</a>
		</p>
		<p>
主要的堆栈跟踪命令有 
        <code>bt</code> 、 
        <code>btp</code> 、 
        <code>btc</code> 和 
        <code>bta</code> 。
      </p>
		<p>
				<code>bt</code> 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址，那么它采用当前寄存器来回溯堆栈。否则，它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了 
        <code>CONFIG_FRAME_POINTER</code> 选项，那么就用帧指针寄存器来维护堆栈，从而就可以正确地执行堆栈回溯。如果没有设置 
        <code>CONFIG_FRAME_POINTER</code> ，那么 
        <code>bt</code> 命令可能会产生错误的结果。
      </p>
		<p>
				<code>btp</code> 命令将进程标识作为参数，并对这个特定进程进行堆栈回溯。
      </p>
		<p>
				<code>btc</code> 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 
        <code>bt</code> ，然后切换到下一个活动 CPU，以此类推。
      </p>
		<p>
				<code>bta</code> 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数，它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下：
      </p>
		<ul>
				<li>D：不可中断状态</li>
				<li>R：正运行</li>
				<li>S：可中断休眠</li>
				<li>T：已跟踪或已停止</li>
				<li>Z：僵死</li>
				<li>U：不可运行</li>
		</ul>
		<p>这类命令中的每一个都会打印出一大堆信息。请查阅下面的
        <a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#resources">参考资料</a>以获取这些字段的详细文档。
      </p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N1027F">
				<b>跟踪当前活动线程的堆栈：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bt<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N10289">
				<b>跟踪标识为 575 的进程的堆栈：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; btp 575<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>
				<a name="N10293">
						<span class="smalltitle">其它命令</span>
				</a>
		</p>
		<p>
下面是在内核调试过程中非常有用的其它几个 KDB 命令。</p>
		<p>
				<code>id</code> 命令以一个地址／符号作为参数，它对从该地址开始的指令进行反汇编。环境变量 
        <code>IDCOUNT</code> 确定要显示多少行输出。
      </p>
		<p>
				<code>ss</code> 命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 
        <code>ssb</code> ，它执行从当前指令指针地址开始的指令（在屏幕上打印指令），直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有 
        <code>call</code> 、 
        <code>return</code> 和 
        <code>jump</code> 。
      </p>
		<p>
				<code>go</code> 命令让系统继续正常执行。一直执行到遇到断点为止（如果已应用了一个断点的话）。
      </p>
		<p>
				<code>reboot</code> 命令立刻重新引导系统。它并没有彻底关闭系统，因此结果是不可预测的。
      </p>
		<p>
				<code>ll</code> 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
      </p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N102D9">
				<b>反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量 
        IDCOUNT ：
      </b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; id schedule<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N102E7">
				<b>执行指令直到它遇到分支转移条件（在本例中为指令 
        jne ）为止：
      </b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; ssb<br /><br /><br /><br /><br />0xc0105355  default_idle+0x25:  cli<br /><br /><br />0xc0105356  default_idle+0x26:  mov  0x14(%edx),%eax<br /><br /><br />0xc0105359  default_idle+0x29:  test %eax, %eax<br /><br /><br />0xc010535b  default_idle+0x2b:  jne  0xc0105361 default_idle+0x31<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<table border="0" cellpadding="0" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<img src="http://www.ibm.com/i/v14/rules/blue_rule.gif" alt="" height="1" width="100%" />
										<br />
										<img alt="" src="http://www.ibm.com/i/c.gif" border="0" height="6" width="8" />
								</td>
						</tr>
				</tbody>
		</table>
		<table class="no-print" align="right" cellpadding="0" cellspacing="0">
				<tbody>
						<tr align="right">
								<td>
										<img src="http://www.ibm.com/i/c.gif" alt="" height="4" width="100%" />
										<br />
										<table border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td valign="middle">
																		<img src="http://www.ibm.com/i/v14/icons/u_bold.gif" alt="" border="0" height="16" width="16" />
																		<br />
																</td>
																<td align="right" valign="top">
																		<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#main" class="fbox">
																				<b>回页首</b>
																		</a>
																</td>
														</tr>
												</tbody>
										</table>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<p>
				<a name="IDAYKQSE">
						<span class="atitle">技巧和诀窍</span>
				</a>
		</p>
		<p>
调试一个问题涉及到：使用调试器（或任何其它工具）找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的，只有老练
的内核黑客才有可能做得到。相反，大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修
正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用，却没有查出无效引用的真正原因。</p>
		<p>结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。</p>
		<p>调试器的主要用途是找到错误的位置、确认症状（在某些情况下还有起因）、确定变量的值，以及确定程序是如何出现这种情况的（即，建立调用堆栈）。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器，并且能迅速地根据调试获取必要的信息，然后继续分析代码以识别起因。</p>
		<p>因此，这里为您介绍了一些技巧，以便您能使用 KDB 快速地取得上述结果。当然，要记住，调试的速度和精确度来自经验、实践和良好的系统知识（硬件和内核内部机理等）。</p>
		<p>
				<a name="N10311">
						<span class="smalltitle">技巧 #1</span>
				</a>
		</p>
		<p>
在 KDB 中，在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址／值和函数地址方面极其有用。同样，输入符号名则返回其虚拟地址。</p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N10320">
				<b>表明函数 
        sys_read 从地址 0xc013db4c 开始：
      </b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; 0xc013db4c<br /><br /><br /><br /><br />0xc013db4c = 0xc013db4c (sys_read)<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>同样，</p>
		<br />
		<a name="N10335">
				<b>同样，表明 
        sys_write 位于地址 0xc013dcc8：
      </b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; sys_write<br /><br /><br /><br /><br />sys_write = 0xc013dcc8 (sys_write)<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>这些有助于在分析堆栈时找到全局数据和函数地址。</p>
		<p>
				<a name="N1034A">
						<span class="smalltitle">技巧 #2</span>
				</a>
		</p>
		<p>
在编译带 KDB 的内核时，只要 
        <code>CONFIG_FRAME_POINTER</code>
选项出现就使用该选项。为此，需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with
frame
pointers”选项。这确保了帧指针寄存器将被用作帧指针，从而产生正确的回溯。实际上，您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如，在
i386 机器上，%ebp 寄存器可以用来回溯整个堆栈。 </p>
		<p>例如，在函数 
        <code>rmqueue()</code> 上执行第一个指令后，堆栈看上去类似于下面这样：
      </p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; md %ebp<br /><br /><br /><br /><br />0xc74c9f38 c74c9f60  c0136c40 000001f0 00000000<br /><br /><br />0xc74c9f48 08053328 c0425238 c04253a8 00000000<br /><br /><br />0xc74c9f58 000001f0  00000246 c74c9f6c c0136a25<br /><br /><br />0xc74c9f68 c74c8000  c74c9f74  c0136d6d c74c9fbc<br /><br /><br />0xc74c9f78 c014fe45  c74c8000  00000000 08053328<br /><br /><br /><br /><br />[0]kdb&gt; 0xc0136c40<br /><br /><br /><br /><br />0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)<br /><br /><br /><br /><br />[0]kdb&gt; 0xc0136a25<br /><br /><br /><br /><br />0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)<br /><br /><br /><br /><br />[0]kdb&gt; 0xc0136d6d<br /><br /><br /><br /><br />0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>我们可以看到 
        <code>rmqueue()</code> 被 
        <code>__alloc_pages</code> 调用，后者接下来又被 
        <code>_alloc_pages</code> 调用，以此类推。
      </p>
		<p>每一帧的第一个双字（double word）指向下一帧，这后面紧跟着调用函数的地址。因此，跟踪堆栈就变成一件轻松的工作了。</p>
		<p>
				<a name="N10398">
						<span class="smalltitle">技巧 #3</span>
				</a>
		</p>
		<p>
				<code>go</code> 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行，则可以提供该地址作为参数。另一个办法是使用 
        <code>rm</code> 命令修改指令指针寄存器，然后只要输入 
        <code>go</code> 。如果您想跳过似乎会引起问题的某个特定指令或一组指令，这就会很有用。但是，请注意，该指令使用不慎会造成严重的问题，系统可能会严重崩溃。
      </p>
		<p>
				<a name="N103AD">
						<span class="smalltitle">技巧 #4</span>
				</a>
		</p>
		<p>
您可以利用一个名为 
        <code>defcmd</code> 的有用命令来定义自己的命令集。例如，每当遇到断点时，您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常，您必须要输入一系列命令，以便能同时执行所有这些工作。 
        <code>defcmd</code> 允许您定义自己的命令，该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下：
      </p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; defcmd name "usage" "help"<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] type the commands here<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] endefcmd<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>例如，可以定义一个（简单的）新命令 
        <code>hari</code> ，它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈：
      </p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; defcmd hari "" "no arguments needed"<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] md 0xc000000 1<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] rd<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] md %ebp 1<br /><br /><br /><br /><br />[0]kdb&gt; [defcmd] endefcmd<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>该命令的输出会是：</p>
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; hari<br /><br /><br /><br /><br />[hari]kdb&gt; md 0xc000000 1<br /><br /><br /><br /><br />0xc000000 00000001 f000e816 f000e2c3 f000e816<br /><br /><br /><br /><br />[hari]kdb&gt; rd<br /><br /><br /><br /><br />eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000<br /><br /><br />....<br /><br /><br />...<br /><br /><br /><br /><br />[hari]kdb&gt; md %ebp 1<br /><br /><br /><br /><br />0xc0467fbc c0467fd0 c01053d2 00000002 000a0200<br /><br /><br /><br /><br />[0]kdb&gt;<br /><br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<p>
				<a name="N1040C">
						<span class="smalltitle">技巧 #5</span>
				</a>
		</p>
		<p>
可以使用 
        <code>bph</code> 和 
        <code>bpha</code> 命令（假如体系结构支持使用硬件寄存器）来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时，我们都可以对此进行控制。当调试数据／内存毁坏问题时这可能会极其方便，在这种情况中您可以用它来识别毁坏的代码／进程。
      </p>
		<p>
				<b>示例</b>
		</p>
		<br />
		<a name="N10423">
				<b>每当将四个字节写入地址 0xc0204060 时就进入内核调试器：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bph 0xc0204060 dataw 4<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<a name="N1042D">
				<b>在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器：</b>
		</a>
		<br />
		<table bgcolor="#eeeeee" border="1" cellpadding="5" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<pre>
												<code class="section">
														<br />[0]kdb&gt; bph 0xc000000 datar 2<br /></code>
										</pre>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<table border="0" cellpadding="0" cellspacing="0" width="100%">
				<tbody>
						<tr>
								<td>
										<img src="http://www.ibm.com/i/v14/rules/blue_rule.gif" alt="" height="1" width="100%" />
										<br />
										<img alt="" src="http://www.ibm.com/i/c.gif" border="0" height="6" width="8" />
								</td>
						</tr>
				</tbody>
		</table>
		<table class="no-print" align="right" cellpadding="0" cellspacing="0">
				<tbody>
						<tr align="right">
								<td>
										<img src="http://www.ibm.com/i/c.gif" alt="" height="4" width="100%" />
										<br />
										<table border="0" cellpadding="0" cellspacing="0">
												<tbody>
														<tr>
																<td valign="middle">
																		<img src="http://www.ibm.com/i/v14/icons/u_bold.gif" alt="" border="0" height="16" width="16" />
																		<br />
																</td>
																<td align="right" valign="top">
																		<a href="http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/index.html#main" class="fbox">
																				<b>回页首</b>
																		</a>
																</td>
														</tr>
												</tbody>
										</table>
								</td>
						</tr>
				</tbody>
		</table>
		<br />
		<br />
		<p>
				<a name="IDAL0QSE">
						<span class="atitle">结束语</span>
				</a>
		</p>

      
对于执行内核调试，KDB 是一个方便的且功能强大的工具。它提供了各种选项，并且使我们能够分析内存内容和数据结构。最妙的是，它不需要用另一台机器来执行调试<img src ="http://www.cnitblog.com/schkui/aggbug/18737.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-01 17:45 <a href="http://www.cnitblog.com/schkui/archive/2006/11/01/18737.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>掌握 Linux 调试技术</title><link>http://www.cnitblog.com/schkui/archive/2006/11/01/18736.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Wed, 01 Nov 2006 09:41:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/01/18736.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18736.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/01/18736.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18736.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18736.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 掌握 Linux 调试技术																						在 Linux 上找出并解决程序错误的主要方法																																																																																																										...&nbsp;&nbsp;<a href='http://www.cnitblog.com/schkui/archive/2006/11/01/18736.html'>阅读全文</a><img src ="http://www.cnitblog.com/schkui/aggbug/18736.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-01 17:41 <a href="http://www.cnitblog.com/schkui/archive/2006/11/01/18736.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用kgdb调试linux内核及内核模块  </title><link>http://www.cnitblog.com/schkui/archive/2006/11/01/18735.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Wed, 01 Nov 2006 09:39:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/01/18735.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18735.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/01/18735.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18735.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18735.html</trackback:ping><description><![CDATA[1. 几种内核调试工具比较<br /><br />kdb：只能在汇编代码级进行调试；<br />     优点是不需要两台机器进行调试。<br /><br />gdb：在调试模块时缺少一些至关重要的功能，它可用来查看内核的运行情况，包括反汇编内核函数。<br /><br />kgdb：能很方便的在源码级对内核进行调试，缺点是kgdb只能进行远程调试，它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)<br /><br />使用kdb和gdb调试内核的方法相对比较简单，这里只描述如何使用kgdb来调试内核。<br /><br />2.软硬件准备<br /><br />环境：<br />一台开发机developer(192.168.16.5 com1)，一台测试机target(192.168.16.30 com2)，都预装redhat 9；一根串口线<br /><br />下载以下软件包：<br />linux内核2.4.23         linux-2.4.23.tar.bz2<br />kgdb内核补丁1.9版       linux-2.4.23-kgdb-1.9.patch<br />可调试内核模块的gdb     gdbmod-1.9.bz2<br /><br />3.ok,开始<br /><br />3.1 测试串口线<br />物理连接好串口线后，使用一下命令进行测试，stty可以对串口参数进行设置<br /><br />在developer上执行：<br />stty ispeed 115200 ospeed 115200 -F /dev/ttyS0<br />echo hello &gt; /dev/ttyS0<br />在target上执行：<br />stty ispeed 115200 ospeed 115200 -F /dev/ttyS1<br />cat /dev/ttyS1<br /><br />串口线没问题的话在target的屏幕上显示hello<br /><br />3.2 安装与配置<br /><br />3.2.1 安装<br /><br />下载linux-2.4.23.tar.bz2，linux-2.4.23-kgdb-1.9.patch，gdbmod-1.9.bz2到developer的/home/liangjian目录<br /><br />*在developer机器上<br /><br />#cd /home/liangjian<br />#bunzip2 linux-2.4.23.tar.bz2<br />#tar -xvf linux-2.4.23.tar<br />#bunzip2 gdbmod-1.9.bz2<br />#cp gdbmod-1.9 /usr/local/bin<br />#cd linux-2.4.23<br />#patch -p1 &lt; ../linux-2.4.23-kgdb-1.9.patch<br />#make menuconfig<br /><br />在Kernel hacking配置项中将以下三项编译进内核<br />KGDB: Remote (serial) kernel debugging with gdb<br />KGDB: Thread analysis<br />KGDB: Console messages through gdb<br /><br />注意在编译内核的时候需要加上-g选项<br />#make dep;make bzImage<br /><br />使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具)<br />#scp arch/i386/boot/bzImage root@192.168.16.30:/boot/vmlinuz-2.4.23-kgdb<br />#scp System.map root@192.168.16.30:/boot/System.map-2.4.23-kgdb<br />#scp arch/i386/kernel/gdbstart  root@192.168.16.30:/sbin<br />gdbstart为kgdb提供的一个工具，用于激活内核钩子，使内核处于调试状态<br /><br />3.2.2 配置<br /><br />*在developer机器上<br /><br />在内核源码目录下编辑一文件.gdbinit(该文件用以对gdb进行初始化)，内容如下：<br />#vi .gdbinit<br />define rmt<br />set remotebaud 115200<br />target remote /dev/ttyS0<br />end<br />#<br />以上在.gdbinit中定义了一个宏rmt，该宏主要是设置使用的串口号和速率<br /><br />*在target机器上<br /><br />编辑/etc/grub.conf文件，加入以下行：<br />#vi /etc/grub.conf<br />title Red Hat Linux (2.4.23-kgdb)<br />    root (hd0,0)<br />    kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1<br />#<br /><br />在root目录下建立一个脚本文件debugkernel，内容如下：<br />#vi debug<br />#!/bin/bash<br />gdbstart -s 115200 -t /dev/ttyS1 &lt;&lt;EOF<br /><br />EOF<br />#chmod +x debugkernel<br />这个脚本主要是调用gdbstart程序设置target机上使用的串口及其速率，并使内核处于调试状态<br /><br />3.3 开始调试<br /><br />target上的内核或内核模块处于调试状态时，可以查看其变量、设置断点、查看堆栈等，并且是源码级的调试，和用gdb调试用户程序一样<br /><br />3.3.1 内核启动后调试<br /><br />*在target机器上<br /><br />重启系统，选择以 2.4.23-kgdb内核启动，启动完成后运行debugkenel，<br />这时内核将停止运行，在控制台屏幕上显示信息，并等待来自developer的<br />串口连接<br /><br />#./debug<br />About to activate GDB stub in the kernel on /dev/ttyS1<br />Waiting for connection from remote gdb...<br /><br />*在developer机器上<br /><br />#cd /home/liangjian/linux-2.4.23<br /># gdb vmlinux<br />GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)<br />Copyright 2003 Free Software Foundation, Inc.<br />GDB is free software, covered by the GNU General Public License, and you are<br />welcome to change it and/or distribute copies of it under certain conditions.<br />Type "show copying" to see the conditions.<br />There is absolutely no warranty for GDB.  Type "show warranty" for details.<br />This GDB was configured as "i386-redhat-linux-gnu"...<br /><br />执行rmt宏<br />(gdb) rmt<br />breakpoint () at kgdbstub.c:1005<br />1005                    atomic_set(&amp;kgdb_setting_breakpoint, 0);<br /><br />这时target上的内核处于调试状态，可以查看其变量、设置断点、查看堆栈等，和用gdb调试用户程序一样<br /><br />查看堆栈<br />(gdb) bt<br />#0  breakpoint () at kgdbstub.c:1005<br />#1  0xc0387f48 in init_task_union ()<br />#2  0xc01bc867 in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc0387f98) at<br />gdbserial.c:158<br />#3  0xc010937b in handle_IRQ_event (irq=3, regs=0xc0387f98, action=0xce5a9860)<br />at irq.c:452<br />#4  0xc0109597 in do_IRQ (regs=<br />      {ebx = -1072671776, ecx = -1, edx = -1070047232, esi = -1070047232, edi<br />= -1070047232, ebp = -1070039092, eax = 0, xds<br />= -1070071784, xes = -1070071784, orig_eax = -253, eip = -1072671729, xcs =<br />16, eflags = 582, esp = -1070039072, xss = -1072671582}) at irq.c:639<br />#5  0xc010c0e8 in call_do_IRQ ()<br /><br />查看jiffies变量的值<br />(gdb) p jiffies<br />$1 = 76153<br /><br />如果想让target上的内核继续运行，执行continue命令<br />(gdb) continue<br />Continuing.<br /><br />3.3.2 内核在引导时调试<br /><br />kgdb可以在内核引导时就对其进行调试，但并不是所有引导过程都是可调试的，如在kgdb 1.9版中，它在init/main.c的start_kernel()函数中插入以下代码：<br />start_kernel()<br />{<br />    ......<br />        smp_init();<br />#ifdef CONFIG_KGDB<br />        if (gdb_enter) {<br />                gdb_hook();             /* right at boot time */<br />        }<br />#endif<br />    ......<br />}<br /><br />所以在smp_init()之前的初始化引导过程是不能调试的。<br /><br />另外要想让target的内核在引导时就处于调试状态，需要修改其/etc/grub.conf文件为如下形式：<br />title Red Hat Linux (2.4.23-kgdb)<br />    root (hd0,0)<br />    kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1 gdb gdbttyS=1 gdbbaud=115200<br /><br />*在target机器上<br /><br />引导2.4.23-kgdb内核，内核将在短暂的运行后暂停并进入调试状态，打印如下信息：<br />Waiting for connection from remote gdb...<br /><br />*在developer机器上<br /><br />#cd /home/liangjian/linux-2.4.23<br /># gdb vmlinux<br />GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)<br />Copyright 2003 Free Software Foundation, Inc.<br />GDB is free software, covered by the GNU General Public License, and you are<br />welcome to change it and/or distribute copies of it under certain conditions.<br />Type "show copying" to see the conditions.<br />There is absolutely no warranty for GDB.  Type "show warranty" for details.<br />This GDB was configured as "i386-redhat-linux-gnu"...<br /><br />执行rmt宏<br />(gdb) rmt<br />breakpoint () at kgdbstub.c:1005<br />1005                    atomic_set(&amp;kgdb_setting_breakpoint, 0);<br /><br />查看当前堆栈<br />(gdb) bt<br />#0  breakpoint () at kgdbstub.c:1005<br />#1  0xc0387fe0 in init_task_union ()<br />#2  0xc01bc984 in gdb_hook () at gdbserial.c:250<br />#3  0xc0388898 in start_kernel () at init/main.c:443<br /><br />在do_basic_setup函数处设置断点，并让内核恢复运行<br />(gdb) b do_basic_setup<br />Breakpoint 1 at 0xc0388913: file current.h, line 9.<br />(gdb) continue<br />Continuing.<br />[New Thread 1]<br />[Switching to Thread 1]<br /><br />Breakpoint 1, do_basic_setup () at current.h:9<br />9               __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));<br /><br />内核在do_basic_setup断点处停止运行后查看当前堆栈<br />(gdb) bt<br />#0  do_basic_setup () at current.h:9<br />(gdb)<br /><br />3.3.3 内核模块调试调试<br /><br />要想调试内核模块，需要相应的gdb支持，kgdb的主页上提供了一个工具gdbmod，它修正了gdb 6.0在解析模块地址时的错误，可以用来正确的调试内核模块<br /><br />*在developer机器上<br /><br />写了个测试用的内核模块orig，如下：<br />void xcspy_func()<br />{<br />    printk("&lt;1&gt;xcspy_func\n");<br />    printk("&lt;1&gt;aaaaaaaaaaa\n");<br />}<br /><br />int xcspy_init()<br />{<br />    printk("&lt;1&gt;xcspy_init_module\n");<br />        <br />    return 0;<br />}<br /><br />void xcspy_exit()<br />{<br />    printk("&lt;1&gt;xcspy_cleanup_module\n");<br />}<br /><br />module_init(xcspy_init);<br />module_exit(xcspy_exit);<br /><br />编译该模块：<br />#cd /home/liangjian/lkm<br />#gcc -D__KERNEL__ -DMODULE -I/home/liangjian/linux-2.4.23/include -O -Wall -g -c -o orig.o orig.c<br />#scp orig.o root@192.168.16.30:/root<br /><br />开始调试：<br /># gdbmod vmlinux<br />GNU gdb 6.0<br />Copyright 2003 Free Software Foundation, Inc.<br />GDB is free software, covered by the GNU General Public License, and you are<br />welcome to change it and/or distribute copies of it under certain conditions.<br />Type "show copying" to see the conditions.<br />There is absolutely no warranty for GDB.  Type "show warranty" for details.<br />This GDB was configured as "i686-pc-linux-gnu"...<br /><br />设置符号文件的搜索路径<br />(gdb) set solib-search-path /home/liangjian/lkm<br /><br />执行rmt宏<br />(gdb) rmt<br />breakpoint () at kgdbstub.c:1005<br />1005                    atomic_set(&amp;kgdb_setting_breakpoint, 0);<br /><br />设置断点使得可以调试内核模块的init函数，查内核源码可知，内核是通过module.c文件的第566行(sys_init_module函数中)mod-&gt;init来调用模块的init函数的<br />(gdb) b module.c:566<br />Breakpoint 1 at 0xc011cd83: file module.c, line 566.<br />(gdb) c<br />Continuing.<br />[New Thread 1352]<br />[Switching to Thread 1352]<br /><br />这时在target机器上执行insmod orig.o，developer则相应的在断点处被暂停，如下<br />                                                                                                                          <br />Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",<br />mod_user=0x80904d8) at module.c:566<br />566             if (mod-&gt;init &amp;&amp; (error = mod-&gt;init()) != 0) {<br /><br />使用step命令进入模块的init函数<br />(gdb) step<br />xcspy_init () at orig.c:12<br />12              printk("&lt;1&gt;xcspy_init_module\n");<br />(gdb) n<br />15      }<br />(gdb)<br /><br />说明：<br />调
试内核模块的非init函数相对比较简单，只要先在target上执行insmod
orig.o，这时由于模块的符号被加载，可以直接在developer的gdb中对想调试的模块函数设置断点，如bt
xcspy_func，后面当xcspy_func被调用时就进入了调试状态。<br />如果想调试内核模块的init函数，由于在执行insmod之前模
块的符号还没有被加载，不能直接对模块的init函数设置断点，所以相对来说要困难一些。可以采用两种变通的方法：1,采用上面介绍的在内核调用模块的
init函数被调用之前的某处插入断点，如bt sys_init_module()或bt
module.c:566；2,在developer上让内核处于运行状态，在target上先执行一遍insmod
orig.o，这时orig.o的符号已经被加载到内存中，可以直接在developer的gdb中对模块的init函数设置断点，如bt
xcspy_init，然后在target上rmmod
orig.o，当下次在target上重新加载orig.o时就进入了调试状态，developer在xcspy_init处被暂停。
<p>原文链接：<a href="http://www.xfocus.net/articles/200509/820.html" target="_blank">http://www.xfocus.net/articles/200509/820.html</a></p><img src ="http://www.cnitblog.com/schkui/aggbug/18735.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-01 17:39 <a href="http://www.cnitblog.com/schkui/archive/2006/11/01/18735.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux2.6 驱动设计――从 2.4 到 2.6 </title><link>http://www.cnitblog.com/schkui/archive/2006/11/01/18734.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Wed, 01 Nov 2006 09:38:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/11/01/18734.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18734.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/11/01/18734.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18734.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18734.html</trackback:ping><description><![CDATA[
		<span class="postbody">Linux2.6 驱动设计――从 2.4 到 2.6
<br />
作者 Ray
<br /><br />
RTEMS版权所有，转载请注明来源ray@rtems" target="_blank"&gt;www.rtems.net，作者ray@rtems
<br /><br />
Linux 2.6 和 2.4 的比较我不想废话，总体来说 2.6 功能更强，但是资源消耗更多。 
<br /><br />
由于 2.6 内核在驱动框架，底层调用上和 2.4 内核有很多差别，所以本文主要是为程序员提供 2.4 到 2.6 迁移的指导。 
<br /><br />
2.6 和 2.4 主要的不同在于 
<br /><br />
? 内核的 API 变化，增加了不少新功能（例如 mem pool ） 
<br /><br />
? 提供 sysfs 用于描述设备树 
<br /><br />
? 驱动模块从 .o 变为 .ko 
<br /><br />
移植 hello word 
<br />
下面是一个最简单的 2.4 驱动： 
<br /><br />
#define MODULE 
<br />
#include &lt;linux/module.h&gt; 
<br />
#include &lt;linux/kernel.h&gt; 
<br />
int init_module(void) 
<br />
{ 
<br />
printk(KERN_INFO "Hello, world\n"); 
<br />
return 0; 
<br />
} 
<br />
void cleanup_module(void) 
<br />
{
<br />
printk(KERN_INFO "Goodbye cruel world\n"); 
<br />
} 
<br /><br />
2.6的hello world版本！ 
<br /><br />
#include &lt; linux/module.h&gt; 
<br />
#include &lt; linux/config.h&gt; 
<br />
#include &lt; linux/init.h&gt; 
<br />
MODULE_LICENSE("GPL");// 新，否则有 waring, 去掉了 #define MODULE, 自动定义 
<br />
static int hello_init(void) 
<br />
{ 
<br />
printk(KERN_ALERT "Hello, world\n"); 
<br />
return 0; 
<br />
} 
<br />
static void hello_exit(void) 
<br />
{ 
<br />
printk(KERN_ALERT "Goodbye, cruel world\n"); 
<br />
} 
<br />
module_init(hello_init);// 必须！！ 
<br />
module_exit(hello_exit); // 必须！！ 
<br /><br />
注意，在 2.4 中 module_init 不是必须的，只要驱动的初始化函数以及析沟函数命名使用了缺省的 init_module 和 cleanup_module 
<br /><br />
编译生成： 
<br />
2.4 
<br /><br />
gcc -D__KERNEL__ -DMODULE -I/usr/src/linux- 2.4.27 /include -O2 -c testmod.c 
<br /><br />
2.6 
<br /><br />
makefile 中要有 obj-m:= hello.o 
<br /><br />
然后： 
<br /><br />
make -C /usr/src/linux- 2.6.11 SUBDIRS=$PWD modules （当然简单的 make 也可以） 
<br /><br />
哈哈够简单！！ 
<br /><br />
其他不同： 
<br />
计数器： 
<br />
以前 2.4 内核使用 MOD_INC_USE_COUNT 增加计数例如： 
<br /><br />
void 
<br /><br />
inc_mod(void) 
<br /><br />
{ 
<br /><br />
MOD_INC_USE_COUNT; 
<br /><br />
} /* end inc_mod */ 
<br /><br /><br />
/************************************************************************ 
<br /><br />
* Calculator DEC 
<br /><br />
************************************************************************/ 
<br /><br />
void 
<br /><br />
dec_mod(void) 
<br /><br />
{ 
<br /><br />
MOD_DEC_USE_COUNT; 
<br /><br />
} /* end dec_mod */ 
<br /><br />
现在这个也过时了 !! 
<br /><br />
2.6 ，用户函数要加载模块，使用： 
<br /><br />
int try_module_get(&amp;module); 
<br /><br />
函数卸载模块使用 
<br /><br />
module_put() 
<br /><br />
而驱动中不必显示定义 inc 和 dec 
<br /><br />
没有 kdev_t 了 
<br />
现在都流行用 dev_t 了 , 而且是 32 位的 
<br /><br />
结构体初始化 
<br />
老版本使用： 
<br /><br />
static struct some_structure = { 
<br /><br />
field1: value, 
<br /><br />
field2: value 
<br /><br />
}; 
<br /><br />
现在流行用： 
<br /><br />
static struct some_structure = { 
<br /><br />
.field1 = value, 
<br /><br />
.field2 = value, 
<br /><br />
... 
<br /><br />
}; 
<br /><br />
malloc.h 
<br />
要用核态内存？用 &lt;linux/slab.h&gt; 好了，
<br /><br />
内存池
<br />
内存管理变化不大，添加了memory pool*(#include&lt;linux/mempool.h&gt;
)。（现在什么都是pool，内存 线程
....）这是为块设备提供的，使用mempool最大的优点是分配内存不会错，也不会等待（怎么这个也和RTEMS学）。对于embed的设备还是有些
用处的。标准调用方式：
<br /><br />
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data); 
<br /><br />
使用mempool的函数 ：
<br /><br />
mempool_alloc_t / mempool_free_t 
<br /><br />
mempool_alloc_slab / mempool_free_slab 
<br /><br />
mempool_alloc / mempool_free 
<br /><br />
mempool_resize 
<br /><br /><br /><br />
结构体赋值 
<br />
以前，驱动中结构体赋值使用： 
<br /><br />
static struct some_structure = { 
<br /><br />
field1: value, 
<br /><br />
field2: value 
<br /><br />
}; 
<br /><br /><br />
现在要用： 
<br /><br />
static struct some_structure = { 
<br /><br />
.field1 = value, 
<br /><br />
.field2 = value, 
<br /><br />
... 
<br /><br />
}; 
<br /><br />
例如 : 
<br /><br />
static struct file_operations yourdev_file_ops = {
<br /><br />
.open = yourdev_open, 
<br /><br />
.read = yourdev_read_file, 
<br /><br />
.write = yourdev_write_file, 
<br /><br />
}; 
<br /><br /><br />
min() ， max() 
<br />
不少程序员定义自己的 min 和 max 宏，大家也知道宏不安全， Linux 定义了 min 和 max 函数（注意不是模板函数，需要自己制定比较类型！） 
<br /><br />
原型变化 
<br />
call_usermodehelper() 
<br /><br />
request_module() 
<br /><br />
函数原型变化了，用到的赶快查 !!! 
<br /><br />
Devfs 
<br />
唉，还没怎么用，就过时了的技术，这就是 linux 。不是我们落伍，是世界变化快 
<br /><br /><br />
字符设备 
<br />
2.6 中主从设备编号不再局限于原来的 8bit 
<br /><br />
register_chrdev() 可以升级为： 
<br /><br />
int register_chrdev_region(dev_t from, // 设备号 unsigned count, // 注册号 char *name); // 名称 
<br /><br />
该函数的动态版本（不知道主设备号时使用） 
<br /><br />
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, 
<br /><br />
unsigned count, char *name); 
<br /><br /><br />
新的 file_operations 
<br />
register_chrdev_region 没有使用 register_chrdev_region 参数，因为设备现在使用 struct cdev 来定义他在 &lt;linux/cdev.h&gt; 中定义。 
<br /><br />
struct cdev { 
<br /><br />
struct kobject kobj; 
<br /><br />
struct module *owner; 
<br /><br />
struct file_operations *ops; 
<br /><br />
struct list_head list; 
<br /><br />
dev_t dev; 
<br /><br />
unsigned int count; 
<br /><br />
}; 
<br /><br />
使用时首先需要分配空间： struct cdev *cdev_alloc(void); 
<br /><br />
然后对其初始化： 
<br /><br />
void cdev_init(struct cdev *cdev, struct file_operations *fops); 
<br /><br />
cdev 使用 kobject_set_name 设置名称：例如： 
<br /><br />
struct cdev *my_cdev = cdev_alloc(); kobject_set_name(&amp;cdev-&gt;kobj, "my_cdev%d", devnum); 
<br /><br />
此后就可以使用 int cdev_add(struct cdev *cdev, dev_t dev, unsigned count); 将 cdev 加入到系统中。 
<br /><br />
删除 cdev 使用 
<br /><br />
void cdev_del(struct cdev *cdev); 
<br /><br />对于没有使用 cdev_add 添加的 cdev 使用 kobject_put(&amp;cdev-&gt;kobj); 删除
也就是使用： struct kobject *cdev_get(struct cdev *cdev); void
cdev_put(struct cdev *cdev); <br /><br />
太极链； 
<br />
struct inode -&gt; i_cdev -&gt; cdev 
<br /><br />
这样就从 inode 链接到 cdev 
<br /><br /><br />
驱动体系： 
<br />
/dev 到 /sys 
<br /><br />
/dev 也过时了，设备文件都跑到 /sys 里了！ 
<br /><br />
# cd /sys 
<br /><br />
# ls 
<br /><br />
block bus class devices firmware kernel module power 
<br /><br /><img src="http://www.linuxfans.org/nuke/modules/Forums/images/smiles/icon_neutral.gif" alt="Neutral" border="0" /><br />
知到了2.6内核的好处，很想在以后的开发中用2.6内核。但是不知道从2.4到2.6驱动的变化有多大，驱动是不是要自己一个个改？
<br />
我初学只能写些简单的驱动，或参考别人的驱动来开发。这种情况用2.6内核搞驱动是不是难度很大？
<br />
希望做过驱动移植的高手关照。</span>
<img src ="http://www.cnitblog.com/schkui/aggbug/18734.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-11-01 17:38 <a href="http://www.cnitblog.com/schkui/archive/2006/11/01/18734.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux内核解读入门</title><link>http://www.cnitblog.com/schkui/archive/2006/10/23/18269.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Mon, 23 Oct 2006 02:51:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/10/23/18269.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/18269.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/10/23/18269.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/18269.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/18269.html</trackback:ping><description><![CDATA[Linux内核解读入门 <br /><br /><br />作者: 火   发布日期: 2006-5-12    查看数: 178    出自: http://www.linuxdiyf.com <br /><br />针对好多Linux 爱好者对内核很有兴趣却无从下口，本文旨在介绍一种解读linux内核源码的入门方法，而不是解说linux复杂的内核机制；<br /><br />一．核心源程序的文件组织：<br /><br />1．
Linux核心源程序通常都安装在/usr/src/linux下，而且它有一个非常简单的编号约定：任何偶数的核心（例如2.0.30）都是一个稳定地
发行的核心，而任何奇数的核心（例如2.1.42）都是一个开发中的核心。本文基于稳定的2.2.5源代码，第二部分的实现平台为 Redhat
Linux 6.0。<br /><br />2．核心源程序的文件按树形结构进行组织，在源程序树的最上层你会看到这样一些目录：<br />●Arch ：arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构，例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录；<br /><br />●Include：
include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下，与 intel
cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录；<br /><br />●Init： 这个目录包含核心的初始化代码(注：不是系统的引导代码)，包含两个文件main.c和Version.c，这是研究核心如何工作的一个非常好的起点。<br /><br />●Mm ：这个目录包括所有独立于 cpu 体系结构的内存管理代码，如页式存储管理内存的分配和释放等；而和体系结构相关的内存管理代码则位于arch/*/mm/，例如arch/i386/mm/Fault.c<br /><br />●Kernel：主要的核心代码，此目录下的文件实现了大多数linux系统的内核函数，其中最重要的文件当属sched.c；同样，和体系结构相关的代码在arch/*/kernel中；<br /><br />●Drivers：
放置系统所有的设备驱动程序；每种驱动程序又各占用一个子目录：如，/block
下为块设备驱动程序，比如ide（ide.c）。如果你希望查看所有可能包含文件系统的设备是如何初始化的，你可以看
drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘，也初始化网络，因为安装nfs文件<br /><br />系统的时候需要网络其他：如, Lib放置核心的库代码； Net,核心与网络相关的代码； Ipc,这个目录包含核心的进程间通讯的代码；Fs ,所有的文件系统代码和各种类型的文件操作代码，它的每一个子目录支持一个文件系统，例如fat和ext2；<br /><br />●Scripts,
此目录包含用于配置核心的脚本文件等。一般，在每个目录下，都有一个 .depend 文件和一个 Makefile
文件，这两个文件都是编译时使用的辅助文件，仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助；而且，在有的目录下还有Readme
文件，它是对该目录下的文件的一些说明，同样有利于我们对内核源码的理解；<br /><br />二．解读实战：为你的内核增加一个系统调用<br /><br />虽
然，Linux 的内核源码用树形结构组织得非常合理、科学，把功能相关联的文件都放在同一个子目录下，这样使得程序更具可读性。然而，Linux
的内核源码实在是太大而且非常复杂，即便采用了很合理的文件组织方法，在不同目录下的文件之间还是有很多的关联，分析核心的一部分代码通常会要查看其它的
几个相关的文件，而且可能这些文件还不在同一个子目录下。体系的庞大复杂和文件之间关联的错综复杂，可能就是很多人对其望而生畏的主要原因。<br /><br />当
然，这种令人生畏的劳动所带来的回报也是非常令人着迷的：你不仅可以从中学到很多的计算机的底层的知识（如下面将讲到的系统的引导），体会到整个操作系统
体系结构的精妙和在解决某个具体细节问题时，算法的巧妙；而且更重要的是：在源码的分析过程中，你就会被一点一点地、潜移默化地专业化；甚至，只要分析十
分之一的代码后，你就会深刻地体会到，什么样的代码才是一个专业的程序员写的，什么样的代码是一个业余爱好者写的。<br /><br />为了使读者能更好的体会到这一特点，下面举了一个具体的内核分析实例，希望能通过这个实例，使读者对 Linux的内核的组织有些具体的认识，从中读者也可以学到一些对内核的分析方法。<br /><br />以下即为分析实例：<br /><br />【一】操作平台：<br /><br />硬件：cpu intel Pentium II ；<br />软件：Redhat Linux 6.0； 内核版本2.2.5<br /><br />【二】相关内核源代码分析：<br /><br />1．
系统的引导和初始化：Linux 系统的引导有好几种方式：常见的有 Lilo,
Loadin引导和Linux的自举引导（bootsect-loader）,而后者所对应源程序为
arch/i386/boot/bootsect.S，它为实模式的汇编程序，限于篇幅在此不做分析；无论是哪种引导方式，最后都要跳转到
arch/i386/Kernel/setup.S， setup.S主要是进行时模式下的初始化，为系统进入保护模式做准备；此后，系统执行
arch/i386/kernel/head.S (对经压缩后存放的内核要先执行
arch/i386/boot/compressed/head.S)； head.S 中定义的一段汇编程序setup_idt
，它负责建立一张256项的 idt 表(Interrupt Descriptor
Table),此表保存着所有自陷和中断的入口地址；其中包括系统调用总控程序 system_call
的入口地址；当然，除此之外，head.S还要做一些其他的初始化工作；<br /><br />2．系统初始化后运行的第一个内核程序asmlinkage
void __init start_kernel(void) 定义在/usr/src/linux/init/main.c中,它通过调用
usr/src/linux/arch/i386/kernel/traps.c 中的一个函数void __init
trap_init(void) 把各自陷和中断服务程序的入口地址设置到 idt
表中,其中系统调用总控程序system_cal就是中断服务程序之一；void __init trap_init(void)
函数则通过调用一个宏set_system_gate(SYSCALL_VECTOR,&amp;system_call)；把系统调用总控程序的入口挂
在中断0x80上；其中SYSCALL_VECTOR是定义在
/usr/src/linux/arch/i386/kernel/irq.h中的一个常量0x80； 而
system_call即为中断总控程序的入口地址；中断总控程序用汇编语言定义在
/usr/src/linux/arch/i386/kernel/entry.S中；<br /><br />3．中断总控程序主要负责保存处理机执行系统调用前的状态,检验当前调用是否合法, 并根据系统调用向量，使处理机跳转到保存在 sys_call_table 表中的相应系统服务例程的入口； 从系统服务例程返回后恢复处理机状态退回用户程序；<br />而
系统调用向量则定义在/usr/src/linux/include/asm-386/unistd.h中；sys_call_table
表定义在/usr/src/linux/arch/i386/kernel/entry.S 中； 同时在
/usr/src/linux/include/asm-386/unistd.h中也定义了系统调用的用户编程接口；<br /><br />4．由此可见 , linux 的系统调用也象 dos 系统的 int 21h 中断服务, 它把0x80 中断作为总的入口, 然后转到保存在 sys_call_table 表中的各种中断服务例程的入口地址 , 形成各种不同的中断服务；<br /><br />由以上源代码分析可知, 要增加一个系统调用就必须在 sys_call_table 表中增加一项 , 并在其中保存好自己的系统服务例程的入口地址,然后重新编译内核，当然，系统服务例程是必不可少的。<br /><br />由此可知在此版linux内核源程序&lt;2．2．5&gt;中,与系统调用相关的源程序文件就包括以下这些：<br />1．arch/i386/boot/bootsect.S<br />2．arch/i386/Kernel/setup.S<br />3．arch/i386/boot/compressed/head.S<br />4．arch/i386/kernel/head.S<br />5．init/main.c<br />6．arch/i386/kernel/traps.c<br />7．arch/i386/kernel/entry.S<br />8．arch/i386/kernel/irq.h<br />9．include/asm-386/unistd.h<br />当然，这只是涉及到的几个主要文件。而事实上，增加系统调用真正要修改文件只有include/asm-386/unistd.h和arch/i386/kernel/entry.S两个；<br /><br />【三】 对内核源码的修改：<br /><br />1．在kernel/sys.c中增加系统服务例程如下：<br /><br />asmlinkage int sys_addtotal(int numdata)<br />{<br />int i=0,enddata=0；<br />while(i&lt;=numdata)<br />enddata+=i++；<br />return enddata；<br />}<br />该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值； 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中，只是要在相应文件中作必要的说明；<br /><br />2．把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中：<br /><br />arch/i386/kernel/entry.S 中的最后几行源代码修改前为：<br />．．． ．．．<br />.long SYMBOL_NAME(sys_sendfile)<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */<br />.long SYMBOL_NAME(sys_vfork) /* 190 */<br />.rept NR_syscalls-190<br />.long SYMBOL_NAME(sys_ni_syscall)<br />.endr<br />修改后为：．．． ．．．<br />.long SYMBOL_NAME(sys_sendfile)<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */<br />.long SYMBOL_NAME(sys_vfork) /* 190 */<br />/* add by I */<br />.long SYMBOL_NAME(sys_addtotal)<br />.rept NR_syscalls-191<br />.long SYMBOL_NAME(sys_ni_syscall)<br />.endr<br /><br />3． 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用：<br /><br />增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下：<br />．．． ．．．<br />#define __NR_sendfile 187<br />#define __NR_getpmsg 188<br />#define __NR_putpmsg 189<br />#define __NR_vfork 190<br />/* add by I */<br />#define __NR_addtotal 191<br /><br />4．测试程序(test.c)如下：<br /><br />#include<br />#include<br />_syscall1(int,addtotal,int, num)<br />main()<br />{<br />int i,j；<br /><br />do<br />printf("Please input a number<br />")；<br />while(scanf("%d",&amp;i)==EOF)；<br />if((j=addtotal(i))==-1)<br />printf("Error occurred in syscall-addtotal()；<br />")；<br />printf("Total from 0 to %d is %d<br />",i,j)；<br />}<br />对修改后的新的内核进行编译，并引导它作为新的操作系统，运行几个程序后可以发现一切正常；在新的系统下对测试程序进行编译(*注：由于原内核并未提供此系统调用，所以只有在编译后的新内核下，此测试程序才能可能被编译通过)，运行情况如下：<br />$gcc -o test test.c<br />$./test<br />Please input a number<br />36<br />Total from 0 to 36 is 666<br />可见，修改成功；<br />而且，对相关源码的进一步分析可知，在此版本的内核中,从/usr/src/linux/arch/i386/kernel/entry.S<br />文件中对 sys_call_table 表的设置可以看出,有好几个系统调用的服务例程都是定义在/usr/src/linux/kernel/sys.c 中的同一个函数：<br />asmlinkage int sys_ni_syscall(void)<br />{<br />return -ENOSYS；<br />}<br />例如第188项和第189项就是如此：<br />．．． ．．．<br />.long SYMBOL_NAME(sys_sendfile)<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */<br />.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */<br />.long SYMBOL_NAME(sys_vfork) /* 190 */<br />．．． ．．．<br />而这两项在文件 /usr/src/linux/include/asm-386/unistd.h 中却申明如下：<br />．．． ．．．<br />#define __NR_sendfile 187<br />#define __NR_getpmsg 188 /* some people actually want streams */<br />#define __NR_putpmsg 189 /* some people actually want streams */<br />#define __NR_vfork 190<br /><br />由
此可见,在此版本的内核源代码中,由于asmlinkage int sys_ni_syscall(void) 函数并不进行任何操作,所以包括
getpmsg, putpmsg
在内的好几个系统调用都是不进行任何操作的，即有待扩充的空调用；但它们却仍然占用着sys_call_table表项，估计这是设计者们为了方便扩充系
统调用而安排的；所以只需增加相应服务例程（如增加服务例程getmsg或putpmsg），就可以达到增加系统调用的作用。<br /><br />结语：当然对于庞大复杂的 linux 内核而言，一篇文章远远不够，而且与系统调用相关的代码也只是内核中极其微小的一部分；但重要的是方法、掌握好的分析方法；所以上的分析只是起个引导的作用，而正真的分析还有待于读者自己的努力。 <img src ="http://www.cnitblog.com/schkui/aggbug/18269.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-10-23 10:51 <a href="http://www.cnitblog.com/schkui/archive/2006/10/23/18269.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 系统内核的调试 </title><link>http://www.cnitblog.com/schkui/archive/2006/06/15/12310.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Thu, 15 Jun 2006 09:48:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/06/15/12310.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/12310.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/06/15/12310.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/12310.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/12310.html</trackback:ping><description><![CDATA[
		<div>调试是软件开发过程中一个必不可少的环节，在 Linux 内核开发的过程中也不可避免地会面对如何调试内核的问题。但是，Linux 系统的开发者出于保证内核代码正确性的考虑，不愿意在 Linux 内核源代码树中加入一个调试器。他们认为内核中的调试器会误导开发者，从而引入不良的修正[1].所以对 Linux 内核进行调试一直是个令内核程序员感到棘手的问题，调试工作的艰苦性是内核级的开发区别于用户级开发的一个显著特点。 
<p>    尽管缺乏一种内置的调试内核的有效方法，但是 Linux 系统在内核发展的过程中也逐渐形成了一些监视内核代码和错误跟踪的技术。同时，许多的补丁程序应运而生，它们为标准内核附加了内核调试的支持。尽管这些补丁有些并不被 Linux 官方组织认可，但他们确实功能完善，十分强大。调试内核问题时，利用这些工具与方法跟踪内核执行情况，并查看其内存和数据结构将是非常有用的。</p><p>    本文将首先介绍 Linux 内核上的一些内核代码监视和错误跟踪技术，这些调试和跟踪方法因所要求的使用环境和使用方法而各有不同，然后重点介绍三种 Linux 内核的源代码级的调试方法。</p><p><strong>    1. Linux 系统内核级软件的调试技术</strong></p><p>    printk（） 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk（） 调试调用，可以直接把所关心的信息打打印到屏幕上，从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器（Linux kernel debugger，kdb）是 Linux 内核的补丁，它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍，大家可以参考。 Kprobes 提供了一个强行进入任何内核例程，并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息，而无需对Linux内核频繁编译和启动，具体使用方法，请参考使用 Kprobes 调试内核。</p><p>    以上介绍了进行Linux内核调试和跟踪时的常用技术和方法。当然，内核调试与跟踪的方法还不止以上提到的这些。这些调试技术的一个共同的特点在于，他们都不能提供源代码级的有效的内核调试手段，有些只能称之为错误跟踪技术，因此这些方法都只能提供有限的调试能力。下面将介绍三种实用的源代码级的内核调试方法。</p><p><strong>    2. 使用KGDB构建Linux内核调试环境</strong></p><p>    kgdb提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样，在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器，一台作为开发机（Development Machine），另一台作为目标机（Target Machine），两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆，在其内部两端的第2脚（TXD）与第3脚（RXD）交叉相连，第7脚（接地脚）直接相连。调试过程中，被调试的内核运行在目标机上，gdb调试器运行在开发机上。</p><p>    目前，kgdb发布支持i386、x86_64、32-bit PPC、SPARC等几种体系结构的调试器。有关kgdb补丁的下载地址见参考资料[4].</p><p>    2.1 kgdb的调试原理</p><p>    安装kgdb调试环境需要为Linux内核应用kgdb补丁，补丁实现的gdb远程调试所需要的功能包括命令处理、陷阱处理及串口通讯3个主要的部分。kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub.调试Stub是Linux内核中的一小段代码，提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议，包含了各种调试命令。当设置断点时，kgdb负责在设置断点的指令前增加一条trap指令，当执行到断点时控制权就转移到调试stub中去。此时，调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb，然后从gdb处接受命令。gdb命令告诉stub下一步该做什么，当stub收到继续执行的命令时，将恢复程序的运行环境，把对CPU的控制权重新交还给内核。</p><p></p><p><br /></p><p align="center"><img height="224" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104831373.gif" width="450" border="0" twffan="done" /></p><p><br /></p><p><span class="smalltitle" twffan="done">2．2 Kgdb的安装与设置</span></p><p>下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。</p><p>2.2.1软硬件准备 </p><p>以下软硬件配置取自笔者进行试验的系统配置情况：</p><p><br /><img height="215" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104832664.gif" width="589" border="0" twffan="done" /><br /></p><p>kgdb补丁的版本遵循如下命名模式：Linux-A-kgdb-B，其中A表示Linux的内核版本号，B为kgdb的版本号。以试验使用的kgdb补丁为例，linux内核的版本为linux-2.6.7，补丁版本为kgdb-2.2。</p><p>物理连接好串口线后，使用以下命令来测试两台机器之间串口连接情况，stty命令可以对串口参数进行设置：</p><p>在development机上执行：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
</pre></td></tr></tbody></table><br /><p>在target机上执行：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
</pre></td></tr></tbody></table><br /><p>在developement机上执行：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>echo hello &gt; /dev/ttyS0
</pre></td></tr></tbody></table><br /><p>在target机上执行：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>cat /dev/ttyS0
</pre></td></tr></tbody></table><p>如果串口连接没问题的话在将在target机的屏幕上显示"hello"。</p><p>2．2．2 安装与配置 </p><p>下面我们需要应用kgdb补丁到Linux内核，设置内核选项并编译内核。这方面的资料相对较少，笔者这里给出详细的介绍。下面的工作在开发机（developement）上进行，以上面介绍的试验环境为例，某些具体步骤在实际的环境中可能要做适当的改动：</p><p>I、内核的配置与编译</p><p> </p><p> </p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2
[root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
[root@lisl tmp]#cd inux-2.6.7
</pre></td></tr></tbody></table><br /><p>请参照目录补丁包中文件README给出的说明，执行对应体系结构的补丁程序。由于试验在i386体系结构上完成，所以只需要安装一下补丁：core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时，请遵循kgdb软件包内series文件所指定的顺序，否则可能会带来预想不到的问题。eth.patch文件是选择以太网口作为调试的连接端口时需要运用的补丁</p><p>。 </p><p>应用补丁的命令如下所示：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]#patch -p1 &lt;../linux-2.6.7-kgdb-2.2/core-lite.patch 
</pre></td></tr></tbody></table><br /><p>如果内核正确，那么应用补丁时应该不会出现任何问题（不会产生*.rej文件）。为Linux内核添加了补丁之后，需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]#make menuconfig
</pre></td></tr></tbody></table><br /><p>在内核配置菜单的Kernel hacking选项中选择kgdb调试项，例如：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>  [*] KGDB: kernel debugging with remote gdb                                                             
       Method for KGDB communication (KGDB: On generic serial port (8250))  ---&gt;  
  [*] KGDB: Thread analysis                                                                            
  [*] KGDB: Console messages through gdb
[root@lisl tmp]#make
  </pre></td></tr></tbody></table><br /><p>编译内核之前请注意Linux目录下Makefile中的优化选项，默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下，编译器要对内核中的某些代码的执行顺序进行改动，所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉-O，否则编译会出问题。为了使编译后的内核带有调试信息，注意在编译内核的时候需要加上-g选项。</p><p>不过，当选择"Kernel debugging-&gt;Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外，选择"kernel debugging with remote gdb"后，配置系统将自动打开"Compile the kernel with debug info"选项。</p><p>内核编译完成后，使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具，如rcp)。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]#scp arch/i386/boot/bzImage root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb
[root@lisl tmp]#scp System.map root@192.168.6.13:/boot/System.map-2.6.7-kgdb
</pre></td></tr></tbody></table><br /><p>如果系统启动使所需要的某些设备驱动没有编译进内核的情况下，那么还需要执行如下操作：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7
[root@lisl tmp]#scp initrd-2.6.7-kgdb root@192.168.6.13:/boot/ initrd-2.6.7-kgdb
</pre></td></tr></tbody></table><br /><p>II、kgdb的启动</p><p>在将编译出的内核拷贝的到target机器之后，需要配置系统引导程序，加入内核的启动选项。以下是kgdb内核引导参数的说明：</p><p><br /><img height="309" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104833364.gif" width="584" border="0" twffan="done" /><br /></p><p>如表中所述，在kgdb 2.0版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时，直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
</pre></td></tr></tbody></table><br /><p>在使用lilo作为引导程序时，需要把kgdb参放在由append修饰的语句中。下面给出使用lilo作为引导器时的配置示例。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>image=/boot/vmlinuz-2.6.7-kgdb
label=kgdb
    read-only
    root=/dev/hda3
append="gdb gdbttyS=1 gdbbaud=115200"
</pre></td></tr></tbody></table><br /><p>保存好以上配置后重新启动计算机，选择启动带调试信息的内核，内核将在短暂的运行后在创建init内核线程之前停下来，打印出以下信息，并等待开发机的连接。</p><p>Waiting for connection from remote gdb...</p><p>在开发机上执行：</p><p><br /></p><p></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>gdb
file vmlinux
set remotebaud 115200
target remote /dev/ttyS0
</pre></td></tr></tbody></table><p>其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接，它是没有经过压缩的内核文件，gdb程序从该文件中得到各种符号地址信息。</p><p>这样，就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后，对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行，进行具体的调试工作。</p><p>在kgdb 2.0之前的版本中，编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下，此时无需更改内核的启动配置文件，直接使用命令：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
</pre></td></tr></tbody></table><br /><p>可以在KGDB内核引导启动完成后建立开发机与目标机之间的调试联系。</p><p>2．2．3 通过网络接口进行调试 </p><p>kgdb也支持使用以太网接口作为调试器的连接端口。在对Linux内核应用补丁包时，需应用eth.patch补丁文件。配置内核时在Kernel hacking中选择kgdb调试项，配置kgdb调试端口为以太网接口，例如：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[*]KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On ethernet)  ---&gt; 
( ) KGDB: On generic serial port (8250)
(X) KGDB: On ethernet
</pre></td></tr></tbody></table><br /><p>另外使用eth0网口作为调试端口时，grub.list的配置如下：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.
5.13/,@192.168. 6.13/ 
</pre></td></tr></tbody></table><br /><p>    其他的过程与使用串口作为连接端口时的设置过程相同。</p><p>    注意：尽管可以使用以太网口作为kgdb的调试端口，使用串口作为连接端口更加简单易行，kgdb项目组推荐使用串口作为调试端口。</p><p>    2.2.4 模块的调试方法</p><p>    内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的，所以develop机的gdb无法得到各种符号地址信息。所以，使用kgdb调试模块所需要解决的一个问题是，需要通过某种方法获得可加载模块的最终加载地址信息，并把这些信息加入到gdb环境中。</p><p>    I、在Linux 2.4内核中的内核模块调试方法</p><p>    在Linux2.4.x内核中，可以使用insmod -m命令输出模块的加载信息，例如：</p><p></p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]# insmod -m hello.ko &gt;modaddr
</pre></td></tr></tbody></table><br /><p>    查看模块加载信息文件modaddr如下：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>.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
……
</pre></td></tr></tbody></table><br /><p>    在这些信息中，我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s 
.rodata 0xc88d80a0 -s .bss 0x c88d833c
</pre></td></tr></tbody></table><br /><p>    这种方法也存在一定的不足，它不能调试模块初始化的代码，因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址，更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。</p><p>    在target机上用上述方法得到模块加载的地址信息，然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中，在内核代码的调用初始化代码之前设置断点。这样，在target机上再次插入模块时，代码将在执行模块初始化之前停下来，这样就可以使用gdb命令调试模块初始化代码了。</p><p>    另外一种调试模块初始化函数的方法是：当插入内核模块时，内核模块机制将调用函数sys_init_module（kernel/modle.c）执行对内核模块的初始化，该函数将调用所插入模块的初始化函数。程序代码片断如下：</p><p></p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>……	……
	if (mod-&gt;init != NULL)
		ret = mod-&gt;init();
……	……
</pre></td></tr></tbody></table><p>在该语句上设置断点，也能在执行模块初始化之前停下来。</p><p>    II、在Linux 2.6.x内核中的内核模块调试方法</p><p>    Linux 2.6之后的内核中，由于module-init-tools工具的更改，insmod命令不再支持-m参数，只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式，我们知道程序中各段的意义如下：</p><p>    。text（代码段）：用来存放可执行文件的操作指令，也就是说是它是可执行程序在内存种的镜像。</p><p>    。data（数据段）：数据段用来存放可执行文件中已初始化全局变量，也就是存放程序静态分配的变量和全局变量。</p><p>    。bss（BSS段）：BSS段包含了程序中未初始化全局变量，在内存中 bss段全部置零。</p><p>    。rodata（只读段）：该段保存着只读数据，在进程映象中构造不可写的段。</p><p>    通过在模块初始化函数中放置一下代码，我们可以很容易地获得模块加载到内存中的地址。</p><p></p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>……
int bss_var;
static int hello_init(void)
{
printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init);

static int data_var=0;
printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&amp;data_var);

printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&amp;bss_var);
……
}
Module_init(hello_init);
</pre></td></tr></tbody></table><br /><p>    这里，通过在模块的初始化函数中添加一段简单的程序，使模块在加载时打印出在内核中的加载地址。。rodata段的地址可以通过执行命令readelf -e hello.ko，取得。rodata在文件中的偏移量并加上段的align值得出。</p><p>    为了使读者能够更好地进行模块的调试，kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似，更多的信息请阅读参考资料[4].</p><p>    2.2.5 硬件断点</p><p>    kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点：执行断点（Execution Breakpoint）、写断点（Write Breakpoint）、访问断点（Access Breakpoint）但不支持I/O访问的断点。目前，kgdb对硬件断点的支持是通过宏来实现的，最多可以设置4个硬件断点，这些宏的用法如下：</p><p></p><p><br /></p><p align="center"><img height="222" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104834837.gif" width="513" border="0" twffan="done" /></p><p><br /></p><p>    在有些情况下，硬件断点的使用对于内核的调试是非常方便的。有关硬件断点的定义和具体的使用说明见参考资料[4]</p><p>    。</p><p>    2.3.在VMware中搭建调试环境</p><p>    kgdb调试环境需要使用两台微机分别充当development机和target机，使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例，创建两台虚拟机，一台作为开发机，一台作为目标机。</p><p>    2.3.1虚拟机之间的串口连接</p><p>    虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上，例如开发机连接到COM1，目标机连接到COM2，然后把两个串口通过串口线相连接。另一种更为简便的方法是：在较高一些版本的VMware中都支持把串口映射到命名管道，把两个虚拟机的串口映射到同一个命名管道。例如，在两个虚拟机中都选定同一个命名管道 <a href="file://./pipe/com_1"><font color="#000000">\\.\pipe\com_1</font></a>，指定target机的COM口为server端，并选择"The other end is a virtual machine"属性；指定development机的COM口端为client端，同样指定COM口的"The other end is a virtual machine"属性。对于IO mode属性，在target上选中"Yield CPU on poll"复选择框，development机不选。这样，可以无需附加任何硬件，利用虚拟机就可以搭建kgdb调试环境。即降低了使用kgdb进行调试的硬件要求，也简化了建立调试环境的过程。</p><p><br /><img height="424" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104835739.jpg" width="600" border="0" twffan="done" /><br /></p><p>    2.3.2 VMware的使用技巧</p><p>    VMware虚拟机是比较占用资源的，尤其是象上面那样在Windows中使用两台虚拟机。因此，最好为系统配备512M以上的内存，每台虚拟机至少分配128M的内存。这样的硬件要求，对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑，在VMware中尽量使用字符界面进行调试工作。同时，Linux系统默认情况下开启了sshd服务，建议使用SecureCRT登陆到Linux进行操作，这样可以有较好的用户使用界面。</p><p>    2.3.3 在Linux下的虚拟机中使用kgdb</p><p>    对于在Linux下面使用VMware虚拟机的情况，笔者没有做过实际的探索。从原理上而言，只需要在Linux下只要创建一台虚拟机作为target机，开发机的工作可以在实际的Linux环境中进行，搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机，所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。（vmware已经推出了Linux下的版本）还可以在development机上配合使用一些其他的调试工具，例如功能更强大的cgdb、图形界面的DDD调试器等，以方便内核的调试工作。</p><p></p><p><br /><img height="463" alt="" src="http://linux.chinaitlab.com/UploadFiles_7565/200605/20060524104845850.jpg" width="527" border="0" twffan="done" /><br /></p><p><span class="smalltitle" twffan="done">    2.4 kgdb的一些特点和不足</span></p><p><span class="smalltitle" twffan="done">    使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高，必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境，但是对系统其他方面的性能也提出了一定的要求，同时也增加了搭建调试环境时复杂程度。另外，kgdb内核的编译、配置也比较复杂，需要一定的技巧，笔者当时做的时候也是费了很多周折。当调试过程结束后时，还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试，也就是说kgdb并不能用于调试系统一开始的初始化引导过程。</span></p><p><span class="smalltitle" twffan="done">    不过，kgdb是一个不错的内核调试工具，使用它可以进行对内核的全面调试，甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下，对内核的调试将更方便。</span></p><p><span class="smalltitle" twffan="done">3. 使用SkyEye构建Linux内核调试环境</span></p><p><span class="smalltitle" twffan="done">    SkyEye是一个开源软件项目（OPenSource Software），SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台，可以模拟多种嵌入式开发板，支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目，它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后，它就可以来仿真嵌入式开发板，在它上面不仅可以调试硬件驱动，还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。</span></p><p><span class="smalltitle" twffan="done">    3.1 SkyEye的安装和μcLinux内核编译</span></p><p><span class="smalltitle" twffan="done">    3.1.1 SkyEye的安装</span></p><p><span class="smalltitle" twffan="done">    SkyEye的安装不是本文要介绍的重点，目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11].由于skyeye面目主要用于嵌入式系统领域，所以在skyeye上经常使用的是μcLinux系统，当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μcLinux 2.6在skyeye上编译的相关资料并不多，所以下面进行详细介绍。</span></p><p><span class="smalltitle" twffan="done">    3.1.2 μcLinux 2.6.x的编译</span></p><p><span class="smalltitle" twffan="done">    要在SkyEye中调试操作系统内核，首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此，正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板，并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。</span></p><p><span class="smalltitle" twffan="done">    I、安装交叉编译环境</span></p><p><span class="smalltitle" twffan="done">    先安装交叉编译器。尽管在一些资料中说明使用工具链arm-elf-tools-20040427.sh ，但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同，经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是：arm-uclinux-tools-base-gcc3.4.0-20040713.sh，关于该交叉编译工具链的下载地址请参见[6].注意以下步骤最好用root用户来执行。</span></p><p></p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]#chmod +x  arm-uclinux-tools-base-gcc3.4.0-20040713.sh
[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh
</pre></td></tr></tbody></table><br /><p>    安装交叉编译工具链之后，请确保工具链安装路径存在于系统PATH变量中。</p><p>    II、制作μcLinux内核</p><p>    得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7].该站点发布的内核版本可能不是最新的，但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里，将使用这种方法来制作最新的μcLinux内核。目前（笔者记录编写此文章时），所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz.</p><p>    下载uClinux-dist.20041215.tar.gz，文件的下载地址请参见[7].</p><p>    下载linux-2.6.9-hsc0.patch.gz，文件的下载地址请参见[8].</p><p>    下载linux-2.6.9.tar.bz2，文件的下载地址请参见[9].</p><p>    现在我们得到了整个的linux-2.6.9源代码，以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。</p><p></p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2
[root@lisl uClinux-dist]# tar -jxvf  linux-2.6.9.tar.bz2
[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0 
</pre></td></tr></tbody></table><br /><p>    或者使用：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz 
[root@lisl uClinux-dist]patch -p0 &lt; linux-2.6.9-hsc0.patch
</pre></td></tr></tbody></table><br /><p>    执行以上过程后，将在linux-2.6.9/arch目录下生成一个补丁目录－armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0)，并将我们打好补丁的Linux内核目录更名为linux-2.6.x。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl uClinux-dist]# rm -rf linux-2.6.x/
[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
</pre></td></tr></tbody></table><br /><p>    III、配置和编译μcLinux内核</p><p>    因为只是出于调试μcLinux内核的目的，这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时，已经预置了某些常用嵌入式开发板的配置文件，因此这里直接使用这些配置文件，过程如下：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl uClinux-dist]# cd linux-2.6.x
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_
deconfig
</pre></td></tr></tbody></table><br /><p>    atmel_deconfig文件是μcLinux发布时提供的一个配置文件，存放于目录linux-2.6.x /arch/armnommu/configs/中。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux-
oldconfig
</pre></td></tr></tbody></table><br /><p>    下面编译配置好的内核：</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
</pre></td></tr></tbody></table><br /><p>    一般情况下，编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核，需要打开内核编译的调试选项-g，使编译后的内核带有调试信息。打开编译选项的方法可以选择：</p><p>    "Kernel debugging-&gt;Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件，为其打开调试开关。方法如下：。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>CFLAGS  += -g 
</pre></td></tr></tbody></table><br /><p>    最容易出现的问题是找不到arm-uclinux-gcc命令的错误，主要原因是PATH变量中没有包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下，它的安装目录是/root/bin/arm-linux-tool/，使用以下命令将路径加到PATH环境变量中。</p><p><br /></p><table cellspacing="0" cellpadding="5" width="100%" bgcolor="#eeeeee" border="1"><tbody><tr><td><pre>Export PATH＝$PATH:/root/bin/arm-linux-tool/bin
</pre></td></tr></tbody></table><br /><p>    IV、根文件系统的制作</p><p>    Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑，这里不打算介绍根文件系统的制作方法，读者可以查阅一些其他的相关资料。值得注意的是，由配置文件skyeye.conf指定了装载到内核中的根文件系统。</p><p>    3.2 使用SkyEye调试</p><p>    编译完μcLinux内核后，就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。</p><p>    需要提醒读者的是，SkyEye的配置文件－skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件，很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时，请首先检查该配置文件然后再进行其他的工作。此时，所有的准备工作已经完成，就可以进行内核的调试工作了。</p><p>    3.3使用SkyEye调试内核的特点和不足</p><p>    在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU，因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外，制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过，当调试过程结束后无需重新制作所要发布的内核。</p><p>    SkyEye只是对系统硬件进行了一定程度上的模拟，所以在SkyEye与真实硬件环境相比较而言还是有一定的差距，这对一些与硬件紧密相关的调试可能会有一定的影响，例如驱动程序的调试。不过对于大部分软件的调试，SkyEye已经提供了精度足够的模拟了。</p><p>    SkyEye的下一个目标是和eclipse结合，有了图形界面，能为调试和查看源码提供一些方便。</p><p>    4. 使用UML调试Linux内核</p><p>    User-mode Linux（UML）简单说来就是在Linux内运行的Linux.该项目是使Linux内核成为一个运行在 Linux 系统之上单独的、用户空间的进程。UML并不是运行在某种新的硬件体系结构之上，而是运行在基于 Linux 系统调用接口所实现的虚拟机。正是由于UML是一个将Linux作为用户空间进程运行的特性，可以使用UML来进行操作系统内核的调试。有关UML的介绍请查阅参考资料[10]、[12].</p><p>    4.1 UML的安装与调试</p><p>    UML的安装需要一台运行Linux 2.2.15以上，或者2.3.22以上的I386机器。对于2.6.8及其以前版本的UML，采用两种形式发布：一种是以RPM包的形式发布，一种是以源代码的形式提供UML的安装。按照UML的说明，以RPM形式提供的安装包比较陈旧且会有许多问题。以二进制形式发布的UML包并不包含所需要的调试信息，这些代码在发布时已经做了程度不同的优化。所以，要想利用UML调试Linux系统内核，需要使用最新的UML patch代码和对应版本的Linux内核编译、安装UML.完成UML的补丁之后，会在arch目录下产生一个um目录，主要的UML代码都放在该目录下。</p><p>    从2.6.9版本之后（包含2.6.9版本的Linux），User-Mode Linux已经随Linux内核源代码树一起发布，它存放于arch/um目录下。</p><p>    编译好UML的内核之后，直接使用gdb运行已经编译好的内核即可进行调试。</p><p>    4.2使用UML调试系统内核的特点和不足</p><p>    目前，用户模式 Linux 虚拟机也存在一定的局限性。由于UML虚拟机是基于Linux系统调用接口的方式实现的虚拟机，所以用户模式内核不能访问主机系统上的硬件设备。因此，UML并不适合于调试那些处理实际硬件的驱动程序。不过，如果所编写的内核程序不是硬件驱动，例如Linux文件系统、协议栈等情况，使用UML作为调试工具还是一个不错的选择。</p><p><strong>5. 内核调试配置选项</strong></p><p>    为了方便调试和测试代码，内核提供了许多与内核调试相关的配置选项。这些选项大部分都在内核配置编辑器的内核开发（kernel hacking）菜单项中。在内核配置目录树菜单的其他地方也还有一些可配置的调试选项，下面将对他们作一定的介绍。</p><p>    Page alloc debugging ：CONFIG_DEBUG_PAGEALLOC：</p><p>    不使用该选项时，释放的内存页将从内核地址空间中移出。使用该选项后，内核推迟移出内存页的过程，因此能够发现内存泄漏的错误。</p><p>    Debug memory allocations ：CONFIG_DEBUG_SLAB：</p><p>    该打开该选项时，在内核执行内存分配之前将执行多种类型检查，通过这些类型检查可以发现诸如内核过量分配或者未初始化等错误。内核将会在每次分配内存前后时设置一些警戒值，如果这些值发生了变化那么内核就会知道内存已经被操作过并给出明确的提示，从而使各种隐晦的错误变得容易被跟踪。</p><p>    Spinlock debugging ：CONFIG_DEBUG_SPINLOCK：</p><p>    打开此选项时，内核将能够发现spinlock未初始化及各种其他的错误，能用于排除一些死锁引起的错误。</p><p>    Sleep-inside-spinlock checking：CONFIG_DEBUG_SPINLOCK_SLEEP：</p><p>    打开该选项时，当spinlock的持有者要睡眠时会执行相应的检查。实际上即使调用者目前没有睡眠，而只是存在睡眠的可能性时也会给出提示。</p><p>    Compile the kernel with debug info ：CONFIG_DEBUG_INFO：</p><p>    打开该选项时，编译出的内核将会包含全部的调试信息，使用gdb时需要这些调试信息。</p><p>    Stack utilization instrumentation ：CONFIG_DEBUG_STACK_USAGE：</p><p>    该选项用于跟踪内核栈的溢出错误，一个内核栈溢出错误的明显的现象是产生oops错误却没有列出系统的调用栈信息。该选项将使内核进行栈溢出检查，并使内核进行栈使用的统计。</p><p>    Driver Core verbose debug messages：CONFIG_DEBUG_DRIVER：</p><p>    该选项位于"Device drivers-&gt; Generic Driver Options"下，打开该选项使得内核驱动核心产生大量的调试信息，并将他们记录到系统日志中。</p><p>    Verbose SCSI error reporting （kernel size +=12K） ：CONFIG_SCSI_CONSTANTS：</p><p>    该选项位于"Device drivers/SCSI device support"下。当SCSI设备出错时内核将给出详细的出错信息。</p><p>    Event debugging：CONFIG_INPUT_EVBUG：</p><p>    打开该选项时，会将输入子系统的错误及所有事件都输出到系统日志中。该选项在产生了详细的输入报告的同时，也会导致一定的安全问题。</p><p>    以上内核编译选项需要读者根据自己所进行的内核编程的实际情况，灵活选取。在使用以上介绍的三种源代码级的内核调试工具时，一般需要选取CONFIG_DEBUG_INFO选项，以使编译的内核包含调试信息。</p><p><strong>    6. 总结</strong></p><p>    上面介绍了一些调试Linux内核的方法，特别是详细介绍了三种源代码级的内核调试工具，以及搭建这些内核调试环境的方法，读者可以根据自己的情况从中作出选择。</p><p>    调试工具（例如gdb）的运行都需要操作系统的支持，而此时内核由于一些错误的代码而不能正确执行对系统的管理功能，所以对内核的调试必须采取一些特殊的方法进行。以上介绍的三种源代码级的调试方法，可以归纳为以下两种策略：</p><p>    I、为内核增加调试Stub，利用调试Stub进行远程调试，这种调试策略需要target及development机器才能完成调试任务。</p><p>    II、将虚拟机技术与调试工具相结合，使Linux内核在虚拟机中运行从而利用调试器对内核进行调试。这种策略需要制作适合在虚拟机中运行的系统内核。</p><p>    由不同的调试策略决定了进行调试时不同的工作原理，同时也形成了各种调试方法不同的软硬件需求和各自的特点。</p><p>    另外，需要说明的是内核调试能力的掌握很大程度上取决于经验和对整个操作系统的深入理解。对系统内核的全面深入的理解，将能在很大程度上加快对Linux系统内核的开发和调试。</p><p>    对系统内核的调试技术和方法绝不止上面介绍所涉及的内容，这里只是介绍了一些经常看到和听到方法。在Linux内核向前发展的同时，内核的调试技术也在不断的进步。希望以上介绍的一些方法能对读者开发和学习Linux有所帮助。</p></div>
<img src ="http://www.cnitblog.com/schkui/aggbug/12310.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-06-15 17:48 <a href="http://www.cnitblog.com/schkui/archive/2006/06/15/12310.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>linux debug 技术</title><link>http://www.cnitblog.com/schkui/archive/2006/05/26/11077.html</link><dc:creator>爱易</dc:creator><author>爱易</author><pubDate>Fri, 26 May 2006 03:03:00 GMT</pubDate><guid>http://www.cnitblog.com/schkui/archive/2006/05/26/11077.html</guid><wfw:comment>http://www.cnitblog.com/schkui/comments/11077.html</wfw:comment><comments>http://www.cnitblog.com/schkui/archive/2006/05/26/11077.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/schkui/comments/commentRss/11077.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/schkui/services/trackbacks/11077.html</trackback:ping><description><![CDATA[
		<p>对于任何一位内核代码的编写者来说，最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合，所以内核代码无法轻易地放在调
试器中执行，而且也很难跟踪。同样，要想复现内核代码中的错误也是相当困难的，因为这种错误可能导致整个系统崩溃，这样也就破坏了可以用来跟踪它们的现
场。</p>
		<p>本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。</p>
		<p>4.1  通过打印调试<br />最普通的调试技术就是监视，即在应用程序编程中，在一些适当的地点调用printf 显示监视信息。调试内核代码的时候，则可以用 printk 来完成相同的工作。</p>
		<p>4.1.1  printk<br />在前面的章节中，我们只是简单假设 printk 工作起来和 printf 很类似。现在则是介绍它们之间一些不同点的时候了。</p>
		<p>其
中一个差别就是，通过附加不同日志级别（loglevel），或者说消息优先级，可让
printk根据这些级别所标示的严重程度，对消息进行分类。一般采用宏来指示日志级别，例如，KERN_INFO，我们在前面已经看到它被添加在一些打
印语句的前面，它就是一个可以使用的消息日志级别。日志级别宏展开为一个字符串，在编译时由预处理器将它和消息文本拼接在一起；这也就是为什么下面的例子
中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子，一个是调试信息，一个是临界信息：</p>
		<p>[quote]<br />printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _);<br />printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);<br />[/quote]</p>
		<p>在头文件 &lt;linux/kernel.h&gt; 中定义了 8 种可用的日志级别字符串。</p>
		<p>KERN_EMERG<br />用于紧急事件消息，它们一般是系统崩溃之前提示的消息。</p>
		<p>KERN_ALERT <br />用于需要立即采取动作的情况。</p>
		<p>KERN_CRIT <br />临界状态，通常涉及严重的硬件或软件操作失败。</p>
		<p>KERN_ERR <br />用于报告错误状态；设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。</p>
		<p>KERN_WARNING <br />对可能出现问题的情况进行警告，这类情况通常不会对系统造成严重问题。</p>
		<p>KERN_NOTICE <br />有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。</p>
		<p>KERN_INFO <br />提示性信息。很多驱动程序在启动的时候，以这个级别打印出它们找到的硬件信息。</p>
		<p>KERN_DEBUG <br />用于调试信息。</p>
		<p>每个字符串（以宏的形式展开）代表一个尖括号中的整数。整数值的范围从0到7，数值越小，优先级就越高。</p>
		<p>没
有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL，这个宏在文件
kernel/printk.c 中指定为一个整数值。在 Linux
的开发过程中，这个默认的级别值已经有过好几次变化，所以我们建议读者始终指定一个明确的级别。</p>
		<p>根据日志级别，内核可能会把消息打印到当前
控制台上，这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。如果优先级小于 console_loglevel
这个整数值的话，消息才能显示出来。如果系统同时运行了 klogd  和 syslogd，则无论 console_loglevel
为何值，内核消息都将追加到 /var/log/messages 中（否则的话，除此之外的处理方式就依赖于对 syslogd 的设置）。如果
klogd 没有运行，这些消息就不会传递到用户空间，这种情况下，就只好查看 /proc/kmsg 了。</p>
		<p>变量
console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL，而且还可以通过sys_syslog
系统调用进行修改。调用 klogd 时可以指定 -c 开关选项来修改这个变量， klogd 的 man
手册页对此有详细说明。注意，要修改它的当前值，必须先杀掉 klogd，再加
-c选项重新启动它。此外，还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的源文件
miscprogs/setlevel.c 里找到这样的一段程序。新优先级被指定为一个 1 到 8 之间的整数值。如果值被设为 1，则只有级别为
0（KERN_EMERG） 的消息才能到达控制台；如果设为 8，则包括调试信息在内的所有消息都能显示出来。</p>
		<p>如果在控制台上工作，而且
常常遇到内核错误（参见本章后面的“调试系统故障”一节）的话，就有必要降低日志级别，因为出错处理代码会把 console_loglevel
增为它的最大数值，导致随后的所有消息都显示在控制台上。如果需要查看调试信息，就有必要提高日志级别；这在远程调试内核，并且在交互会话未使用文本控制
台的情况下，是很有帮助的。</p>
		<p>从2.1.31这个版本起，可以通过文本文件 /proc/sys/kernel/printk
来读取和修改控制台的日志级别。这个文件容纳了 4
个整数值。读者可能会对前面两个感兴趣：控制台的当前日志级别和默认日志级别。例如，在最近的这些内核版本中，可以通过简单地输入下面的命令使所有的内核
消息得到显示：</p>
		<p>[quote]<br /> # echo 8 &gt; /proc/sys/kernel/printk<br />[/quote]</p>
		<p>不过，如果仍在 2.0 版本下的话，就需要使用 setlevel 这样的工具了。</p>
		<p>现在大家应该清楚为什么在 hello.c范例中使用 &lt;1&gt; 这些标记了，它们用来确保这些消息能在控制台上显示出来。</p>
		<p>对
于控制台日志策略，Linux考虑到了某些灵活性，也就是说，可以发送消息到一个指定的虚拟控制台（假如控制台是文本屏幕的话）。默认情况下，“控制台”
就是当前地虚拟终端。可以在任何一个控制台设备上调用 ioctl（TIOCLINUX），来指定接收消息的虚拟终端。下面的 setconsole
 程序，可选择专门用来接收内核消息的控制台；这个程序必须由超级用户运行，在 misc-progs 目录里可以找到它。下面是程序的代码：</p>
		<p>[quote]<br />int main(int argc, char **argv) <br />{<br />    char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */</p>
		<p>    if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */<br />    else {<br />        fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1);<br />    }<br />    if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)&lt;0) {    /* use stdin */<br />        fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",<br />                argv[0], strerror(errno));<br />        exit(1);<br />    }<br />    exit(0);<br />}<br />[/quote]</p>
		<p>setconsole
使用了特殊的ioctl命令：TIOCLINUX ，这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX
时，需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字，接下去的字节所具有的功能则由这个子命令决定。在
setconsole 中，使用的子命令是 11，后面那个字节（存于bytes[1]中）标识虚拟控制台。关于 TIOCLINUX
的详尽描述可以在内核源码中的 drivers/char/tty_io.c 文件得到。</p>
		<p>4.1.2  消息如何被记录<br />printk
函数将消息写到一个长度为 LOG_BUF_LEN（定义在 kernel/printk.c
中）字节的循环缓冲区中，然后唤醒任何正在等待消息的进程，即那些睡眠在 syslog 系统调用上的进程，或者读取 /proc/kmesg
的进程。这两个访问日志引擎的接口几乎是等价的，不过请注意，对 /proc/kmesg 进行读操作时，日志缓冲区中被读取的数据就不再保留，而
syslog 系统调用却能随意地返回日志数据，并保留这些数据以便其它进程也能使用。一般而言，读 /proc 文件要容易些，这使它成为
klogd 的默认方法。</p>
		<p>手工读取内核消息时，在停止klogd之后，可以发现 /proc 文件很象一个FIFO，读进程会阻塞在里面以等待更多的数据。显然，如果已经有 klogd 或其它的进程正在读取相同的数据，就不能采用这种方法进行消息读取，因为会与这些进程发生竞争。</p>
		<p>如
果循环缓冲区填满了，printk就绕回缓冲区的开始处填写新数据，覆盖最陈旧的数据，于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处
相比，这个问题可以忽略不计。例如，循环缓冲区可以使系统在没有记录进程的情况下照样运行，同时覆盖那些不再会有人去读的旧数据，从而使内存的浪费减到最
少。Linux消息处理方法的另一个特点是，可以在任何地方调用printk，甚至在中断处理函数里也可以调用，而且对数据量的大小没有限制。而这个方法
的唯一缺点就是可能丢失某些数据。</p>
		<p>klogd 运行时，会读取内核消息并将它们分发到 syslogd，syslogd 随后查看
/etc/syslog.conf ，找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分；这两者的允许值均定义在
&lt;sys/syslog.h&gt; 中。内核消息由 LOG_KERN 设施记录，并以 printk
中使用的优先级记录（例如，printk 中使用的 KERN_ERR对应于syslogd 中的 LOG_ERR）。如果没有运行
klogd，数据将保留在循环缓冲区中，直到某个进程读取或缓冲区溢出为止。</p>
		<p>如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志，则
可以为 klogd 指定 -f (file) 选项，指示 klogd 将消息保存到某个特定的文件，或者修改 /etc/syslog.conf
来适应自己的需求。另一种可能的办法是采取强硬措施：杀掉klogd，而将消息详细地打印到空闲的虚拟终端上。*</p>
		<p>[quote]<br />注： 例如，使用下面的命令可设置 10 号终端用于消息的显示：<br />setlevel 8<br />setconsole 10<br />[/quote]</p>
		<p>或者在一个未使用的 xterm 上执行cat /proc/kmesg来显示消息。</p>
		<p>4.1.3  开启及关闭消息<br />在
驱动程序开发的初期阶段，printk
对于调试和测试新代码是相当有帮助的。不过，当正式发布驱动程序时，就得删除这些打印语句，或至少让它们失效。不幸的是，你可能会发现这样的情况，在删除
了那些已被认为不再需要的提示消息后，又需要实现一个新的功能（或是有人发现了一个
bug），这时，又希望至少把一部分消息重新开启。这两个问题可以通过几个办法解决，以便全局地开启或禁止消息，并能对个别消息进行开关控制。</p>
		<p>我们在这里给出了一个编写 printk 调用的方法，可个别或全局地对它们进行开关；这个技巧是定义一个宏，在需要时，这个宏展开为一个printk（或printf）调用。</p>
		<p>可以通过在宏名字中删减或增加一个字母，打开或关闭每一条打印语句。</p>
		<p>编译前修改 CFLAGS 变量，则可以一次关闭所有消息。</p>
		<p>同样的打印语句既可以用在内核态也可以用在用户态，因此，关于这些额外的信息，驱动和测试程序可以用同样的方法来进行管理。</p>
		<p>下面这些来自 scull.h 的代码，就实现了这些功能。</p>
		<p>[quote]<br />#undef PDEBUG             /* undef it, just in case */<br />#ifdef SCULL_DEBUG<br />#  ifdef _ _KERNEL_ _<br />     /* This one if debugging is on, and kernel space */<br />#    define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, <br />                                         ## args)<br />#  else<br />     /* This one for user space */<br />#    define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)<br />#  endif<br />#else<br />#  define PDEBUG(fmt, args...) /* not debugging: nothing */<br />#endif</p>
		<p>#undef PDEBUGG<br />#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */<br />[/quote]<br /><br />符
号 PDEBUG
依赖于是否定义了SCULL_DEBUG，它能根据代码所运行的环境选择合适的方式显示信息：内核态运行时使用printk系统调用；用户态下则使用
libc调用fprintf，向标准错误设备进行输出。符号PDEBUGG则什么也不做；它可以用来将打印语句注释掉，而不必把它们完全删除。</p>
		<p>为了进一步简化这个过程，可以在 Makefile加上下面几行：</p>
		<p>[quote]<br /># Comment/uncomment the following line to disable/enable debugging<br />DEBUG = y</p>
		<p># Add your debugging flag (or not) to CFLAGS<br />ifeq ($(DEBUG),y)<br />  DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines<br />else<br />  DEBFLAGS = -O2<br />endif</p>
		<p>CFLAGS += $(DEBFLAGS)<br />[/quote]</p>
		<p>本节所给出的宏依赖于gcc 对ANSI C预编译器的扩展，这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖并不是什么问题，因为内核对 gcc 特性的依赖更强。此外，Makefile依赖于 GNU 的make 版本；基于同样的道理，这也不是什么问题。</p>
		<p>如果读者熟悉 C 预编译器，可以将上面的定义进行扩展，实现“调试级别”的概念，这需要定义一组不同的级别，并为每个级别赋一个整数（或位掩码），用以决定各个级别消息的详细程度。</p>
		<p>但
是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点，对读者来说，我们无法预知最合适的点在哪里。记住，预处
理程序的条件语句（以及代码中的常量表达式）只在编译时执行，要再次打开或关闭消息必须重新编译。另一种方法就是使用C条件语句，它在运行时执行，因此可
以在程序运行期间打开或关闭消息。这是个很好的功能，但每次代码执行时系统都要进行额外的处理，甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法
接受的。</p>
		<p>在很多情况下，本节提到的这些宏都已被证实是很有用的，仅有的缺点是每次开启和关闭消息显示时都要重新编译模块。</p>
		<p>4.2  通过查询调试<br />上一节讲述了 printk 是如何工作的以及如何使用它，但还没谈到它的缺点。</p>
		<p>由
于 syslogd 会一直保持对其输出文件的同步刷新，每打印一行都会引起一次磁盘操作，因此大量使用 printk 会严重降低系统性能。从
syslogd
的角度来看，这样的处理是正确的。它试图把每件事情都记录到磁盘上，以防系统万一崩溃时，最后的记录信息能反应崩溃前的状况；然而，因处理调试信息而使系
统性能减慢，是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日志文件的名字前面，前缀一个减号符解决。*</p>
		<p>[quote]<br />注： 这个减号是个“特殊”标记，避免 syslogd 在每次出现新信息时都去刷新磁盘文件，这些内容记述在 syslog.conf(5) 中，这个手册页很值得一读。<br />[/quote]</p>
		<p>
				<br />修
改配置文件带来的问题在于，在完成调试之后改动将依旧保留；即使在一般的系统操作中，当希望尽快把信息刷新到磁盘时，也是如此。如果不愿作这种持久性修改
的话，另一个选择是运行一个非 klogd 程序（如前面介绍的cat /proc/kmesg），但这样并不能为通常的系统操作提供一个合适的环境。</p>
		<p>多数情况中，获取相关信息的最好方法是在需要的时候才去查询系统信息，而不是持续不断地产生数据。实际上，每个 Unix 系统都提供了很多工具，用于获取系统信息，如：ps、netstat、vmstat等等。</p>
		<p>驱动程序开发人员对系统进行查询时，可以采用两种主要的技术：在 /proc 文件系统中创建文件，或者使用驱动程序的 ioctl 方法。/proc 方式的另一个选择是使用 devfs，不过用于信息查找时，/proc 更为简单一些。</p>
		<p>4.2.1  使用 /proc 文件系统<br />/proc
文件系统是一种特殊的、由程序创建的文件系统，内核使用它向外界输出信息。/proc
下面的每个文件都绑定于一个内核函数，这个函数在文件被读取时，动态地生成文件的“内容”。我们已经见到过这类文件的一些输出情况，例如，
/proc/modules 列出的是当前载入模块的列表。</p>
		<p>Linux系统对/proc的使用很频繁。现代Linux系统中的很多工具都是
通过 /proc 来获取它们的信息，例如 ps、top 和 uptime。有些设备驱动程序也通过 /proc
输出信息，你的驱动程序当然也可以这么做。因为 /proc 文件系统是动态的，所以驱动程序模块可以在任何时候添加或删除其中的文件项。</p>
		<p>特
征完全的 /proc 文件项相当复杂；在所有的这些特征当中，有一点要指出的是，这些 /proc
文件不仅可以用于读出数据，也可以用于写入数据。不过，大多数时候，/proc
文件项是只读文件。本节将只涉及简单的只读情形。如果有兴趣实现更为复杂的事情，读者可以先在这里了解基础知识，然后参考内核源码来建立完整的认识。</p>
		<p>所有使用 /proc 的模块必须包含 &lt;linux/proc_fs.h&gt;，通过这个头文件定义正确的函数。</p>
		<p>为
创建一个只读 /proc 文件，驱动程序必须实现一个函数，用于在文件读取时生成数据。当某个进程读这个文件时（使用 read
系统调用），请求会通过两个不同接口的其中之一发送到驱动程序模块，使用哪个接口取决于注册情况。我们先把注册放到本节后面，先直接讲述读接口。</p>
		<p>无论采用哪个接口，在这两种情况下，内核都会分配一页内存（也就是 PAGE_SIZE 个字节），驱动程序向这片内存写入将返回给用户空间的数据。</p>
		<p>推荐的接口是 read_proc，不过还有一个名为 get_info 的老一点的接口。</p>
		<p>[quote]<br />int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data); <br />[/quote]</p>
		<p>参
数表中的 page 指针指向将写入数据的缓冲区；start
被函数用来说明有意义的数据写在页面的什么位置（对此后面还将进一步谈到）；offset 和 count 这两个参数与在 read
实现中的用法相同。eof 参数指向一个整型数，当没有数据可返回时，驱动程序必须设置这个参数；data
参数是一个驱动程序特有的数据指针，可用于内部记录。*</p>
		<p>[quote]<br />注： 纵览全书，我们还会发现这样的一些指针；它们表示了这类处理中有关的“对象”，与C++ 中的同类处理有些相似。<br />[/quote]</p>
		<p>这个函数可以在2.4内核中使用，如果使用我们的 sysdep.h 头文件，那么在2.2内核中也可以用这个函数。</p>
		<p>[quote]<br />int (*get_info)(char *page, char **start, off_t offset, int count);  <br />[/quote]</p>
		<p>get_info
是一个用来读取 /proc 文件的较老接口。所有的参数与 read_proc 中的对应参数用法相同。缺少的是报告到达文件尾的指针和由data
指针带来的面向对象风格。这个函数可以用在所有我们感兴趣的内核版本中（尽管在它 2.0 版本的实现中有一个额外未用的参数）。</p>
		<p>这两个函数的返回值都是实际放入页面缓冲区的数据的字节数，这一点与 read 函数对其它类型文件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标记，而 start 的用法就有点复杂了。</p>
		<p>对于 /proc 文件系统的用户扩展，其最初实现中的主要问题在于，数据传输只使用单个内存页面。这样就把用户文件的总体尺寸限制在了 4KB 以内（或者是适合于主机平台的其它值）。start 参数在这里就是用来实现大数据文件的，不过该参数可以被忽略。</p>
		<p>如
果 proc_read 函数不对 *start 指针进行设置（它最初为 NULL），内核就会假定 offset
参数被忽略，并且数据页包含了返回给用户空间的整个文件。反之，如果需要通过多个片段创建一个更大的文件，则可以把 *start
赋值为页面指针，因此调用者也就知道了新数据放在缓冲区的开始位置。当然，应该跳过前 offset
个字节的数据，因为这些数据已经在前面的调用中返回。</p>
		<p>长久以来，关于 /proc 文件还有另一个主要问题，这也是 start
意图解决的一个问题。有时，在连续的 read 调用之间，内核数据结构的 ASCII
表述会发生变化，以至于读进程发现前后两次调用所获得的数据不一致。如果把 *start 设为一个小的整数值，调用程序可以利用它来增加
filp-&gt;f_pos 的值，而不依赖于返回的数据量，因此也就使 f_pos 成为read_proc 或 get_info
程序中的一个内部记录值。例如，如果 read_proc 函数从一个大的结构数组返回数据，并且这些结构的前 5
个已经在第一次调用中返回，那么可将 *start 设置为
5。下次调用中这个值将被作为偏移量；驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作“hack”，可以在
/fs/proc/generic.c 中看到。</p>
		<p>现在我们来看个例子。下面是scull 设备 read_proc 函数的简单实现：<br /><br />[quote]<br />int scull_read_procmem(char *buf, char **start, off_t offset,<br />                   int count, int *eof, void *data)<br />{<br />    int i, j, len = 0;<br />    int limit = count - 80; /* Don't print more than this */</p>
		<p>    for (i = 0; i &lt; scull_nr_devs &amp;&amp; len &lt;= limit; i++) {<br />        Scull_Dev *d = &amp;scull_devices[ i];<br />        if (down_interruptible(&amp;d-&gt;sem))<br />                return -ERESTARTSYS;<br />        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",<br />                       i, d-&gt;qset, d-&gt;quantum, d-&gt;size);<br />        for (; d &amp;&amp; len &lt;= limit; d = d-&gt;next) { /* scan the list */<br />            len += sprintf(buf+len, "  item at %p, qset at %p\n", d, <br />                                    d-&gt;data);<br />            if (d-&gt;data &amp;&amp; !d-&gt;next) /* dump only the last item <br />                                                    - save space */<br />                for (j = 0; j &lt; d-&gt;qset; j++) {<br />                    if (d-&gt;data[j])<br />                        len += sprintf(buf+len,"    % 4i: %8p\n",<br />                                                    j,d-&gt;data[j]);<br />                }<br />        }<br />        up(&amp;scull_devices[ i].sem);<br />    }<br />    *eof = 1;<br />    return len;<br />}<br />[/quote]</p>
		<p>这是一个相当典型的 read_proc 实现。它假定决不会有这样的需求，即生成多于一页的数据，因此忽略了 start 和 offset 值。但是，小心不要超出缓冲区，以防万一。</p>
		<p>使用 get_info 接口的 /proc 函数与上面说明的 read_proc 非常相似，除了没有最后的那两个参数。既然这样，则通过返回少于调用者预期的数据（也就是少于 count 参数），来提示已到达文件尾。 </p>
		<p>一
旦定义好了一个 read_proc 函数，就需要把它与一个 /proc
文件项连接起来。依赖于将要支持的内核版本，有两种方法可以建立这样的连接。最容易的方法是简单地调用
create_proc_read_entry，但这只能用于2.4内核（如果使用我们的 sysdep.h 头文件，则也可用于 2.2
内核）。下面就是 scull 使用的调用，以 /proc/scullmem 的形式来提供 /proc 功能。<br /><br />[quote]<br />create_proc_read_entry("scullmem", <br />                       0    /* default mode */,<br />                       NULL /* parent dir */, <br />                       scull_read_procmem, <br />                       NULL /* client data */);<br />[/quote]<br /><br />这
个函数的参数表包括：/proc
文件项的名称、应用于该文件项的文件许可权限（0是个特殊值，会被转换为一个默认的、完全可读模式的掩码）、文件父目录的
proc_dir_entry 指针（我们使用 NULL 值使该文件项直接定位在 /proc 下）、指向 read_proc
的函数指针，以及将传递给 read_proc 函数的数据指针。</p>
		<p>目录项指针（proc_dir_entry）可用来在 /proc
下创建完整的目录层次结构。不过请注意，将文件项置于 /proc
的子目录中有更为简单的方法，即把目录名称作为文件项名称的一部分――只要目录本身已经存在。例如，有个新的约定，要求设备驱动程序对应的 /proc
文件项应转移到子目录 driver/ 中；scull 可以简单地指定它的文件项名称为 driver/scullmem，从而把它的 /proc
文件放到这个子目录中。</p>
		<p>当然，在模块卸载时，/proc 中的文件项也应被删除。 remove_proc_entry 就是用来撤消 create_proc_read_entry 所做工作的函数。</p>
		<p>[quote]<br /> remove_proc_entry("scullmem", NULL /* parent dir */);<br />[/quote]</p>
		<p>另
一个创建 /proc 文件项的方法是，创建并初始化一个 proc_dir_entry 结构，并将该结构传递给函数
proc_register_dynamic (2.0 版本)或 proc_register（2.2
版本，如果结构中的索引节点号为0，该函数即认为是动态文件）。作为一个例子，当在2.0内核的头文件下进行编译时，考虑下面 scull
所使用的这些代码：</p>
		<p>[quote]<br />static int scull_get_info(char *buf, char **start, off_t offset,<br />                int len, int unused)<br />{<br />    int eof = 0;<br />    return scull_read_procmem (buf, start, offset, len, &amp;eof, NULL);<br />}</p>
		<p>struct proc_dir_entry scull_proc_entry = {<br />        namelen:    8,<br />        name:       "scullmem",<br />        mode:       S_IFREG | S_IRUGO,<br />        nlink:      1,<br />        get_info:   scull_get_info,<br />};</p>
		<p>static void scull_create_proc()<br />{<br />    proc_register_dynamic(&amp;proc_root, &amp;scull_proc_entry);<br />}</p>
		<p>static void scull_remove_proc()<br />{<br />    proc_unregister(&amp;proc_root, scull_proc_entry.low_ino);<br />}<br />[/quote]</p>
		<p>代码声明了一个使用 get_info 接口的函数，并填写了一个 proc_dir_entry 结构，用于对文件系统进行注册。</p>
		<p>这
段代码借助sysdep.h 中宏定义的支持，提供了 2.0 和 2.4 内核之间的兼容性。因为 2.0 内核不支持
read_proc，它使用了 get_info 接口。如果对 #ifdef 作一些更多的处理，可以使这段代码在 2.2 内核中使用
read_proc，不过这样收益并不大。</p>
		<p>4.2.2  ioctl 方法<br />ioctl是作用于文件描述符之上的一个系统调用，我们会在下一章介绍它的用法；它接收一个“命令”号，用以标识将执行的命令；以及另一个（可选的）参数，通常是个指针。</p>
		<p>做为替代 /proc文件系统的方法，可以为调试设计若干ioctl命令。这些命令从驱动程序复制相关数据到用户空间，在用户空间中可以查看这些数据。</p>
		<p>使用ioctl 获取信息比起 /proc 来要困难一些，因为需要另一个程序调用 ioctl 并显示结果。这个程序是必须编写并编译的，而且要和测试模块配合一致。从另一方面来说，相对实现 /proc 文件所需的工作，驱动程序的编码则更为容易些。</p>
		<p>有时 ioctl 是获取信息的最好方法，因为它比起读 /proc 要快得多。如果在数据写到屏幕之前要完成某些处理工作，以二进制获取数据要比读取文本文件有效得多。此外，ioctl 并不要求把数据分割成不超过一个内存页面的片断。</p>
		<p>ioctl
方法的一个优点是，在结束调试之后，用来取得信息的这些命令仍可以保留在驱动程序中。/proc文件对任何查看这个目录的人都是可见的(很多人可能会纳闷
“这些奇怪的文件是用来做什么的”)，然而与 /proc文件不同，未公开的 ioctl
命令通常都不会被注意到。此外，万一驱动程序有什么异常，这些命令仍然可以用来调试。唯一的缺点就是模块会稍微大一些。</p>
		<p>4.3  通过监视调试<br />有时，通过监视用户空间中应用程序的运行情况，可以捕捉到一些小问题。监视程序同样也有助于确认驱动程序工作是否正常。例如，看到 scull 的 read 实现如何响应不同数据量的 read 请求后，我们就可以判断它是否工作正常。</p>
		<p>有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数，插入打印语句，或者在 strace 状态下运行程序。在检查内核代码时，最后一项技术最值得关注，我们将在此对它进行讨论。</p>
		<p>strace
命令是一个功能非常强大的工具，它可以显示程序所调用的所有系统调用。它不仅可以显示调用，而且还能显示调用参数，以及用符号方式表示的返回值。当系统调
用失败时，错误的符号值（如 ENOMEM）和对应的字符串（如Out of memory）都能被显示出来。strace
有许多命令行选项；最为有用的是 -t，用来显示调用发生的时间；-T，显示调用所花费的时间；
-e，限定被跟踪的调用类型；-o，将输出重定向到一个文件中。默认情况下，strace将跟踪信息打印到 stderr 上。</p>
		<p>strace从内核中接收信息。这意味着一个程序无论是否按调试方式编译（用 gcc 的 -g选项）或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样，strace 也可以跟踪一个正在运行的进程。</p>
		<p>跟踪信息通常用于生成错误报告，然后发给应用开发人员，但是它对内核编程人员来说也同样非常有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的；strace 允许我们检查每次调用中输入和输出数据的一致性。</p>
		<p>例如，下面的屏幕信息显示了 strace ls /dev &gt; /dev/scull0 命令的最后几行：</p>
		<p>[quote]<br />[...]<br />open("/dev", O_RDONLY|O_NONBLOCK)     = 4<br />fcntl(4, F_SETFD, FD_CLOEXEC)         = 0<br />brk(0x8055000)                        = 0x8055000<br />lseek(4, 0, SEEK_CUR)                 = 0<br />getdents(4, /* 70 entries */, 3933)   = 1260<br />[...]<br />getdents(4, /* 0 entries */, 3933)    = 0<br />close(4)                              = 0<br />fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0<br />ioctl(1, TCGETS, 0xbffffa5c)          = -1 ENOTTY (Inappropriate ioctl<br />                                                     for device)<br />write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000<br />write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96<br />write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325<br />close(1)                              = 0<br />_exit(0)                              = ?<br />[/quote]</p>
		<p>很
明显，ls 完成对目标目录的检索后，在首次对 write 的调用中，它试图写入 4KB 数据。很奇怪（对于 ls
来说），实际只写了4000个字节，接着它重试这一操作。然而，我们知道scull的 write 实现每次最多只写一个量子（scull
中设置的量子大小为4000个字节），所以我们所预期的就是这样的部分写入。经过几个步骤之后，每件工作都顺利通过，程序正常退出。</p>
		<p>另一个例子，让我们来对 scull 设备进行读操作（使用 wc 命令）：</p>
		<p>[quote]<br />[...]<br />open("/dev/scull0", O_RDONLY)           = 4<br />fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0<br />read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000<br />read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421<br />read(4, "", 16384)                      = 0<br />fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0<br />ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0<br />write(1, "   7421 /dev/scull0\n", 20)   = 20<br />close(4)                                = 0<br />_exit(0)                                = ?<br />[/quote]</p>
		<p>正
如所料，read
每次只能读取4000个字节，但数据总量与前面例子中写入的数量是相同的。与上面的写跟踪相对比，请读者注意本例中重试工作是如何组织的。为了快速读取数
据，wc 已被优化了，因而它绕过了标准库，试图通过一次系统调用读取更多的数据。可以从跟踪的 read 行中看到 wc 每次均试图读取 16KB
数据。</p>
		<p>Linux行家可以在 strace 的输出中发现很多有用信息。如果觉得这些符号过于拖累的话，则可以仅限于监视文件方法（open，read 等）是如何工作的。</p>
		<p>就个人观点而言，我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的 perror 调用在用于调试时信息还不够详细，而 strace 能够确切查明系统调用的哪个参数引发了错误，这一点对调试是大有帮助的。</p>
		<p>4.4  调试系统故障<br />即使采用了所有这些监视和调试技术，有时驱动程序中依然会有错误，这样的驱动程序在执行时就会产生系统故障。在出现这种情况时，获取尽可能多的信息对解决问题是至关重要的。</p>
		<p>注
意，“故障”不意味着“panic”。Linux
代码非常健壮（用术语讲即为鲁棒，robust），可以很好地响应大部分错误：故障通常会导致当前进程崩溃，而系统仍会继续运行。如果在进程上下文之外发
生故障，或是系统的重要组成被损害时，系统才有可能
panic。但如果问题出在驱动程序中时，通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时，为进程上下文分配的一
些内存可能会丢失；例如，由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而，由于内核在进程中止时会对已打开的设备调用 close
操作，驱动程序仍可以释放由 open 方法分配的资源。</p>
		<p>我们已经说过，当内核行为异常时，会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息。尽管它们对于初学者来说相当晦涩，不过处理器在出错时转储出的这些数据包含了许多值得关注的信息，通常足以查明程序错误，而无需额外的测试。</p>
		<p>4.4.1  oops消息<br />大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。</p>
		<p>由
处理器使用的地址都是虚拟地址，而且通过一个复杂的称为页表（见第 13
章中的“页表”一节）的结构映射为物理地址。当引用一个非法指针时，页面映射机制就不能将地址映射到物理地址，此时处理器就会向操作系统发出一个“页面失
效”的信号。如果地址非法，内核就无法“换页”到并不存在的地址上；如果此时处理器处于超级用户模式，系统就会产生一个“oops”。</p>
		<p>值得注意的是，2.0 版本之后引入的第一个增强是，当向用户空间移动数据或者移出时，无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用，所以正常情况（地址都正确时）就可以更有效地得到处理。</p>
		<p>oops
显示发生错误时处理器的状态，包括 CPU
寄存器的内容、页描述符表的位置，以及其它看上去无法理解的信息。这些消息由失效处理函数（arch/*/kernel/traps.c）中的
printk 语句产生，就象前面“printk”一节所介绍的那样分发出来。</p>
		<p>让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时，就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针（EIP），即出错指令的地址。</p>
		<p>[quote]<br />Unable to handle kernel NULL pointer dereference at virtual address 00000000<br /> printing eip:</p>
		<p>c48370c3<br />*pde = 00000000<br />Oops: 0002<br />CPU:    0<br />EIP:    0010:[&lt;c48370c3&gt;]<br />EFLAGS: 00010286<br />eax: ffffffea   ebx: c2281a20   ecx: c48370c0   edx: c2281a40<br />esi: 4000c000   edi: 4000c000   ebp: c38adf8c   esp: c38adf8c<br />ds: 0018   es: 0018   ss: 0018<br />Process ls (pid: 23171, stackpage=c38ad000)<br />Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \<br />            0000010e <br />       4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \<br />            4000c000 <br />       0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \<br />            00000004 <br />Call Trace: [&lt;c01356e6&gt;] [&lt;c010b860&gt;] <br />Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00 <br />[/quote]</p>
		<p>这个消息是通过对 faulty  模块的一个设备进行写操作而产生的，faulty 这个模块专为演示出错而编写。faulty.c 中 write 方法的实现很简单：</p>
		<p>[quote]<br />ssize_t faulty_write (struct file *filp, const char *buf, size_t count,<br />loff_t *pos)<br />{<br />    /* make a simple fault by dereferencing a NULL pointer */<br />    *(int *)0 = 0;<br />    return 0;<br />}<br />[/quote]</p>
		<p>正如读者所见，我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值，所以错误发生，内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中，faulty 模块还有更多有意思的错误状态。</p>
		<p>[quote]<br />char faulty_buf[1024];</p>
		<p>ssize_t faulty_read (struct file *filp, char *buf, size_t count, <br />                     loff_t *pos)<br />{<br />    int ret, ret2;<br />    char stack_buf[4];</p>
		<p>    printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count);<br />    /* the next line oopses with 2.0, but not with 2.2 and later */<br />    ret = copy_to_user(buf, faulty_buf, count);<br />    if (!ret) return count; /* we survived */</p>
		<p>    printk(KERN_DEBUG "didn't fail: retry\n");<br />    /* For 2.2 and 2.4, let's try a buffer overflow  */<br />    sprintf(stack_buf, "1234567\n");<br />    if (count &gt; 8) count = 8; /* copy 8 bytes to the user */<br />    ret2 = copy_to_user(buf, stack_buf, count);<br />    if (!ret2) return count;<br />    return ret2;<br />}<br />[/quote]</p>
		<p>这
段程序首先从一个全局缓冲区读取数据，但并不检查数据的长度，然后通过对一个局部缓冲区进行写入操作，制造一次缓冲区溢出。第一个操作仅在 2.0
内核会导致 oops 的发生，因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops；然而，由于 return
指令把指令指针带到了不知道的地方，所以这种错误很难跟踪，所能获得的仅是如下的信息：</p>
		<p>[quote]<br />EIP:    0010:[&lt;00000000&gt;]<br />[...]<br />Call Trace: [&lt;c010b860&gt;] <br />Code:  Bad EIP value.<br />[/quote]</p>
		<p>用
户处理 oops
消息的主要问题在于，我们很难从十六进制数值中看出什么内在的意义；为了使这些数据对程序员更有意义，需要把它们解析为符号。有两个工具可用来为开发人员
完成这样的解析：klogd 和 ksymoops。前者只要运行就会自行进行符号解码；后者则需要用户有目的地调用。下面的讨论，使用了在我们第一个
oops 例子中通过使用NULL 指针而产生的出错信息。</p>
		<p>使用 klogd<br />klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下，klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在，可是有时开发者必须给它一定的帮助。</p>
		<p>当 faulty 的一个oops 输出送达系统日志时，转储信息看上去会是下面的情况（注意 EIP 行和 stack 跟踪记录中已经解码的符号）：</p>
		<p>[quote]<br />Unable to handle kernel NULL pointer dereference at virtual address \<br />     00000000 <br /> printing eip: <br />c48370c3 <br />*pde = 00000000 <br />Oops: 0002 <br />CPU:    0 <br />EIP:    0010:[faulty:faulty_write+3/576] <br />EFLAGS: 00010286 <br />eax: ffffffea   ebx: c2c55ae0   ecx: c48370c0   edx: c2c55b00 <br />esi: 0804d038   edi: 0804d038   ebp: c2337f8c   esp: c2337f8c <br />ds: 0018   es: 0018   ss: 0018 <br />Process cat (pid: 23413, stackpage=c2337000) <br />Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \<br />            00000001<br />       0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \<br />            0804d038<br />       00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \<br />            00000004<br />Call Trace: [sys_write+214/256] [system_call+52/56]  <br />Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00  <br />[/quote]</p>
		<p>klogd
提供了大多数必要信息用于发现问题。在这个例子中，我们看到指令指针（EIP）正执行于函数 faulty_write
中，因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数的第3个字节上，而函数整体长度为 576
个字节。注意这些数值都是十进制的，而非十六进制。</p>
		<p>然而，当错误发生在可装载模块中时，为了获取错误相关的有用信息，开发者还必须注意一些
情况。klogd 在开始运行时装入所有可用符号，并随后使用这些符号。如果在 klogd
已经对自身初始化之后（一般在系统启动时），装载某个模块，那 klogd 将不会有这个模块的符号信息。强制
klogd取得这些信息的办法是，发送一个 SIGUSR1 信号给 klogd
进程，这种操作在时间顺序上，必须是在模块已经装入（或重新装载）之后，而在进行任何可能引起 oops 的处理之前。</p>
		<p>还可以在运行 klogd 时加上 -p 选项，这会使它在任何发现 oops 消息的时刻重新读入符号信息。不过，klogd 的man 手册不推荐这个方法，因为这使 klogd 在出问题之后再向内核查询信息。而发生错误之后，所获得的信息可能是完全错误的了。</p>
		<p>为
了使 klogd 正确地工作，必须给它提供符号表文件 System.map 的一个当前复本。通常这个文件在 /boot
中；如果从一个非标准的位置编译并安装了一个内核，就需要把 System.map 拷贝到 /boot，或告知 klogd
到什么位置查看。如果符号表与当前内核不匹配，klogd 就会拒绝解析符号。假如一个符号被解析在系统日志中，那么就有理由确信它已被正确解析了。</p>
		<p>使用 ksymoops<br />有
些时候，klogd
对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址，又要获得对应的符号，而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外，往
往还需要更多的信息。对 klogd 来说，在出错期间被杀掉，也是常用的事情。在这些情况下，可以调用一个更为强大的 oops
分析器，ksymoops 就是这样的一个工具。</p>
		<p>在 2.3 开发系列之前，ksymoops 是随内核源码一起发布的，位于
scripts 目录之下。它现在则在自己的FTP 站点上，对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核，或许还可以从
ftp://ftp.ocs.com.au/pub/ksymoops 站点上获取这个工具的升级版本。</p>
		<p>为了取得最佳的工作状态，除错误消息之外，ksymoops 还需要很多信息；可以使用命令行选项告诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项：</p>
		<p>System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。<br />模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块，以便获得它们的符号信息。如果未提供这个列表，ksymoops 会查看 /proc/modules。<br />在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。<br />当
前正运行的内核映像的复本注意，ksymoops 需要的是一个直接的内核映像，而不是象 vmlinuz、zImage 或 bzImage
这样被大多数系统所使用的压缩版本。默认是不使用内核映像，因为大多数人都不会保存这样的一个内核。如果手边就有这样一个符合要求的内核的话，就应该采用
-v 选项告知 ksymoops 它的位置。<br />已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块，不过在开发中，几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。</p>
		<p>虽
然 ksymoops 会访问 /proc 中的文件来取得它所需的信息，但这样获得的结果是不可靠的。在 oops 发生和 ksymoops
运行的时间间隙中，系统几乎一定会重新启动，这样取自 /proc 的信息就可能与故障发生时的实际状态不符合。只要有可能，最好在引起 oops
发生之前，保存 /proc/modules 和 /proc/ksyms 的复本。</p>
		<p>我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页，这是一个很好的资料文档。</p>
		<p>这
个工具命令行中的最后一个参数是 oops 消息的位置；如果缺少这个参数，ksymoops 会按Unix
的惯例去读取标准输入设备。运气好的话，消息可以从系统日志中重新恢复；在发生很严重的崩溃情况时，我们可能不得不将这些消息从屏幕上抄下来，然后再敲进
去（除非用的是串口控制台，这对内核开发人员来说，是非常棒的工具）。</p>
		<p>注意，当 oops 消息已经被 klogd 处理过时，ksymoops 将会陷于混乱。如果 klogd 已经运行，而且 oops 发生后系统仍在运行，那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。</p>
		<p>如果没有明确地提供全部的上述信息，ksymoops 会发出警告。对于载入模块未作符号定义这类的情况，它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。</p>
		<p>ksymoops 的输出类似如下：</p>
		<p>[quote]<br />&gt;&gt;EIP; c48370c3 &lt;[faulty]faulty_write+3/20&gt;   &lt;=====<br />Trace; c01356e6 &lt;sys_write+d6/100&gt;<br />Trace; c010b860 &lt;system_call+34/38&gt;<br />Code;  c48370c3 &lt;[faulty]faulty_write+3/20&gt;<br />00000000 &lt;_EIP&gt;:<br />Code;  c48370c3 &lt;[faulty]faulty_write+3/20&gt;   &lt;=====<br />   0:   c7 05 00 00 00    movl   $0x0,0x0   &lt;=====<br />Code;  c48370c8 &lt;[faulty]faulty_write+8/20&gt;<br />   5:   00 00 00 00 00 <br />Code;  c48370cd &lt;[faulty]faulty_write+d/20&gt;<br />   a:   31 c0             xorl   %eax,%eax<br />Code;  c48370cf &lt;[faulty]faulty_write+f/20&gt;<br />   c:   89 ec             movl   %ebp,%esp<br />Code;  c48370d1 &lt;[faulty]faulty_write+11/20&gt;<br />   e:   5d                popl   %ebp<br />Code;  c48370d2 &lt;[faulty]faulty_write+12/20&gt;<br />   f:   c3                ret    <br />Code;  c48370d3 &lt;[faulty]faulty_write+13/20&gt;<br />  10:   8d b6 00 00 00    leal   0x0(%esi),%esi<br />Code;  c48370d8 &lt;[faulty]faulty_write+18/20&gt;<br />  15:   00 <br />[/quote]</p>
		<p>正
如上面所看到的，ksymoops 提供的 EIP 和内核堆栈信息与 klogd
所做的很相似，不过要更为准确，而且是十六进制形式的。可以注意到，faulty_write 函数的长度被正确地报告为 0x20个字节。这是因为
ksymoops 读取了模块的目标文件，并从中获得了全部的有用信息。</p>
		<p>而且在这个例子中，还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确切地判断发生了些什么事情；这里很明显，错误在于一个向 0 地址写入数据 0 的指令。</p>
		<p>ksymoops
的一个有趣特点是，它可以移植到几乎所有 Linux 可以运行的平台上，而且还利用了 bfd （二进制格式描述）库同时支持多种计算机结构。走出
PC 的世界，我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似（为了便于排版有几行被打断了）：</p>
		<p>[quote]<br />Unable to handle kernel NULL pointer dereference<br />tsk-&gt;mm-&gt;context = 0000000000000734<br />tsk-&gt;mm-&gt;pgd = fffff80003499000<br />              \/ ____ \/<br />              "@'/ .. \`@"<br />             /_| \_ _/ |_\<br />                \_ _ _/<br />ls(16740): Oops<br />TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \<br />Y: 00800000<br />g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \<br />g3: 0000000000000018<br />g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \<br />g7: 0000000000000001<br />o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \<br />o3: fffff8001224f168<br />o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \<br />ret_pc: 0000000000457fb4<br />l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \<br />l3: 000000000002c400<br />l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \<br />l7: 0000000070028cbc<br />i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \<br />i3: 000000000002c400<br />i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \<br />i7: 0000000000410114<br />Caller[0000000000410114]<br />Caller[000000007007cba4]<br />Instruction DUMP: 01000000 90102000 81c3e008 &lt;c0202000&gt; \<br />30680005 01000000 01000000 01000000 01000000<br />[/quote]</p>
		<p>请注意，指令转储并不是从引起错误的那个指令开始，而是之前的三条指令：这是因为 RISC 平台以并行的方式执行多条指令，这样可能产生延期的异常，因此必须能回溯最后的几条指令。</p>
		<p>下面是当从 TSTATE 行开始输入数据时，ksymoops 所打印出的信息：<br /><br />[quote]<br />&gt;&gt;TPC; 0000000001000128 &lt;[faulty].text.start+88/a0&gt;   &lt;=====<br />&gt;&gt;O7;  0000000000457fb4 &lt;sys_write+114/160&gt;<br />&gt;&gt;I7;  0000000000410114 &lt;linux_sparc_syscall+34/40&gt;<br />Trace; 0000000000410114 &lt;linux_sparc_syscall+34/40&gt;<br />Trace; 000000007007cba4 &lt;END_OF_CODE+6f07c40d/????&gt;<br />Code;  000000000100011c &lt;[faulty].text.start+7c/a0&gt;<br />0000000000000000 &lt;_TPC&gt;:<br />Code;  000000000100011c &lt;[faulty].text.start+7c/a0&gt;<br />   0:   01 00 00 00       nop <br />Code;  0000000001000120 &lt;[faulty].text.start+80/a0&gt;<br />   4:   90 10 20 00       clr  %o0     ! 0 &lt;_TPC&gt;<br />Code;  0000000001000124 &lt;[faulty].text.start+84/a0&gt;<br />   8:   81 c3 e0 08       retl <br />Code;  0000000001000128 &lt;[faulty].text.start+88/a0&gt;   &lt;=====<br />   c:   c0 20 20 00       clr  [ %g0 ]   &lt;=====<br />Code;  000000000100012c &lt;[faulty].text.start+8c/a0&gt;<br />  10:   30 68 00 05       b,a   %xcc, 24 &lt;_TPC+0x24&gt; \<br />                        0000000001000140 &lt;[faulty]faulty_write+0/20&gt;<br />Code;  0000000001000130 &lt;[faulty].text.start+90/a0&gt;<br />  14:   01 00 00 00       nop <br />Code;  0000000001000134 &lt;[faulty].text.start+94/a0&gt;<br />  18:   01 00 00 00       nop <br />Code;  0000000001000138 &lt;[faulty].text.start+98/a0&gt;<br />  1c:   01 00 00 00       nop <br />Code;  000000000100013c &lt;[faulty].text.start+9c/a0&gt;<br />  20:   01 00 00 00       nop <br />[/quote]</p>
		<p>要打印出上面显示的反汇编代码，我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要这些信息，是因为 SPARC64 用户空间的本地结构是32位的)。本例中，使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。</p>
		<p>读
者可能会抱怨对调用的跟踪并没带回什么值得注意的信息；然而，SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中：07 和 I7
寄存器保存了最后调用的两个函数的指令指针，这就是它们出现在调用跟踪记录边上的原因。在这个例子中，我们可以看到，故障指令位于一个由
sys_write 调用的函数中。</p>
		<p>要注意的是，无论平台/结构是怎样的一种配合情况，用来显示反汇编代码的格式与 objdump
程序所使用的格式是一样的。objdump 是个很强大的工具；如果想查看发生故障的完整函数，可以调用命令： objdump -d
faulty.o（再次重申，对于 SPARC64 平台，需要使用特殊选项：--target elf64-sparc-architecture
sparc:v9）。</p>
		<p>关于 objdump 和它的命令行选项的更多信息，可以参阅这个命令的手册页帮助。</p>
		<p>学习对
oops
消息进行解码，需要一定的实践经验，并且了解所使用的目标处理器，以及汇编语言的表达习惯等。这样的准备是值得的，因为花费在学习上的时间很快会得到回
报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识，仍有必要花些时间对此进行学习，因为Unix 的语法与 Intel
的语法并不一样。（在 as 命令 infor 页的“i386-specific”一章中，对这种差异进行了很好的描述。）</p>
		<p>4.4.2  系统挂起<br />尽
管内核代码中的大多数错误仅会导致一个oops
消息，但有时它们则会将系统完全挂起。如果系统挂起了，任何消息都无法打印。例如，如果代码进入一个死循环，内核就会停止进行调度，系统不会再响应任何动
作，包括 Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然；要么就是亡羊补牢，在发生挂起后调试代码。</p>
		<p>通
过在一些关键点上插入 schedule 调用可以防止死循环。schedule
函数（正如读者猜到的）会调用调度器，并因此允许其他进程“偷取”当然进程的CPU时间。如果该进程因驱动程序的错误而在内核空间陷入死循环，则可以在跟
踪到这种情况之后，借助 schedule 调用杀掉这个进程。</p>
		<p>当然，应该意识到任何对 schedule
的调用都可能给驱动程序带来代码重入的问题，因为 schedule
允许其他进程开始运行。假设驱动程序进行了合适的锁定，这种重入通常还并不致于带来问题。不过，一定不要在驱动程序持有spinlock
的任何时候调用 schedule。</p>
		<p>如果驱动程序确实会挂起系统，而又不知该在什么位置插入 schedule 调用时，最好的方法是加入一些打印信息，并把它们写入控制台（通过修改 console_loglevel 的数值）。</p>
		<p>有
时系统看起来象挂起了，但其实并没有。例如，如果键盘因某种奇怪的原因被锁住了就会发生这种情况。运行专为探明此种情况而设计的程序，通过查看它的输出情
况，可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器；只要它保持更新，就说明 scheduler
正在工作。如果没有使用图形显示，则可以运行一个程序让键盘LED闪烁，或不时地开关软驱马达，或不断触动扬声器（通常蜂鸣声是令人烦恼的，应尽量避免；
可改为寻求 ioctl 命令 KDMKTONE ），来检查 scheduler 是否工作正常。O’Reilly
FTP站点上可以找到一个例子（misc-progs/heartbeat.c），它会使键盘LED不断闪烁。</p>
		<p>如果键盘不接收输入，最佳的
处理方法是从网络登录到系统中，杀掉任何违例的进程，或是重新设置键盘（用 kdb_mode
-a）。然而，如果没有可用的网络用来帮助恢复的话，即使发现了系统挂起是由键盘死锁造成的也没有用了。如果是这样的情况，就应该配置一种可替代的输入设
备，以便至少可以正常地重启系统。比起去按所谓的“大红钮”，在你的计算机上，通过替代的输入设备来关机或重启系统要更为容易些，而且它可以免去fsck
对磁盘的长时间扫描。</p>
		<p>例如，这种替代输入设备可以是鼠标。1.10或更新版本的 gpm
鼠标服务器可以通过命令行选项支持类似的功能，不过仅限于文本模式。如果没有网络连接，并且以图形方式运行，则建议采用某些自定义的解决方案，比如，设置
一个与串口线 DCD 针脚相连的开关，并编写一个查询 DCD 信号状态变化的脚本，用于从外界干预键盘已被死锁的系统。</p>
		<p>对于上述情形，
一个不可缺少的工具是“magic SysRq key”，2.2 和后期版本内核中，在其它体系结构上也可利用得到它。SysRq
魔法键是通过PC键盘上的 ALT 和 SysRq 组合键来激活的，在 SPARC 键盘上则是 ALT 和 Stop
组合键。连同这两个键一起按下的第三个键，会执行许多有用动作中的其中一种，这些动作如下：</p>
		<p>r<br />在无法运行 kbd_mode 的情况中，关闭键盘的 raw 模式。</p>
		<p>k <br />激活“留意安全键”（SAK）功能。SAK 将杀掉当前控制台上运行的所有进程，留下一个干净的终端。</p>
		<p>s <br />对所有磁盘进行紧急同步。</p>
		<p>u <br />尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着 s 动作之后被调用，它可以在系统处于严重故障状态时节省很多检查文件系统的时间。</p>
		<p>b <br />立即重启系统。注意先要同步并重新挂装磁盘。</p>
		<p>p <br />打印当前的寄存器信息。</p>
		<p>t <br />打印当前的任务列表。</p>
		<p>m <br />打印内存信息。</p>
		<p>还
有其它的一些 SysRq 功能；要获得完整列表，可参阅内核源码 Documentation 目录下的sysrq.txt 文件。注意，SysRq
功能必须明确地在内核配置中被开启，出于安全原因，大多数发行系统并未开启它。不过，对于一个用于驱动程序开发的系统来说，为开启 SysRq
功能而带来的重新编译新内核的麻烦是值得的。SysRq 必须在运行时通过下面的命令启动：</p>
		<p>[quote]<br />echo 1 &gt; /proc/sys/kernel/sysrq<br />[/quote]</p>
		<p>在
复现系统的挂起故障时，另一个要采取的预防措施是，把所有的磁盘都以只读的方式挂装在系统上（或干脆就卸装它们）。如果磁盘是只读的或者并未挂装，就不会
发生破坏文件系统或致使文件系统处于不一致状态的危险。另一个可行方法是，使用通过 NFS
(网络文件系统)将其所有文件系统挂装入系统的计算机。这个方法要求内核具有“NFS-Root”的能力，而且在引导时还需传入一些特定参数。如果采用这
种方法，即使我们不借助于 SysRq，也能避免任何文件系统的崩溃，因为NFS 服务器管理文件系统的一致性，而它并不受驱动程序的影响。</p>
		<p>4.5  调试器和相关工具<br />最后一种调试模块的方法就是使用调试器来一步步地跟踪代码，查看变量和计算机寄存器的值。这种方法非常耗时，应该尽量避免。不过，某些情况下通过调试器对代码进行细粒度的分析是很有价值的。</p>
		<p>在内核中使用交互式调试器是一个很复杂的问题。出于对系统所有进程的整体利益考虑，内核在它自己的地址空间中运行。其结果是，许多用户空间下的调试器所提供的常用功能很难用于内核之中，比如断点和单步调试等。本节着眼于调试内核的几种方法；它们中的每一种都各有利弊。</p>
		<p>4.5.1  使用 gdb<br />gdb在探究系统内部行为时非常有用。在我们这个层次上，熟练使用调试器，需要掌握 gdb 命令、了解目标平台的汇编代码，还要具备对源代码和优化后的汇编码进行匹配的能力。</p>
		<p>启动调试器时必须把内核看作是一个应用程序。除了指定未压缩的内核映像文件名外，还应该在命令行中提供“core 文件”的名称。对于正运行的内核，所谓 core 文件就是这个内核在内存中的核心映像，/proc/kcore。典型的 gdb 调用如下所示：</p>
		<p>gdb /usr/src/linux/vmlinux /proc/kcore</p>
		<p>第一个参数是未经压缩的内核可执行文件的名字，而不是 zImage 或 bzImage 以及其他任何压缩过的内核。</p>
		<p>gdb
命令行的第二个参数是是 core 文件的名字。与其它 /proc中的文件类似，/proc/kcore也是在被读取时产生的。当在
/proc文件系统中执行 read 系统调用时，它会映射到一个用于数据生成而不是数据读取的函数上；我们已在“使用
/proc文件系统”一节中介绍了这个特性。kcore 用来按照 core
文件的格式表示内核“可执行文件”；由于它要表示对应于所有物理内存的整个内核地址空间，所以是一个非常巨大的文件。在 gdb
的使用中，可以通过标准gdb命令查看内核变量。例如，p jiffies可以打印从系统启动到当前时刻的时钟滴答数。</p>
		<p>从gdb
打印数据时，内核仍在运行，不同数据项的值会在不同时刻有所变化；然而，gdb为了优化对 core
文件的访问，会将已经读到的数据缓存起来。如果再次查看jiffies变量，仍会得到和上次一样的值。对通常的 core
文件来说，对变量值进行缓存是正确的，这样可避免额外的磁盘访问。但对“动态的”core 文件来说就不方便了。解决方法是在需要刷新gdb
缓冲区的时候，执行命令core-file /proc/kcore；调试器将使用新的 core
文件并丢弃所有的旧信息。不过，读新数据时并不总是需要执行core-file 命令；gdb 以几KB大小的小数据块形式读取 core
文件，缓存的仅是已经引用的若干小块。</p>
		<p>对内核进行调试时，gdb 通常能提供的许多功能都不可用。例如，gdb 不能修改内核数据；因为在处理其内存映像之前，gdb 期望把待调试程序运行在自己的控制之下。同样，也不能设置断点或观察点，或者单步跟踪内核函数。</p>
		<p>如果用调试选项（-g）编译了内核，产生的 vmlinux 会比没有使用 -g选项的更适合于gdb。不过要注意，用 -g选项编译内核需要大量的磁盘空间（每个目标文件和内核自身都会比通常的大三倍甚至更多）。</p>
		<p>在
非PC类计算机上，情况则不尽相同。在 Alpha 上，make boot会在生成可启动映像前将调试信息去掉，所以最终会获得 vmlinux 和
vmlinux.gz 两个文件。gdb 可以使用前者，后者用来启动。在SPARC上，默认情况则是不把内核（至少是2.0内核）调试信息去掉。</p>
		<p>当
用 -g选项编译内核并且和 /proc/kcore一起使用 vmlinux 运行调试器时，gdb
可以返回很多内核内部信息。例如，可以使用下面的命令来转储结构数据，如p *module_list、p
*module_list-&gt;next 和 p *chrdevs[4]-&gt;fops 等。为了在使用 p
命令时取得最好效果，有必要保留一份内核映射表和随手可及的源码。 </p>
		<p>利用 gdb
可在当前内核上执行的另一个有用任务是，通过disassemble命令（可缩写为 disass
）或是“检查指令”（x/i）命令对函数进行反汇编。disassemble 命令的参数可以是函数名或是内存范围；而 x/i
则使用一个内存地址做为参数，也可以是符号名称的形式。例如，可以用 x/20i 反汇编 20
条指令。注意，不能反汇编一个模块的函数，因为调试器作用的是 vmlinux，它并不知道模块的情况。如果试图通过地址反汇编模块代码，gdb
很有可能会返回“Cannot access memory at xxxx（不能访问 xxxx
处的内存）”这样的信息。基于同样的原因，也不能查看属于模块的数据项。如果已知道变量的地址，可以从 /dev/mem
中读出它们的值，但要弄明白从系统内存中分解出的原始数据的含义，难度是相当大的。</p>
		<p>如果需要反汇编模块函数，最好对模块的目标文件用
objdump
工具进行处理。很不幸，该工具只能对磁盘上的文件复本进行处理，而不能对运行中的模块进行处理；因此，由objdump给出的地址都是未经重定位的地址，
与模块的运行环境无关。对未经链接的目标文件进行反汇编的另一个不利因素在于，其中的函数调用仍是未作解析的，所以就无法轻松地区分是对 printk
的调用呢，还是对 kmalloc 的调用。</p>
		<p>正如上面看到的，当目的在于查看内核的运行情况时，gdb是一个有用的工具，但对于设备驱动程序的调试，它还缺少一些至关重要的功能。</p>
		<p>4.5.2  kdb 内核调试器<br />很
多读者可能会奇怪这一点，即为什么不把一些更高级的调试功能直接编译进内核呢。答案很简单，因为 Linus
不信任交互式的调试器。他担心这些调试器会导致一些不良的修改，也就是说，修补的仅是一些表面现象，而没有发现问题的真正原因所在。因此，没有在内核中内
建调试器。</p>
		<p>然而，其他的内核开发人员偶尔也会用到一些交互式的调试工具。kdb 就是其中一种内建的内核调试器，它在
oss.sgi.com 上以非正式的补丁形式提供。要使用
kdb，必须首先获得这个补丁（取得的版本一定要和内核版本相匹配），然后对当前内核源码进行 patch
操作，再重新编译并安装这个内核。注意，kdb 仅可用于 IA-32(x86) 系统（虽然用于 IA-64
的一个版本在主流内核源码中短暂地出现过，但很快就被删去了）。</p>
		<p>一旦运行的是支持 kdb 的内核，有几个方法可以进入 kdb 的调试状态。在控制台上按下 Pause（或 Break）键将启动调试。当内核发生 oops，或到达某个断点时，也会启动 kdb。无论是哪一种情况，都看到下面这样的消息：</p>
		<p>[quote]<br />Entering kdb (0xc1278000) on processor 1 due to Keyboard Entry [1]kdb&gt;  <br />[/quote]</p>
		<p>注意，当 kdb 运行时，内核所做的每一件事情都会停下来。当激活 kdb 调试时，系统不应运行其他的任何东西；尤其是，不要开启网络――当然，除非是在调试网络驱动程序。一般来说，如果要使用 kdb 的话，最好在启动时进入单用户模式。</p>
		<p>作为一个例子，考虑下面这个快速的 scull 调试过程。假定驱动程序已被载入，可以象下面这样指示 kdb 在 scull_read 函数中设置一个断点：</p>
		<p>[quote]<br />[1]kdb&gt; bp scull_read<br />Instruction(i) BP #0 at 0xc8833514 (scull_read)     is enabled on cpu 1</p>
		<p>[1]kdb&gt; go<br />[/quote]</p>
		<p>bp 命令指示 kdb 在内核下一次进入 scull_read 时停止运行。随后我们输入 go 继续执行。在把一些东西放入 scull 的某个设备之后，我们可以在另一个终端的 shell 中运行 cat 命令尝试读取这个设备，这样一来就会产生如下的状态：</p>
		<p>[quote]<br />Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515<br />Instruction(i) breakpoint #0 at 0xc8833514<br />scull_read+0x1:   movl   %esp,%ebp<br />[0]kdb&gt;<br />[/quote]</p>
		<p>我们现在正处于 scull_read 的开头位置。为了查明是怎样到达这个位置的，我们可以看看堆栈跟踪记录：</p>
		<p>[quote]<br />[0]kdb&gt; bt<br />    EBP       EIP         Function(args)<br />0xc3109c5c 0xc8833515  scull_read+0x1<br />0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, <br />0x1000, 0x804ad78)<br />0xbffffc88 0xc010bec0  system_call<br />[0]kdb&gt;<br />[/quote]</p>
		<p>kdb 试图打印出调用跟踪所记录的每个函数的参数列表。然而，它往往会被编译器所使用的优化技巧弄糊涂。所以在这个例子中，虽然 scull_read 实际只有四个参数，kdb 却打印出了五个。</p>
		<p>下面我们来看看如何查询数据。mds 命令是用来对数据进行处理的；我们可以用下面的命令查询 scull_devices 指针的值：</p>
		<p>[quote]<br />[0]kdb&gt; mds scull_devices 1<br />c8836104: c4c125c0 ....<br />[/quote]</p>
		<p>在这里，我们请求查看的是从 scull_devices 指针位置开始的一个字大小（4个字节）的数据；应答告诉我们设备数据数组的起始地址位于 c4c125c0。要查看设备结构自身的数据值，我们需要用到这个地址：</p>
		<p>[quote]<br />[0]kdb&gt; mds c4c125c0 <br />c4c125c0: c3785000  ....<br />c4c125c4: 00000000  ....<br />c4c125c8: 00000fa0  ....<br />c4c125cc: 000003e8  ....<br />c4c125d0: 0000009a  ....<br />c4c125d4: 00000000  ....<br />c4c125d8: 00000000  ....<br />c4c125dc: 00000001  ....<br />[/quote]</p>
		<p>上
面的8行分别对应于 Scull_Dev 结构中的8个成员。因此，通过显示的这些数据，我们可以知道，第一个设备的内存是从 0xc3785000
开始分配的，链表中没有下一个数据项，量子大小为 4000（十六进制形式为 fa0）字节，量子集大小为 1000（十六进制形式为
3e8）,这个设备中有 154 个字节（十六进制形式为 9a）的数据，等等。</p>
		<p>kdb 还可以修改数据。假设我们要从设备中削减一些数据：</p>
		<p>[quote]<br />[0]kdb&gt; mm c4c125d0 0x50<br />0xc4c125d0 = 0x50<br />[/quote]</p>
		<p>接下来对设备的 cat 操作所返回的数据就会少于上次。</p>
		<p>kdb 还有许多其他的功能，包括单步调试（根据指令，而不是C源代码行），在数据访问中设置断点，反汇编代码，跟踪链表，访问寄存器数据等等。加上 kdb 补丁之后，在内核源码树的 Documentation/kdb 目录可以找到完整的手册页。</p>
		<p>4.5.3  集成的内核调试器补丁<br />有
很多内核开发人员为一个名为“集成的内核调试器”的非正式补丁作出过贡献，我们可将其简称为 IKD（integrated kernel
debugger）。IKD 提供了很多值得关注的内核调试工具。x86 是这个补丁的主要平台，不过它也可以用于其它的结构体系之上。IKD
补丁可以从 ftp://ftp.kernel.org/pub/linux/kernel/people/andrea/ikd
下载。它是一个必须应用于内核源码的patch 补丁；因为这个 patch 是与版本相关的，所以要确保下载的补丁与正使用的内核版本相一致。</p>
		<p>IKD
补丁的功能之一是内核堆栈调试。如果开启这个功能，内核就会在每个函数调用时检查内核堆栈的空闲空间的大小，如果过小的话就会强制产生一个
oops。如果内核中的某些事情引起堆栈崩溃，这个工具就能用来帮助查找问题。这其实也就是一种“堆栈计量表”的功能，可以在任何特定的时刻查看堆栈的填
充程度。</p>
		<p>IKD
补丁还包含了一些用于发现内核死锁的工具。如果某个内核过程持续时间过久而没有得到调度的话，“软件死锁”探测器就会强制产生一个
oops。这是简单地通过对函数调用进行计数来实现的，如果计数值超过了一个预定义的阈值，探测器就会动作，并中止一些工作。IKD
的另一个功能是可以连续地把程序计数器打印到虚拟控制台上，这可以作为跟踪死锁的最后手段。“信号量死锁”探测器则是在某个进程的 down
调用持续时间过久时强制产生 oops。</p>
		<p>IKD 中的其它调试功能包括内核的跟踪功能，它可以记录内核代码的执行路径。还有一些内存调试工具，包括一个内存泄漏探测器和一些称为“poisoner”的工具，它们在跟踪内存崩溃问题时非常有用。</p>
		<p>最后，IKD 也包含前一节讨论过的 kdb 调试器。不过，IKD 补丁中的 kdb 版本有些老。如果需要 kdb 的话，我们推荐直接从 oss.sgi.com 获取当前的版本。</p>
		<p>4.5.4  kgdb 补丁<br />kgdb
是一个在Linux 内核上提供完整的 gdb 调试器功能的补丁，不过仅限于 x86
系统。它通过串口连线以钩子的形式挂入目标调试系统进行工作，而在远端运行 gdb。使用 kgdb
时需要两个系统――一个用于运行调试器，另一个用于运行待调试的内核。和 kdb 一样，kgdb 目前可从 oss.sgi.com 获得。</p>
		<p>设
置 kgdb包括安装内核补丁并引导打过补丁之后的内核两个步骤。两个系统之间需要通过串口电缆（或空调制解调器电缆）进行连接，在 gdb
这一侧，需要安装一些支持文件。kgdb 补丁把详细的用法说明放在了文件 Documentation/i386/gdb-serial.txt
中；我们在这里就不再赘述。建议读者阅读关于“调试模块”的说明：接近末尾的地方，有一些出于这个目的而编写的很好的 gdb 宏。 </p>
		<p>4.5.5  内核崩溃转储分析器<br />崩
溃转储分析器使系统能把发生 oops
时的系统状态记录下来，以便在随后空闲的时候查看这些信息。如果是对于一个异地用户的驱动程序进行支持，这些工具就会特别有用。用户可能不太愿意把
oops
复制下来，因此安装崩溃转储系统可以使技术支持人员不必依赖于用户的工作，也能获得用于跟踪用户问题的必要信息。也正是出于这样的原因，可供利用的崩溃转
储分析器都是由那些对用户系统进行商业支持的公司开发的，这也就不足为奇了。</p>
		<p>目前有两个崩溃转储分析器的补丁可以用于 Linux。在编写本节的时候，这两个工具都比较新，而且都处在不断的变化之中。与其提供可能已经过时的详细信息，我们倒不如只是给出一个概观，并指点读者在哪里可以找到更多的信息。</p>
		<p>第
一个分析器是 LKCD（Linux Kernel Crash Dumps,“Linux内核崩溃转储”）。这个工具仍可以从
oss.sgi.com 上获得。当内核发生 oops 时，LKCD
会把当前系统状态（主要指内存）写入事先指定好的转储设备中。这个转储设备必须是一个系统交换区。下次重启中（在存储交换功能开启之前）系统会运行一个称
为 LCRASH 的工具，来生成崩溃的概要记录，并可选择地把转储的复本保存在一个普通文件中。LCRASH
可以交互方式地运行，提供了很多调试器风格的命令，用以查询系统状态。</p>
		<p>LKCD 目前只支持 Intel 32位体系结构，并只能用在 SCSI 磁盘的交换分区上。</p>
		<p>另
一个崩溃转储设施可以从 www.missioncriticallinux.com 获得。这个崩溃转储子系统直接在目录 /var/dumps
中创建崩溃转储文件，而且并不使用交换区。这样就使某些事情变得更为容易，但也意味着在知道问题已经出现在哪里的时候，文件系统已被系统修改。生成的崩溃
转储的格式是标准的 core 文件格式，所以可以利用 gdb
这类工具进行事后的分析。这个工具包也提供了另外的分析器，可以从崩溃转储文件中解析出比 gdb 更丰富的信息。</p>
		<p>4.5.6  用户模式的 Linux 虚拟机<br />用
户模式 Linux 是一个很有意思的概念。它作为一个独立的可移植的 Linux 内核而构建，包含在子目录 arch/um
中。然而，它并不是运行在某种新的硬件上，而是运行在基于 Linux 系统调用接口所实现的虚拟机之上。因此，用户模式 Linux 可以使
Linux 内核成为一个运行在 Linux 系统之上单独的、用户模式的进程。</p>
		<p>把一个内核的复本当作用户模式下的进程来运行可以带来很多
好处。因为它运行在一个受约束的虚拟处理器之上，所以有错误的内核不会破坏“真正的”系统。对软/硬件的不同配置可以在相同的框架中轻易地进行尝试。并
且，对于内核开发人员来说最值得注目的特点在于，可以很容易地利用 gdb 或其它调试器对用户模式 Linux
进行处理。归根结底，它只是一个进程。很明显，用户模式 Linux 有潜力加快内核的开发过程。</p>
		<p>迄今为止，用户模式 Linux
虚拟机还未在主流内核中发布；要下载它，必须访问它的 web
站点（http://user-mode-linux.sourceforge.net）。需要提醒的是，它仅可以集成到 2.4.0 之后的早期
2.4 内核版本中；当然等到本书出版的时候，版本支持方面可能会做得更好。</p>
		<p>目前，用户模式 Linux
虚拟机也存在一些重大的限制，不过大部分可能很快就会得到解决。虚拟处理器当前只能工作于单处理器模式；虽然虚拟机可以毫无问题地运行在 SMP
系统上，但它仍是把主机模拟成单 CPU
模式。不过，对于驱动编写者来说，最大的麻烦在于，用户模式内核不能访问主机系统上的硬件设备。因此，尽管用户模式
Linux虚拟机对于本书中的大多数样例驱动程序的调试非常有用，却无法用于调试那些处理实际硬件的驱动程序。最后一点，用户模式
Linux虚拟机仅能运行在 IA-32 体系结构之上。</p>
		<p>因为对所有这些问题的修补工作正在进行之中，所以在不远的将来，对于 Linux 设备驱动程序的开发人员，用户模式 Linux虚拟机可能会成为一个不可或缺的工具。</p>
		<p>4.5.7  Linux 跟踪工具包<br />Linux 跟踪工具包（LTT）是一个内核补丁，包含了一组可以用于内核事件跟踪的相关工具集。跟踪内容包括时间信息，而且还能合理地建立在一段指定时间内所发生事件的完整图形化描述。因此，LTT不仅能用于调试，还能用来捕捉性能方面的问题。</p>
		<p>在 Web 站点 www.opersys.com/LTT 上，可以找到 LTT 以及大量的资料。</p>
		<p>4.5.8  Dynamic Probes <br />Dynamic
Probes （或 DProbes ）是 IBM 为基于 IA-32 结构的Linux 发布的一种调试工具（遵循 GPL
协议）。它可以在系统的几乎任何一个地方放置一个“探针”，既可以是用户空间也可以是内核空间。这个探针由一些当控制到达指定地点即开始执行的代码（用一
种特别设计的，面向堆栈的语言编写）组成。这种代码能把信息传送回用户空间，修改寄存器，或者完成许多其它的工作。DProbes
很有用的特点是，一旦内核编译进了这个功能，探针就可以插到一个运行系统的任一个位置，而无需重建内核或重新启动。DProbes 也可以协同 LTT
工具在任意位置插入新的跟踪事件。</p>DProbes 工具可以从 IBM 的开放源码站点，即 [url]http://oss.software.ibm.com[/url] 上下载。<img src ="http://www.cnitblog.com/schkui/aggbug/11077.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/schkui/" target="_blank">爱易</a> 2006-05-26 11:03 <a href="http://www.cnitblog.com/schkui/archive/2006/05/26/11077.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>