用gdb调试内核类型于调试应用程序进程,kgdb支持gdb的执行控制命令、栈跟踪和线程分析等。但kgdb不支持watchpoint,kgdb通过gdb宏来执行watchpoint。
调试内核的命令说明如下:
(1)停止内核执行
用户在gdb终端中按下Ctrl + C键,gdb将发送停止消息给kgdb stub,kgdbstub控制内核的运行,并与gdb通信。
(2)继续内核运行
gdb命令"(gdb) c"告诉kgdb stub继续内核运行,直到遇到一个断点,或者gdb执行Ctrl +C,或其他原因,内核运行才停顿下来。
(3)断点
gdb断点(Breakpoints)用于在一个函数或代码行处暂停内核运行,设置断点命令如:"(gdb) bmodule.c:2168"。
(4)进入代码
使用命令"(gdb) step"进入一个函数或在暂停后执行下一个程序行;使用命令"(gdb)next"跳过一个函数执行下一个程序行或暂停后执行下一个程序;
(5)栈跟踪(Stack Trace)
使用命令"(gdb) bt"或"(gdb)backtrace"显示程序栈,它显示了调用函数的层次列表,表明了函数的调用函数。该命令还打印调解函数的参数值。
例如,运行命令backtrace的样例列出如下:
(gdb) backtrace#0 breakpoint () at gdbstub.c:1160#1 0xc0188b6c in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc02c9f9c) at gdbserial.c:143#2 0xc0108809 in handle_IRQ_event (irq=3, regs=0xc02c9f9c, action=0xc12fd200)3 0xc0108a0d in do_IRQ (regs={ebx = 1072672288, ecx = 0, edx = 1070825472, esi = 1070825472, edi = 1072672288, ebp = 1070817328, eax = 0, xds = 1072693224, xes = 1072693224, orig_eax = 253, eip = 1072672241, xcs = 16, eflags = 582, esp = 1070817308, xss = 1072672126}) at irq.c:621#4 0xc0106e04 in ret_from_intr () at af_packet.c:1878#5 0xc0105282 in cpu_idle () at process.c:135#6 0xc02ca91f in start_kernel () at init/main.c:599#7 0xc01001cf in L6 () at af_packet.c:1878Cannot access memory at address 0x8e000
除非栈帧数作为命令backtrace的参数外,gdb仅在栈跟踪走出了可访问地址空间时才停止打印栈的信息。上面例子中,函数调用层次次序从上到下为:ret_from_intr,do_IRQ, handle_IRQ_event, gdb_interrupt。
放置一个断点在函数ext2_readlink,并访问一个符号链接,以便运行到该断点。 设置断点方法如下:
(gdb) br ext2_readlinkBreakpoint 2 at 0xc0158a05: file symlink.c, line 25.(gdb) cContinuing.
在测试机上运行命令"ls -l/boot/vmlinuz"显示一个符号链接。在测试机上,内核会运行到上述断点处,并暂停。然后,将断点信息传回开发机。在开发机上,用户可以查看栈或运行其他调试命令。例如:运行栈跟踪命令,显示的结果列出如下:
Breakpoint 2, ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25 25 char *s = (char *)dentry >d_inode >u.ext2_i.i_data; (gdb) bt#0 ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25#1 0xc013b027 in sys_readlink (path=0xbfffff77 "/boot/vmlinuz", buf=0xbfffed84 "\214\005", bufsiz=4096) at stat.c:262#2 0xc0106d83 in system_call () at af_packet.c:1878#3 0x804aec8 in ?? () at af_packet.c:1878#4 0x8049697 in ?? () at af_packet.c:1878#5 0x400349cb in ?? () at af_packet.c:1878
上述栈跟踪中,gdb打印一些无效的栈帧(#3~#5),这是因为gdb不知道在哪里停止栈跟踪,可以忽略这些无效的栈帧。
系统调用readlink在函数system_call进入内核,该函数显示在af_packet.c中,这是不对的,因为对于汇编语言文件的函数,gdb不能指出正常的代码行。但gdb可以正确处理在C语言文件中内联汇编代码。更多的调用层次是:sys_readlink和ext2_readlink。
在调试完后,用户可用"删除"命令和"继续"命令删除断点,并继续内核的运行,方法如下:
(gdb) deleteDelete all breakpoints? (y or n) y(gdb) cContinuing.
(6)内联函数
使用gdb栈跟踪命令通常足够找出一个函数的调用层次关系。但当其中一个栈帧在扩展的内联函数中,或者从一个内联函数访问另一个内联函数时,栈跟踪命令是不够用的,栈跟踪仅显示在内联函数中的源代码文件名和语句的行号,通过查看外面的函数,这可能知道调用的内联函数,但如果调用了两次内联函数,它就不知道是哪个内联函数了。
下面的处理流程可用来找出内联函数信息:
在栈跟踪中,gdb还与函数名一起显示代码地址,在调用了一个内联函数的语句中,gdb显示了这些代码调用和被调用的地址。脚本disasfun.sh可用来反汇编,源代码从vmlinux文件引用一个内核函数。文件vmlinux含有内核函数的绝对地址,因此,在汇编代码中看见的地址是在内存中的地址。
下面是一个样例。
配置内核时,kgdb应打开线程分析(CONFIG_KGDB_THREAD),gdb应连接到目标内核。
用"Ctrl+C"中断内核,放置一个断点在函数__down处,并继续运行,方法如下:
Program received signal SIGTRAP, Trace/breakpoint trap.breakpoint () at gdbstub.c:11601160 }(gdb) break __downBreakpoint 1 at 0xc0105a43: file semaphore.c, line 62.(gdb) cContinuing.
为了让程序运行到断点处,在目标机上运行"manlilo"。程序会运行到断点,gdb会进入命令行模式。输入栈跟踪命令,显示如下:
Breakpoint 1, __down (sem=0xc7393f90) at semaphore.c:6262 add_wait_queue_exclusive(&sem->wait, &wait);(gdb) backtrace#0 __down (sem=0xc7393f90) at semaphore.c:62#1 0xc0105c70 in __down_failed () at af_packet.c:1878#2 0xc011433b in do_fork (clone_flags=16657, stack_start=3221199556, regs=0xc7393fc4, stack_size=0) at /mnt/work/build/old-pc/linux-2.4.6-kgdb/include/asm/semaphore.h:120#3 0xc010594b in sys_vfork (regs={ebx = 1074823660, ecx = 1074180970, edx = 1074823660, esi = -1073767732, edi = 134744856, ebp = -1073767712, eax = 190, xds = 43, xes = 43, orig_eax = 190, eip = 1074437320, xcs = 35, eflags = 518, esp = -1073767740, xss = 43}) at process.c:719
在函数sys_vfork中行号显示为719,这与文件process.c中的行号一致,查看该文件可得到确认,方法如下:
(gdb) list process.c:719714 * do not have enough call-clobbered registers to hold all715 * the information you need.716 */717 asmlinkage int sys_vfork(struct pt_regs regs)718 {719 return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0);720 }721722 114 static inline void down(struct semaphore * sem)115 {116 #if WAITQUEUE_DEBUG117 CHECK_MAGIC(sem->__magic);118 #endif119120 __asm__ __volatile__( <-----121 "# atomic down operation\n\t"122 LOCK "decl %0\n\t"
上述代码中,在箭头所指示语句处,得到的信息仅是它在do_fork的一个扩展的内联函数down中。gdb还打印了在do_fork中从下一个被调用的函数开始代码的绝对地址:0xc011433b。这里我们用脚本disasfun找出该地址所对应的代码行。命令"disasfunvmlinux do_fork"输出的部分结果显示如下:
if ((clone_flags & CLONE_VFORK) && (retval > 0))c011431d: 8b 7d 08 mov 0x8(%ebp),%edic0114320: f7 c7 00 40 00 00 test $0x4000,%edic0114326: 74 13 je c011433b <do_fork+0x707>c0114328: 83 7d d4 00 cmpl $0x0,0xffffffd4(%ebp)c011432c: 7e 0d jle c011433b <do_fork+0x707>#if WAITQUEUE_DEBUGCHECK_MAGIC(sem->__magic);#endif __asm__ __volatile__(c011432e: 8b 4d d0 mov 0xffffffd0(%ebp),%ecxc0114331: f0 ff 4d ec lock decl 0xffffffec(%ebp)c0114335: 0f 88 68 95 13 00 js c024d8a3 <stext_lock+0x7bf> down(&sem); return retval;c011433b: 8b 45 d4 mov 0xffffffd4(%ebp),%eax <-----c011433e: e9 8d 00 00 00 jmp c01143d0 <do_fork+0x79c>Looking at the code in fork.c we know where above code is:fork_out:if ((clone_flags & CLONE_VFORK) && (retval > 0)) down(&sem)
(7)线程分析
gdb具有分析应用程序线程的特征,它提供了应用程序创建的线程的列表,它允许开发者查看其中任何一个线程。gdb的特征可用来与kgdb一起查看内核线程。gdb能提供内核中所有线程的列表。开发者可指定一个线程进行分析。像backtrace,inforegi这样的gdb命令接着可以显示指定线程上下文的信息。
应用程序创建的所有线程分享同一地址空间,相似地,所有内核线程共享内核地址空间。每个内核线程的用户地址空间可能不同,因此,gdb线程可较好地分析内核代码和驻留在内核空间的数据结构。
gdb info给出了关于gdb线程分析方面更多的信息。下面列出一个内核线程分析的样例:
gdb命令"info threads"给出了内核线程的列表,显示如下:
(gdb) info thr 21 thread 516 schedule_timeout (timeout=2147483647) at sched.c:411 20 thread 515 schedule_timeout (timeout=2147483647) at sched.c:411 19 thread 514 schedule_timeout (timeout=2147483647) at sched.c:411 18 thread 513 schedule_timeout (timeout=2147483647) at sched.c:411 17 thread 512 schedule_timeout (timeout=2147483647) at sched.c:411 16 thread 511 schedule_timeout (timeout=2147483647) at sched.c:411 15 thread 438 schedule_timeout (timeout=2147483647) at sched.c:411 14 thread 420 schedule_timeout (timeout=-1013981316) at sched.c:439 13 thread 406 schedule_timeout (timeout=-1013629060) at sched.c:439 12 thread 392 do_syslog (type=2, buf=0x804dc20 "run/utmp", len=4095) at printk.c:182 11 thread 383 schedule_timeout (timeout=2147483647) at sched.c:411 10 thread 328 schedule_timeout (timeout=2147483647) at sched.c:411 9 thread 270 schedule_timeout (timeout=-1011908724) at sched.c:439 8 thread 8 interruptible_sleep_on (q=0xc02c8848) at sched.c:814 7 thread 6 schedule_timeout (timeout=-1055490112) at sched.c:439 6 thread 5 interruptible_sleep_on (q=0xc02b74b4) at sched.c:814 5 thread 4 kswapd (unused=0x0) at vmscan.c:736 4 thread 3 ksoftirqd (__bind_cpu=0x0) at softirq.c:387 3 thread 2 context_thread (startup=0xc02e93c8) at context.c:101 2 thread 1 schedule_timeout (timeout=-1055703292) at sched.c:439* 1 thread 0 breakpoint () at gdbstub.c:1159(gdb)
如上所显示,gdb为每个线程设定在gdb中唯一的id,当gdb内部引用一个线程时,可以使用这个id。例如:线程7(PID7)具有gdb id 8。为了分析内核线程8 ,我们指定线程9给gdb。gdb接着切换到该线程中,准备做更多的分析。
下面是分析线程的命令显示:
(gdb) thr 9[Switching to thread 9 (thread 270)]#0 schedule_timeout (timeout=-1011908724) at sched.c:439439 del_timer_sync(&timer);(gdb) bt#0 schedule_timeout (timeout=-1011908724) at sched.c:439#1 0xc0113f36 in interruptible_sleep_on_timeout (q=0xc11601f0, timeout=134) at sched.c:824#2 0xc019e77c in rtl8139_thread (data=0xc1160000) at 8139too.c:1559#3 0xc010564b in kernel_thread (fn=0x70617773, arg=0x6361635f, flags=1767859560) at process.c:491#4 0x19 in uhci_hcd_cleanup () at uhci.c:3052#5 0x313330 in ?? () at af_packet.c:1891Cannot access memory at address 0x31494350(gdb) info regieax 0xc38fdf7c -1013981316ecx 0x86 134edx 0xc0339f9c -1070358628ebx 0x40f13 266003esp 0xc3af7f74 0xc3af7f74ebp 0xc3af7fa0 0xc3af7fa0esi 0xc3af7f8c -1011908724edi 0xc3af7fbc -1011908676eip 0xc011346d 0xc011346deflags 0x86 134cs 0x10 16ss 0x18 24ds 0x18 24es 0x18 24fs 0xffff 65535gs 0xffff 65535fctrl 0x0 0fstat 0x0 0ftag 0x0 0fiseg 0x0 0fioff 0x0 0foseg 0x0 0fooff 0x0 0---Type <return> to continue, or q <return> to quit---fop 0x0 0(gdb) thr 7[Switching to thread 7 (thread 6)]#0 schedule_timeout (timeout=-1055490112) at sched.c:439439 del_timer_sync(&timer);(gdb) bt#0 schedule_timeout (timeout=-1055490112) at sched.c:439#1 0xc0137ef2 in kupdate (startup=0xc02e9408) at buffer.c:2826#2 0xc010564b in kernel_thread (fn=0xc3843a64, arg=0xc3843a68, flags=3280222828) at process.c:491#3 0xc3843a60 in ?? ()Cannot access memory at address 0x1f4(gdb)
用户可从http://sourceware.org/gdb/download/下载进程信息宏ps和psname,ps宏提供了运行在内核的线程的名字和ID。运行结果显示如下:
(gdb) ps0 swapper1 init2 keventd3 ksoftirqd_C4 kswapd5 bdflush6 kupdated8 khubd270 eth0328 portmap383 syslogd392 klogd406 atd420 crond438 inetd511 mingetty512 mingetty513 mingetty514 mingetty515 mingetty516 mingetty(gdb) The psname macro can be used to get name of a thread when it's id is known.(gdb) psname 88 khubd(gdb) psname 7(gdb) (7)Watchpoints
(7)Watchpoints
kgdbstub使用x86处理器的调试特征支持硬件断点,这些断点不需要代码修改。它们使用调试寄存器。x86体系中的ia32处理器有4个硬件断点可用。每个硬件断点可以是下面三个类型之一:
执行断点 当代码在断点地址执行时,触发执行断点。由于硬件断点有限,建议通过gdbbreak命令使用软件断点,除非可避免修改代码。
写断点当系统对在断点地址的内存位置进行写操作时,触发一个写断点。写断点可以放置可变长度的数据。写断点的长度指示为观察的数据类型长度,1表示为字节数据,2表示为2字节数据,3表示为4字节数据。
访问断点 当系统读或写断点地址的内存时,触发一个访问断点。访问断点也有可变长度数据类型。
ia-32处理器不支持IO断点。
因为gdb stub目前不使用gdb用于硬件断点的协议,因此,它通过gdb宏访问硬件断点。硬件断点的gdb宏说明如下:
1)hwebrk – 放置一个执行断点。
用法:hwebrk breakpointno address
2)hwwbrk – 放置一个写断点。
用法:hwwbrk breakpointno length address
3)hwabrk – 放置一个访问断点。
用法:hwabrk breakpointno length address
4)hwrmbrk – 删除一个断点
用法:hwrmbrk breakpointno
5)exinfo – 告诉是否有一个软件或硬件断点发生。如果硬件断点发生,打印硬件断点的序号。
这些命令要求的参数说明如下:
breakpointno – 0~3
length - 1~3
address - 16进制内存位置(没有0x),如:c015e9bc
用户模式Linux(User ModeLinux,UML)不同于其他Linux虚拟化项目,UML尽量将它自己作为一个普通的程序。UML与其他虚拟化系统相比,优点说明如下:
良好的速度
UML编译成本地机器的代码,像主机上的其他已编译应用程序一样运行。它比在软件上应用整个硬件构架的虚拟机快得多。另一方面,UML不需要考虑依赖于特定CPU的虚拟化系统的硬件特异性。
获益于Liunx更新
每次Linux的改进,UML自动得到这些功能,虚拟化系统并不一定能从更新中获益。
弹性编码
内核需要与硬件或虚拟硬件交互,但UML可将交互看作其他方式。例如:可以将这些交互转换成共享的库,其他程序可以在使用时连接该库。它还可作为其他应用程序的子shell启动,能任何其他程序的stin/stdout使用。
可移植性
UML将来可以移植到x86 Windows, PowerPC Linux, x86 BSD或其他系统上运行。
从Linux2.6.9版本起,用户模式Linux(User modeLinux,UML)已随Linux内核源代码一起发布,它存放于arch/um目录下。编译好UML的内核之后,可直接用gdb运行编译好的内核并进行调试。
用户模式Linux(User modeLinux,UML)将Linux内核的一部分作为用户空间的进程运行,称为客户机内核。UML运行在基于Linux系统调用接口所实现的虚拟机。UML运行的方式如图1所示。UML像其他应用程序一样与一个"真实"的Linux内核(称为"主机内核")交互。应用程序还可运行在UML中,就像运行在一个正常的Linux内核下。
图1 UML在Linux系统中运行的位置
使用UML的优点列出如下:
如果UML崩溃,主机内核还将运行完好。
可以用非root用户运行UML。
可以像正常进程一样调试UML。
在不中断任何操作下与内核进行交互。
用UML作为测试新应用程序的"沙箱",用于测试可能有伤害的程序。
可以用UML安全地开发内核。
可以同时运行不同的发布版本。
由于UML基于以Linux系统调用接口实现的虚拟机,UML无法访问主机的硬件设备。因此,UML不适合于调试与硬件相关的驱动程序。
(1)获取源代码
从http://www.kernel.org/下载linux-2.6.24.tar.bz2,解压缩源代码,方法如下:
host% bunzip2 linux-2.6.24.tar.bz2
host% tar xf linux-2.6.24.tar
host% cd linux-2.6.24
(2)配置UML模式内核
如果使用缺省配置,那么,方法如下:
host% make defconfig ARCH=um
如果运行配置界面,方法如下:
host% make menuconfig ARCH=um
如果不使用缺省配置defconfig,那么,内核编译将使用主机的配置文件,该配置文件在主机/boot目录下。对于UML模式内核来说,这是不对的,它将编译产生缺乏重要的驱动程序和不能启动的UML。
以编译UML时,每个make命令应加上选项"ARCH=um",或者设置环境变量"export ARCH=um"。
当再次配置时,可以先运行下面的命令清除所有原来编译产生的影响:
host% make mrproper
host% make mrproper ARCH=um
内核提供了配置选项用于内核调试,这些选项大部分在配置界面的kernelhacking菜单项中。一般需要选取CONFIG_DEBUG_INFO选项,以使编译的内核包含调试信息。
(3)编译UML模式内核
编译内核的方法如下:
host% make ARCH=um
当编译完成时,系统将产生名为"linux"的UML二进制。查看方法如下:
host% ls -l linux
-rwxrwxr-x 2 jdike jdike 18941274 Apr 7 15:18 linux
由于UML加入了调试符号,UML模式内核变得很大,删除这些符号将会大大缩小内核的大小,变为与标准内核接近的UML二进制。
现在,用户可以启动新的UML模式内核了。
(4)UML的工具
使用UML和管理UML的工具说明如下:
UMLd – 用于创建UML实例、管理实例启动/关闭的后台程序。
umlmgr –用于管理正运行的UML实例的前台工具程序。
UML Builder – 编译根文件系统映像(用于UML模式操作系统安装)。
uml switch2 用于后台传输的用户空间虚拟切换。
VNUML – 基于XML的语言,定义和启动基于UML的虚拟网络场景。
UMLazi – 配置和运行基于虚拟机的UML的管理工具。
vmon – 运行和监管多个UML虚拟机的轻量级工具,用Python 书写。
umvs –umvs是用C++和Bash脚本写的工具,用于管理UML实例。该应用程序的目的是简化UML的配置和管理。它使用了模板,使得编写不同的UML配置更容易。
MLN - MLN (My Linux Network)是一个perl程序,用于从配置文件创建UML系统的完整网络,使得虚拟网络的配置和管理更容易。MLN基于它的描述和简单的编程语言编译和配置文件系统模板,并用一种组织方式存储它们。它还产生每个虚拟主机的启动和停止脚本,在一个网络内启动和停止单个虚拟机。MLN可以一次使用几个独立的网络、项目,甚至还可以将它们连接在一起。
Marionnet – 一个完全的虚拟网络实验,基于UML,带有用户友好的图形界面。
(1)启动UML
为了运行UML实例,用户需要运行Linux操作系统主机和带有自己文件系统的UML客户机。用户可以从http://uml.nagafix.co.uk/下载UML(如:kernel)和客户机文件系统(如:root_fs),运行UML实例的方法如下:
$ ./kernel ubda= root_fs mem=128M
上述命令中,参数mem指定虚拟机的内存大小;参数ubda表示根文件系统root_fs作为虚拟机第一个块设备,虚拟机用/dev/udba表示虚拟机的第一个块设备,与Linux主机系统的第一个物理块设备/dev/sda类似。
用户还可以自己创建虚拟块设备,例如:建立交换分区并在UML上使用它的方法如下:
$ dd if=/dev/zero of=swap bs=1M count=128
$ ./kernel ubda= root_fs ubdb=swap mem=128M
上述命令,创建了128M的交换分区,作为第二个块设备ubdb,接着,启动UML模式内核,用ubdb作为它的交换分区。
(2)登录
预打包的文件系统有一个带有"root"密码的root帐户,还有一个带有"user"密码的user帐户。用户登录后可以进入虚拟机。预打包的文件系统已安装了各种命令和实用程序,用户还可容易地添加工具或程序。
还有一些其他登录方法,说明如下:
在虚拟终端上登录
每个已配置(设备存在于/dev,并且/etc/inittab在上面运行了一个getty)的虚拟终端有它自己的xterm。.
通过串行线登录
在启动输出中,找到类似下面的一行:
serial line 0 assigned pty /dev/ptyp1
粘贴用户喜爱的终端程序到相应的tty,如:minicom,方法如下:
host% minicom -o -p /dev/ttyp1
通过网络登录
如果网络正运行,用户可用telnet连接到虚拟机。
虚拟机运行后,用户可像一般Linux一样运行各种shell命令和应用程序。
可以粘附UML串行线和控制台到多个类型的主机I/O通道,通过命令行指定,用户可以粘附它们到主机ptys, ttys,文件描述子和端口。常用连接方法说明如下:
让UML控制台出现在不用的主机控制台上。
将两个虚拟机连接在一起,一个粘到pty,另一个粘附到相应的tty。
创建可从网络访问的虚拟,粘附虚拟机的控制台到主机的一个端口。
(1)指定设备
用选项"con"或"ssl"(分别代表控制台和串行线)指定设备。例如:如果用户想用3号控制台或10号串行线交互,命令行选项分别为"con3"和"ssl10"。
例如:指定pty给每个串行线的样例选项列出如下:
ssl=pty ssl0=tty:/dev/tty0 ssl1=tty:/dev/tty1
(2)指定通道
可以粘附UML设备到多个不同类型的通道,每个类型有不同的指定方法,分别说明如下:
伪终端为:device=pty,pts终端为:device=pts
UML分配空闲的主机伪终端给。用户可以通过粘附终端程序到相应的tty访问伪终端,方法如下:
screen /dev/pts/n
screen /dev/ttyxx
minicom -o -p /dev/ttyxx #minicom似乎不能处理pts设备
kermit #启动它,打开设备,然后连接设备
终端为:device=tty:tty设备文件
UML将粘附设备到指定的tty,例如:一个样例选项列出如下:
con1=tty:/dev/tty3
上面语句将粘附UML的控制台1到主机的/dev/tty3。如果用户指定的tty是tty/pty对的slave端,则相应的pty必须已打开。
xterms为:device=xterm
UML将运行一个xterm,并且将设备粘附到xterm。
端口为:device=port:端口号
上述选项将粘附UML设备到指定的主机端口。例如:粘附控制台1到主机的端口9000,方法如下:
con1=port:9000
粘附所有串行线到主机端口,方法如下:
ssl=port:9000
用户可以通过telnet到该端口来访问这些设备,每个激活的telnet会话得到不同的设备,如果有比粘附到端口的UML设备多的telnet连接到一个端口,格外的telnet会话将阻塞正存在的telnet断线,或直到其他设备变为激活(如:通过在/etc/inittab中设置激活)。
已存在的文件描述子:device=文件描述子
如果用户在UML命令行中建立了一个文件描述子,他可以粘附UML设备到文件描述子。这最常用于在指定所有其他控制台后将主控制台放回到stdin和stdout上。方法如下:
con0=fd:0,fd:1 con=pts
null设备:device=null
与"none"选项相比,上述选项允许打开设备,但读将阻塞,并且写将成功,但数据会被丢掉。
无设备:device=none
上述选项将引起设备消失。如果你正使用devfs,设备将不出现在/dev下。如果设备出现,尝试打开它将返回错误-ENODEV。
用户还可以指定不同的输入和输出通道给一个设备,最常用的用途是重粘附主控制到stdin和stdout。例如:一个样例选项列出如下:
ssl3=tty:/dev/tty2,xterm
上述诗句将引起在主机/dev/tty3上的串行线3接受输入,显示输出在xterm上。
如果用户决定将主控制台从stdin/stdout移开,初始的启动输出将出现在用户正运行UML所在的终端。然而,一旦控制台驱动程序已初始化,启动及随后的输出将出现在控制台0所在的地方。
UML实例可以用网络访问主机、本地网络上的其他机器和网络的其他部分。新的辅助程序uml_net进行主机建立时需要root权限。
当前UML虚拟机有5种传输类型用于与其他主机交换包,分别是:ethertap,TUN/TAP,Multicast,交换机后台(switch daemon),slip,slirp和pcap。
TUN/TAP, ethertap,slip和slirp传输允许UML实例与主机交换包。它们可定向到主机或主机可扮作路由器提供对其他物理或虚拟机的访问。
pcap传输是一个综合的仅读接口,用libpcap二进制从主机上的接口收集包并过滤包。这对于构建预配置的交通监管器或sniffer来说,是有用的。
后台和多播传输提供了完全虚拟的网络络其他虚拟机器。该网络完全从物理网络断开,除非某一个虚拟机扮作网关。
如何选择这些主机传输类型 '用户可根据用途进行选择,选择方法说明如下:
ethertap – 如果用户想对主机网络进行访问,并且运行以2.2以前版本时,使用它。
TUN/TAP – 如果用户想访问主机网络,可使用它。TUN/TAP 运行在2.4以后的版本,比ethertap更有效率。TUN/TAP 传输还能使用预置的设备,避免对uml_net辅助程序进行setuid操作。
Multicast – 如果用户期望建立一个纯虚拟网络,并且仅想建立UML,就使用它。
交换机后台 – 如果用户想建立一个纯虚拟网络,并且不介意为了得到较好执行效率而建立后台,就使用它。
slip – 没有特殊理由不要运行slip后端,除非ethertap和TUN/TAP不可用。
slirp – 如果用户在主机上没有root权限对建立网络进行访问,或者如果用户不想分配对UML分配IP时,使用它。
pcap – 对实际的网络连接没有太多用途,但用于主机上监管网络交通很有用。
(1)网络建立通用步骤
首先,用户必须已在UML中打开虚拟网络。如果运行下载的预编译的内核,则已打开虚拟网络。如果用户自己编译内核,则在配置界面上的"Networkdevice support"菜单中,打开"Network device support"和三种传输选项。
下一步是提供网络设备给虚拟机,通过在内核命令行中进行描述,格式如下:
eth <n> =<transport> ,<transport args>
例如:一个虚拟以太网设备可以如下粘附到一个主机ethertap上:
eth0=ethertap,tap0,fe:fd:0:0:0:1,192.168.0.254
上述语句在虚拟机内部建立eth0,粘附它自己到主机/dev/tap0,指定一个以太网地址,并指定给主机tap0接口一个IP地址。
一旦用户决定如何建立设备后,就可以启动UML、登录、配置设备的UML侧,并设置对外界的路由。此后,UML就可以与网络任何其他机器(物理或虚拟的)通信。
(2)用户空间后台
从http://www.user-mode-linux.org/cvs/tools/下载工具uml_net和uml_switch,编译并安装。uml_switch是在UML系统之间管理虚拟网络的后台,而不用连接到主机系统的网络。uml_switch将在UNIX域的socket上监听连接,并在连接到UNIX域的客户端之间转发包。
(3)指定以太网地址
TUN/TAP,ethertap和daemon接口允许用户给虚拟以太网设备指定硬件地址。但通常不需要指定硬件地址。如果命令行没有指定硬件地址,它将提供地址为fe:fd:nn:nn:nn:nn,其中,nn.nn.nn.nn是设备IP地址。这种方法通常足够保证有唯一的硬件地址。
(4)UML接口建立
一旦用命令行描述网络设备,用户在启动UML和登录后,第一件事应是建立接口,方法如下:
UML# ifconfig ethn ip-address up
此时,用户应可以ping通主机。为了能查看网络,用户设置缺省的路由为到达主机,方法如下:
UML# route add default gw host ip
例如:主机IP为192.168.0.4,设置路由方法如下:
UML# route add default gw 192.168.0.4
注意:如果UML不能与物理以太网上其他主机通信,可能是因为网络路由自动建立,可以运行"route–n"查看路由,结果类似如下:
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
掩码不是255.255.255.255,因此,就使用到用户主机的路由替换它,方法如下:
UML# route del -net 192.168.0.0 dev eth0 netmask255.255.255.0
UML# route add -host 192.168.0.4 dev eth0
添加缺省的路由到主机,将允许UML与用户以太网上任何机器交换包。
(5)多播
在多个UML之间建立一个虚拟网络的最简单方法是使用多播传输。用户的系统必须在内核中打开多播(multicast),并且在主机上必须有一个多播能力的网络设备。通常它是eth0。
为了使用多播,运行两个UML,命令行带有"eth0=mcast"选项。登录后,用户在每个虚拟机上用不同的IP地址配置以太网设备,方法如下:
UML1# ifconfig eth0 192.168.0.254
UML2# ifconfig eth0 192.168.0.253
这两个虚拟机应能相互通信。
传输设置的整个命令行选项列出如下:
ethn=mcast,ethernet address,multicast address,multicastport,ttl
(6)TUN/TAP和uml_net辅助程序
TUN/TAP驱动程序实现了虚拟网卡的功能,TUN表示虚拟的是点对点设备,TAP表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装。利用TUN/TAP驱动,可以将tcp/ip协议栈处理好的网络分包传给任何一个使用TUN/TAP驱动的进程,由进程重新处理后再发到物理链路中。
TUN/TAP是与主机交换包的较好机制,主机建立TUN/TAP较简单的方法是使用uml_net辅助程序,它包括插入tun.o内核模块、配置设备、建立转发IP、路由和代理ARP。
如果在设备的主机侧指定了IP地址,uml_net将在主机上做所有的建立工作。粘附设备到TUN/TAP设备的命令行格式列出如下:
eth <n> =tuntap,,,<host IP address>
例如:下面参数将粘附UML的eth0到下一个可用的tap设备,指定IP地址192.168.0.254么tap设备的主机侧,并指定一个基于IP地址的以太网地址。
eth0=tuntap,,,192.168.0.254
(7)带有预配置的tap设备的TUN/TAP
如果用户没有更好的uml_net,可以预先建立TUN/TAP。步骤如下:
用工具tunctl创建tap设备,方法如下:
host# tunctl -u uid
上述命令中,uid是用户的ID或UML将运行登录的用户名。
配置设备IP地址,方法如下:
host# ifconfig tap0 192.168.0.254 up
建立路由和ARP,方法如下:
host# bash -c 'echo 1 >/proc/sys/net/ipv4/ip_forward'
host# route add -host 192.168.0.253 dev tap0
host# bash -c 'echo 1 >/proc/sys/net/ipv4/conf/tap0/proxy_arp'
host# arp -Ds 192.168.0.253 eth0 pub
注意:这个配置没有重启机时失效,每次主机启动时,应重新设置它。最好的方法是用一个小应用程序,每次启动时,读出配置文件重新建立设置的配置。
使用网桥
为了不使用2个IP地址和ARP,还可通过对UML使用网桥提供对用户LAN直接访问,方法如下:
host# brctl addbr br0
host# ifconfig eth0 0.0.0.0 promisc up
host# ifconfig tap0 0.0.0.0 promisc up
host# ifconfig br0 192.168.0.1 netmask 255.255.255.0 up
host# brctl stp br0 off
host# brctl setfd br0 1
host# brctl sethello br0 1
host# brctl addif br0 eth0
host# brctl addif br0 tap0
注意:用户应该用eth0的IP地址通过ifconfig建立"br0"。
运行UML
一旦设备建立好后,运行UML,命令格式为: eth0=tuntap,devicename,例如:一个样例列出如下:
eth0=tuntap,tap0
如果用户不再使用tap设置,可以用下面命令删除它:
host# tunctl -d tap device
最后,tunctl有一个"-b"(用于简捷模式)切换,仅输出它所创建的tap设备的名字。它很适合于被一个脚本使用,方法如下:
host# TAP=`tunctl -u 1000 -b`
(8)交换机后台
交换机后台uml_switch以前称为uml_router,它提供了创建整个虚拟网络的机制。缺省下,它不提供对主机网络的连接。
首先,用户需要运行uml_switch,无参数运行时,表示它将监听缺省的unix域socket。使用选项"-unixsocket"可指定不同的socket,"-hub"可将交换机后台变为集线器(Hub)。如果用户期望交换机后台连接到主机网络(允许UML访问通过主机访问外部的网络),可使用选项"-taptap0"。
uml_switch还可作为后台运行,方法如下:
host% uml_switch [ options ] < /dev/null> /dev/null
内核命令行交换机的通用命令行格式列出如下:
ethn=daemon,ethernet address,socket type,socket
通常只需要指定参数"daemon",其他使用缺省参数,如果用户运行没有参数的交换机后台,在同一台机器上使用选项"eth0=daemon"运行UML,etho驱动程序会直接粘附它自己到交换机后台。参数socket为unix域socket的文件名,用于uml_switch和UML之间网络通信。
(9)Slirp
slirp通常使用外部程序/usr/bin/slirp,仅通过主机提供IP网络连接。它类似于防火墙的IP伪装,跃然传输有用户空间进行,而不是由内核进行。slirp不在主机上建立任何接口或改变路由。slirp在主机上不需要root权限或运行setuid。
slirp命令行的通用格式为:
ethn=slirp,ethernet address,slirp path
在UML上,用户应使用没有网关IP的etho设置缺省路由,方法如下:
UML# route add default dev eth0
slirp提供了UML可使用的大量有用IP地址,如:10.0.2.3,是DNS服务器的一个别名,定义在主机/etc/resolv.conf中,或者它是slirp的选项"dns"中给定的IP地址。
(10)pcap
pcap对网络上传输的数据包进行截获和过滤。通过命令行或pcap传输粘附到UML以太网设备uml_mconsole工具,语法格式如下:
ethn=pcap,host interface,filter expression,option1,option2
其中,expression和option1、option2是可选的。
这个接口是主机上用户想嗅探(sniff)的任何网络设备,过滤器表达式(filterexpression)与工具tcpdump使用的一样,option1为"promisc"或"nopromisc",控制pcap是否将主机接口设为"promiscuous"(混杂)模式;option2为"optimize"或"nooptimize",表示是否使用pcap表达式优化器。
一个设置pcap的样例列出如下:
eth0=pcap,eth0,tcp
eth1=pcap,eth0,!tcp
上述语句将引起在主机eth0上的UML eth0将所有的tcp发出,并且在主机eth0上的UMLeth1将发出所有非tcp包。
(11)用户建立主机
主机上的网络设备需要配置IP地址,还需要用值为1484的mtu配置tap设备。slip设置还需要配置点到点(pointopoint)地址,方法如下:
host# ifconfig tap0 arp mtu 1484 192.168.0.251 up
host# ifconfig sl0 192.168.0.251 pointopoint 192.168.0.250up
如果正建立tap设备,就将路由设置到UML IP。方法如下:
UML# route add -host 192.168.0.250 gw 192.168.0.251
为了允许网络上其他主机看见这个虚拟机,化理ARP设置如下:
host# arp -Ds 192.168.0.250 eth0 pub
最后,将主机设置到路由包,方法如下:
host# echo 1 > /proc/sys/net/ipv4/ip_forward
在虚拟机间共享文件系统的方法是使用ubd(UML BlockDevice)块设备驱动程序的写拷贝(copy-on-write,COW)分层能力实现。COW支持在仅读的共享设备上分层读写私有设备。一个虚拟机的写数据存储在它的私有设备上,而读来自任一请求块有效的设备。如果请求的块有效,读取私有设备,如果无效,就读取共享设备。
用这种方法,数据大部分在多个虚拟机间共享,每个虚拟机有多个小文件用于存放虚拟机所做的修改。当大量UML从一个大的根文件系统启动时,这将节约大量磁盘空间。它还提供执行性能,因为主机可用较小的内存缓存共享数据,主机的内存而不是硬盘提供UML硬盘请求服务。
可通过简单地加COW文件的名字到合适的ubd,实现加一个COW层到存在的块设备文件。方法如下:
ubd0=root_fs_cow,root_fs_debian_22
上述语句中,"root_fs_cow"是私有的COW文件,"root_fs_debian_22"是存在的共享文件系统。COW文件不必要存在,如果它不存在,驱动程序将创建并初始化它。一旦COW文件已初始化,可在以命令行中使用它,方法如下:
ubd0=root_fs_cow
后备文件(backing file)的名字存在COW文件头中,因此在命令行中继续指定它将是多余的。
COW文件是稀疏的,因此它的长度不同于硬盘的实际使用长度。可以用命令"ls –ls"查看硬盘的实际消耗,用"ls–l"查看COW文件和后备文件(backing file)的长度,方法如下:
host% ls -l cow.debian debian2.2
-rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian
-rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2
host% ls -ls cow.debian debian2.2
880 -rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16cow.debian
525832 -rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42debian2.2
从上述显示结构,用户会发现COW文件实际硬盘消耗小于1M,面不是492M。
一旦文件系统用作一个COW文件的仅读后备文件,不要直接从它启动或修改它。这样,会使使用经的任何COW文件失效。后备文件在创建时它的修改时间mtime和大小size存放在COW文件头中,它们必须相匹配。如果不匹配,驱动程序将拒绝使用COW文件。
如果用户手动地改变后备文件或COW头,将得到一个崩溃的文件系统。
操作COW文件的方法说明如下:
(1)删除后备文件
由于UML存放后备文件名和它的修改时间mtime在COW头中,如果用户删除后文件文件,这些信息将变成无效的。因此,删除后备文件的步骤如下:
用保护时间戳的方式删除文件。通常,使用"-p"选项。拷贝操作命令"cp –a"中的"-a"隐含了"-p"。
通过启动UML更新COW头,命令行指定COW文件和新的后备文件的位置,方法如下:
ubda=COW file,新的后备文件位置
UML将注意到命令行和COW头之间的不匹配,检查新后文件路径的大小和修改时间mtime,并更新COW头。
如果当用户删除后备文件时忘记保留时间戳,用户可手动整理mtime,方法如下:
host% mtime=UML认定的修改时间mtime; \
touch --date="`date -d 1970-01-01\ UTC\ $mtime\ seconds`"后备文件
注意如果对真正修改过而不是刚删除的后备文件进行上述操作,那么将会文件崩溃,用户将丢失文件系统。
(2)uml_moo :将COW文件与它的后备文件融合
依赖于用户如何使用UML和COW设备,系统可能建议每隔一段时间融合COW文件中的变化到后备文件中。用户可以用工具uml_moo完成该操作,方法如下:
host% uml_moo COW file new backing file
由于信息已在COW文件头中,因此,不必指定后备文件。
uml_moo在缺省下创建一个新的后备文件,它还有一个破坏性的融合选项,直接将融合COW文件到它当前的后备文件。当后备文件仅有一个COW文件与它相关时,该选项很有用。如果多个COW与一个后备文件相关,融合选项"-d"将使所有其他的COW无效。但是,如果硬盘空间不够时,使用融合选项"-d"很方便快捷,方法如下:
host% uml_moo -d COW file
(3)uml_mkcow :创建新COW文件
正常创建COW文件的方法是以UML命令行中指定一个不存在的COW文件,让UML创建COW文件。但是,用户有时想创建一个COW文件,但不想启动UML。此时,可以使用uml_mkcow工具。方法如下:
host% uml_mkcow 新COW文件 存在的后备文件
如果用户想销毁一个存在的COW文件,可以加"-f"选项强制重写旧的COW文件,方法如下:
host% uml_mkcow -f 存在的COW文件 存在的后备文件
如果根文件系统硬盘空间不够大,或者想使用不同于ext2的文件系统,用户就可能想创建和挂接新的UML文件系统,用户可以用如下方法创建UML的根文件系统:
(1)创建文件系统的文件
使用命令dd创建一个合适尺寸的空文件,用户可以创建稀疏文件,该文件直到实际使用时才分配硬盘空间。例如:下面的命令创建一个100M填满0的稀疏文件:
host% dd if=/dev/zero of=new_filesystem seek=100 count=1bs=1M
(2)指定文件给一个UML设备
在UML命令行上加入下面的选项:
ubdd=new_filesystem
上述命令中,ubdd应确保没被使用。
(3)创建和挂接文件系统
创建和挂接文件系统方法如下:
host# mkreiserfs /dev/ubdd
UML# mount /dev/ubdd /mnt
如果用户在UML中想访问主机上的文件,用户可将主机当作独立的机器,可以使用nfs从主机挂接目录,或者用scp和rcp拷贝文件到虚拟机,因为UML运行在主机上,它能象其他进程一样访问这些文件,并使它们在虚拟机内部可用,而不需要使用网络。
还可以使用hostfs虚拟文件系统,用户通过它可以挂接一个主机目录到UML文件系统,并像在主机上一样访问该目录中的文件。
(1)使用hostfs
首先,确认虚拟机内部是否有hostfs可用,方法如下:
UML# cat /proc/filesystems
如果没有列出hostfs,则需要重编译内核,配置hostfs,将它编译成一个内核模块,并用"insmod"插入该内核模块。
挂接hostfs文件系统,例如:将hostfs挂接到虚拟机的/mnt/host下,方法如下:
UML# mount none /mnt/host -t hostfs
如果用户不想挂接主机的root目录,他可以用"-o"选项指定挂接的子目录。例如:挂接主机的/home到虚拟机的/mnt/home,方法如下:
UML# mount none /mnt/home -t hostfs -o /home
(2)hostfs命令行选项
在UML命令行选项可使用hostfs选项,用来指定多个hostfs挂接到一个主机目录或阻止hostfs用户从主机上销毁数据,方法如下:
hostfs=directory,options
当前可用的选项是"append",用来阻止所有的文件在追加方式打开,并不允许删除文件。
(3)hostfs作为根文件系统
还可以通过hostfs从主机上的目录而不是在一个文件中的标准文件系统启动UML。最简单的方法是用loop挂接一个存在的root_fs文件,方法如下:
host# mount root_fs uml_root_dir -o loop
用户需要将/etc/fstab中的文件类型改变为"hostfs",fstab中的该行列出如下:
none / hostfs defaults 1 1
接着用户可以用chown将目录中root拥有的所有文件改变为用户拥有,方法如下:
host# find . -uid 0 -exec chown user {} \;
如果用户不想用上面的命令改变文件属主,用户可以用root身份运行UML。
接着,确保UML内核编译进hostfs,而不是以内核模块方式包含hostfs。那么,加入下面的命令行运行UML:
root=/dev/root rootflags=/path/to/uml/root rootfstype=hostfs
加入上述选项后,UML应该像正常的一样启动。
(4)编译hostfs
如果hostfs不在内核中,用户需要编译hostfs,用户可以将它编译进内核或内核模块。用户在内核配置界面上选项hostfs,并编译和安装内核。
因为UML运行为正常的Linux进程,用户可以用gdb像调试其他进程一样调试内核,稍微不同的是:因为内核的线程已用系统调用ptrace进行拦截跟踪,因此,gdb不能ptrace它们。UML已加入了解决此问题的机制。
为了调试内核,用户需要从源代码编译,确保打开CONFIG_DEBUGSYM和CONFIG_PT_PROXY配置选项。它们分别用来确保编译内核带有"-g"选项和打开ptrace代理,以便gdb能与UML一起工作调试内核。
(1)在gdb下启动内核
用户可以在命令行中放入"debug"选项,在启动UML时将内核放在gdb的控制之下。用户可以得到一个运行gdb的xterm,内核将送一些命令到gdb,停在"start_kernel"处,用户可以输入"next","step"或"cont"运行内核。
(2)检查睡眠的进程
并非每个bug在当前运行的进程中,有时候,当进程在信号量上或其他类似原因死锁时,原本不应该挂起的进程在内核中挂起。这种情况下,用户在gdb中用"Ctrl+C"时,得到一个跟踪栈,用户将可以看见到不相关的空闲线程。
用户本想看到的是不应该睡眠的进程的栈,为了看到睡眠的进程,用户可以在主机上用命令ps得到该进程的主机进程id。
用户将gdb与当前线程分离,方法如下:
(UML gdb) det
然后将gdb粘附到用户感兴趣的线程上,方法如下:
(UML gdb) att <host pid>
查看该线程的栈,方法如下:
(UML gdb) bt
(3)在UML上运行ddd
ddd可以工作于UML,用户可以主机上运行ddd,它给gdb提供了图形界面。运行ddd的步骤如下:
启动ddd,方法如下:
host% ddd linux
得到gdb的pid
用命令ps可以得到ddd启动的gdb的pid。
运行UML
在运行UML的命令行中加上选项"debug=parentgdb-pid=<pid>",启动并登录UML。
在ddd的gdb命令行中输入"att 1",gdb显示如下:
0xa013dc51 in __kill ()
(gdb)
在gdb中输入"c",UML将继续运行,用户可接着像调试其他进程一样调试了。
(4)调试内核模块
gdb已支持调试动态装载入进程的代码,这需要在UML下调试内核模块。调试内核模块有些复杂,用户需要告诉gdb装入UML的对象文件名以及它在内存中的位置。接着,它能读符号表,并从装载地址指出所有的符号。
当用户在rmmod内核模块后重装载它时,可得到更多信息。用户必须告诉gdb忘记所有它的符号,包括主UML的符号,接着再装载回所有的符号。
用户可以使用脚本umlgdb进行内核模块的重装载和读取它的符号表。用户还可以手动进行一步步处理完成符号表的获取工作。下面分别说明这两种方法。
1)运行脚本umlgdb调试内核模块
运行脚本umlgd较容易获取内核模块的符号表。
首先,用户应告诉内核模块所在的位置,在脚本中有一个列表类似如下:
set MODULE_PATHS {
"fat" "/usr/src/uml/linux-2.6.18/fs/fat/fat.ko"
"isofs" "/usr/src/uml/linux-2.6.18/fs/isofs/isofs.ko"
"minix" "/usr/src/uml/linux-2.6.18/fs/minix/minix.ko"
}
用户将上述列表改为将调试的内核模块的路径,接着,从UML的顶层目录运行该脚本,显示如下:
Start UML as: ./linux <kernelswitches> debug gdb-pid=21903
GNU gdb 5.0rh-5 Red Hat Linux 7.1
Copyright 2001 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 undercertain 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"...
(gdb) b sys_init_module
Breakpoint 1 at 0xa0011923: file module.c, line 349.
(gdb) att 1
在用户运行UML后,用户只需要在"att 1"按回车,并继续执行它。方法如下:
Attaching to program: /home/jdike/linux/2.4/um/./linux, process1
0xa00f4221 in __kill ()
(UML gdb) c
Continuing.
此时,当用户用insmod插入内核模块,显示列出如下:
Breakpoint 1, sys_init_module (name_user=0x805abb0 "hostfs",
mod_user=0x8070e00) at module.c:349
349 char *name, *n_name, *name_tmp = NULL;
(UML gdb) finish
Run till exit from #0 sys_init_module (name_user=0x805abb0"hostfs",
mod_user=0x8070e00) at module.c:349
0xa00e2e23 in execute_syscall (r=0xa8140284) atsyscall_kern.c:411
411 else res = EXECUTE_SYSCALL(syscall, regs);
Value returned is $1 = 0
(UML gdb)
p/x (int)module_list +module_list->size_of_struct
$2 = 0xa9021054
(UML gdb) symbol-file ./linux
Load new symbol table from "./linux" ' (y or n) y
Reading symbols from ./linux...
done.
(UML gdb)
add-symbol-file/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o 0xa9021054
add symbol table from file"/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o" at
.text_addr = 0xa9021054
(y or n) y
Reading symbols from/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o...
done.
(UML gdb) p *module_list
$1 = {size_of_struct = 84, next = 0xa0178720, name = 0xa9022de0"hostfs",
size = 9016, uc = {usecount = {counter = 0}, pad = 0}, flags =1,
nsyms = 57, ndeps = 0, syms = 0xa9023170, deps = 0x0, refs =0x0,
init = 0xa90221f0 <init_hostfs>,cleanup = 0xa902222c<exit_hostfs>,
ex_table_start = 0x0, ex_table_end = 0x0, persist_start =0x0,
persist_end = 0x0, can_unload = 0, runsize = 0, kallsyms_start =0x0,
kallsyms_end = 0x0,
archdata_start = 0x1b855 <Address 0x1b855 out ofbounds>,
archdata_end = 0xe5890000 <Address 0xe5890000 outof bounds>,
kernel_data = 0xf689c35d <Address 0xf689c35d outof bounds>}
>> Finished loading symbols forhostfs ...
(2)手动调试内核模块
在调试器中启动内核,并用insmod或modprobe装载内核模块。在gdb中执行下面命令:
(UML gdb) p module_list
这是已装载进内核的内核模块列表,通常用户期望的内核模块在module_lis中。如果不在,就进入下一个链接,查看name域,直到找到用户调试的内核模块。获取该结构的地址,并加上module.size_of_struct值,gdb可帮助获取该值,方法如下:
(UML gdb) printf "%#x\n", (int)module_listmodule_list->size_of_struct
从内核模块开始处的偏移偶尔会改变,因此,应检查init和cleanup的地址,方法如下:
(UML gdb) add-symbol-file /path/to/module/on/hostthat_address
如果断点不在正确的位置或不工作等 ,用户可以查看内核模块结构,init和cleanup域应该类似如下:
init = 0x588066b0 <init_hostfs>,cleanup = 0x588066c0<exit_hostfs>
如果名字正确,但它们有偏移,那么,用户应该将偏移加到add-symbol-file所在地址上。
当用户想装载内核模块的新版本时,需要让gdb删除旧内核模块的所有符号。方法如下:
(UML gdb) symbol-file
接着,从内核二进制重装载符号,方法如下:
(UML gdb) symbol-file /path/to/kernel
然后,重复上面的装载符号过程。还需要重打开断点。
(5)粘附gdb到内核
如果用户还没有在gdb下运行内核,用户可以通过给跟踪线程发送一个SIGUSR1,用于以后粘附gdb到内核。控制台第一行的输出鉴别它的id,显示类似如下:
tracing thread pid = 20093
发送信号的方法如下:
host% kill -USR1 20093
上述命令运行后,用户将可看见带有gdb运行的xterm。
如果用户已将mconsole(UML的控制台)编译进UML,那么可用mconsole客户端启动gdb,方法如下:
(mconsole) (mconsole) config gdb=xterm
上述命令运行后,用户将可看见带有gdb运行的xterm。
(6)使用可替换的调试器
UML支持粘附到一个已运行的调试器,而不是启动gdb本身。当gdb是一些UI的子进程(如:emacs或ddd)时,这将是有用的。它还被用于在UML上运行非gdb的调试器。下面是一个使用strace作为可替代调试器的例子。
用户需要得到调试器的pid,并将pid用"gdb-pid=<pid>"选项与"debug"选项一起传递。
如果用户在UI下使用gdb,那么,应告诉UML"att 1",那么,UI将粘附到UML。
下面以替换调试器strace为例,用户可以用strace调试实际的内核,方法如下:
在shell中运行下述命令
host%
sh -c 'echo pid=$$; echo -n hit return; read x; exec strace -p 1-o strace.out'
用"debug"和"gdb-pid=<pid>"运行UML。
strace输出将出现在输出文件中。
注意:运行下面的命令,结果不同于前面命令。
host% strace ./linux
上述命令将仅strace主UML线程,跟踪的线程不做任何实际的内核操作。它仅标识出虚拟机。而使用上述的strce将显示虚拟机低层的活动情况。
在代码里面老能看到 BUG_ON() , WARN_ON() 这样的宏 , 类似 我们日常编程里面的断言(assert)。
在include/asm-generic/bug.h
#ifdef CONFIG_BUG #ifdef CONFIG_GENERIC_BUG#ifndef __ASSEMBLY__struct bug_entry { unsigned long bug_addr;#ifdef CONFIG_DEBUG_BUGVERBOSE const char *file; unsigned short line;#endif unsigned short flags;};#endif #define BUGFLAG_WARNING (1<<0)#endif #ifndef HAVE_ARCH_BUG#define BUG() do { printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __FUNCTION__); panic("BUG!"); } while (0)#endif #ifndef HAVE_ARCH_BUG_ON#define BUG_ON(condition) do { if (unlikely(condition? BUG(); } while(0)#endif #ifndef __WARN#ifndef __ASSEMBLY__extern void warn_on_slowpath(const char *file, const int line);#define WANT_WARN_ON_SLOWPATH#endif#define __WARN() warn_on_slowpath(__FILE__, __LINE__)#endif #ifndef WARN_ON#define WARN_ON(condition) ({ int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on? __WARN(); unlikely(__ret_warn_on); })#endif #else #ifndef HAVE_ARCH_BUG#define BUG()#endif #ifndef HAVE_ARCH_BUG_ON#define BUG_ON(condition) do { if (condition) ; } while(0)#endif #ifndef HAVE_ARCH_WARN_ON#define WARN_ON(condition) ({ int __ret_warn_on = !!(condition); unlikely(__ret_warn_on); })#endif#endif #define WARN_ON_ONCE(condition) ({ static int __warned; int __ret_warn_once = !!(condition); if (unlikely(__ret_warn_once? if (WARN_ON(!__warned? __warned = 1; unlikely(__ret_warn_once); }) #ifdef CONFIG_SMP# define WARN_ON_SMP(x) WARN_ON(x)#else# define WARN_ON_SMP(x) do { } while (0)#endif
锁验证器
内核锁验证器(Kernel lockvalidator)可以在死锁发生前检测到死锁,即使是很少发生的死锁。它将每个自旋锁与一个键值相关,相似的锁仅处理一次。加锁时,查看所有已获取的锁,并确信在其他上下文中没有已获取的锁,在新获取锁之后被获取。解锁时,确信正被解开的锁在已获取锁的顶部。
.
Validate spinlocks vs interrupts behavior.
当加锁动态发生时,锁验证器映射所有加锁规则,该检测由内核的spinlocks、rwlocks、mutexes和rwsems等锁机制触发。不管何时锁合法性检测器子系统检测到一个新加锁场景,它检查新规则是否违反正存在的规则集,如果新规则与正存在的规则集一致,则加入新规则,内核正常运行。如果新规则可能创建一个死锁场景,那么这种创建死锁的条件会被打印出来。
当判断加锁的有效性时,所有可能的"死锁场景"会被考虑到:假定任意数量的CPU、任意的中断上下文和任务上下文群、运行所有正存在的加锁场景的任意组合。在一个典型系统中,这意味着有成千上万个独立的场景。这就是为什么称它为"加锁正确性"验证器,对于所有被观察的规则来说,锁验证器用数学的确定性证明死锁不可能发生,假定锁验证器实现本身正确,并且它内部的数据结构不会被其他内核子系统弄坏。
还有,验证器的属性"所有可能的场景"也使查找变得复杂,特别是多CPU、多上下文竞争比单个上下文规则复杂得多,
为了增加验证器的效率,不是将每个锁实例进行映射,而是映射每个锁类型。例如:内核中所有的结构inode对象有inode->inotify_mutex,如果缓存了10000个inode,将会有10000个锁对象。但->inotify_mutex是单个锁类型,所有->inotify_mutex发生的加锁活动都归入单个锁类型。
Lock-class
验证器操作的基本对象是锁类Lock-class,一个锁类是一组锁,逻辑上有同样的加锁规则,尽管锁可能有多个实例。例如:在结构inode中的一个锁是一个类,而每个节点有它自己的锁类实例。
验证器跟踪锁类的状态和不同锁类之间的依赖性。验证器维护一个有关状态和依赖性是否正确的滚动证据。
不像一个锁实例,锁类lock-class它本身从不消失:当lock-class注册使用后,所有随后锁类的使用都会被附加到该lock-class上。
联系客服