#
远程调试环境由宿主机GDB和目标机调试stub共同构成,两者通过串口或TCP连接。使用GDB标准远程串行协议协同工作,实现对目标机上的系统内核和
上层应用的监控和调试功能。调试stub是嵌入式系统中的一段代码,作为宿主机GDB和目标机调试程序间的一个媒介而存在。
就目前而言,嵌入式Linux系统中,主要有三种远程调试方法,分别适用于不同场合的调试工作:用ROM
Monitor调试目标机程序、用KGDB调试系统内核和用gdbserver调试用户空间程序。这三种调试方法的区别主要在于,目标机远程调试stub
的存在形式的不同,而其设计思路和实现方法则是大致相同的。 而我们最常用的是调试应用程序。就是采用gdb+gdbserver的方式进行调试。
在很多情况下,用户需要对一个应用程序进行反复调试,特别是复杂的程序。采用GDB方法调试,由于嵌入式系统资源有限性,一般不能直接在目标系统上进行调
试,通常采用gdb+gdbserver的方式进行调试。Gdbserver在目标系统中运行,gdb则在宿主机上运行。 要进行GDB调试,目标
系统必须包括gdbserver程序,宿主机也必须安装gdb程序。一般linux发行版中都有一个可以运行的gdb,但开发人员不能直接使用该发行版中
的gdb来做远程调试,而要获取gdb的源代码包,针对arm平台作一个简单配置,重新编译得到相应gdb。gdb的源代码包可以从http:
//ftp.cs.pu.edu.tw/Linux/sourceware/gdb/releases/下载,最新版本为gdb-6.4。下载到某个目
录,笔者下载到自己的用户目录:/home/vicky。 下载完后,进入/home/vicky目录,配置编译步骤如下: #tar jxvf gdb-6.4-tar-bz2 #cd gdb-6.4 #./configure --target=arm-linux --prefix=/usr/local/arm-gdb -v #make (这一步的时候可能会有问题,提示一个函数中(具体函数名不记得了)parse error,就是unsigned前边多了一个”}”,你用vi进入那一行把它删掉就行了。) #make install #export PATH=$PATH:/usr/local/arm-gdb 进入gdbserver目录: #./configure --target=arm-linux –host=arm-linux #make CC=/usr/local/arm/2.95.3/bin/arm-linux-gcc (这一步要指定arm-linux-gcc的位置,可能跟你的不一样) 没有错误的话就在gdbserver目录下生成gdbserver可执行文件,把它烧写到flash的根文件系统分区,或通过nfs mount的方式都可以。只要保证gdbserver能在开发板上运行就行。 下
面就可以用gdb+gdbserver调试我们开发板上的程序了。在目标板上运行gdbserver,其实就是在宿主机的minicom下,我的red
hat linux装在vmware下的。我是在minicom下#mount 192.168.2.100:/ /tmp后做的(这里参数-o
nolock可以不加,不加这一步执行得反而更快些),hello和gdbserver都是位于linux根目录下,把主机根目录挂在到开发板的/tmp
目录下。 要进行gdb调试,首先要在目标系统上启动gdbserver服务。在gdbserver所在目录下输入命令: (minicom下) #cd /tmp #./gdbserver 192.168.2.100:2345 hello 192.168.2.100为宿主机IP,在目标系统的2345端口开启了一个调试进程,hello为要调试的程序。 出现提示: Process /tmp/hello created: pid=80 Listening on port 2345
(另一个终端下) #cd / #export PATH=$PATH:/usr/local/arm-gdb/bin #arm-linux-gdb hello (gdb) target remote 192.168.2.223:2345 (192.168.2.223为开发板IP) 出现提示: Remote debugging using 192.168.2.223:2345 [New thread 80] [Switching to thread 80] 0x40002a90 in ??() 同时在minicom下提示: Remote debugging from host 192.168.2.100 (gdb) 连接成功,这时候就可以输入各种gdb命令如list、run、next、step、break等进行程序调试了。 以上针对通过nfs mount和tftp的方式,只能在主机上调试好后下载到开发板上运行,如果有错误要反复这个过程,繁琐不说,有些程序只能在开发板上调试。所以笔者采用了gdbserver的远程调试方式。希望对大家调试程序有用!
作者:晏渭川 随着Linux2.6的发布,由于2.6内核做了教的改动,各个设备的驱动程序在不同程度上要 进行改写。为了方便各位Linux爱好者我把自己整理的这分文档share出来。该文当列举 了2.6内核同以前版本的绝大多数变化,可惜的是由于时间和精力有限没有详细列出各个 函数的用法。 特别声明:该文档中的内容来自http://lwn.net,该网也上也有各个函数的较为详细的 说明可供各位参考。如果需要该文档的word版的朋友, 请mail到weiriver@sohu.com索 取。
1、 使用新的入口 必须包含 <linux/init.h> module_init(your_init_func); module_exit(your_exit_func); 老版本:int init_module(void); void cleanup_module(voi); 2.4中两种都可以用,对如后面的入口函数不必要显示包含任何头文件。 2、 GPL MODULE_LICENSE("Dual BSD/GPL"); 老版本:MODULE_LICENSE("GPL"); 3、 模块参数 必须显式包含<linux/moduleparam.h> module_param(name, type, perm); module_param_named(name, value, type, perm); 参数定义 module_param_string(name, string, len, perm); module_param_array(name, type, num, perm); 老版本:MODULE_PARM(variable,type); MODULE_PARM_DESC(variable,type); 4、 模块别名 MODULE_ALIAS("alias-name"); 这是新增的,在老版本中需在/etc/modules.conf配置,现在在代码中就可以实现。 5、 模块计数 int try_module_get(&module); module_put(); 老版本:MOD_INC_USE_COUNT 和 MOD_DEC_USE_COUNT 6、 符号导出 只有显示的导出符号才能被其他模块使用,默认不导出所有的符号,不必使用EXPORT_NO _SYMBOLS 老板本:默认导出所有的符号,除非使用EXPORT_NO_SYMBOLS 7、 内核版本检查 需要在多个文件中包含<linux/module.h>时,不必定义__NO_VERSION__ 老版本:在多个文件中包含<linux/module.h>时,除在主文件外的其他文件中必须定义_ _NO_VERSION__,防止版本重复定义。 8、 设备号 kdev_t被废除不可用,新的dev_t拓展到了32位,12位主设备号,20位次设备号。 unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode); 老版本:8位主设备号,8位次设备号 int MAJOR(kdev_t dev); int MINOR(kdev_t dev); 9、 内存分配头文件变更 所有的内存分配函数包含在头文件<linux/slab.h>,而原来的<linux/malloc.h>不存在 老版本:内存分配函数包含在头文件<linux/malloc.h> 10、 结构体的初试化 gcc开始采用ANSI C的struct结构体的初始化形式: static struct some_structure = { .field1 = value, .field2 = value, .. }; 老版本:非标准的初试化形式 static struct some_structure = { field1: value, field2: value, .. }; 11、 用户模式帮助器 int call_usermodehelper(char *path, char **argv, char **envp, int wait); 新增wait参数 12、 request_module() request_module("foo-device-%d", number); 老版本: char module_name[32]; printf(module_name, "foo-device-%d", number); request_module(module_name); 13、 dev_t引发的字符设备的变化 1、取主次设备号为 unsigned iminor(struct inode *inode); unsigned imajor(struct inode *inode); 2、老的register_chrdev()用法没变,保持向后兼容,但不能访问设备号大于256的设备 。 3、新的接口为 a)注册字符设备范围 int register_chrdev_region(dev_t from, unsigned count, char *name); b)动态申请主设备号 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char *name); 看了这两个函数郁闷吧^_^!怎么和file_operations结构联系起来啊?别急! c)包含 <linux/cdev.h>,利用struct cdev和file_operations连接 struct cdev *cdev_alloc(void); void cdev_init(struct cdev *cdev, struct file_operations *fops); int cdev_add(struct cdev *cdev, dev_t dev, unsigned count); (分别为,申请cdev结构,和fops连接,将设备加入到系统中!好复杂啊!) d)void cdev_del(struct cdev *cdev); 只有在cdev_add执行成功才可运行。 e)辅助函数 kobject_put(&cdev->kobj); struct kobject *cdev_get(struct cdev *cdev); void cdev_put(struct cdev *cdev); 这一部分变化和新增的/sys/dev有一定的关联。 14、 新增对/proc的访问操作 <linux/seq_file.h> 以前的/proc中只能得到string, seq_file操作能得到如long等多种数据。 相关函数: static struct seq_operations 必须实现这个类似file_operations得数据中得各个成 员函数。 seq_printf(); int seq_putc(struct seq_file *m, char c); int seq_puts(struct seq_file *m, const char *s); int seq_escape(struct seq_file *m, const char *s, const char *esc); int seq_path(struct seq_file *m, struct vfsmount *mnt, struct dentry *dentry, char *esc); seq_open(file, &ct_seq_ops); 等等 15、 底层内存分配 1、<linux/malloc.h>头文件改为<linux/slab.h> 2、分配标志GFP_BUFFER被取消,取而代之的是GFP_NOIO 和 GFP_NOFS 3、新增__GFP_REPEAT,__GFP_NOFAIL,__GFP_NORETRY分配标志 4、页面分配函数alloc_pages(),get_free_page()被包含在<linux/gfp.h>中 5、对NUMA系统新增了几个函数: a) struct page *alloc_pages_node(int node_id, unsigned int gfp_mask, unsigned int order); b) void free_hot_page(struct page *page); c) void free_cold_page(struct page *page); 6、 新增Memory pools <linux/mempool.h> mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data); void *mempool_alloc(mempool_t *pool, int gfp_mask); void mempool_free(void *element, mempool_t *pool); int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask); 16、 per-CPU变量 get_cpu_var(); put_cpu_var(); void *alloc_percpu(type); void free_percpu(const void *); per_cpu_ptr(void *ptr, int cpu) get_cpu_ptr(ptr) put_cpu_ptr(ptr) 老版本使用 DEFINE_PER_CPU(type, name); EXPORT_PER_CPU_SYMBOL(name); EXPORT_PER_CPU_SYMBOL_GPL(name); DECLARE_PER_CPU(type, name); DEFINE_PER_CPU(int, mypcint); 2.6内核采用了可剥夺得调度方式这些宏都不安全。 17、 内核时间变化 1、现在的各个平台的HZ为 Alpha: 1024/1200; ARM: 100/128/200/1000; CRIS: 100; i386: 1000; IA-64: 1024; M68K: 100; M68K-nommu: 50-1000; MIPS: 100/128/1000; MIPS64: 100; PA-RISC: 100/1000; PowerPC32: 100; PowerPC64: 1000; S/390: 100; SPARC32: 100; SPARC64: 100; SuperH: 100/1000; UML: 100; v850: 24-100; x86-64: 1000. 2、由于HZ的变化,原来的jiffies计数器很快就溢出了,引入了新的计数器jiffies_64 3、#include <linux/jiffies.h> u64 my_time = get_jiffies_64(); 4、新的时间结构增加了纳秒成员变量 struct timespec current_kernel_time(void); 5、他的timer函数没变,新增 void add_timer_on(struct timer_list *timer, int cpu); 6、新增纳秒级延时函数 ndelay(); 7、POSIX clocks 参考kernel/posix-timers.c 18、 工作队列(workqueue) 1、任务队列(task queue )接口函数都被取消,新增了workqueue接口函数 struct workqueue_struct *create_workqueue(const char *name); DECLARE_WORK(name, void (*function)(void *), void *data); INIT_WORK(struct work_struct *work, void (*function)(void *), void *data); PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); 2、申明struct work_struct结构 int queue_work(struct workqueue_struct *queue, struct work_struct *work); int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay); int cancel_delayed_work(struct work_struct *work); void flush_workqueue(struct workqueue_struct *queue); void destroy_workqueue(struct workqueue_struct *queue); int schedule_work(struct work_struct *work); int schedule_delayed_work(struct work_struct *work, unsigned long delay); 19、 新增创建VFS的"libfs" libfs给创建一个新的文件系统提供了大量的API. 主要是对struct file_system_type的实现。 参考源代码: drivers/hotplug/pci_hotplug_core.c drivers/usb/core/inode.c drivers/oprofile/oprofilefs.c fs/ramfs/inode.c fs/nfsd/nfsctl.c (simple_fill_super() example) 20、 DMA的变化 未变化的有: void *pci_alloc_consistent(struct pci_dev *dev, size_t size, dma_addr_t *dma_handle); void pci_free_consistent(struct pci_dev *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle); 变化的有: 1、 void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag); void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle); 2、列举了映射方向: enum dma_data_direction { DMA_BIDIRECTIONAL = 0, DMA_TO_DEVICE = 1, DMA_FROM_DEVICE = 2, DMA_NONE = 3, }; 3、单映射 dma_addr_t dma_map_single(struct device *dev, void *addr, size_t size, enum dma_data_direction direction); void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction); 4、页面映射 dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction); void dma_unmap_page(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction); 5、有关scatter/gather的函数: int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); void dma_unmap_sg(struct device *dev, struct scatterlist *sg, int nhwentries, enum dma_data_direction direction); 6、非一致性映射(Noncoherent DMA mappings) void *dma_alloc_noncoherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag); void dma_sync_single_range(struct device *dev, dma_addr_t dma_handle, unsigned long offset, size_t size, enum dma_data_direction direction); void dma_free_noncoherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle); 7、DAC (double address cycle) int pci_dac_set_dma_mask(struct pci_dev *dev, u64 mask); void pci_dac_dma_sync_single(struct pci_dev *dev, dma64_addr_t dma_addr, size_t len, int direction); 21、 互斥 新增seqlock主要用于: 1、少量的数据保护 2、数据比较简单(没有指针),并且使用频率很高 3、对不产生任何副作用的数据的访问 4、访问时写者不被饿死 <linux/seqlock.h> 初始化 seqlock_t lock1 = SEQLOCK_UNLOCKED; 或seqlock_t lock2; seqlock_init(&lock2); void write_seqlock(seqlock_t *sl); void write_sequnlock(seqlock_t *sl); int write_tryseqlock(seqlock_t *sl); void write_seqlock_irqsave(seqlock_t *sl, long flags); void write_sequnlock_irqrestore(seqlock_t *sl, long flags); void write_seqlock_irq(seqlock_t *sl); void write_sequnlock_irq(seqlock_t *sl); void write_seqlock_bh(seqlock_t *sl); void write_sequnlock_bh(seqlock_t *sl); unsigned int read_seqbegin(seqlock_t *sl); int read_seqretry(seqlock_t *sl, unsigned int iv); unsigned int read_seqbegin_irqsave(seqlock_t *sl, long flags); int read_seqretry_irqrestore(seqlock_t *sl, unsigned int iv, long flags); 22、 内核可剥夺 <linux/preempt.h> preempt_disable(); preempt_enable_no_resched(); preempt_enable_noresched(); preempt_check_resched(); 23、 眠和唤醒 1、原来的函数可用,新增下列函数: prepare_to_wait_exclusive(); prepare_to_wait(); 2、等待队列的变化 typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync); void init_waitqueue_func_entry(wait_queue_t *queue, wait_queue_func_t func); 24、 新增完成事件(completion events) <linux/completion.h> init_completion(&my_comp); void wait_for_completion(struct completion *comp); void complete(struct completion *comp); void complete_all(struct completion *comp); 25、 RCU(Read-copy-update) rcu_read_lock(); void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg); 26、 中断处理 1、中断处理有返回值了。 IRQ_RETVAL(handled); 2、cli(), sti(), save_flags(), 和 restore_flags()不再有效,应该使用local_save _flags() 或local_irq_disable()。 3、synchronize_irq()函数有改动 4、新增int can_request_irq(unsigned int irq, unsigned long flags); 5、 request_irq() 和free_irq() 从 <linux/sched.h>改到了 <linux/interrupt.h> 27、 异步I/O(AIO) <linux/aio.h> ssize_t (*aio_read) (struct kiocb *iocb, char __user *buffer, size_t count, loff_t pos); ssize_t (*aio_write) (struct kiocb *iocb, const char __user *buffer, size_t count, loff_t pos); int (*aio_fsync) (struct kiocb *, int datasync); 新增到了file_operation结构中。 is_sync_kiocb(struct kiocb *iocb); int aio_complete(struct kiocb *iocb, long res, long res2); 28、 网络驱动 1、struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *)); struct net_device *alloc_etherdev(int sizeof_priv); 2、新增NAPI(New API) void netif_rx_schedule(struct net_device *dev); void netif_rx_complete(struct net_device *dev); int netif_rx_ni(struct sk_buff *skb); (老版本为netif_rx()) 29、 USB驱动 老版本struct usb_driver取消了,新的结构体为 struct usb_class_driver { char *name; struct file_operations *fops; mode_t mode; int minor_base; }; int usb_submit_urb(struct urb *urb, int mem_flags); int (*probe) (struct usb_interface *intf, const struct usb_device_id *id); 30、 block I/O 层 这一部分做的改动最大。不祥叙。 31、 mmap() int remap_page_range(struct vm_area_struct *vma, unsigned long from, unsigned long to, unsigned long size, pgprot_t prot); int io_remap_page_range(struct vm_area_struct *vma, unsigned long from, unsigned long to, unsigned long size, pgprot_t prot); struct page *(*nopage)(struct vm_area_struct *area, unsigned long address, int *type); int (*populate)(struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock); int install_page(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long addr, struct page *page, pgprot_t prot); struct page *vmalloc_to_page(void *address); 32、 零拷贝块I/O(Zero-copy block I/O) struct bio *bio_map_user(struct block_device *bdev, unsigned long uaddr, unsigned int len, int write_to_vm); void bio_unmap_user(struct bio *bio, int write_to_vm); int get_user_pages(struct task_struct *task, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm_area_struct **vmas); 33、 高端内存操作kmaps void *kmap_atomic(struct page *page, enum km_type type); void kunmap_atomic(void *address, enum km_type type); struct page *kmap_atomic_to_page(void *address); 老版本:kmap() 和 kunmap()。 34、 驱动模型 主要用于设备管理。 1、 sysfs 2、 Kobjects
推荐文章: http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kernel26/index.shtml http://www-900.ibm.com/developerWorks/cn/linux/l-inside/index.shtml
2.6里不需要再定义“__KERNEL__”和“MODULE”了。 用下面的Makefile文件编译:
代码:
|
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules |
摘要: The Linux Kernel Module Programming Guide
Peter Jay Salzman
Michael Burian
Ori Pomerantz
... 阅读全文
Rusty Russell, mailing list netfilter@lists.samba.org
$Revision: 1.3 $ $Date: 2002/06/05 13:21:56 $
简体中文:洋鬼鬼·NetSnake
感谢 网中人netmanforever@yahoo.com 提供的繁体参照
此文档描述在Linux2.4 内核中,如何使用iptables过滤不正确的包 (译者:Packet在很多专业书籍中译为分组,此处根据大部分人的习惯,仍译为包)
1. 简介 |
2. 官方站点及邮件列表 |
3. 那么,什么是Packet Filter? |
3.1 我为什么需要Packet Filter? |
3.2 如何在Linux下进行包过滤? |
3.2.1 iptables |
3.2.2 创建永久性规则 |
4. 你算老几,凭什么玩弄我的内核? |
5. Rusty的真正的包过滤快速指南 |
6. 包是如何穿过过滤器的 |
7. 使用iptables |
7.1 当计算机启动后你会看到的 |
7.2 对单个规则的操作 |
7.3 过滤规格 |
7.3.1 指定源和目的IP地址 |
7.3.2 反向指定 |
7.3.3 协议指定 |
7.3.4 接口指定 |
7.3.5 分片指定 |
7.3.6 iptables扩展:新的匹配 |
7.3.6.1 TCP 扩展 |
7.3.6.1.1 TCP标志的解释 |
7.3.6.2 UDP 扩展 |
7.3.6.3 ICMP扩展 |
7.3.6.4 其他匹配的扩展 |
7.3.6.5 状态匹配 |
7.4 目标规格 |
7.4.1 用户定义链 |
7.4.2 iptables扩展:新目标 |
7.4.3 特殊的内建目标 |
7.5 对整个链进行操作 |
7.5.1 创建新链 |
7.5.2 删除链 |
7.5.3 清空一个链 |
7.5.4 对链进行列表 |
7.5.5 重置(清零)计数器 |
7.5.6 设置原则(默认规则) |
8. 使用ipchains和ipfwadm |
9. NAT和包过滤的混合使用 |
10. iptables和ipchains之间的差别 |
11. 对制定包过滤器的建议 |
1. 简介 |
欢迎,亲爱的读者。 |
这篇文章假设你知道有关IP地址、网络地址、网络掩码、选路和DNS。如果不知道,我建议你先阅读网络概念的HowTo(Network Concepts HOWTO)。 |
这篇HOWTO并非一个简要的介绍(会让你发热、发毛,没有安全感),也非一个完全的原始的披露(最吃苦耐劳的人也会被搅晕,不过必定会有所斩获)。 |
你的网络并不安全。问题在于,必须获取快速、简洁的通讯,但又必须限于良好的、无恶意的行为,就如同在嘈杂的大戏院里,你可以高谈阔论,但是绝不能大喊:着火了!。这篇HOWTO不能解决这种问题。 |
(译者:所有安全都只是相对的,否则根本不会产生这种东西了) |
因此,你只能决定在哪方面妥协。我想帮助你使用一些可用的工具和一些通常需要注意的漏洞,希望你将它们用在好的一面,而不是出于恶意的目的 -- 另一个同样重要的问题。 |
(C) 2000 Paul `Rusty' Russell. Licenced under the GNU GPL. |
|
2、 官方站点及邮件列表位置 |
这里有三个官方站点:
o Thanks to Filewatcher http://netfilter.filewatcher.org.
o Thanks to The Samba Team and SGI http://netfilter.samba.org.
o Thanks to Harald Welte http://netfilter.gnumonks.org.
你可以通过以下站点访问全部相关站点。 http://www.netfilter.org and http://www.iptables.org
以下是netfilter官方邮件列表 http://www.netfilter.org/contact.html#list. |
|
3.那么,什么是包过滤器? |
包过滤器是这样一种软件:它检查通过的每个包的头部,然后决定如何处置它们。可以这样对待它们:丢弃(也就是说,如果这个包从未被接受,那么丢弃它),通过(也就是说,让包通过),或者更复杂的(操作)。 |
Linux下,包过滤内建在内核中(内核模块,或者内建),而且我们还有处理包的一些技巧,不过检查头部和处理包的一般性原则仍在这里。 |
|
3.1 我为何要包过滤? |
控制、安全、警戒。 |
|
控制: |
当你用你的Linux服务器把你的内部网和另一个网络(就是Internet吧)连起来,你可以决定哪些通信是允许的,哪些
不允许。例如,包头部包含了包的目标地址,你可以阻碍包发送到(你)确定的几个外部网络,另一个例子,我用NetScape连接到Dilbert
archives。页面上有来自doubleclick.net的广告,然后NetScape浪费了我的时间愉快的下载他们。
告诉包过滤器禁止任何来自或者发往doubleclick.net地址的包,问题就解决了。(当然有更好的办法,见Junkbuster)。 |
|
安全: |
当Linux服务器是混乱的Internet和你良好的、有序的网络之间唯一的东西时,
你最好能知道哪些东西可以进入你的大门。例如,你可以允许所有(包)从你的网络
发出,不过你可能会为来自外部的著名的“Ping of Death”而焦急。另一个例子,你不希望
外人telnet到你的Linux服务器,尽管所有账户都有密码。或许你只想(像绝大多数人)成为
Internet的旁观者,而非它的服务器(也可能愿意是吧)。简单的不允许任何人接入,设置
包过滤器拒绝所有进入的包(是不错的办法)。 |
|
警戒: |
有时,本地网络上错误配置的机器可能会向外部喷射出大量的包。最好是当(网络中)出现任何不正常现象时,让包过滤器告诉你。这样你可能可以做点什么,或者你天生就很好奇。 |
|
3.2 如何在Linux下进行包过滤? |
Linux内核在其1.1系列中就有了包过滤功能。第一代,由Alan Cox
1994年移植于BSD的ipfw。这在Linux 2.0中由Jos
Vos和其他人进行了加强;用户空间工具'ipfwadm'可用来控制内核过滤规则。1998年中,我在Michael
Neuling的帮助下,为Linux 2.2进行了重写,推出了用户空间工具'ipchains'。最后,1999年中,基于Linux
2.4的第四代工具,'iptables',和其他内核的改写正式推出。这就是这个iptables的HOWTO文档的所在。 |
译者:userspace根据台湾同胞的说法,是用来区别系统内存中的适用范围的,分为核心空间和使用者空间,不必深究) |
你需要包含netfilter架构的内核。netfilter是Linux中的一个通用框架,也可以插入(plug in)其他内容(如iptables模块)。也就是说你需要2.3.15及以后版本,而且在配置内核时对CONFIG_NETFILTER回答'Y'。 |
iptables这个工具用来和内核交互并告诉它哪些包应该过滤。除非你是程序员或者
特别好奇,否则这就是你用来控制包过滤的了。 |
|
3.2.1. iptables |
iptables工具向内核的包过滤表中插入和删除规则。这就意味着无论怎样设置,启动后信息都会丢失;请参看“制定永久性规则”(Making Rules Permanent)来确定如何保证下次启动这些规则能被恢复。 |
iptables是ipfwadm和ipchains的替代品。如果你是它们的使用者,请参看
“使用ipchains和ipfwadm”,如何轻松使用iptables。 |
|
3.2.2 创建永久性规则 |
你当前的防火墙设置保存在内核中,所以重启后就会丢失。你可以试着用iptables-save和iptables-restore脚本来保存他们,并由一个文件恢复。 |
|
4. 你算老几,凭什么玩弄我的内核? |
我是Rusty Russell。Linux IP防火墙的维护者,也是一个适当的时候出现在适当的地方的coder。我写了ipchains(参见“如何在Linux下进行包过滤?”看看实际的工作其实由哪些人完成),并希望能学到足够的东西修正这次的包过滤。 |
WatchGuard,一个非常出色的防火墙公司,总之一堆广告,此处省略一千字…… |
在此,我想澄清一个误解:我不是内核专家,我了解它,是因为我的核心工作让我接触了他们:David S.
Miller, Alexey Kuznetsov, Andi Kleen, Alan
Cox。无论如何,他们做了最深层的工作,轮到我时,已经非常安全和容易了。 |
|
5. Rusty的真正的包过滤快速指南 |
绝大部分人只有一个PPP连接到Internet,而且不希望有人由此进入他们的网络或者防火墙: |
# 插入connection-tracking模块(如国内建在内核中就不需要) |
# insmod ip_conntrack |
# insmod ip_conntrack_ftp |
|
# 对创建大量新的连接创建一个链,除非这些连接来自内部。 |
# iptables -N block |
# iptables -A block -m state --state ESTABLISHED,RELATED -j ACCEPT |
# iptables -A block -m state --state NEW -i ! ppp0 -j ACCEPT |
# iptables -A block -j DROP |
|
# 由INPUT和FORWARD链跳往(刚刚创建的)那条链。 |
# iptables -A INPUT -j block |
# iptables -A FORWARD -j block |
|
6. 包是如何穿过过滤器的 |
内核由'filter'表中的以下三个规则开始。这些被称为防火墙链或就叫链。这三个链分别是 INPUT、OUTPUT和FORWARD。 |
对于ASCII艺术迷来说,链好象这样:(注意:这与2.0和2.2内核非常不同) |
译者:ASCII艺术,这里指的是利用纯ASCII文本作图 |
_____ Incoming / \ Outgoing -->[Routing ]--->|FORWARD|-------> [Decision] \_____/ ^ | | v ____ ___ / \ / \ |OUTPUT| |INPUT| \____/ \___/ ^ | | ----> Local Process ----
|
三个圈代表上面说的三个链。当包到达图中的一个圈,那个链就检查并确定包的命运。
如果链决定DROP包,包在那里就被杀死。但是如果链决定让包ACCEPT,包就继续在图中前进。 |
一个链是规则的列表。每个规则都会说:'如果包头看上去像这个的话,那么就这样处理'。
如果规则和包不匹配,由链中的下一个规则处理。最后,如果再也没有要进行处理的规则了,
内核就根据链的原则(policy,有时称为默认规则)来决定应当如何做。在一个注重安全的
系统中,原则通常是让内核丢弃这个包。 |
1. 当一个包进入时(就是由以太网卡),内核首先检查包的目的地。这被称作“选路”。 |
2. 如果它就是进入本机的,包会向图中的下方移动,到达INPUT链。如果到了这里,任何等待这个包的进程都会收到它。 |
3. 否则,如果内核未被允许转发,或者不知道该如何转发这个包,它会被丢弃。如果允许转发,而且包的目的地是另一个网络接口(如果你有另一个的话),那么包向我们图中的右边行进,到达FORWARD链。如果允许通过(ACCEPT),它就被送了出去。 |
4. 最后,服务器上运行的程序可以发送网络包。这些包马上通过OUTPUT链。如果被允(ACCEPT),那么包继续向可以到达它的目的地的网络接口发送。 |
|
7. 使用iptables |
iptables有着非常详尽的使用手册(man iptables),而且如果你需要某个选项更详细的介绍。看看“iptables和ipchains的差别”可能对你非常有用。 |
使用iptables你可以做很多不同的事。开始的内建的三个链INPUT、OUTPUT和FORWARD是不能被删除的。让我们看看整个链的管理。 |
1. 创建一个新的链 (-N)。 |
2. 删除一个空链(-X)。 |
3.修改内建链的原则(-P)。 |
4. 显示链中的规则(表)(-L)。 |
5. 清空一个链(-F)。 |
6. 将链中所有规则的包和字节计数器清零(-Z)。 |
有几种办法操作链中的规则: |
1. 向链中添加一条新规则(-A)。 |
2. 在链中某个位置插入一条新规则(-I)。 |
3. 替换某个位置的规则(-R)。 |
4. 删除链中某个位置的规则,或者是第一个被匹配的。(-D)。 |
|
7.1. 当计算机启动后你会看到的 |
ptables可以作为模块,称为'iptables_filter.o,可以在第一次运行iptables时自动被装载。也可以永久性的编到内核中。 |
在所有iptables命令执行之前(当心:某些发布版会在初始化脚本中运行iptables),所有内建链中都没有任何规
则('INPUT'、'FORWARD'和'OUTPUT'),所有链的原则都是ACCEPT。你可以在装载iptable_filter模块时,提供
'forward=0'选项来修改FORWARD的默认原则。 |
|
7.2. 对单个规则的操作 |
这是基本的包过滤:管理规则,添加(-A)和删除(-D)命令可能是最常用的。其他的(-I插入和-R替换)只是简单的扩展而已。 |
每个规则都有一组条件来匹配包,和如果匹配了该如何做(target)。例如,你可能希望丢弃所有来自127.0.0.1的ICMP包。这样我们的条件就
是协议必须是ICMP,而且源地址必须是127.0.0.1,我们的目标是丢弃(DROP)。127.0.0.1是一个回送接口,即使你没有真正的网络连
接它也会存在。你可以用ping程序生成这样的包(它简单的发送ICMP 类型8(echo request),所有愿意响应的主机都会用ICMP
类型0(echo reply)来响应)。这对于测试非常有用。 |
# ping -c 1 127.0.0.1 |
PING 127.0.0.1 (127.0.0.1): 56 data bytes |
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.2 ms |
--- 127.0.0.1 ping statistics --- |
1 packets transmitted, 1 packets received, 0% packet loss |
round-trip min/avg/max = 0.2/0.2/0.2 ms |
# iptables -A INPUT -s 127.0.0.1 -p icmp -j DROP |
# ping -c 1 127.0.0.1 |
PING 127.0.0.1 (127.0.0.1): 56 data bytes |
--- 127.0.0.1 ping statistics --- |
1 packets transmitted, 0 packets received, 100% packet loss |
这里,第一个ping是成功的('-c 1'告诉ping只发送一个包) |
然后我们可以向'INPUT'链中添加(-A)一个规则,制定来自127.0.0.1('-s 127.0.0.1')的ICMP协议('-p icmp')包都将被丢弃('-j DROP')。 |
然后我们测试我们的规则,用第二个ping。在程序放弃等待永远不可能的响应之前,会暂停一下。 |
我们可以用两种办法中的任一种删除规则。首先,因为知道这是INPUT链中唯一的规则,我们用编号删除: |
# iptables -D INPUT 1 |
删除INPUT链中的编号为1的规则 |
第二种办法是 -A 命令的映射,不过用-D替换-A。当你的链中规则很复杂,而你不想计算它们的编号的时候这就十分有用了。这样的话,我们可以使用: |
# iptables -D INPUT -s 127.0.0.1 -p icmp -j DROP |
-D的语法必须和-A(或者-I或者-R)一样精确。如果链中有多个相同的规则,只会删除第一个。 |
|
7.3 过滤规格 |
我们已经看了,用'-p'指定协议,用'-s'指定源地址,不过还有其他选项我们可以用来指定包的特征。下面是一个详细的手册。 |
|
7.3.1 指定源和目的IP地址 |
源('-s','--source'或'--src')和目的('-d','--destination'或'--
dst')IP地址可以用四种办法指定。最常用的方法是使用全名,就像'localhost'或者'www.linuxhq.com'。第二种办法是指定
IP地址,如'127.0.0.1'。 |
第三和第四种办法允许指定一组IP地址,就像'199.95.207.0/24'或者
'199.95.207.0/255.255.255.0'。这指定了从199.95.207.0到199.95.207.255范围内的所有IP地址。
'/'后面的数字说明哪部分IP地址是有效的。'32'或者'255.255.255.255‘为默认的(匹配整个IP地址)。用'/0'来指定任何IP
地址,像这样: |
# '-s 0/0'在这里是多余的 |
# iptables -A INPUT -s 0/0 -j DROP |
这很少用到,这和上面出现过的不指定'-s'结果完全一样。 |
|
7.3.2 反向指定 |
很多标记,包括'-s'(或'--source')和'-d'('--destination')标记可以在前面加上'!'标志(读作'not'),来匹配所有和给出的 NOT 的地址。例如, '-s ! localhost'匹配所有不是来自本机的包。 |
|
7.3.3 协议指定 |
可以用'-p'(或'--protocol')指定协议。协议可以是数字(如果你知道IP的协议数值)或者像'TCP'、'UDP'或者'ICMP'这类的名称。大小写无所谓,所以'tcp'和'TCP'一样。 |
协议名称前可加上'!',以反向解释它,例如'-p ! TCP'将匹配所有不是TCP的包。 |
|
7.3.4 接口指定 |
'-i'(或'--in-interface')和'-o'(或'--out-interface')选项指定匹配的接口名。接口可以是包进入的('-i')或者送出('-o')的物理设备。你可以用ifconfig命令列出当前'up'的接口。(也就是说正在工作的)。 |
通过INPUT链的包不会有送出接口,所以在这个链中'-o'永远不会匹配。同样,通过OUTPUT链的包也没有进入接口,这个链中的'-i'也不会被匹配。 |
只有通过FORWARD链的包才有进入和送出两个接口。 |
可以指定一个当前不存在的接口。在这个接口可用之前,规则不能匹配任何东西。这对于拨号PPP连接及类似的非常有用(通常是ppp0接口)。 |
一个特殊情况,接口名后面是一个'+',那就会匹配以这个字符串开头的所有接口(无论当前是否存在)。例如,指定一个匹配所有ppp接口的规则,要用到-i ppp+选项。 |
接口名也可以在前面插入 '!',来匹配所有与指定接口不同的包,如-i ! ppp+。 |
|
7.3.5 分片指定 |
译者:为帮助大家理解,此处附上IP数据报的格式,摘自《Internetworking with TCP/IP》
0 | 4 | 8 | 16 | 19 | 24 | 31 | 版本号 | 首部长度 | 服务类型 | 总长度 | 标志符 | 标志 | 分片偏移量 | 寿命 | 协议 | 首部效验和 | 源IP地址 | 目的IP地址 | IP选项 | 填充 | 数据 | …… |
有时一个包太大,不可能适合所有线路。这样的话,包会被分成片,然后当作多个包发送。最终重组这些分片来重建整个包。 | 分片的问题是,被检查的初始片含有整个头部字段(IP+TCP,UDP和ICMP),但随后的包只有一部分头(没有附加协议字段的IP),因此,检查后面的分片的头部(就像有TCP、UDP和ICMP一样)是不可能的。 | 如果你在做NAT或连接追踪,那么所有分片在包过滤代码处理以前都会合并,所以你不需要为分片担心。 | 还请注意,到filter表中的INPUT链(或者任何由NF_IP_LOCAL_IN钩子程序钩入的表)的包实际上由核心IP栈片重组后到达。 | 否则,理解分片是如何被过滤规则处理的就非常重要了。任何过滤规则要求我们没有的信息,将被认为不匹配。这意味着(分片的)
第一片像普通的包一样被处理。第二及后面的片则不会。因此,规则 -p TCP --sport
www(指定源端口为'www')永远不会匹配一个分片(的包)(除了第一片),相反的规则 -p TCP --sport ! www也不会。 | 无论如何,你可以用'-f'(或'--fragment')标记指定专门处理第二及以后的分片的规则。当然也可以指定一个规则,让它不去匹配第二及以后的分片,在'-f'前加上'!'。 | 通常,让第二及以后的分片通过被认为是安全的,因为如果过滤处理了第一片,那么就无法在目标主机上进行重组。不过,已知的Bug是发送分片可能会轻易的让主机崩溃。你自己看着办吧。 | 网络高手注意:当这类检查进行时,畸形的包(防火墙读取的ICMP代码和类型过短的TCP、UDP和ICMP包)都将被丢弃。所以TCP分片从位置8开始。(译者:什么意思?大概是指IP包中的首部字段位置) | 例如,下面的规则会丢弃任何发往192.168.1.1的分片。 | # iptables -A OUTPUT -f -d 192.168.1.1 -j DROP |
| 7.3.6 iptables扩展:新的匹配 | iptables是可扩展的,也就是包括内核和iptables工具都可以扩充新的特性。 | 下列部分扩展是标准的,其他的则是派生的。其他人可以做出扩展并发布给合适的人。 | 内核扩展一般位于内核模块子目录,诸如/lib/modules/2.4.0-test10/kernel/net/ipv4/netfilter。如果你使用了CONFIG_KMOD设置来编译内核,那么它们要求被装载,所以你不需要手工插入。 | iptables程序扩展通常是位于/usr/local/lib/iptables/下的共享库,当然也可能在/lib/iptables或者/usr/lib/iptables,具体的要根据不同的发行版本来确定。 | 扩展有两种:新的目标,新的匹配(我们马上会谈到新的目标)。有些协议自动给出新的测试:如下所示,现有的包括TCP、UDP和ICMP。 | 这样,你可以在命令行中在 '-p'选项后指定新的测试,就可以载入扩展(模块)了。当允许扩展时,可以用'-m'选项装入扩展。 | 在选项后面('-p','-j'或者'-m')加上 '-h'或'--help'来获取扩展的帮助。 | # iptables -p tcp --help |
| 7.3.6.1. TCP 扩展 | 如果指定了'-p tcp',那么TCP扩展将自动加载,并提供下列选项(不匹配分片)。 | --tcp-flags | 可附加一个'!'。有两个标志字串可以通过TCP标记来过滤。第一个标志字符串是mask:你想要测验的标志列表。第二个指出哪些将要被设置。例如: | # iptables -A INPUT --protocol tcp --tcp-flags ALL SYN,ACK -j DROP | 意思是所有标志都将被测试('ALL'和'SYN, ACK,FIN,RST,URG,PSH'同义),不过只设置SYN和ACK。当然也可以用'NONE'表示无标志。 | --syn | 前面的'!'是可选的,是'--tcp-flags SYN, RST, ACK, SYN'的缩写 | --source-port | 后面可以跟一个'!',可以是单个TCP端口,或一段端口。可以是/etc/services中的端口名或者数字。端口范围格式是低端口名 : 高端口名,或者(指定大于或等于给出的端口)是端口名 + :,或者(指定小于或等于给出的端口)是: + 端口名。 | --sport | 就是 '--source-port'。 | --destination-port | --dport | 和上面类似,不过是指定匹配的目的端口(范围)。 | --tcp-option | 可以跟一个'!'和一个数字,匹配的是TCP选项和数字相等的包。如果试图用
这个TCP选项匹配一个没有完整的TCP包头的包,那么这个包会被自动丢弃。 |
| 7.3.6.1.1. TCP标志的解释 | 有时只允许单向的TCP连接会很有用。例如,你可能会允许连接到外部WWW服务器,但不会允许来自那个服务器的连接。 | 最简单的举动可能是阻止来自那个服务器的包,可惜,TCP连接需要包双向传送(才能正常工作)。 | 解决办法是,只阻挡那些用来请求连接的包。这些包称为SYN包(OK,从技术上说,它们的SYN标志被设置,而没有设置RST和ACK标志,不过我们简单的称为SYN包)。通过只阻止这种包,我们就可以阻止来自那些地方的连接企图。 | '--syn'标志是这样用的:只对指定了TCP协议的规则有效。例如,指定来自192.168.1.1的连接请求。 | -p TCP -s 192.168.1.1 --syn | 当然也可以在前面加上'!',意即所有不是初始连接的包。 |
| 7.3.6.2 UDP 扩展 | 这些扩展在指定'-p udp'时自动加载。可以提供 '--source-port'、'--sport'、'--destination-port'和'--dport'等和TCP类似的选项。 |
| 7.3.6.3 ICMP扩展 | 这些扩展在指定'-p icmp'时自动加载。只提供一个新的选项: | --icmp-type | 可以跟'!',icmp类型名称(如'host-unreachable')或者数值(如'3'),或者数值类型/代码(如'3/3')。用'-p icmp --help'可以列出可用的icmp类型名。 |
| 7.3.6.4 其他匹配的扩展 | 这些netfilter包中的其他扩展尚属于演示阶段,(如果安装了的话)可以用'-m'来启用。 | mac | --mac-source | 可以跟一个'!',后面是以太网地址,用冒号分隔的16近制表示,如`--mac-source 00:60:08:91:CC:B7'。 | limit | 此模块必须明确指定'-m limit'或'--match limit'。用来限制匹配的速率。就像抑制记录信息。只会匹配给定的数字/每秒(默认是每小时3个匹配,和5个触发)。可以有两个参数: | --limit | 后面跟数字:指定每秒钟允许的匹配最大平均数。这个数字可以指定
明确的单位,使用'/second'、`/minute'、`/hour' 或者 `/day',或者
只写一部分(如'5/second'和'5/s'一样)。 | --limit-burst | 后面跟一个数字,指明在上面的limit起作用前最大的触发值。 | 这个匹配(项)通常和LOG目标结合起来使用,以对速率限制进行记录。
为了理解它是如何工作的,我们来看看下面这条规则,它使用默认限制参数
记录包。 | # iptables -A FORWARD -m limit -j LOG | 当这条规则第一次启用时,包开始被记录。实际上,由于默认触发是5,前五个包会被记录。然后,每隔20分钟再记录一次包,无
论这期间有多少包到达。而且,每个不匹配包的20分钟间隔里,会恢复一个触发(值)。如果100分钟都没有包到达这个规则,那么所有触发都会恢复,回到起
点。 | 提示:你目前不能以大于59小时的时间来创建这种规则,所以如果你设置一个平均率为一天,那么你的触发率必须小于3。 | 你也可以将此模块用于避免使用快速响应速率的各类拒绝服务攻击(DoS,Denial of Server)。 | (译者:以下是较著名的攻击) | Syn-flood protection: | # iptables -A FORWARD -p tcp --syn -m limit --limit 1/s -j ACCEPT | | Furtive port scanner: | # iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST RST -m limit --limit 1/s -j ACCEPT | | Ping of death: | # iptables -A FORWARD -p icmp --icmp-type echo-request -m limit --limit 1/s -j ACCEPT | 这个模块工作原理类似于“节流阀”,以下是图示。 |
rate (pkt/s) ^ .---. | / DoS \ | / \ Edge of DoS -|.....:.........\....................... DoS的边界 = = (limit * | /: \ limit-burst) | / : \ .-. | / : \ / \ | / : \ / \ End of DoS -|/....:..............:.../.......\..../. DoS结束 = limit | : :`-' `--' -------------+-----+--------------+------------------> time (s) LOGIC => Match | Didn't Match | Match
| 我们匹配由五个包触发的每秒一个包,不过每秒钟第四个包才开始进入(这个规则),进行三秒钟,然后重新开始。 | <--Flood 1--> <---Flood 2--->
Total ^ Line __-- YNNN Packets| Rate __-- YNNN | mum __-- YNNN 10 | Maxi __-- Y | __-- Y | __-- Y | __-- YNNN |- YNNN 5 | Y | Y Key: Y -> Matched Rule | Y N -> Didn't Match Rule | Y |Y 0 +--------------------------------------------------> Time (seconds) 0 1 2 3 4 5 6 7 8 9 10 11 12
| 你可以看见,前五个包是允许超过一个包/每秒(这个速率)的,然后就开始限制。
如果有一个暂停,那么另一个触发也是允许的,但不能超过规则设置的最大速率。 | owner | --uid-owner userid | 根据给出的有效的(数值)user id来匹配包的创建进程。 | --gid-owner groupid | 根据给出的有效的(数值)group id 来匹配包的创建进程。 | --pid-owner processid | 根据给出的process id 来匹配包的创建进程。 | --sid-owner sessionid | 根据给出的 session group 来匹配包的创建进程。 | unclean | 这是试验性模块,必须明确指定'-m unclean'或者'--match unclean'。
它对包进行各种随机判断。此模块还未通过审核,所以不要用在安全设施上。
(可能造成更糟糕的结果,它自己可能还有Bug)。没有提供选项。 |
| 7.3.6.5 状态匹配 | 最有用的匹配标准是'state'扩展。它负责解释'ip_conntrack'模块的connection-tracking分析。
这是推荐使用的(好东东)。 | 通过指定'-m state'来允许附加的'--state'选项,匹配用逗号分割的状态列表('!'标志表明不符合那些状态(的状态))。 | NEW | 由新连接创建的包 | ESTABLISHED | 属于已存在连接的包(也就是说,响应的包) | RELATED | 和一个已存在连接有关,但不是它的一部分的包。如ICMP错误,或者(已加载FTP模块)一个建立FTP数据连接的包。 | INVALID | 由于以下原因而不能被识别的包:包括内存不足和不是相应当前任何连接的ICMP错误。通常这些包会被丢弃。 | 这个强大的匹配扩展的一个例子: | # iptables -A FORWARD -i ppp0 -m state ! --state NEW -j DROP |
| 7.4 目标规格 | 现在,我们知道了如何对包进行测试,但是我们还需要告诉那些匹配的包应该如何做。这被称作规则的目标。 | 有两个很简单的内建目标:DROP和ACCEPT。我们已经看过了。如果包匹配的规则,其目标是这二者中的一个,那么不再考虑更多的规则了:包的命运已经决定。 | 除此之外有两种目标:扩展的和用户定义的链。 |
| 7.4.1 用户定义链 | iptables一个强大的特点是由ipchains继承来的可以让用户创建新的链,附加在三个内建的链上(INPUT、
FORWARD和OUTPUT)。按照惯例,用户定义链使用小写以区分他们。(我们会在“Operations on an Entire
Chains”中描述如何创建新的用户定义链)。 | 当包匹配的链的目标是一个用户定义链时,包就转移到用户定义链中的规则。如果
没有决定包的命运,那么包在(用户定义链)中的移动就结束了,并回到当前链的下一个规则。 | 搞搞ASCII艺术吧。考虑两个(笨蛋)链:INPUT(内建的)和test(用户定义的)。 | `INPUT' `test' ---------------------------- ---------------------------- | Rule1: -p ICMP -j DROP | | Rule1: -s 192.168.1.1 | |--------------------------| |--------------------------| | Rule2: -p TCP -j test | | Rule2: -d 192.168.1.1 | |--------------------------| ---------------------------- | Rule3: -p UDP -j DROP | ---------------------------- | 考虑一个由192.168.1.1到1.2.3.4的TCP包。它进入INPUT链,由Rule1检查 - 不匹配。
Rule2匹配,那么它的目标就是test,所以下一个检查由test开始。test中的第一个规则
Rule1是匹配的,但是没有指定目标,所以由第二个规则Rule2检查。结果是不匹配,而我们
到达了链的尾部。于是回到INPUT链,因为刚刚被Rule2检查,所以现在由Rule3来检查,仍然
不匹配。 | 所以这个包的路线是: | v __________________________ `INPUT' | / `test' v ------------------------|--/ -----------------------|---- | Rule1 | /| | Rule1 | | |-----------------------|/-| |----------------------|---| | Rule2 / | | Rule2 | | |--------------------------| -----------------------v---- | Rule3 /--+___________________________/ ------------------------|--- v
| 用户定义链可以跳转到另一个用户定义链(不过不能循环:如果发现循环,包就会被丢弃)。 |
| 7.4.2 iptables扩展:新目标 | 其他类型的扩展是目标。目标扩展由内核模块组成,而且iptables的一个可选扩展提供了新的命令行选项。有几个扩展是包含在默认netfilter发布中的。 | LOG | --log-level | 跟一个级别名称或数字。合适的名字是(忽略大小写)'debug'、'info'、'notice'、'warning'、
'err'、'crit'、'alert'和'emerg',相当于数字7到0。请参考syslog.conf的手册获取这些级别的说明。默认是
'warning'。 | --log-prefix | 跟一个最多29个字符的字符串,它被写入到log信息的开始处,这样可以区别出来。 | 这个模块最有用的就是跟在limit match后面,这样你就不会被你的log淹没了。 | REJECT | 此模块和'DROP'效果一样,除了会发送一个'port unreachable'的ICMP错误报文。注意如果属于以下情况,ICMP错误报文不会发送: | o 包一开始就是ICMP错误报文,或者是未知的ICMP类型。 | o 包被作为无头的分片过滤了。 | o 我们已经向那里发送了太多的ICMP错误报文(参见/proc/sys/net/ipv4/icmp ratelimit)。 |
| 7.4.3 特殊的内建目标 | 有两个特殊的内建目标:RETURN和QUEUE。 | RETURN如同到达这个链的尾部:如果是内建的链的规则,那么这个链的默认规则将被执行。如果是用户定义链,当跳至这个链中的这条规则(包含RETURN)时,回到前面的链继续匹配。 | QUEUE是一个特别的目标,会为用户空间进程队列这个包。要这样使用,需要两个部件: | o 一个"queue handler",处理用户空间与内核之间的机制。 | o 和一个用户空间用来接收的应用程序,可能是操作,以及对包进行裁决。 | IPv4 iptables的标准queue handler是 ip_queue 模块,跟随内核发布并标记为实验中。 | 下面是一个如何用iptables为用户空间进程队列包的快速例子: | # modprobe iptable_filter | # modprobe ip_queue | # iptables -A OUTPUT -p icmp -j QUEUE | 在这个例子中,本地生成的送出ICMP包(如由ping产生)到达ip_queue模块,然后包被试图送往用户空间应用。如果没有用户空间应用在(那儿)等着,包就被丢弃了。 | 要写一个用户空间应用,需要libipq API。和iptables一起发布。在CVS的testsuite
tools(如redirect.c)中可以找到相关例子。 | 可以通过这里检查ip_queue的状态: | /proc/net/ip_queue | 队列的最大长度(也就是不包含返回包的送往用户空间包的数量)可以通过这里控制: | /proc/sys/net/ipv4/ip_queue_maxlen | 默认队列长度是1024。一旦达到这个长度,新的包就会被丢弃,直到队列长度小于这个值。好的协议如TCP,会对丢弃的包作出拥挤的解释,而且在队列满了后会很理想的将它挡回。无论如何,如果默认值太小的话,最好是多实验以决定队列的最大长度。 |
| 7.5 对整个链进行操作 | iptables一个非常有用的特性是可以将链中相关的规则成组。你可以随意给链取名,不过我建议使用小写字母以避免与内建的链和目标产生冲突。链的名称最长为31个字母。 |
| 7.5.1 创建新链 | 让我们创建一个新链。因为我是个充满想象的家伙,我叫它test。使用'-N'或'--new-chain'选项: | # iptables -N test | 如此简单,现在你可以像上面说的那样放入规则了。 |
| 7.5.2 删除链 | 删除一个链同样简单,使用 '-X'或'--delete-chain'选项。为什么是'-X'?嗯,因为所有合适的字母都已经被使用了。 | # iptables -X test | 有几个删除链的限制:他们必须是空的(见下面的"Flushing a Chain")而且他们不能是任何规则的目标。你也不能删除任何一个内建的链。 | 如果你不指定链名的话,所有可以被删除的用户定义链都将被删除。 |
| 7.5.3 清空一个链 | 这是清除一个链中所有规则的简单方法,使用'-F' 或 '--flush'命令。 | # iptables -F FORWARD | 如果不指定链的话,所有链都将被清空。 |
| 7.5.4 对链进行列表 | 用'-L'或'--list'命令,你可以列出一个链中的所有规则。 | 用户定义链中的'refcnt'是有多少链的规则指向了它。这个值必须为0,然后才可以删除这个链。 | 如果链名被忽略,所有链都将被列出,即便是空的。 | '-L'可以有三个选项。'-n'(数字)选项对于阻止iptables试图查找IP地址时非常有用,因为(如果你像大多数人一样使用DNS)如果你的DNS设置不太合适的话,可能会造成长时间的停顿,或者你滤掉了DNS请求。它还会让TCP或UDP端口以数字显示。 | '-v'选项显示所有规则的细节,包括饱和字节计数器,TOS比较,以及接口。否则这些值是被忽略的。 | 注意,报和字节计数器可以分别使用'K'、'M'或者'G'来代替1000、1,000,000 和1,000,000,000。使用'-x'(扩展数字)标志来打印整个值,不管它有多大。 |
| 7.5.5 重置(清零)计数器 | 可以重置计数器非常有用。可以用'-Z'或'--zero'来完成。 | 考虑下面的: | # iptables -L FORWARD | # iptables -Z FORWARD | 在上述例子中,有些包在'-L'和'-Z'命令之间通过。因此,你可以把'-L'和'-Z'一起使用,读取时就清空计数器。 |
| 7.5.6 设置原则(默认规则) | 我们在前面讨论包是如何通过链的时候,已经解释了当包到达内建链的尾部时会发生什么。这时,链的原则就决定包的命运。只有内建的链(INPUT、OUTPUT和FORWARD)有原则,因为如果包到达用户定义链的尾部会返回到前面的链。
| 原则可以是ACCEPT或DROP,例如: | # iptables -P FORWARD DROP |
| 8. 使用ipchains和ipfwadm | netflter发布中有ipchains.o和ipfwadm.o模块。把其中一个加载到你的内核(注意:他们和ip_tables.o不兼容)。然后你就可以像以前那样使用ipchains和ipfwadm了。 | 这在一段时间内仍然被支持。我认为合理的计算方式是 2*(替代发布 - 初始的稳定版本),超过了这个时间,就应当使用替代的稳定版本了。这意味着在Linux 2.6或2.8中对它们的支持很可能被放弃。 |
| 9. NAT和包过滤的混合使用 | 想要做网络地址转换(参见NAT HowTo)和包过滤的已很常见。好消息是他们可以混合起来使用的,而且工作得非常好。 | 你可以完全忽略你的NAT,来定义你的包过滤。包过滤看见的包的源及目标是“真正”的源和目标。例如,如果你将任何发往
1.2.3.4
80端口的包DNAT到10.1.1.1的8080端口。包过滤器看见的是包发往10.1.1.1的8080端口(真正的目的地),而非1.2.3.4
的80端口。同样,你可以忽略伪装:看到的是包的真实外部IP地址(如10.1.1.1),而响应的则返回到那里。 | 你可以使用'state'匹配扩展,使包过滤器不需要做任何额外的工作,因为无论如何,NAT都会要求连接跟踪。扩展NAT HowTo中简单的伪装例子,以禁止任何来自ppp0接口的新的连接,你可以这样: | #对送至ppp0的包进行伪装 | iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE | # 禁止由ppp0进入的新的或不合适的包 | iptables -A INPUT -i ppp0 -m state --state NEW,INVALID -j DROP | iptables -A FORWARD -i ppp0 -m state --state NEW,INVALID -j DROP | # 开启IP转发 | echo 1 > /proc/sys/net/ipv4/ip_forward |
| 10. iptables和ipchains之间的差别 | o 首先,内建链的名称从小写改成了大写,因为现在的INPUT和OUTPUT链只获取指向本地和本地生成的包。他们用来检查所有进入和发送的包。 | o '-i'标志现在表示进入接口的意思,而且只适用于INPUT和FORWORD链。FORWORD或OUTPUT链中的规则应该将'-i'改为'-o'。 | o TCP和UDP端口现在必须用--source-port或--sport(或者--destination-port/--dport)拼写,而且必须放在'-p tcp'或'-p udp'选项之后,因为TCP或UDP扩展是分别加载的。 | o TCP -y 标志现在是 --syn,而且必须在'-p tcp'之后。 | o DENY目标现在是DROP. | o 对单个链,可以在列出其工作同时清零。 | o 清空内建链同时清除了原则计数器。 | o 列出链给出的是一个计数器的微型的快照。 | o REJECT和LOG现在是扩展目标,意思是他们是独立的内核模块。 | o 链的名称最多可以是31个字符。 | o MASQ现在是MASQUERADE而且使用不同的语法。REDRIRECT,在保留相同的名字时,也经历了语法的改变。参见NAT-HOWTO以获取配置它们的更多信息。 | o -o选项不再用于将包传递给用户空间设备了(见上面的-i)。现在通过 QUEUE目标传递到用户空间。 | o 很可能还有一些我也忘了。 |
| 11. 对制定包过滤器的建议 | 在计算机安全领域中,最明智的办法是阻挡所有东西,然后对需要的开启。这通常称为“凡是没有明确允许的都是禁止的”。我建议这样做如果安全是你最关心的。 | 不要运行任何你不需要的服务,即使你认为你已经阻碍了对它们的访问。 | 如果你创建专用防火墙,开始时不运行任何东西,并阻止所有包,然后添加服务并让需要的包通过。 | 我强调安全:结合tcp-wrappers(对于包过滤器本身的连接),代理(通过包过滤器的连接),路由验证和包过滤。路
由验证是如果包来自未预期的接口那么将被删除:例如,如果你的内部网络地址是10.1.1.0/24,而一个包的源地址是你的外部接口,那么它将被丢弃。
对一个接口如ppp0来说可以这样: | # echo 1 > /proc/sys/net/ipv4/conf/ppp0/rp_filter | 或者对所有已有的或将有的接口: | # for f in /proc/sys/net/ipv4/conf/*/rp_filter; do | # echo 1 > $f | # done | Debian在可能的范围了将这些设为默认。如果你使用非对称路由(如你期望包来自一个其他的方向),你可能需要在这些接口上禁止这一过滤。 | 记录对于当工作不正常时设置防火墙非常有用,但是在一个作为产品的防火墙上,总是应当将它与'limit'匹配结合,以防止有人充斥你的记录。 | 我极力推荐对安全系统使用连接追踪:它虽然会造成负担,因为所有连接都被追踪。但是对于控制对你的网络的访问非常有用。如果
你的内核没有自动加载而且没有内建,你需要加载'ip_conntrack.o'这个模块。如果想要精确追踪复杂的协议,你需要加载合适的相关模块(如
'ip_conntrack_ftp.o')。 | # iptables -N no-conns-from-ppp0 | # iptables -A no-conns-from-ppp0 -m state --state ESTABLISHED,RELATED -j ACCEPT | # iptables -A no-conns-from-ppp0 -m state --state NEW -i ! ppp0 -j ACCEPT | # iptables -A no-conns-from-ppp0 -i ppp0 -m limit -j LOG --log-prefix "Bad packet from ppp0:" | # iptables -A no-conns-from-ppp0 -i ! ppp0 -m limit -j LOG --log-prefix "Bad packet not from ppp0:" | # iptables -A no-conns-from-ppp0 -j DROP | | # iptables -A INPUT -j no-conns-from-ppp0 | # iptables -A FORWARD -j no-conns-from-ppp0 | 建造一个好的防火墙超越了这个HOWTO的范围,不过我的建议是“一切从严”。请参见Security HOWTO获取更多信息,来测试和探索你的服务器。 |
| all pages ended here. |
|
级别: 初级
Hariprasad Nellitheertha, 软件工程师, IBM
2003 年 9 月 01 日
调
试内核问题时,能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB
提供了这种功能。在本文中您将了解如何使用 KDB 所提供的功能,以及如何在 Linux 机器上安装和设置 KDB。您还将熟悉 KDB
中可以使用的命令以及设置和显示选项。
Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。
设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理),但是如果您需要编译内核方面的帮助,请参阅本文结尾处的
参考资料一节。
在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。
入门
KDB 项目是由 Silicon Graphics 维护的(请参阅
参考资料以获取链接),您需要从它的
FTP 站点下
载与内核版本有关的补丁。(在编写本文时)可用的最新 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。
这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。
将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件:
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2
#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2
|
您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。
现在,应用这些补丁:
#patch -p1 <kdb-v4.2-2.4.20-common-1
#patch -p1 <kdb-v4.2-2.4.20-i386-1
|
这些补丁应该干净利落地加以应用。查找任何以 .rej 结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题,那么补丁的应用就不会有任何问题。
接下来,需要构建内核以支持 KDB。第一步是设置
CONFIG_KDB 选项。使用您喜欢的配置机制(xconfig 和 menuconfig 等)来完成这一步。转到结尾处的“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项。
您还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项(如果有的话)则设置
CONFIG_FRAME_POINTER 标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDB off by default”选项。这将设置
CONFIG_KDB_OFF 标志,并且在缺省情况下将关闭 KDB。我们将在后面一节中对此进行详细介绍。
保存配置,然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。
初始化并设置环境变量
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux
源代码树(当然是在打了补丁之后)的 KDB
目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重
新构建并重新安装内核。
激活 KDB
如果编译期间没有选中
CONFIG_KDB_OFF ,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将
kdb=on 标志传递给内核或者通过在挂装了 /proc 之后执行该工作:
#echo "1" >/proc/sys/kernel/kdb
|
倒过来执行上述步骤则会取消激活 KDB。也就是说,如果缺省情况下 KDB 是打开的,那么将
kdb=off 标志传递给内核或者执行下面这个操作将会取消激活 KDB:
#echo "0" >/proc/sys/kernel/kdb
|
在引导期间还可以将另一个标志传递给内核。
kdb=early 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。
调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台(请参阅
参考资料以获取这方面的帮助)并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。
KDB 命令
KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。
内存显示和修改
这一类别中最常用的命令是
md 、
mdr 、
mm 和
mmW 。
md 命令以一个地址/符号和行计数为参数,显示从该地址开始的
line-count 行的内存。如果没有指定
line-count ,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么
md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。
mdr 命令带有地址/符号以及字节计数,显示从指定的地址开始的
byte-count 字节数的初始内存内容。它本质上和
md 一样,但是它不显示起始地址并且不在结尾显示字符转换。
mdr 命令较少使用。
mm 命令修改内存内容。它以地址/符号和新内容作为参数,用
new-contents 替换地址处的内容。
mmW 命令更改从地址开始的
W 个字节。请注意,
mm 更改一个机器字。
示例
显示从 0xc000000 开始的 15 行内存:
将内存位置为 0xc000000 上的内容更改为 0x10:
[0]kdb> mm 0xc000000 0x10
|
寄存器显示和修改
这一类别中的命令有
rd 、
rm 和
ef 。
rd 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了
c 参数,则
rd 显示处理器的控制寄存器;如果带有
d 参数,那么它就显示调试寄存器;如果带有
u 参数,则显示上一次进入内核的当前任务的寄存器组。
rm 命令修改寄存器的内容。它以寄存器名称和
new-contents 作为参数,用
new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。
ef 命令以一个地址作为参数,它显示指定地址处的异常帧。
示例
显示通用寄存器组:
断点
常用的断点命令有
bp 、
bc 、
bd 、
be 和
bl 。
bp 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。
bpa 命令对 SMP 系统中的所有处理器应用断点。
bph 命令强制在支持硬件寄存器的系统上使用它。
bpha 命令类似于
bpa 命令,差别在于它强制使用硬件寄存器。
bd 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。
be 命令启用断点。该命令的参数也是断点号。
bl 命令列出当前的断点集。它包含了启用的和禁用的断点。
bc 命令从断点表中除去断点。它以具体的断点号或
* 作为参数,在后一种情况下它将除去所有断点。
示例
对函数
sys_write() 设置断点:
列出断点表中的所有断点:
清除断点号 1:
>堆栈跟踪
主要的堆栈跟踪命令有
bt 、
btp 、
btc 和
bta 。
bt 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了
CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置
CONFIG_FRAME_POINTER ,那么
bt 命令可能会产生错误的结果。
btp 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。
btc 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行
bt ,然后切换到下一个活动 CPU,以此类推。
bta 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下:
- D:不可中断状态
- R:正运行
- S:可中断休眠
- T:已跟踪或已停止
- Z:僵死
- U:不可运行
这类命令中的每一个都会打印出一大堆信息。请查阅下面的
参考资料以获取这些字段的详细文档。
示例
跟踪当前活动线程的堆栈:
跟踪标识为 575 的进程的堆栈:
其它命令
下面是在内核调试过程中非常有用的其它几个 KDB 命令。
id 命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量
IDCOUNT 确定要显示多少行输出。
ss 命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是
ssb ,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有
call 、
return 和
jump 。
go 命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。
reboot 命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。
ll 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
示例
反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量
IDCOUNT :
执行指令直到它遇到分支转移条件(在本例中为指令
jne )为止:
[0]kdb> ssb
0xc0105355 default_idle+0x25: cli
0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax
0xc0105359 default_idle+0x29: test %eax, %eax
0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
|
技巧和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练
的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修
正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。
技巧 #1
在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。
示例
表明函数
sys_read 从地址 0xc013db4c 开始:
[0]kdb> 0xc013db4c
0xc013db4c = 0xc013db4c (sys_read)
|
同样,
同样,表明
sys_write 位于地址 0xc013dcc8:
[0]kdb> sys_write
sys_write = 0xc013dcc8 (sys_write)
|
这些有助于在分析堆栈时找到全局数据和函数地址。
技巧 #2
在编译带 KDB 的内核时,只要
CONFIG_FRAME_POINTER
选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with
frame
pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在
i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。
例如,在函数
rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:
[0]kdb> md %ebp
0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000
0xc74c9f48 08053328 c0425238 c04253a8 00000000
0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25
0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc
0xc74c9f78 c014fe45 c74c8000 00000000 08053328
[0]kdb> 0xc0136c40
0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
[0]kdb> 0xc0136a25
0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
[0]kdb> 0xc0136d6d
0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
|
我们可以看到
rmqueue() 被
__alloc_pages 调用,后者接下来又被
_alloc_pages 调用,以此类推。
每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。
技巧 #3
go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用
rm 命令修改指令指针寄存器,然后只要输入
go 。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。
技巧 #4
您可以利用一个名为
defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。
defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:
[0]kdb> defcmd name "usage" "help"
[0]kdb> [defcmd] type the commands here
[0]kdb> [defcmd] endefcmd
|
例如,可以定义一个(简单的)新命令
hari ,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:
[0]kdb> defcmd hari "" "no arguments needed"
[0]kdb> [defcmd] md 0xc000000 1
[0]kdb> [defcmd] rd
[0]kdb> [defcmd] md %ebp 1
[0]kdb> [defcmd] endefcmd
|
该命令的输出会是:
[0]kdb> hari
[hari]kdb> md 0xc000000 1
0xc000000 00000001 f000e816 f000e2c3 f000e816
[hari]kdb> rd
eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
....
...
[hari]kdb> md %ebp 1
0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
[0]kdb>
|
技巧 #5
可以使用
bph 和
bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。
示例
每当将四个字节写入地址 0xc0204060 时就进入内核调试器:
[0]kdb> bph 0xc0204060 dataw 4
|
在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:
[0]kdb> bph 0xc000000 datar 2
|
结束语
对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试
摘要: 掌握 Linux 调试技术
在 Linux 上找出并解决程序错误的主要方法
... 阅读全文
1. 几种内核调试工具比较 kdb:只能在汇编代码级进行调试; 优点是不需要两台机器进行调试。 gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。 kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试) 使用kdb和gdb调试内核的方法相对比较简单,这里只描述如何使用kgdb来调试内核。 2.软硬件准备 环境: 一台开发机developer(192.168.16.5 com1),一台测试机target(192.168.16.30 com2),都预装redhat 9;一根串口线 下载以下软件包: linux内核2.4.23 linux-2.4.23.tar.bz2 kgdb内核补丁1.9版 linux-2.4.23-kgdb-1.9.patch 可调试内核模块的gdb gdbmod-1.9.bz2 3.ok,开始 3.1 测试串口线 物理连接好串口线后,使用一下命令进行测试,stty可以对串口参数进行设置 在developer上执行: stty ispeed 115200 ospeed 115200 -F /dev/ttyS0 echo hello > /dev/ttyS0 在target上执行: stty ispeed 115200 ospeed 115200 -F /dev/ttyS1 cat /dev/ttyS1 串口线没问题的话在target的屏幕上显示hello 3.2 安装与配置 3.2.1 安装 下载linux-2.4.23.tar.bz2,linux-2.4.23-kgdb-1.9.patch,gdbmod-1.9.bz2到developer的/home/liangjian目录 *在developer机器上 #cd /home/liangjian #bunzip2 linux-2.4.23.tar.bz2 #tar -xvf linux-2.4.23.tar #bunzip2 gdbmod-1.9.bz2 #cp gdbmod-1.9 /usr/local/bin #cd linux-2.4.23 #patch -p1 < ../linux-2.4.23-kgdb-1.9.patch #make menuconfig 在Kernel hacking配置项中将以下三项编译进内核 KGDB: Remote (serial) kernel debugging with gdb KGDB: Thread analysis KGDB: Console messages through gdb 注意在编译内核的时候需要加上-g选项 #make dep;make bzImage 使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具) #scp arch/i386/boot/bzImage root@192.168.16.30:/boot/vmlinuz-2.4.23-kgdb #scp System.map root@192.168.16.30:/boot/System.map-2.4.23-kgdb #scp arch/i386/kernel/gdbstart root@192.168.16.30:/sbin gdbstart为kgdb提供的一个工具,用于激活内核钩子,使内核处于调试状态 3.2.2 配置 *在developer机器上 在内核源码目录下编辑一文件.gdbinit(该文件用以对gdb进行初始化),内容如下: #vi .gdbinit define rmt set remotebaud 115200 target remote /dev/ttyS0 end # 以上在.gdbinit中定义了一个宏rmt,该宏主要是设置使用的串口号和速率 *在target机器上 编辑/etc/grub.conf文件,加入以下行: #vi /etc/grub.conf title Red Hat Linux (2.4.23-kgdb) root (hd0,0) kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1 # 在root目录下建立一个脚本文件debugkernel,内容如下: #vi debug #!/bin/bash gdbstart -s 115200 -t /dev/ttyS1 <<EOF EOF #chmod +x debugkernel 这个脚本主要是调用gdbstart程序设置target机上使用的串口及其速率,并使内核处于调试状态 3.3 开始调试 target上的内核或内核模块处于调试状态时,可以查看其变量、设置断点、查看堆栈等,并且是源码级的调试,和用gdb调试用户程序一样 3.3.1 内核启动后调试 *在target机器上 重启系统,选择以 2.4.23-kgdb内核启动,启动完成后运行debugkenel, 这时内核将停止运行,在控制台屏幕上显示信息,并等待来自developer的 串口连接 #./debug About to activate GDB stub in the kernel on /dev/ttyS1 Waiting for connection from remote gdb... *在developer机器上 #cd /home/liangjian/linux-2.4.23 # gdb vmlinux GNU gdb Red Hat Linux (5.3post-0.20021129.18rh) Copyright 2003 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"... 执行rmt宏 (gdb) rmt breakpoint () at kgdbstub.c:1005 1005 atomic_set(&kgdb_setting_breakpoint, 0); 这时target上的内核处于调试状态,可以查看其变量、设置断点、查看堆栈等,和用gdb调试用户程序一样 查看堆栈 (gdb) bt #0 breakpoint () at kgdbstub.c:1005 #1 0xc0387f48 in init_task_union () #2 0xc01bc867 in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc0387f98) at gdbserial.c:158 #3 0xc010937b in handle_IRQ_event (irq=3, regs=0xc0387f98, action=0xce5a9860) at irq.c:452 #4 0xc0109597 in do_IRQ (regs= {ebx = -1072671776, ecx = -1, edx = -1070047232, esi = -1070047232, edi = -1070047232, ebp = -1070039092, eax = 0, xds = -1070071784, xes = -1070071784, orig_eax = -253, eip = -1072671729, xcs = 16, eflags = 582, esp = -1070039072, xss = -1072671582}) at irq.c:639 #5 0xc010c0e8 in call_do_IRQ () 查看jiffies变量的值 (gdb) p jiffies $1 = 76153 如果想让target上的内核继续运行,执行continue命令 (gdb) continue Continuing. 3.3.2 内核在引导时调试 kgdb可以在内核引导时就对其进行调试,但并不是所有引导过程都是可调试的,如在kgdb 1.9版中,它在init/main.c的start_kernel()函数中插入以下代码: start_kernel() { ...... smp_init(); #ifdef CONFIG_KGDB if (gdb_enter) { gdb_hook(); /* right at boot time */ } #endif ...... } 所以在smp_init()之前的初始化引导过程是不能调试的。 另外要想让target的内核在引导时就处于调试状态,需要修改其/etc/grub.conf文件为如下形式: title Red Hat Linux (2.4.23-kgdb) root (hd0,0) kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1 gdb gdbttyS=1 gdbbaud=115200 *在target机器上 引导2.4.23-kgdb内核,内核将在短暂的运行后暂停并进入调试状态,打印如下信息: Waiting for connection from remote gdb... *在developer机器上 #cd /home/liangjian/linux-2.4.23 # gdb vmlinux GNU gdb Red Hat Linux (5.3post-0.20021129.18rh) Copyright 2003 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"... 执行rmt宏 (gdb) rmt breakpoint () at kgdbstub.c:1005 1005 atomic_set(&kgdb_setting_breakpoint, 0); 查看当前堆栈 (gdb) bt #0 breakpoint () at kgdbstub.c:1005 #1 0xc0387fe0 in init_task_union () #2 0xc01bc984 in gdb_hook () at gdbserial.c:250 #3 0xc0388898 in start_kernel () at init/main.c:443 在do_basic_setup函数处设置断点,并让内核恢复运行 (gdb) b do_basic_setup Breakpoint 1 at 0xc0388913: file current.h, line 9. (gdb) continue Continuing. [New Thread 1] [Switching to Thread 1] Breakpoint 1, do_basic_setup () at current.h:9 9 __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); 内核在do_basic_setup断点处停止运行后查看当前堆栈 (gdb) bt #0 do_basic_setup () at current.h:9 (gdb) 3.3.3 内核模块调试调试 要想调试内核模块,需要相应的gdb支持,kgdb的主页上提供了一个工具gdbmod,它修正了gdb 6.0在解析模块地址时的错误,可以用来正确的调试内核模块 *在developer机器上 写了个测试用的内核模块orig,如下: void xcspy_func() { printk("<1>xcspy_func\n"); printk("<1>aaaaaaaaaaa\n"); } int xcspy_init() { printk("<1>xcspy_init_module\n"); return 0; } void xcspy_exit() { printk("<1>xcspy_cleanup_module\n"); } module_init(xcspy_init); module_exit(xcspy_exit); 编译该模块: #cd /home/liangjian/lkm #gcc -D__KERNEL__ -DMODULE -I/home/liangjian/linux-2.4.23/include -O -Wall -g -c -o orig.o orig.c #scp orig.o root@192.168.16.30:/root 开始调试: # gdbmod vmlinux GNU gdb 6.0 Copyright 2003 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu"... 设置符号文件的搜索路径 (gdb) set solib-search-path /home/liangjian/lkm 执行rmt宏 (gdb) rmt breakpoint () at kgdbstub.c:1005 1005 atomic_set(&kgdb_setting_breakpoint, 0); 设置断点使得可以调试内核模块的init函数,查内核源码可知,内核是通过module.c文件的第566行(sys_init_module函数中)mod->init来调用模块的init函数的 (gdb) b module.c:566 Breakpoint 1 at 0xc011cd83: file module.c, line 566. (gdb) c Continuing. [New Thread 1352] [Switching to Thread 1352] 这时在target机器上执行insmod orig.o,developer则相应的在断点处被暂停,如下 Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001", mod_user=0x80904d8) at module.c:566 566 if (mod->init && (error = mod->init()) != 0) { 使用step命令进入模块的init函数 (gdb) step xcspy_init () at orig.c:12 12 printk("<1>xcspy_init_module\n"); (gdb) n 15 } (gdb) 说明: 调
试内核模块的非init函数相对比较简单,只要先在target上执行insmod
orig.o,这时由于模块的符号被加载,可以直接在developer的gdb中对想调试的模块函数设置断点,如bt
xcspy_func,后面当xcspy_func被调用时就进入了调试状态。 如果想调试内核模块的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处被暂停。
原文链接:http://www.xfocus.net/articles/200509/820.html
Linux2.6 驱动设计――从 2.4 到 2.6
作者 Ray
RTEMS版权所有,转载请注明来源ray@rtems" target="_blank">www.rtems.net,作者ray@rtems
Linux 2.6 和 2.4 的比较我不想废话,总体来说 2.6 功能更强,但是资源消耗更多。
由于 2.6 内核在驱动框架,底层调用上和 2.4 内核有很多差别,所以本文主要是为程序员提供 2.4 到 2.6 迁移的指导。
2.6 和 2.4 主要的不同在于
? 内核的 API 变化,增加了不少新功能(例如 mem pool )
? 提供 sysfs 用于描述设备树
? 驱动模块从 .o 变为 .ko
移植 hello word
下面是一个最简单的 2.4 驱动:
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void)
{
printk(KERN_INFO "Hello, world\n");
return 0;
}
void cleanup_module(void)
{
printk(KERN_INFO "Goodbye cruel world\n");
}
2.6的hello world版本!
#include < linux/module.h>
#include < linux/config.h>
#include < linux/init.h>
MODULE_LICENSE("GPL");// 新,否则有 waring, 去掉了 #define MODULE, 自动定义
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);// 必须!!
module_exit(hello_exit); // 必须!!
注意,在 2.4 中 module_init 不是必须的,只要驱动的初始化函数以及析沟函数命名使用了缺省的 init_module 和 cleanup_module
编译生成:
2.4
gcc -D__KERNEL__ -DMODULE -I/usr/src/linux- 2.4.27 /include -O2 -c testmod.c
2.6
makefile 中要有 obj-m:= hello.o
然后:
make -C /usr/src/linux- 2.6.11 SUBDIRS=$PWD modules (当然简单的 make 也可以)
哈哈够简单!!
其他不同:
计数器:
以前 2.4 内核使用 MOD_INC_USE_COUNT 增加计数例如:
void
inc_mod(void)
{
MOD_INC_USE_COUNT;
} /* end inc_mod */
/************************************************************************
* Calculator DEC
************************************************************************/
void
dec_mod(void)
{
MOD_DEC_USE_COUNT;
} /* end dec_mod */
现在这个也过时了 !!
2.6 ,用户函数要加载模块,使用:
int try_module_get(&module);
函数卸载模块使用
module_put()
而驱动中不必显示定义 inc 和 dec
没有 kdev_t 了
现在都流行用 dev_t 了 , 而且是 32 位的
结构体初始化
老版本使用:
static struct some_structure = {
field1: value,
field2: value
};
现在流行用:
static struct some_structure = {
.field1 = value,
.field2 = value,
...
};
malloc.h
要用核态内存?用 <linux/slab.h> 好了,
内存池
内存管理变化不大,添加了memory pool*(#include<linux/mempool.h>
)。(现在什么都是pool,内存 线程
....)这是为块设备提供的,使用mempool最大的优点是分配内存不会错,也不会等待(怎么这个也和RTEMS学)。对于embed的设备还是有些
用处的。标准调用方式:
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data);
使用mempool的函数 :
mempool_alloc_t / mempool_free_t
mempool_alloc_slab / mempool_free_slab
mempool_alloc / mempool_free
mempool_resize
结构体赋值
以前,驱动中结构体赋值使用:
static struct some_structure = {
field1: value,
field2: value
};
现在要用:
static struct some_structure = {
.field1 = value,
.field2 = value,
...
};
例如 :
static struct file_operations yourdev_file_ops = {
.open = yourdev_open,
.read = yourdev_read_file,
.write = yourdev_write_file,
};
min() , max()
不少程序员定义自己的 min 和 max 宏,大家也知道宏不安全, Linux 定义了 min 和 max 函数(注意不是模板函数,需要自己制定比较类型!)
原型变化
call_usermodehelper()
request_module()
函数原型变化了,用到的赶快查 !!!
Devfs
唉,还没怎么用,就过时了的技术,这就是 linux 。不是我们落伍,是世界变化快
字符设备
2.6 中主从设备编号不再局限于原来的 8bit
register_chrdev() 可以升级为:
int register_chrdev_region(dev_t from, // 设备号 unsigned count, // 注册号 char *name); // 名称
该函数的动态版本(不知道主设备号时使用)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor,
unsigned count, char *name);
新的 file_operations
register_chrdev_region 没有使用 register_chrdev_region 参数,因为设备现在使用 struct cdev 来定义他在 <linux/cdev.h> 中定义。
struct cdev {
struct kobject kobj;
struct module *owner;
struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
使用时首先需要分配空间: struct cdev *cdev_alloc(void);
然后对其初始化:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev 使用 kobject_set_name 设置名称:例如:
struct cdev *my_cdev = cdev_alloc(); kobject_set_name(&cdev->kobj, "my_cdev%d", devnum);
此后就可以使用 int cdev_add(struct cdev *cdev, dev_t dev, unsigned count); 将 cdev 加入到系统中。
删除 cdev 使用
void cdev_del(struct cdev *cdev);
对于没有使用 cdev_add 添加的 cdev 使用 kobject_put(&cdev->kobj); 删除
也就是使用: struct kobject *cdev_get(struct cdev *cdev); void
cdev_put(struct cdev *cdev);
太极链;
struct inode -> i_cdev -> cdev
这样就从 inode 链接到 cdev
驱动体系:
/dev 到 /sys
/dev 也过时了,设备文件都跑到 /sys 里了!
# cd /sys
# ls
block bus class devices firmware kernel module power
知到了2.6内核的好处,很想在以后的开发中用2.6内核。但是不知道从2.4到2.6驱动的变化有多大,驱动是不是要自己一个个改?
我初学只能写些简单的驱动,或参考别人的驱动来开发。这种情况用2.6内核搞驱动是不是难度很大?
希望做过驱动移植的高手关照。
今天试着配置了一下Cscope,用起来确实蛮方便的,如果和Ctags配合使用的好的话,的确可以和windows平台下的sourceinsight有的一拼。不过这个熟练的过程不会太轻松:-|
cscope
cscope应该不需要安装,一般linux的发行版都会带的有(也有可能是vim的dependence
package,这个我没有研究),在shell下输入cscope -V可以查看cscope的版本号,我这边是15.5。
-
cscope的配置和简单使用可以参考:The
Vim/Cscope tutorial,貌似还没有简体翻译版本,看了一下确实很简单,所以没人翻译啦。
-
还有就是man cscope(under shell)和:help cscope(in vim)
我配完之后有一个小问题:Cscope里对split window query
mode的键盘绑定是Ctrl-Blankspace,对于中文用户来说,这一般也是中文输入法的short-key。这就需要修改
~/.vim/plugin/cscope_maps.vim文件,把其中对ctrl+blankspace(在vim中表示为ctrl+@)的绑定修改
为你特性的组合键。
ctags
ctags
可以在sf的站点上下载,编译安装后就可以使用了。
简单的使用方法是:
-
在你存放源代码的文件夹下运行ctags *,即对所有问题件做tag
-
此时,用vim -t funcname就可以直接用vim打开含有funcname函数的文件,光标也停留在该文件
-
在vim中,使用<ctrl+]>可以跳转到光标所在位置的函数实现(类似于vim帮助中的调准),使用<ctrl+T>可以跳转回之前的光标位置。
其他参考资料
Linux内核解读入门
作者: 火 发布日期: 2006-5-12 查看数: 178 出自: http://www.linuxdiyf.com
针对好多Linux 爱好者对内核很有兴趣却无从下口,本文旨在介绍一种解读linux内核源码的入门方法,而不是解说linux复杂的内核机制;
一.核心源程序的文件组织:
1.
Linux核心源程序通常都安装在/usr/src/linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如2.0.30)都是一个稳定地
发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心。本文基于稳定的2.2.5源代码,第二部分的实现平台为 Redhat
Linux 6.0。
2.核心源程序的文件按树形结构进行组织,在源程序树的最上层你会看到这样一些目录: ●Arch :arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录;
●Include:
include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel
cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
●Init: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的一个非常好的起点。
●Mm :这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/*/mm/,例如arch/i386/mm/Fault.c
●Kernel:主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/*/kernel中;
●Drivers:
放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block
下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看
drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化网络,因为安装nfs文件
系统的时候需要网络其他:如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码;Fs ,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2;
●Scripts,
此目录包含用于配置核心的脚本文件等。一般,在每个目录下,都有一个 .depend 文件和一个 Makefile
文件,这两个文件都是编译时使用的辅助文件,仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助;而且,在有的目录下还有Readme
文件,它是对该目录下的文件的一些说明,同样有利于我们对内核源码的理解;
二.解读实战:为你的内核增加一个系统调用
虽
然,Linux 的内核源码用树形结构组织得非常合理、科学,把功能相关联的文件都放在同一个子目录下,这样使得程序更具可读性。然而,Linux
的内核源码实在是太大而且非常复杂,即便采用了很合理的文件组织方法,在不同目录下的文件之间还是有很多的关联,分析核心的一部分代码通常会要查看其它的
几个相关的文件,而且可能这些文件还不在同一个子目录下。体系的庞大复杂和文件之间关联的错综复杂,可能就是很多人对其望而生畏的主要原因。
当
然,这种令人生畏的劳动所带来的回报也是非常令人着迷的:你不仅可以从中学到很多的计算机的底层的知识(如下面将讲到的系统的引导),体会到整个操作系统
体系结构的精妙和在解决某个具体细节问题时,算法的巧妙;而且更重要的是:在源码的分析过程中,你就会被一点一点地、潜移默化地专业化;甚至,只要分析十
分之一的代码后,你就会深刻地体会到,什么样的代码才是一个专业的程序员写的,什么样的代码是一个业余爱好者写的。
为了使读者能更好的体会到这一特点,下面举了一个具体的内核分析实例,希望能通过这个实例,使读者对 Linux的内核的组织有些具体的认识,从中读者也可以学到一些对内核的分析方法。
以下即为分析实例:
【一】操作平台:
硬件:cpu intel Pentium II ; 软件:Redhat Linux 6.0; 内核版本2.2.5
【二】相关内核源代码分析:
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还要做一些其他的初始化工作;
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,&system_call);把系统调用总控程序的入口挂
在中断0x80上;其中SYSCALL_VECTOR是定义在
/usr/src/linux/arch/i386/kernel/irq.h中的一个常量0x80; 而
system_call即为中断总控程序的入口地址;中断总控程序用汇编语言定义在
/usr/src/linux/arch/i386/kernel/entry.S中;
3.中断总控程序主要负责保存处理机执行系统调用前的状态,检验当前调用是否合法, 并根据系统调用向量,使处理机跳转到保存在 sys_call_table 表中的相应系统服务例程的入口; 从系统服务例程返回后恢复处理机状态退回用户程序; 而
系统调用向量则定义在/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中也定义了系统调用的用户编程接口;
4.由此可见 , linux 的系统调用也象 dos 系统的 int 21h 中断服务, 它把0x80 中断作为总的入口, 然后转到保存在 sys_call_table 表中的各种中断服务例程的入口地址 , 形成各种不同的中断服务;
由以上源代码分析可知, 要增加一个系统调用就必须在 sys_call_table 表中增加一项 , 并在其中保存好自己的系统服务例程的入口地址,然后重新编译内核,当然,系统服务例程是必不可少的。
由此可知在此版linux内核源程序<2.2.5>中,与系统调用相关的源程序文件就包括以下这些: 1.arch/i386/boot/bootsect.S 2.arch/i386/Kernel/setup.S 3.arch/i386/boot/compressed/head.S 4.arch/i386/kernel/head.S 5.init/main.c 6.arch/i386/kernel/traps.c 7.arch/i386/kernel/entry.S 8.arch/i386/kernel/irq.h 9.include/asm-386/unistd.h 当然,这只是涉及到的几个主要文件。而事实上,增加系统调用真正要修改文件只有include/asm-386/unistd.h和arch/i386/kernel/entry.S两个;
【三】 对内核源码的修改:
1.在kernel/sys.c中增加系统服务例程如下:
asmlinkage int sys_addtotal(int numdata) { int i=0,enddata=0; while(i<=numdata) enddata+=i++; return enddata; } 该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中,只是要在相应文件中作必要的说明;
2.把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:
arch/i386/kernel/entry.S 中的最后几行源代码修改前为: ... ... .long SYMBOL_NAME(sys_sendfile) .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ .long SYMBOL_NAME(sys_vfork) /* 190 */ .rept NR_syscalls-190 .long SYMBOL_NAME(sys_ni_syscall) .endr 修改后为:... ... .long SYMBOL_NAME(sys_sendfile) .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ .long SYMBOL_NAME(sys_vfork) /* 190 */ /* add by I */ .long SYMBOL_NAME(sys_addtotal) .rept NR_syscalls-191 .long SYMBOL_NAME(sys_ni_syscall) .endr
3. 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用:
增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下: ... ... #define __NR_sendfile 187 #define __NR_getpmsg 188 #define __NR_putpmsg 189 #define __NR_vfork 190 /* add by I */ #define __NR_addtotal 191
4.测试程序(test.c)如下:
#include #include _syscall1(int,addtotal,int, num) main() { int i,j;
do printf("Please input a number "); while(scanf("%d",&i)==EOF); if((j=addtotal(i))==-1) printf("Error occurred in syscall-addtotal(); "); printf("Total from 0 to %d is %d ",i,j); } 对修改后的新的内核进行编译,并引导它作为新的操作系统,运行几个程序后可以发现一切正常;在新的系统下对测试程序进行编译(*注:由于原内核并未提供此系统调用,所以只有在编译后的新内核下,此测试程序才能可能被编译通过),运行情况如下: $gcc -o test test.c $./test Please input a number 36 Total from 0 to 36 is 666 可见,修改成功; 而且,对相关源码的进一步分析可知,在此版本的内核中,从/usr/src/linux/arch/i386/kernel/entry.S 文件中对 sys_call_table 表的设置可以看出,有好几个系统调用的服务例程都是定义在/usr/src/linux/kernel/sys.c 中的同一个函数: asmlinkage int sys_ni_syscall(void) { return -ENOSYS; } 例如第188项和第189项就是如此: ... ... .long SYMBOL_NAME(sys_sendfile) .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ .long SYMBOL_NAME(sys_vfork) /* 190 */ ... ... 而这两项在文件 /usr/src/linux/include/asm-386/unistd.h 中却申明如下: ... ... #define __NR_sendfile 187 #define __NR_getpmsg 188 /* some people actually want streams */ #define __NR_putpmsg 189 /* some people actually want streams */ #define __NR_vfork 190
由
此可见,在此版本的内核源代码中,由于asmlinkage int sys_ni_syscall(void) 函数并不进行任何操作,所以包括
getpmsg, putpmsg
在内的好几个系统调用都是不进行任何操作的,即有待扩充的空调用;但它们却仍然占用着sys_call_table表项,估计这是设计者们为了方便扩充系
统调用而安排的;所以只需增加相应服务例程(如增加服务例程getmsg或putpmsg),就可以达到增加系统调用的作用。
结语:当然对于庞大复杂的 linux 内核而言,一篇文章远远不够,而且与系统调用相关的代码也只是内核中极其微小的一部分;但重要的是方法、掌握好的分析方法;所以上的分析只是起个引导的作用,而正真的分析还有待于读者自己的努力。
适应操作系统:redhat
如果要正常使用ssh客户端和ssh服务,需要安装一个server和client
1。重新启动sshd
引文: /etc/init.d/sshd restart 2。限制root远程登录
往往有些不怀好心的人想通过暴力破解来获取系统的id,如果被他们获得root用户的id,那是非常危险的,所以我们除了限制ssh的ip段外,还可以限制root远程登录来达到一定的安全保障
引文: vi /etc/ssh/sshd_config
ESC /#PermitRootLogin yes
修改为PermitRootLogin no 记得把#去掉
改好以后,我们按照上面的方法重新启动sshd
3。限制远程登录的ip地址
这里,我们可以修改/etc/hosts.allow,也可以修改/etc/hosts.deny 一般情况下,我们在/etc/hosts.deny里头禁止掉所有的用户,内容如下
引文: sshd:ALL@ALL:spawn /bin/echo `date` %c >> /var/log/tcp_wrapper 然后在/etc/hosts.allow文件里头设置允许的ip段或者是ip地址 例如: 引文: sshd: 202.201.0.0/255.255.255.0,202.201.177.90 这样的效果是允许202.201.0.0的这个C类网路的所有用户和202.201.177.90这个用户拉
重新按照上面的方法重新启动sshd使之生效
网络数据采集分析工具TcpDump的简介
顾名思义,TcpDump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。tcpdump就是一种免费的网络分析工具,尤其其提供了源代码,公开了接口,因此具备很强的可扩展性,对于网络维护和入侵者都是非常有用的工具。tcpdump存在于基本的FreeBSD系统中,由于它需要将网络界面设置为混杂模式,普通用户不能正常执行,但具备root权限的用户可以直接执行它来获取网络上的信息。因此系统中存在网络分析工具主要不是对本机安全的威胁,而是对网络上的其他计算机的安全存在威胁。
我们用尽量简单的话来定义tcpdump,就是:dump the traffice on a network.,根据使用者的定义对网络上的数据包进行截获的包分析工具。作为互联网上经典的的系统管理员必备工具,tcpdump以其强大的功能,灵活的截取策略,成为每个高级的系统管理员分析网络,排查问题等所必备的东西之一。tcpdump提供了源代码,公开了接口,因此具备很强的可扩展性,对于网络维护和入侵者都是非常有用的工具。tcpdump存在于基本的FreeBSD系统中,由于它需要将网络界面设置为混杂模式,普通用户不能正常执行,但具备root权限的用户可以直接执行它来获取网络上的信息。因此系统中存在网络分析工具主要不是对本机安全的威胁,而是对网络上的其他计算机的安全存在威胁。
网络数据采集分析工具TcpDump的安装
在 linux下 tcpdump的安装十分简单,一般由两种安装方式。一种是以rpm包的形式来进行安装。另外一种是以源程序的形式安装。 rpm包的形式安装:这种形式的安装是最简单的安装方法,rpm包是将软件编译后打包成二进制的格式,通过rpm命令可以直接安装,不需要修改任何东西。以超级用户登录,使用命令如下: #rpm -ivh tcpdump-3_4a5.rpm 这样 tcpdump就顺利地安装到你的 linux系统中。怎么样,很简单吧。
源程序的安装:既然rpm包的安装很简单,为什么还要采用比较复杂的源程序安装呢?其实,linux一个最大的诱人之处就是在她上面有很多软件是提供源程序的,人们可以修改源程序来满足自己的特殊的需要。所以我特别建议朋友们都采取这种源程序的安装方法。 - 第一步 取得源程序 在源程序的安装方式中,我们首先要取得tcpdump的源程序分发包,这种分发包有两种形式,一种是tar压缩包(tcpdump-3_4a5.tar.Z),另一种是rpm的分发包(tcpdump-3_4a5.src.rpm)。这两种形式的内容都是一样的,不同的仅仅是压缩的方式.tar的压缩包可以使用如下命令解开:
#tar xvfz tcpdump-3_4a5.tar.Z rpm的包可以使用如下命令安装: #rpm -ivh tcpdump-3_4a5.src.rpm 这样就把tcpdump的源代码解压到/usr/src/redhat/SOURCES目录下.
- 第二步 做好编译源程序前的准备活动
在编译源程序之前,最好已经确定库文件libpcap已经安装完毕,这个库文件是tcpdump软件所需的库文件。同样,你同时还要有一个标准的c语言编译器。在linux下标准的c 语言编译器一般是gcc。 在tcpdump的源程序目录中。有一个文件是Makefile.in,configure命令就是从Makefile.in文件中自动产生Makefile文件。在Makefile.in文件中,可以根据系统的配置来修改BINDEST 和 MANDEST 这两个宏定义,缺省值是 BINDEST = @sbindir@ MANDEST = @mandir@ 第一个宏值表明安装tcpdump的二进制文件的路径名,第二个表明tcpdump的man 帮助页的路径名,你可以修改它们来满足系统的需求。
- 第三步 编译源程序
使用源程序目录中的configure脚本,它从系统中读出各种所需的属性。并且根据Makefile.in文件自动生成Makefile文件,以便编译使用.make 命令则根据Makefile文件中的规则编译tcpdump的源程序。使用make install命令安装编译好的tcpdump的二进制文件。 总结一下就是: # tar xvfz tcpdump-3_4a5.tar.Z # vi Makefile.in # . /configure # make # make install
3 网络数据采集分析工具TcpDump的使用 普通情况下,直接启动 tcpdump将监视第一个网络界面上所有流过的数据包。 # tcpdump tcpdump: listening on fxp0 11:58:47.873028 202.102.245.40.netbios-ns > 202.102.245.127.netbios-ns: udp 50 11:58:47.974331 0:10:7b:8:3a:56 > 1:80:c2:0:0:0 802.1d ui/C len=43 0000 0000 0080 0000 1007 cf08 0900 0000 0e80 0000 902b 4695 0980 8701 0014 0002 000f 0000 902b 4695 0008 00 11:58:48.373134 0:0:e8:5b:6d:85 > Broadcast sap e0 ui/C len=97 ffff 0060 0004 ffff ffff ffff ffff ffff 0452 ffff ffff 0000 e85b 6d85 4008 0002 0640 4d41 5354 4552 5f57 4542 0000 0000 0000 00 ^C tcpdump支持相当多的不同参数,如使用-i参数指定tcpdump监听的网络界面,这在计算机具有多个网络界面时非常有用,使用-c参数指定要监听的数据包数量,使用-w参数指定将监听到的数据包写入文件中保存,等等。 然而更复杂的tcpdump参数是用于过滤目的,这是因为网络中流量很大,如果不加分辨将所有的数据包都截留下来,数据量太大,反而不容易发现需要的数据包。使用这些参数定义的过滤规则可以截留特定的数据包,以缩小目标,才能更好的分析网络中存在的问题。tcpdump使用参数指定要监视数据包的类型、地址、端口等,根据具体的网络问题,充分利用这些过滤规则就能达到迅速定位故障的目的。请使用man tcpdump查看这些过滤规则的具体用法。
显然为了安全起见,不用作网络管理用途的计算机上不应该运行这一类的网络分析软件,为了屏蔽它们,可以屏蔽内核中的bpfilter伪设备。一般情况下网
络硬件和TCP/IP堆栈不支持接收或发送与本计算机无关的数据包,为了接收这些数据包,就必须使用网卡的混杂模式,并绕过标准的TCP/IP堆栈才行。
在FreeBSD下,这就需要内核支持伪设备bpfilter。因此,在内核中取消bpfilter支持,就能屏蔽tcpdump之类的网络分析工具。 并且当网卡被设置为混杂模式时,系统会在控制台和日志文件中留下记录,提醒管理员留意这台系统是否被用作攻击同网络的其他计算机的跳板。 May 15 16:27:20 host1 /kernel: fxp0: promiscuous mode enabled
虽然网络分析工具能将网络中传送的数据记录下来,但是网络中的数据流量相当大,如何对这些数据进行分析、分类统计、发现并报告错误却是更关键的问题。网络
中的数据包属于不同的协议,而不同协议数据包的格式也不同。因此对捕获的数据进行解码,将包中的信息尽可能的展示出来,对于协议分析工具来讲更为重要。昂
贵的商业分析工具的优势就在于它们能支持很多种类的应用层协议,而不仅仅只支持tcp、udp等低层协议。 从上面tcpdump的输出可以看出,tcpdump对截获的数据并没有进行彻底解码,数据包内的大部分内容是使用十六进制的形式直接打印输出的。显然这不利于分析网络故障,通常的解决办法是先使用带-w参数的tcpdump 截获数据并保存到文件中,然后再使用其他程序进行解码分析。当然也应该定义过滤规则,以避免捕获的数据包填满整个硬盘。FreeBSD提供的一个有效的解码程序为tcpshow,它可以通过Packages Collection来安装。 # pkg_add /cdrom/packages/security/tcpshow* # tcpdump -c 3 -w tcpdump.out tcpdump: listening on fxp0 # tcpshow < tcpdump.out --------------------------------------------------------------------------- Packet 1 TIME:12:00:59.984829 LINK:00:10:7B:08:3A:56 -> 01:80:C2:00:00:00 type=0026 <*** No decode support for encapsulated protocol ***> --------------------------------------------------------------------------- Packet 2 TIME:12:01:01.074513 (1.089684) LINK:00:A0:C9:AB:3C:DF -> FF:FF:FF:FF:FF:FF type=ARP ARP:htype=Ethernet ptype=IP hlen=6 plen=4 op=request sender-MAC-addr=00:A0:C9:AB:3C:DF sender-IP-address=202.102.245.3 target-MAC-addr=00:00:00:00:00:00 target-IP-address=202.102.245.3 --------------------------------------------------------------------------- Packet 3 TIME:12:01:01.985023 (0.910510) LINK:00:10:7B:08:3A:56 -> 01:80:C2:00:00:00 type=0026 <*** No decode support for encapsulated protocol ***> tcpshow能以不同方式对数据包进行解码,并以不同的方式显示解码数据,使用者可以根据其手册来选择最合适的参数对截获的数据包进行分析。从上面的例子中可以看出,tcpshow支持的协议也并不丰富,对于它不支持的协议就无法进行解码。 除了tcpdump之
外,FreeBSD的Packages
Collecion中还提供了Ethereal和Sniffit两个网络分析工具,以及其他一些基于网络分析方式的安全工具。其中Ethereal运行在
X Window 下,具有不错的图形界面,Sniffit使用字符窗口形式,同样也易于操作。然而由于tcpdump对
过滤规则的支持能力更强大,因此系统管理员仍然更喜欢使用它。对于有经验的网络管理员,使用这些网络分析工具不但能用来了解网络到底是如何运行的,故障出
现在何处,还能进行有效的统计工作,如那种协议产生的通信量占主要地位,那个主机最繁忙,网络瓶颈位于何处等等问题。因此网络分析工具是用于网络管理的宝
贵系统工具。为了防止数据被滥用的网络分析工具截获,关键还是要在网络的物理结构上解决。常用的方法是使用交换机或网桥将信任网络和不信任网络分隔开,可
以防止外部网段窃听内部数据传输,但仍然不能解决内部网络与外部网络相互通信时的数据安全问题。如果没有足够的经费将网络上的共享集线器升级为以太网交换
机,可以使用FreeBSD系统执行网桥任务。这需要使用option BRIDGE编译选项重新定制内核,此后使用bridge命令启动网桥功能。 tcpdump采用命令行方式,它的命令格式为: tcpdump [ -adeflnNOpqStvx ] [ -c 数量 ] [ -F 文件名 ] [ -i 网络接口 ] [ -r 文件名] [ -s snaplen ] [ -T 类型 ] [ -w 文件名 ] [表达式 ] (1). tcpdump的选项介绍 -a 将网络地址和广播地址转变成名字; -d 将匹配信息包的代码以人们能够理解的汇编格式给出; -dd 将匹配信息包的代码以c语言程序段的格式给出; -ddd 将匹配信息包的代码以十进制的形式给出; -e 在输出行打印出数据链路层的头部信息; -f 将外部的Internet地址以数字的形式打印出来; -l 使标准输出变为缓冲行形式; -n 不把网络地址转换成名字; -t 在输出的每一行不打印时间戳; -v 输出一个稍微详细的信息,例如在ip包中可以包括ttl和服务类型的信息; -vv 输出详细的报文信息; -c 在收到指定的包的数目后,tcpdump就会停止; -F 从指定的文件中读取表达式,忽略其它的表达式; -i 指定监听的网络接口; -r 从指定的文件中读取包(这些包一般通过-w选项产生); -w 直接将包写入文件中,并不分析和打印出来; -T 将监听到的包直接解释为指定的类型的报文,常见的类型有rpc (远程过程调用)和snmp(简单网络管理协议;)(2). tcpdump的表达式介绍 表达式是一个正则表达式, tcpdump利用它作为过滤报文的条件,如果一个报文满足表达式的条件,则这个报文将会被捕获。如果没有给出任何条件,则网络上所有的信息包将会被截获。在表达式中一般如下几种类型的关键字。
第一种是关于类型的关键字,主要包括host,net,port, 例如 host 210.27.48.2,指明
210.27.48.2是一台主机,net 202.0.0.0 指明 202.0.0.0是一个网络地址,port 23
指明端口号是23。如果没有指定类型,缺省的类型是host.
第二种是确定传输方向的关键字,主要包括src , dst ,dst or src, dst and src
,这些关键字指明了传输的方向。举例说明,src 210.27.48.2 ,指明ip包中源地址是210.27.48.2 , dst net
202.0.0.0 指明目的网络地址是202.0.0.0 。如果没有指明方向关键字,则缺省是src or dst关键字。
第三种是协议的关键字,主要包括fddi,ip,arp,rarp,tcp,udp等类型。Fddi指明是在FDDI(分布式光纤数据接口网络)上的特定
的网络协议,实际上它是"ether"的别名,fddi和ether具有类似的源地址和目的地址,所以可以将fddi协议包当作ether的包进行处理和
分析。其他的几个关键字就是指明了监听的包的协议内容。如果没有指定任何协议,则 tcpdump将会监听所有协议的信息包。
除了这三种类型的关键字之外,其他重要的关键字如下:gateway,
broadcast,less,greater,还有三种逻辑运算,取非运算是 'not ' '! ',
与运算是'and','&&';或运算 是'or'
,'││';这些关键字可以组合起来构成强大的组合条件来满足人们的需要,下面举几个例子来说明。 A想要截获所有210.27.48.1 的主机收到的和发出的所有的数据包: # tcpdump host 210.27.48.1 B想要截获主机210.27.48.1 和主机210.27.48.2 或210.27.48.3的通信,使用命令:(在命令行中适用 括号时,一定要 # tcpdump host 210.27.48.1 and \ (210.27.48.2 or 210.27.48.3 \) C如果想要获取主机210.27.48.1除了和主机210.27.48.2之外所有主机通信的ip包,使用命令: # tcpdump ip host 210.27.48.1 and ! 210.27.48.2 D如果想要获取主机210.27.48.1接收或发出的telnet包,使用如下命令: # tcpdump tcp port 23 host 210.27.48.1
(3). tcpdump的输出结果介绍 下面我们介绍几种典型的 tcpdump命令的输出信息 A,数据链路层头信息 使用命令 # tcpdump --e host ice ice 是一台装有 linux的主机,她的MAC地址是0:90:27:58:AF:1A H219是一台装有SOLARIC的SUN工作站,它的MAC地址是8:0:20:79:5B:46;上一条命令的输出结果如下所示: 21:50:12.847509 eth0 < 8:0:20:79:5b:46 0:90:27:58:af:1a ip 60: h219.33357 > ice.telne t 0:0(0) ack 22535 win 8760 (DF)
分析:21:50:12是显示的时间, 847509是ID号,eth0 <表示从网络接口eth0 接受该数据包,eth0
>表示从网络接口设备发送数据包, 8:0:20:79:5b:46是主机H219的MAC地址,它表明是从源地址H219发来的数据包.
0:90:27:58:af:1a是主机ICE的MAC地址,表示该数据包的目的地址是ICE . ip 是表明该数据包是IP数据包,60
是数据包的长度, h219.33357 > ice.telnet
表明该数据包是从主机H219的33357端口发往主机ICE的TELNET(23)端口. ack 22535
表明对序列号是222535的包进行响应. win 8760表明发送窗口的大小是8760. B,ARP包的TCPDUMP输出信息 使用命令 # tcpdump arp 得到的输出结果是: 22:32:42.802509 eth0 > arp who-has route tell ice (0:90:27:58:af:1a) 22:32:42.802902 eth0 < arp reply route is-at 0:90:27:12:10:66 (0:90:27:58:af:1a) 分
析: 22:32:42是时间戳, 802509是ID号, eth0 >表明从主机发出该数据包, arp表明是ARP请求包,
who-has route tell ice表明是主机ICE请求主机ROUTE的MAC地址。
0:90:27:58:af:1a是主机ICE的MAC地址。
C,TCP包的输出信息 用TCPDUMP捕获的TCP包的一般输出信息是: src > dst: flags data-seqno ack window urgent options src
> dst:表明从源地址到目的地址, flags是TCP包中的标志信息,S 是SYN标志, F (FIN), P (PUSH) , R
(RST) "." (没有标记); data-seqno是数据包中的数据的顺序号, ack是下次期望的顺序号,
window是接收缓存的窗口大小, urgent表明数据包中是否有紧急指针. Options是选项.
D,UDP包的输出信息 用TCPDUMP捕获的UDP包的一般输出信息是: route.port1 > ice.port2: udp lenth UDP十分简单,上面的输出行表明从主机ROUTE的port1端口发出的一个UDP数据包到主机ICE的port2端口,类型是UDP, 包的长度是lenth
调试是软件开发过程中一个必不可少的环节,在 Linux 内核开发的过程中也不可避免地会面对如何调试内核的问题。但是,Linux 系统的开发者出于保证内核代码正确性的考虑,不愿意在 Linux 内核源代码树中加入一个调试器。他们认为内核中的调试器会误导开发者,从而引入不良的修正[1].所以对 Linux 内核进行调试一直是个令内核程序员感到棘手的问题,调试工作的艰苦性是内核级的开发区别于用户级开发的一个显著特点。
尽管缺乏一种内置的调试内核的有效方法,但是 Linux 系统在内核发展的过程中也逐渐形成了一些监视内核代码和错误跟踪的技术。同时,许多的补丁程序应运而生,它们为标准内核附加了内核调试的支持。尽管这些补丁有些并不被 Linux 官方组织认可,但他们确实功能完善,十分强大。调试内核问题时,利用这些工具与方法跟踪内核执行情况,并查看其内存和数据结构将是非常有用的。 本文将首先介绍 Linux 内核上的一些内核代码监视和错误跟踪技术,这些调试和跟踪方法因所要求的使用环境和使用方法而各有不同,然后重点介绍三种 Linux 内核的源代码级的调试方法。 1. Linux 系统内核级软件的调试技术 printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用,可以直接把所关心的信息打打印到屏幕上,从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器(Linux kernel debugger,kdb)是 Linux 内核的补丁,它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍,大家可以参考。 Kprobes 提供了一个强行进入任何内核例程,并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息,而无需对Linux内核频繁编译和启动,具体使用方法,请参考使用 Kprobes 调试内核。 以上介绍了进行Linux内核调试和跟踪时的常用技术和方法。当然,内核调试与跟踪的方法还不止以上提到的这些。这些调试技术的一个共同的特点在于,他们都不能提供源代码级的有效的内核调试手段,有些只能称之为错误跟踪技术,因此这些方法都只能提供有限的调试能力。下面将介绍三种实用的源代码级的内核调试方法。 2. 使用KGDB构建Linux内核调试环境 kgdb提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样,在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器,一台作为开发机(Development Machine),另一台作为目标机(Target Machine),两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆,在其内部两端的第2脚(TXD)与第3脚(RXD)交叉相连,第7脚(接地脚)直接相连。调试过程中,被调试的内核运行在目标机上,gdb调试器运行在开发机上。 目前,kgdb发布支持i386、x86_64、32-bit PPC、SPARC等几种体系结构的调试器。有关kgdb补丁的下载地址见参考资料[4]. 2.1 kgdb的调试原理 安装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的控制权重新交还给内核。
2.2 Kgdb的安装与设置 下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。 2.2.1软硬件准备 以下软硬件配置取自笔者进行试验的系统配置情况:
kgdb补丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的内核版本号,B为kgdb的版本号。以试验使用的kgdb补丁为例,linux内核的版本为linux-2.6.7,补丁版本为kgdb-2.2。 物理连接好串口线后,使用以下命令来测试两台机器之间串口连接情况,stty命令可以对串口参数进行设置: 在development机上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
|
在target机上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
|
在developement机上执行:
在target机上执行:
如果串口连接没问题的话在将在target机的屏幕上显示"hello"。 2.2.2 安装与配置 下面我们需要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工作在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要做适当的改动: I、内核的配置与编译
[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
|
请参照目录补丁包中文件README给出的说明,执行对应体系结构的补丁程序。由于试验在i386体系结构上完成,所以只需要安装一下补丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时,请遵循kgdb软件包内series文件所指定的顺序,否则可能会带来预想不到的问题。eth.patch文件是选择以太网口作为调试的连接端口时需要运用的补丁 。 应用补丁的命令如下所示:
[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
|
如果内核正确,那么应用补丁时应该不会出现任何问题(不会产生*.rej文件)。为Linux内核添加了补丁之后,需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。
[root@lisl tmp]#make menuconfig
|
在内核配置菜单的Kernel hacking选项中选择kgdb调试项,例如:
[*] KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On generic serial port (8250)) --->
[*] KGDB: Thread analysis
[*] KGDB: Console messages through gdb
[root@lisl tmp]#make
|
编译内核之前请注意Linux目录下Makefile中的优化选项,默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下,编译器要对内核中的某些代码的执行顺序进行改动,所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉-O,否则编译会出问题。为了使编译后的内核带有调试信息,注意在编译内核的时候需要加上-g选项。 不过,当选择"Kernel debugging->Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外,选择"kernel debugging with remote gdb"后,配置系统将自动打开"Compile the kernel with debug info"选项。 内核编译完成后,使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具,如rcp)。
[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
|
如果系统启动使所需要的某些设备驱动没有编译进内核的情况下,那么还需要执行如下操作:
[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
|
II、kgdb的启动 在将编译出的内核拷贝的到target机器之后,需要配置系统引导程序,加入内核的启动选项。以下是kgdb内核引导参数的说明:
如表中所述,在kgdb 2.0版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时,直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。
title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
|
在使用lilo作为引导程序时,需要把kgdb参放在由append修饰的语句中。下面给出使用lilo作为引导器时的配置示例。
image=/boot/vmlinuz-2.6.7-kgdb
label=kgdb
read-only
root=/dev/hda3
append="gdb gdbttyS=1 gdbbaud=115200"
|
保存好以上配置后重新启动计算机,选择启动带调试信息的内核,内核将在短暂的运行后在创建init内核线程之前停下来,打印出以下信息,并等待开发机的连接。 Waiting for connection from remote gdb... 在开发机上执行:
gdb
file vmlinux
set remotebaud 115200
target remote /dev/ttyS0
|
其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接,它是没有经过压缩的内核文件,gdb程序从该文件中得到各种符号地址信息。 这样,就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后,对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行,进行具体的调试工作。 在kgdb 2.0之前的版本中,编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下,此时无需更改内核的启动配置文件,直接使用命令:
[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
|
可以在KGDB内核引导启动完成后建立开发机与目标机之间的调试联系。 2.2.3 通过网络接口进行调试 kgdb也支持使用以太网接口作为调试器的连接端口。在对Linux内核应用补丁包时,需应用eth.patch补丁文件。配置内核时在Kernel hacking中选择kgdb调试项,配置kgdb调试端口为以太网接口,例如:
[*]KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On ethernet) --->
( ) KGDB: On generic serial port (8250)
(X) KGDB: On ethernet
|
另外使用eth0网口作为调试端口时,grub.list的配置如下:
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/
|
其他的过程与使用串口作为连接端口时的设置过程相同。 注意:尽管可以使用以太网口作为kgdb的调试端口,使用串口作为连接端口更加简单易行,kgdb项目组推荐使用串口作为调试端口。 2.2.4 模块的调试方法 内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的,所以develop机的gdb无法得到各种符号地址信息。所以,使用kgdb调试模块所需要解决的一个问题是,需要通过某种方法获得可加载模块的最终加载地址信息,并把这些信息加入到gdb环境中。 I、在Linux 2.4内核中的内核模块调试方法 在Linux2.4.x内核中,可以使用insmod -m命令输出模块的加载信息,例如:
[root@lisl tmp]# insmod -m hello.ko >modaddr
|
查看模块加载信息文件modaddr如下:
.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
……
|
在这些信息中,我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。
(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s
.rodata 0xc88d80a0 -s .bss 0x c88d833c
|
这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。 在target机上用上述方法得到模块加载的地址信息,然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中,在内核代码的调用初始化代码之前设置断点。这样,在target机上再次插入模块时,代码将在执行模块初始化之前停下来,这样就可以使用gdb命令调试模块初始化代码了。 另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数sys_init_module(kernel/modle.c)执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下:
…… ……
if (mod->init != NULL)
ret = mod->init();
…… ……
|
在该语句上设置断点,也能在执行模块初始化之前停下来。 II、在Linux 2.6.x内核中的内核模块调试方法 Linux 2.6之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式,我们知道程序中各段的意义如下: 。text(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。 。data(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。 。bss(BSS段):BSS段包含了程序中未初始化全局变量,在内存中 bss段全部置零。 。rodata(只读段):该段保存着只读数据,在进程映象中构造不可写的段。 通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。
……
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",&data_var);
printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);
……
}
Module_init(hello_init);
|
这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。。rodata段的地址可以通过执行命令readelf -e hello.ko,取得。rodata在文件中的偏移量并加上段的align值得出。 为了使读者能够更好地进行模块的调试,kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似,更多的信息请阅读参考资料[4]. 2.2.5 硬件断点 kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点:执行断点(Execution Breakpoint)、写断点(Write Breakpoint)、访问断点(Access Breakpoint)但不支持I/O访问的断点。目前,kgdb对硬件断点的支持是通过宏来实现的,最多可以设置4个硬件断点,这些宏的用法如下:
在有些情况下,硬件断点的使用对于内核的调试是非常方便的。有关硬件断点的定义和具体的使用说明见参考资料[4] 。 2.3.在VMware中搭建调试环境 kgdb调试环境需要使用两台微机分别充当development机和target机,使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例,创建两台虚拟机,一台作为开发机,一台作为目标机。 2.3.1虚拟机之间的串口连接 虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上,例如开发机连接到COM1,目标机连接到COM2,然后把两个串口通过串口线相连接。另一种更为简便的方法是:在较高一些版本的VMware中都支持把串口映射到命名管道,把两个虚拟机的串口映射到同一个命名管道。例如,在两个虚拟机中都选定同一个命名管道 \\.\pipe\com_1,指定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进行调试的硬件要求,也简化了建立调试环境的过程。
2.3.2 VMware的使用技巧 VMware虚拟机是比较占用资源的,尤其是象上面那样在Windows中使用两台虚拟机。因此,最好为系统配备512M以上的内存,每台虚拟机至少分配128M的内存。这样的硬件要求,对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑,在VMware中尽量使用字符界面进行调试工作。同时,Linux系统默认情况下开启了sshd服务,建议使用SecureCRT登陆到Linux进行操作,这样可以有较好的用户使用界面。 2.3.3 在Linux下的虚拟机中使用kgdb 对于在Linux下面使用VMware虚拟机的情况,笔者没有做过实际的探索。从原理上而言,只需要在Linux下只要创建一台虚拟机作为target机,开发机的工作可以在实际的Linux环境中进行,搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机,所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。(vmware已经推出了Linux下的版本)还可以在development机上配合使用一些其他的调试工具,例如功能更强大的cgdb、图形界面的DDD调试器等,以方便内核的调试工作。
2.4 kgdb的一些特点和不足 使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高,必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境,但是对系统其他方面的性能也提出了一定的要求,同时也增加了搭建调试环境时复杂程度。另外,kgdb内核的编译、配置也比较复杂,需要一定的技巧,笔者当时做的时候也是费了很多周折。当调试过程结束后时,还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试,也就是说kgdb并不能用于调试系统一开始的初始化引导过程。 不过,kgdb是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。 3. 使用SkyEye构建Linux内核调试环境 SkyEye是一个开源软件项目(OPenSource Software),SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台,可以模拟多种嵌入式开发板,支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目,它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后,它就可以来仿真嵌入式开发板,在它上面不仅可以调试硬件驱动,还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。 3.1 SkyEye的安装和μcLinux内核编译 3.1.1 SkyEye的安装 SkyEye的安装不是本文要介绍的重点,目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11].由于skyeye面目主要用于嵌入式系统领域,所以在skyeye上经常使用的是μcLinux系统,当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μcLinux 2.6在skyeye上编译的相关资料并不多,所以下面进行详细介绍。 3.1.2 μcLinux 2.6.x的编译 要在SkyEye中调试操作系统内核,首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此,正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板,并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。 I、安装交叉编译环境 先安装交叉编译器。尽管在一些资料中说明使用工具链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用户来执行。
[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
|
安装交叉编译工具链之后,请确保工具链安装路径存在于系统PATH变量中。 II、制作μcLinux内核 得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7].该站点发布的内核版本可能不是最新的,但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里,将使用这种方法来制作最新的μcLinux内核。目前(笔者记录编写此文章时),所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz. 下载uClinux-dist.20041215.tar.gz,文件的下载地址请参见[7]. 下载linux-2.6.9-hsc0.patch.gz,文件的下载地址请参见[8]. 下载linux-2.6.9.tar.bz2,文件的下载地址请参见[9]. 现在我们得到了整个的linux-2.6.9源代码,以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。
[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
|
或者使用:
[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz
[root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
|
执行以上过程后,将在linux-2.6.9/arch目录下生成一个补丁目录-armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0),并将我们打好补丁的Linux内核目录更名为linux-2.6.x。
[root@lisl uClinux-dist]# rm -rf linux-2.6.x/
[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
|
III、配置和编译μcLinux内核 因为只是出于调试μcLinux内核的目的,这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时,已经预置了某些常用嵌入式开发板的配置文件,因此这里直接使用这些配置文件,过程如下:
[root@lisl uClinux-dist]# cd linux-2.6.x
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_
deconfig
|
atmel_deconfig文件是μcLinux发布时提供的一个配置文件,存放于目录linux-2.6.x /arch/armnommu/configs/中。
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux-
oldconfig
|
下面编译配置好的内核:
[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
|
一般情况下,编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核,需要打开内核编译的调试选项-g,使编译后的内核带有调试信息。打开编译选项的方法可以选择: "Kernel debugging->Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件,为其打开调试开关。方法如下:。
最容易出现的问题是找不到arm-uclinux-gcc命令的错误,主要原因是PATH变量中没有包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下,它的安装目录是/root/bin/arm-linux-tool/,使用以下命令将路径加到PATH环境变量中。
Export PATH=$PATH:/root/bin/arm-linux-tool/bin
|
IV、根文件系统的制作 Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑,这里不打算介绍根文件系统的制作方法,读者可以查阅一些其他的相关资料。值得注意的是,由配置文件skyeye.conf指定了装载到内核中的根文件系统。 3.2 使用SkyEye调试 编译完μcLinux内核后,就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。 需要提醒读者的是,SkyEye的配置文件-skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件,很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时,请首先检查该配置文件然后再进行其他的工作。此时,所有的准备工作已经完成,就可以进行内核的调试工作了。 3.3使用SkyEye调试内核的特点和不足 在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU,因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外,制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过,当调试过程结束后无需重新制作所要发布的内核。 SkyEye只是对系统硬件进行了一定程度上的模拟,所以在SkyEye与真实硬件环境相比较而言还是有一定的差距,这对一些与硬件紧密相关的调试可能会有一定的影响,例如驱动程序的调试。不过对于大部分软件的调试,SkyEye已经提供了精度足够的模拟了。 SkyEye的下一个目标是和eclipse结合,有了图形界面,能为调试和查看源码提供一些方便。 4. 使用UML调试Linux内核 User-mode Linux(UML)简单说来就是在Linux内运行的Linux.该项目是使Linux内核成为一个运行在 Linux 系统之上单独的、用户空间的进程。UML并不是运行在某种新的硬件体系结构之上,而是运行在基于 Linux 系统调用接口所实现的虚拟机。正是由于UML是一个将Linux作为用户空间进程运行的特性,可以使用UML来进行操作系统内核的调试。有关UML的介绍请查阅参考资料[10]、[12]. 4.1 UML的安装与调试 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代码都放在该目录下。 从2.6.9版本之后(包含2.6.9版本的Linux),User-Mode Linux已经随Linux内核源代码树一起发布,它存放于arch/um目录下。 编译好UML的内核之后,直接使用gdb运行已经编译好的内核即可进行调试。 4.2使用UML调试系统内核的特点和不足 目前,用户模式 Linux 虚拟机也存在一定的局限性。由于UML虚拟机是基于Linux系统调用接口的方式实现的虚拟机,所以用户模式内核不能访问主机系统上的硬件设备。因此,UML并不适合于调试那些处理实际硬件的驱动程序。不过,如果所编写的内核程序不是硬件驱动,例如Linux文件系统、协议栈等情况,使用UML作为调试工具还是一个不错的选择。 5. 内核调试配置选项 为了方便调试和测试代码,内核提供了许多与内核调试相关的配置选项。这些选项大部分都在内核配置编辑器的内核开发(kernel hacking)菜单项中。在内核配置目录树菜单的其他地方也还有一些可配置的调试选项,下面将对他们作一定的介绍。 Page alloc debugging :CONFIG_DEBUG_PAGEALLOC: 不使用该选项时,释放的内存页将从内核地址空间中移出。使用该选项后,内核推迟移出内存页的过程,因此能够发现内存泄漏的错误。 Debug memory allocations :CONFIG_DEBUG_SLAB: 该打开该选项时,在内核执行内存分配之前将执行多种类型检查,通过这些类型检查可以发现诸如内核过量分配或者未初始化等错误。内核将会在每次分配内存前后时设置一些警戒值,如果这些值发生了变化那么内核就会知道内存已经被操作过并给出明确的提示,从而使各种隐晦的错误变得容易被跟踪。 Spinlock debugging :CONFIG_DEBUG_SPINLOCK: 打开此选项时,内核将能够发现spinlock未初始化及各种其他的错误,能用于排除一些死锁引起的错误。 Sleep-inside-spinlock checking:CONFIG_DEBUG_SPINLOCK_SLEEP: 打开该选项时,当spinlock的持有者要睡眠时会执行相应的检查。实际上即使调用者目前没有睡眠,而只是存在睡眠的可能性时也会给出提示。 Compile the kernel with debug info :CONFIG_DEBUG_INFO: 打开该选项时,编译出的内核将会包含全部的调试信息,使用gdb时需要这些调试信息。 Stack utilization instrumentation :CONFIG_DEBUG_STACK_USAGE: 该选项用于跟踪内核栈的溢出错误,一个内核栈溢出错误的明显的现象是产生oops错误却没有列出系统的调用栈信息。该选项将使内核进行栈溢出检查,并使内核进行栈使用的统计。 Driver Core verbose debug messages:CONFIG_DEBUG_DRIVER: 该选项位于"Device drivers-> Generic Driver Options"下,打开该选项使得内核驱动核心产生大量的调试信息,并将他们记录到系统日志中。 Verbose SCSI error reporting (kernel size +=12K) :CONFIG_SCSI_CONSTANTS: 该选项位于"Device drivers/SCSI device support"下。当SCSI设备出错时内核将给出详细的出错信息。 Event debugging:CONFIG_INPUT_EVBUG: 打开该选项时,会将输入子系统的错误及所有事件都输出到系统日志中。该选项在产生了详细的输入报告的同时,也会导致一定的安全问题。 以上内核编译选项需要读者根据自己所进行的内核编程的实际情况,灵活选取。在使用以上介绍的三种源代码级的内核调试工具时,一般需要选取CONFIG_DEBUG_INFO选项,以使编译的内核包含调试信息。 6. 总结 上面介绍了一些调试Linux内核的方法,特别是详细介绍了三种源代码级的内核调试工具,以及搭建这些内核调试环境的方法,读者可以根据自己的情况从中作出选择。 调试工具(例如gdb)的运行都需要操作系统的支持,而此时内核由于一些错误的代码而不能正确执行对系统的管理功能,所以对内核的调试必须采取一些特殊的方法进行。以上介绍的三种源代码级的调试方法,可以归纳为以下两种策略: I、为内核增加调试Stub,利用调试Stub进行远程调试,这种调试策略需要target及development机器才能完成调试任务。 II、将虚拟机技术与调试工具相结合,使Linux内核在虚拟机中运行从而利用调试器对内核进行调试。这种策略需要制作适合在虚拟机中运行的系统内核。 由不同的调试策略决定了进行调试时不同的工作原理,同时也形成了各种调试方法不同的软硬件需求和各自的特点。 另外,需要说明的是内核调试能力的掌握很大程度上取决于经验和对整个操作系统的深入理解。对系统内核的全面深入的理解,将能在很大程度上加快对Linux系统内核的开发和调试。 对系统内核的调试技术和方法绝不止上面介绍所涉及的内容,这里只是介绍了一些经常看到和听到方法。在Linux内核向前发展的同时,内核的调试技术也在不断的进步。希望以上介绍的一些方法能对读者开发和学习Linux有所帮助。
int strncasecmp(const char *s1, const char *s2, size_t n) { const unsigned char *c1 = s1, *c2 = s2; unsigned char ch; int d = 0;
while ( n-- ) { /* toupper() expects an unsigned char (implicitly cast to int) as input, and returns an int, which is exactly what we want. */ d = toupper(ch = *c1++) - toupper(*c2++); if ( d || !ch ) break; }
return d; }
int strcasecmp(const char *s1, const char *s2) { const unsigned char *c1 = s1, *c2 = s2; unsigned char ch; int d = 0;
while ( 1 ) { /* toupper() expects an unsigned char (implicitly cast to int) as input, and returns an int, which is exactly what we want. */ d = toupper(ch = *c1++) - toupper(*c2++); if ( d || !ch ) break; }
return d; }
int strcmp(const char *s1, const char *s2) { const unsigned char *c1 = s1, *c2 = s2; unsigned char ch; int d = 0;
while ( 1 ) { d = (int)(ch = *c1++) - (int)*c2++; if ( d || !ch ) break; }
return d; }
int strncmp(const char *s1, const char *s2, size_t n) { const unsigned char *c1 = s1, *c2 = s2; unsigned char ch; int d = 0;
while ( n-- ) { d = (int)(ch = *c1++) - (int)*c2++; if ( d || !ch ) break; }
return d; }
// 一般用途 template<class T>struct SP_traits { static void Release(T* ptr) { delete ptr; } };
// 特殊用途 template<> struct SP_traits<somlist>{ static void Release(T* ptr) { somlist::iterator iter; for(iter= ptr->begin() ;iter!=ptr->end();iter++) delete (*iter) ; delete ptr; } }
template<class T> class SP//simple smart pointer { public: SP():ptr(NULL) { } SP(T*p):ptr(p) { }
void Release() { if( ptr != NULL ) { SP_traits<T>::Release(ptr); } ptr = NULL; } T* operator=(T*p){ Release(); ptr = p; return ptr; } T** operator &() const { return &ptr; } T** operator &() { return &ptr; } T& operator *() { return *ptr; } T& operator *() const { return *ptr; } T* operator->() { return ptr; } T* operator->() const { return ptr; } operator T*() { return ptr; } operator T*() const { return ptr; } T& operator []( int i ) { return ptr[ i ]; } T& operator []( int i ) const { return ptr[ i ]; } bool operator == ( const T* p ){ return ptr == p; } bool operator != ( const T* p ){ return ptr != p; } bool operator == ( const T* p ) const { return ptr == p; } bool operator != ( const T* p ) const { return ptr != p; }
~SP() { Release(); } PM_BOOL IsValid() { return ptr != NULL; } void Attach( T* ptr_new ) { Release(); ptr = ptr_new; } void Detach( T* ptr_new ) { ptr = NULL; } T* ptr; };
|