Linux内核调试方法总结
Linux内核调试方法总结
- 一 调试前的准备
- 二 内核中的bug
- 三 内核调试配置选项
- 1 内核配置
- 2 调试原子操作
- 四 引发bug并打印信息
- 1 BUG()和BUG_ON()
- 2 dump_stack()
- 五 printk()
- 1 printk函数的健壮性
- 2 printk函数脆弱之处
- 3 LOG等级
- 4 记录缓冲区
- 5 syslogd/klogd
- 6 dmesg
- 7 注意
- 8 内核printk和日志系统的总体结构
- 9 动态调试
- 六 内存调试工具
- 1 MEMWATCH
- 2 YAMD
- 3 Electric Fence
- 七 strace
- 八 OOPS
- 1 ksymoops
- 2 kallsyms
- 3 Kdump
- 九 KGDB
- 1 kgdb的调试原理
- 2 Kgdb的安装与设置
- 3 在VMware中搭建调试环境
- 4 kgdb的一些特点和不足
- 十 使用SkyEye构建Linux内核调试环境
- 1 SkyEye的安装和μcLinux内核编译
- 2 使用SkyEye调试
- 3 使用SkyEye调试内核的特点和不足
- 十一 KDB
- 1 入门
- 2 初始化并设置环境变量
- 3 激活 KDB
- 4 KDB 命令
- 5 技巧和诀窍
- 6 结束语
- 十二 Kprobes
- 1 安装
- 2 编写 Kprobes 模块
- 3 使用 Kprobes 更好地进行调试
Linux内核调试方法
kdb:只能在汇编代码级进行调试;
优点是不需要两台机器进行调试。
gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。
kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)
printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用,可以直接把所关心的信息打打印到屏幕上,从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器(Linux kernel debugger,kdb)是 Linux 内核的补丁,它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍,大家可以参考。 Kprobes 提供了一个强行进入任何内核例程,并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息,而无需对Linux内核频繁编译和启动,具体使用方法,请参考使用 Kprobes 调试内核。
/proc文件系统
在 /proc 文件系统中,对虚拟文件的读写操作是一种与内核通信的手段,要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。清单 6 给出了 dmesg 显示的最后几条消息。
清单 6. 查看来自 LKM 的内核输出
[root@plato]# dmesg | tail -5
cs: IO port probe 0xa00-0xaff: clean.
eth0: Link is down
eth0: Link is up, running at 100Mbit half-duplex
my_module_init called. Module is now loaded.
my_module_cleanup called. Module is now unloaded.
可以在内核输出中看到这个模块的消息。现在让我们暂时离开这个简单的例子,来看几个可以用来开发有用 LKM 的内核 API。
调试工具
使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。在内核中使用交互式调试器是一个很复杂的问题。内核在它自己的地址空间中运行。许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。
目录 |
内核bug跟踪
oops消息分析
(1)oops消息产生机制
oops(也称 panic),称程序运行崩溃,程序崩溃后会产生oops消息。应用程序或内核线程的崩溃都会产生oops消息,通常发生oops时,系统不会发生死机,而在终端或日志中打印oops信息。
当使用NULL指针或不正确的指针值时,通常会引发一个 oops 消息,这是因为当引用一个非法指针时,页面映射机制无法将虚拟地址映像到物理地址,处理器就会向操作系统发出一个"页面失效"的信号。内核无法"换页"到并不存在的地址上,系统就会产生一个"oops"。
oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的printk 语句产生。较为重要的信息就是指令指针(EIP),即出错指令的地址。
由于很难从十六进制数值中看出含义,可使用符号解析工具klogd。klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。klogd在缺省情况下运行并进行符号解码。
通常Oops文本由klogd从内核缓冲区里读取并传给syslogd,由syslogd写到syslog文件中,该文件典型为/var/log/messages(依赖于/etc/syslog.conf)。如果klogd崩溃了,用户可"dmesg > file"从内核缓冲区中读取数据并保存下来。还可用"cat /proc/kmsg > file"读取数据,此时,需要用户中止传输,因为kmsg是一个"永不结束的文件"。
当保护错误发生时,klogd守护进程自动把内核日志信息中的重要地址翻译成它们相应的符号。klogd执行静态地址翻译和动态地址翻译。静态地址翻译使用System.map文件将符号地址翻译为符号。klogd守护进程在初始化时必须能找到system.map文件。
动态地址翻译通常对内核模块中的符号进行翻译。内核模块的内存从内核动态内存池里分配,内核模块中符号的位置在内核装载后才最终确定。
Linux内核提供了调用,允许程序决定装载哪些模块和它们在内存中位置。通过这些系统调用,klogd守护进程生成一张符号表用于调试发生在可装载模块中的保护错误。内核模块的装载或者卸载都会自动向klogd发送信号,klogd可将内核模块符号的地址动态翻译为符号字符串。
(2)产生oops的样例代码
使用空指针和缓冲区溢出是产生oops的两个最常见原因。下面两个函数faulty_write和faulty_read是一个内核模块中的写和读函数,分别演示了这两种情况。当内核调用这两个函数时,会产生oops消息。
函数faulty_write删除一个NULL指针的引用,由于0不是一个有效的指针值,内核将打印oops信息,并接着,杀死调用些函数的进程。ssize_t faulty_write (struct file *filp, const char _ _user *buf, size_t count, loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
函数faulty_write产生oops信息列出如下(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
00000000
printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Process cat (pid: 23413, stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
00000001 0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 0804d038 00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 00000004
Call Trace: [sys_write+214/256] [system_call+52/56]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
上述oops消息中,字符串 3/576 表示处理器正处于函数的第3个字节上,函数整体长度为 576 个字节。 函数faulty_read拷贝一个字符串到本地变量,由于字符串比目的地数组长造成缓冲区溢出。当函数返回时,缓冲区溢出导致产生oops信息。因为返回指令引起指令指针找不到运行地址,这种错误很难发现和跟踪。
ssize_t faulty_read(struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
int ret;
char stack_buf[4];
/* Let's try a buffer overflow */
memset(stack_buf, 0xff, 20);
if (count > 4)
count = 4;
/* copy 4 bytes to the user */
ret = copy_to_user(buf, stack_buf, count);
if (!ret)
return count;
return ret;
}
函数faulty_read产生oops信息列出如下:
EIP: 0010:[<00000000>]
Unable to handle kernel paging request at virtual address ffffffff printing eip: ffffffff Oops: 0000 [#5] SMP CPU: 0 EIP: 0060:[] Not tainted EFLAGS: 00010296 (2.6.6) EIP is at 0xffffffff eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78 ds: 007b es: 007b ss: 0068 Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace: [] sys_read+0x42/0x70 [] syscall_call+0x7/0xb
Code: Bad EIP value.
在上述oops消息中,由于缓冲区溢出,仅能看到函数调用栈的一部分,看不见函数名vfs_read和faulty_read,并且代码(Code)处仅输出"bad EIP value.",列在栈上开始处的地址"ffffffff"表示内核栈已崩溃。
(3)oops信息分析
面对产生的oops信息,首先应查找源程序发生oops的位置,通过查看指令指令寄存器EIP的值,可以找到位置,如:EIP: 0010:[faulty:faulty_write+3/576]。
再查找函数调用栈(call stack)可以得到更多的信息。从函数调用栈可辨别出局部变量、全局变量和函数参数。例如:在函数faulty_read的oops信息的函数调用栈中,栈顶为ffffffff,栈顶值应为一个小于ffffffff的值,为此值,说明再找不回调用函数地址,说明有可能因缓冲区溢出等原因造成指针错误。
在x86构架上,用户空间的栈从0xc0000000以下开始,递归值bfffda70可能是用户空间的栈地址。实际上它就是传递给read系统调用的缓冲区地址,系统调用read进入内核时,将用户空间缓冲区的数据拷贝到内核空间缓冲区。
如果oops信息显示触发oops的地址为0xa5a5a5a5,则说明很可能是因为没有初始化动态内存引起的。
另外,如果想看到函数调用栈的符号,编译内核时,请打开CONFIG_KALLSYMS选项。
klogd 提供了许多信息来帮助分析。为了使 klogd 正确地工作,必须在 /boot 中提供符号表文件 System.map。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。
有时内核错误会将系统完全挂起。例如代码进入一个死循环,系统不会再响应任何动作。这时可通过在一些关键点上插入 schedule 调用可以防止死循环。
系统崩溃重启动
由于内核运行错误,在某些极端情况下,内核会运行崩溃,内核崩溃时会导致死机。为了解决此问题,内核引入了快速装载和重启动新内核机制。内核通过kdump在崩溃时触发启动新内核,存储旧内存映像以便于调试,让系统在新内核上运行 ,从而避免了死机,增强了系统的稳定性。
(1)工具kexec介绍
kexec是一套系统调用,允许用户从当前正执行的内核装载另一个内核。用户可用shell命令"yum install kexec-tools"安装kexec工具包,安装后,就可以使用kexec命令。
工具kexec直接启动进入一个新内核,它通过系统调用使用户能够从当前内核装载并启动进入另一个内核。在当前内核中,kexec执行BootLoader的功能。在标准系统启动和kexec启动之间的主要区别是:在kexec启动期间,依赖于硬件构架的固件或BIOS不会被执行来进行硬件初始化。这将大大降低重启动的时间。
为了让内核的kexec功能起作用,内核编译配置是应确认先择了"CONFIG_KEXEC=y",在配置后生成的.config文件中应可看到此条目。
工具kexec的使用分为两步,首先,用kexec将调试的内核装载进内存,接着,用kexec启动装载的内核。
装载内核的语法列出如下:
kexec -l kernel-image --append=command-line-options --initrd=initrd-image
上述命令中,参数kernel-image为装载内核的映射文件,该命令不支持压缩的内核映像文件bzImage,应使用非压缩的内核映射文件vmlinux;参数initrd-image为启动时使用initrd映射文件;参数command-line-options为命令行选项,应来自当前内核的命令行选项,可从文件"/proc/cmdline"中提取,该文件的内容列出如下:
^-^$ cat /proc/cmdline
ro root=/dev/VolGroup00/LogVol00 rhgb quiet
例如:用户想启动的内核映射为/boot/vmlinux,initrd为/boot/initrd,则kexec加载命令列出如下:
Kexec –l /boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd
还可以加上选项-p或--load-panic,表示装载新内核在系统内核崩溃使用。
在内核装载后,用下述命令启动装载的内核,并进行新的内核中运行:
kexec -e
当kexec将当前内核迁移到新内核上运行时,kexec拷贝新内核到预保留内存块,该保留位置如图1所示, 原系统内核给kexec装载内核预保留一块内存(在图中的阴影部分),用于装载新内核,其他内存区域在未装载新内核时,由原系统内核使用。
在x86构架的机器上,系统启动时需要使用第一个640KB物理内存,用于内核装载,kexec在重启动进入转储捕捉的内核之前备份此区域。相似地,PPC64构架的机器在启动里需要使用第一个32KB物理内核,并需要支持64K页,kexec备份第一个64KB内存。
(2)kdump介绍
kdump是基于kexec的崩溃转储机制(kexec-based Crash Dumping),无论内核内核需要转储时,如:系统崩溃时,kdump使用kexec快速启动进入转储捕捉的内核。在这里,原运行的内核称为系统内核或原内核,新装载运行的内核称为转储捕捉的内核或装载内核或新内核。
在重启动过程中,原内核的内存映像被保存下来,并且转储捕捉的内核(新装载的内核)可以访问转储的映像。用户可以使用命令cp和scp将内存映射拷贝到一个本地硬盘上的转储文件或通过网络拷贝到远程计算机上。
当前仅x86, x86_64, ppc64和ia64构架支持kdump和kexec。
当系统内核启动时,它保留小部分内存给转储(dump)捕捉的内核,确保了来自系统内核正进行的直接内存访问(Direct Memory Access:DMA)不会破坏转储捕捉的内核。命令kexec –p装载新内核到这个保留的内存。
在崩溃前,所有系统内核的核心映像编码为ELF格式,并存储在内核的保留区域。ELF头的开始物理地址通过参数elfcorehdr=boot传递到转储捕捉的内核。
通过使用转储捕捉的内核,用户可以下面两种方式访问内存映像或旧内存:
(1)通过/dev/oldmem设备接口,捕捉工具程序能读取设备文件并以原始流的格式写出内存,它是一个内存原始流的转储。分析和捕捉工具必须足够智能以判断查找正确信息的位置。
(2)通过/proc/vmcore,能以ELF格式文件输出转储信息,用户可以用GDB(GNU Debugger)和崩溃调试工具等分析工具调试转储文件。
(3)建立快速重启动机制和安装工具
1)安装工具kexec-tools
可以下载源代码编译安装工具kexec-tools。由于工具kexec-tools还依赖于一些其他的库,因此,最好的方法是使用命令"yum install kexec-tools"从网上下载安装并自动解决依赖关系。
2)编译系统和转储捕捉的内核
可编译独立的转储捕捉内核用于捕捉内核的转储,还可以使用原系统内核作为转储捕捉内核,在这种情况下,不需要再编译独立的转储捕捉内核,但仅支持重定位内核的构架才可以用作转储捕捉的内核,如:构架i386和ia64支持重定位内核。
对于系统和转储捕捉内核来说,为了打开kdump支持,内核需要设置一些特殊的配置选项,下面分别对系统内核和转储捕捉内核的配置选项进行说明:
系统内核的配置选项说明如下:
- 在菜单条目"Processor type and features."中打开选项"kexec system call",使内核编译安装kexe系统调用。配置文件.config生成语句"CONFIG_KEXEC=y"。
- 在菜单条目"Filesystem"->"Pseudo filesystems."中打开选项"sysfs file system support",使内核编译安装文件系统sysfs.配置文件.config生成语句"CONFIG_SYSFS=y"。
- 在菜单条目"Kernel hacking."中打开选项"Compile the kernel with debug info ",使内核编译安装后支持调试信息输出,产生调试符号用于分析转储文件。配置文件.config生成语句"CONFIG_DEBUG_INFO=Y"。
转储捕捉内核配置选项(不依赖于处理器构架)说明如下:
- 在菜单条目"Processor type and features"中打开选项"kernel crash dumps",配置文件.config生成语句" CONFIG_CRASH_DUMP=y"。
- 在菜单条目"Filesystems"->"Pseudo filesystems"中打开选项"/proc/vmcore support",配置文件.config生成语句"CONFIG_PROC_VMCORE=y"。
转储捕捉内核配置选项(依赖于处理器构架i386和x86_64)说明如下:
- 在处理器构架i386上,在菜单条目"Processor type and features"中打开高端内存支持,配置文件.config生成语句"CONFIG_HIGHMEM64G=y"或"CONFIG_HIGHMEM4G"。
- 在处理器构架i386和x86_64上,在菜单条目"rocessor type and features"中关闭对称多处理器支持,配置文件.config生成语句"CONFIG_SMP=n"。如果配置文件中的设置为"CONFIG_SMP=y",则可在装载转储捕捉内核的内核命令行上指定"maxcpus=1"。
- 如果想构建和使用可重定位内核,在菜单条目"rocessor type and featuresIf"中打开选项"Build a relocatable kernel",配置文件.config生成语句"CONFIG_RELOCATABLE=y"。
- 在菜单"Processor type and features"下的条目"Physical address where the kernel is loaded"设置合适的值用于内核装载的物理地址。它仅在打开了"kernel crash dumps"时出现。合适的值依赖于内核是否可重定位。
如果设置了值"CONFIG_PHYSICAL_START=0x100000",则表示使用可重定位内核。它将编译内核在物理地址1MB处,内核是可重定位的,因此,内核可从任何物理地址运行。Kexec BootLoader将装载内核到用于转储捕捉内核的内核保留区域。
否则,将使用启动参数"crashkernel=Y@X"指定第二个内核保留内核区域的开始地址,其中,Y表示内存区域的大小,X表示保留给转储捕捉内核的内存区域的开始地址,通过X为16MB (0x1000000),因此用户可设置"CONFIG_PHYSICAL_START=0x1000000"。
在配置完内核后,编译和安装内核及内核模块。
3)扩展的crashkernel语法
在系统内核的启动命令行选项中,通常语法"crashkernel=size[@offset]"对于大多数据配置已够用了,但有时候保留的内存依赖于系统RAM。此时可通过扩展的crashkernel命令行对内存进行 限制避免从机器上移去一部分内核后造成系统不可启动。扩展的crashkernel语法列出如下:
crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]
其中,range=start-[end]。
例如:crashkernel=512M-2G:64M,2G-:128M,含义为:如果内存小于512M,不设置保留内存,如果内存为512M到2G之间,设置保留内存区域为64M,如果内存大于128M,设置保留内存区域为128M。
4)启动进入系统内核
必要时更新BootLoader。然后用参数"crashkernel=Y@X"启动系统内核,如:crashkernel=64M@16M,表示告诉系统内核保留从物理地址0x01000000 (16MB)开始的64MB大小给转储捕捉内核使用。通常x86和x86_64平台设置"crashkernel=64M@16M",ppc64平台设置"crashkernel=128M@32M"。
5)装载转储捕捉内核
在启动进入系统内核后,需要装载转储捕捉内核。根据处理器构架和映射文件的类型(可否重定位),可以选择装载不压缩的vmlinux或压缩的bzImage/vmlinuz内核映像。选择方法说明如下:
对于i386和x86_64平台:
- 如果内核不是可重定位的,使用vmlinux。
- 如果内核是可重定位的,使用bzImage/vmlinuz。
对于ppc64平台:
- 使用vmlinux。
对于ia64平台:
- 使用vmlinux或vmlinuz.gz。
kexec -p <dump-capture-kernel-vmlinux-image> \
--initrd=<initrd-for-dump-capture-kernel> --args-linux \
--append="root=<root-dev> <arch-specific-options>"
如果用户使用压缩的bzImage/vmlinuz映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-bzImage>\
--initrd=<initrd-for-dump-capture-kernel> \
--append="root=<root-dev> <arch-specific-options>"
注意:参数--args-linux在ia64平台中不用指定。
下面是在装载转储捕捉内核时使用的构架特定命令行选项:
- 对于i386, x86_64和ia64平台,选项为"1 irqpoll maxcpus=1 reset_devices"。
- 对于ppc64平台,选项为"1 maxcpus=1 noirqdistrib reset_devices"。
在装载转储捕捉内核时需要注意的事项说明如下:
- 缺省设置下,ELF头以ELF64格式存储,以支持多于4GB内核的系统,在i386上,kexec自动检查物理RAM尺寸是否超过4GB限制,如果没有超过,使用ELF32。因此,在非PAE系统上ELF头总是使用ELF32格式。
- 选项--elf32-core-headers可用于强制产生ELF32头,这是必要的,因为在32位系统上,GDB当前不能打开带有ELF64头的vmcore文件。
- 在转储捕捉内核中,启动参数irqpoll减少了由于共享中断引起的驱动程序初始化失败。
- 用户必须以命令mount输出的根设备名的格式指定<root-dev>。
- 启动参数"1"将转储捕捉内核启动进入不支持网络的单用户模式。如果用户想使用网络,需要设置为3。
- 通常不必让转储捕捉内核以SMP方式运行。因此,通常编译一个单CPU转储捕捉内核或装载转储捕捉内核时指定选项"maxcpus=1"。
6)内核崩溃时触发内核启动
在装载转储捕捉内核后,如果系统发生崩溃(Kernel Panic),系统将重启动进入转储捕捉内核。重启动的触发点在函数die(), die_nmi()和sysrq处理例程(按ALT-SysRq-c组合键)。
下面条件将执行一个崩溃触发点:
- 如果检测到硬件锁住,并且配置了"NMI watchdog",系统将调用函数die_nmi()启动进入转储捕捉内核。
- 如果调用了函数die(),并且该线程的pid为0或1,或者在中断上下文中调用die(),或者设置了panic_on_oops并调用了die(),系统将启动进入转储捕捉内核。
- 在powerpc系统,当一个软复位产生时,所有的CPU调用die(),并且系统将启动进入转储捕捉内核。
- 为了测试目的,用户可以使用"ALT-SysRq-c","echo c > /proc/sysrq-trigger"触发一个崩溃,或者写一个内核模块强制内核崩溃。
7)写出转储文件
在转储捕捉内核启动后,可用下面的命令写出转储文件:
cp /proc/vmcore <dump-file>
用户还可以将转储内存作为设备/dev/oldmem以线性原始流视图进行访问,使用下面的命令创建该设备:
mknod /dev/oldmem c 1 12
使用命令dd拷贝转储内存的特定部分,拷贝整个内存的命令列出如下:
dd if=/dev/oldmem of=oldmem.001
8)转储文件分析
在分析转储映像之前,用户应重启动进入一个稳定的内核。用户可以用GDB对拷贝出的转储进行有限分析。编译vmlinux时应加上-g选项,才能生成调试用的符号,然后,用下面的命令调试vmlinux:
gdb vmlinux <dump-file>
SysRq魔术组合键打印内核信息
SysRq"魔术组合键"是一组按键,由键盘上的"Alt+SysRq+[CommandKey]"三个键组成,其中CommandKey为可选的按键。SysRq魔术组合键根据组合键的不同,可提供控制内核或打印内核信息的功能。SysRq魔术组合键的功能说明如表1所示。
键名 | 功能说明 |
b | 在没有同步或卸载硬盘的情况下立即启动。 |
c | 为了获取崩溃转储执行kexe重启动。 |
d | 显示被持的所有锁。 |
e | 发送信号SIGTERM给所有进程,除了init外。 |
f | 将调用oom_kill杀死内存热进程。 |
g | 在平台ppc和sh上被kgdb使用。 |
h | 显示帮助信息。 |
i | 发送信号SIGKILL给所有的进程,除了init外。 |
k | 安全访问密钥(Secure Access Key,SAK)杀死在当前虚拟终端上的所有程序。 |
m | 转储当前的内存信息到控制台。 |
n | 用于设置实时任务为可调整nice的。 |
o | 将关闭系统(如果配置为支持)。 |
p | 打印当前寄存器和标识到控制台。 |
q | 将转储所有正运行定时器的列表。 |
r | 关闭键盘Raw模式并设置为XLATE模式。 |
s | 尝试同步所有挂接的文件系统。 |
t | 将转储当前的任务列表和它们的信息到控制台。 |
u | 尝试以仅读的方式重挂接所有已挂接的文件系统。 |
v | 转储Voyager SMP处理器信息到控制台。 |
w | 转储的所有非可中断(已阻塞)状态的任务。 |
x | 在平台ppc/powerpc上被xmon(X监视器)接口使用。 |
0~9 | 设备控制台日志级别,控制将打印到控制台的内核信息。例如:0仅打印紧急信息,如:PANIC和OOPS信息。 |
默认SysRq组合键是关闭的。可用下面的命令打开此功能:
# echo 1 > /proc/sys/kernel/sysrq
关闭此功能的命令列出如下:
# echo 0 > /proc/sys/kernel/sysrq
如果想让此功能总是起作用,可在/etc/sysctl.conf文件中设置kernel.sysrq值为1。 系统重新启动以后,此功能将会自动打开。
打开SysRq组合键功能后,有终端访问权限的用户就可以自用它打印内核信息了。
注意:SysRq组合键在X windows上是无法使用的。必须先要切换到文本虚拟终端下。如果在图形界面,可以按Ctrl+Alt+F1切换到虚拟终端。在串口终端上,需要先在终端上发送Break信号,然后在5秒内输入sysrq组合键。如果用户有root权限,可把commandkey字符写入到/proc/sysrq-trigger文件,触发一个内核信息打印,打印的信息存放在/var/log/messages中。下面是一个命令样例:^-^$ echo 't' > sysrq-trigger
^-^vim /var/log/messages
Oct 29 17:51:43 njllinux kernel: SysRq : Show State
Oct 29 17:51:43 njllinux kernel: task PC stack pid father
Oct 29 17:51:43 njllinux kernel: init S ffffffff812b76a0 0 1 0
Oct 29 17:51:43 njllinux kernel: ffff81013fa97998 0000000000000082 0000000000000000 ffff81013fa9795c
Oct 29 17:51:43 njllinux kernel: 000000003fa97978 ffffffff81583700 ffffffff81583700 ffff81013fa98000
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff81013fa98350 000000003c352a50 ffff81013fa98350
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: 000300000004 ffff8101333cb090
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040c2e>] sys_pause+0x19/0x22
Oct 29 17:51:43 njllinux kernel: [<ffffffff8100c291>] tracesys+0xd0/0xd5
Oct 29 17:51:43 njllinux kernel:
Oct 29 17:51:43 njllinux kernel: lighttpd S ffffffff812b76a0 0 3365 1
Oct 29 17:51:43 njllinux kernel: ffff810132d49b18 0000000000000082 0000000000000000 ffff810132d49adc
Oct 29 17:51:43 njllinux kernel: ffff81013fb2d148 ffffffff81583700 ffffffff81583700 ffff8101354896a0
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff8101354899f0 0000000032d49ac8 ffff8101354899f0
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040722>] ? __mod_timer+0xbb/0xcd
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2ee>] schedule_timeout+0x8d/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040100>] ? process_timeout+0x0/0xb
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2e9>] ? schedule_timeout+0x88/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff810b9498>] do_sys_poll+0x2a8/0x370
……
命令strace
命令strace 显示程序调用的所有系统调用。使用 strace 工具,用户可以清楚地看到这些调用过程及其使用的参数,了解它们与操作系统之间的底层交互。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。
starce 的另一个用处是解决和动态库相关的问题。当对一个可执行文件运行ldd时,它会告诉你程序使用的动态库和找到动态库的位置
strace命令行选项说明如表1。常用的选项为-t, -T, -e, -o等。
选项 | 说明 |
-c | 统计每个系统调用执行的时间、次数和出错的次数等。 |
-d | 输出一些strace自身的调试信息到标准输出。 |
-f | 跟踪当前进程由系统调用fork产生的子进程。 |
-ff | 如果使用选项-o filename,则将跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。 |
-F | 尝试跟踪vfork调用.在-f时,vfork不被跟踪。 |
-h | 输出简要的帮助信息。 |
-i | 在系统调用的时候打印指令指针。 |
-q | 禁止输出关于粘附和脱离的信息,发生在输出重定向到文件且直接而不是粘附运行命令时。 |
-r | 依赖于每个系统调用的入口打印相对时间戳。 |
-t | 在输出中的每一行前加上时间信息。 |
-tt | 在输出中的每一行前加上时间信息,包括毫秒。 |
-ttt | 毫秒级输出,以秒表示时间。 |
-T | 显示系统调用所花费的时间。 |
-v | 输出所有的系统调用的信息。一些关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出。 |
-V | 输出strace的版本信息。 |
-x | 以十六进制形式输出非ASCII标准字符串。 |
-xx | 所有字符串以十六进制形式输出。 |
-a column | 以特定的列数对齐返回值,缺省值为40。 |
-e expr | 指定一个表达式,用来控制如何跟踪.格式如下: [qualifier=][!]value1[,value2]... qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一。value是用来限定的符号或数字。默认的qualifier是 trace。感叹号是否定符号。 |
-eopen | 等价于 -e trace=open,表示只跟踪open调用。而-etrace!=open表示跟踪除了open以外的其他调用。 |
-e trace=set | 只跟踪指定的系统调用。例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用。默认的为set=all。 |
-e trace=file | 只跟踪文件名作为参数的系统调用,一般为文件操作。 |
-e trace=process | 只跟踪有关进程控制的系统调用。 |
-e trace=network | 只跟踪与网络有关的所有系统调用。 |
-e strace=signal | 跟踪所有与系统信号有关的系统调用。 |
-e trace=ipc | 跟踪所有与进程间通信有关的系统调用。 |
-o filename | 将strace的输出写入文件filename。 |
-p pid | 跟踪指定的进程pid。 |
-s strsize | 指定最大字符串打印长度,默认值为32。 |
-u username | 以username的UID和GID执行命令。 |
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0
uname({sys="Linux", node="sammy", ...}) = 0
brk(0) = 0x804c000
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001...
fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0
old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3) = 0
open("/lib/tls/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\360U\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0
用函数printk打印内核信息
Linux内核用函数printk打印调试信息,该函数的用法与C库打印函数printf格式类似,但在内核使用。用户可在内核代码中的某位置加入函数printk,直接把所关心的信息打打印到屏幕上或日志文件中。
函数printk根据日志级别(loglevel)对调试信息进行分类。日志级别用宏定义,展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此函数printk中的日志级别和格式字符串间不能有逗号。
下面两个 printk 的例子,一个是调试信息,一个是临界信息:printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
样例:在用户空间或内核中开启及关闭打印调试消息 用户还可以在内核或用户空间应用程序定义统一的函数打印调试信息,可在Makefile文件中打开或关闭调试函数。定义方法列出如下:
/*debug_on_off.h*/
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
#ifdef _ _KERNEL_ _
/* This one if debugging is on, and kernel space */
#define PDEBUG(fmt,args...) printk(KERN_DEBUG "scull: " fmt, ## args)
#else
/* This one for user space */
#define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
#endif
#else
#define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif
在文件Makefile加上下面几行:
# Comment/uncomment the following line to disable/enable debugging
DEBUG = y
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O"
else
DEBFLAGS = -O2
endif
CFLAGS += $(DEBFLAGS)
更改makefile中的DEBUG值,需要调试信息时,DEBUG = y,不需要时,DEBUG赋其它值。再用make编译即可。
内核探测kprobe
kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具,如:收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。工作机制是:用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。
kprobe允许用户编写内核模块添加调试信息到内核。当在远程机器上调试有bug的程序而日志/var/log/messages不能看出错误时,kprobe显得非常有用。用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以输出所需要的调试信息了。
内核探测分为kprobe, jprobe和kretprobe(也称return probe,返回探测)三种。kprobe可插入内核中任何指令处;jprobe插入内核函数入口,方便于访问函数的参数;return probe用于探测指定函数的返回值。
内核模块的初始化函数init安装(或注册)了多个探测函数,内核模块的退出函数exit将注销它们。注册函数(如:register_kprobe())指定了探测器插入的地方、探测点触发的处理例程。
(1)配置支持kprobe的内核
配置内核时确信在.config文件中设置了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。
配置了CONFIG_KALLSYMS_ALL,kprobe可用函数kallsyms_lookup_name从地址解析代码。配置了CONFIG_DEBUG_INFO后,可以用命令"objdump -d -l vmlinux"查看源到对象的代码映射。
调试文件系统debugfs含有kprobe的调试接口,可以查看注册的kprobe列表,还可以关闭/打开kprobe。
查看系统注册probe的方法列出如下:
#cat /debug/kprobes/list
c015d71a k vfs_read+0x0
c011a316 j do_fork+0x0
c03dedc5 r tcp_v4_rcv+0x0
第一列表示探测点插入的内核地址,第二列表示内核探测的类型,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探测点的"符号+偏移"。如果被探测的函数属于一个模块,模块名也被指定。
打开和关闭kprobe的方法列出如下:
#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled
(2)kprobe样例
Linux内核源代码在目录samples/kpobges下提供了各种kprobe类型的探测处理例程编写样例,分别对应文件kprobe_example.c、jprobe_example.c和kretprobe_example.c,用户稍加修改就可以变成自己的内核探测模块。下面仅说明kprobe类型的探测例程。
样例kprobe_example是kprobe类型的探测例程内核模块,显示了在函数do_fork被调用时如何使用kprobe转储栈和选择的寄存器。当内核函数do_fork被调用创建一个新进程时,在控制台和/var/log/messages中将显示函数printk打印的跟踪数据。样例kprobe_example列出如下(在samples/kprobe_example.c中):
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
/* 对于每个探测,用户需要分配一个kprobe对象*/
static struct kprobe kp = {
.symbol_name = "do_fork",
};
/* 在被探测指令执行前,将调用预处理例程 pre_handler,用户需要定义该例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
" flags = 0x%lx\n",
p->addr, regs->ip, regs->flags); /*打印地址、指令和标识*/
#endif
#ifdef CONFIG_PPC
printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
" msr = 0x%lx\n",
p->addr, regs->nip, regs->msr);
#endif
/* 在这里可以调用内核接口函数dump_stack打印出栈的内容*/
return 0;
}
/* 在被探测指令执行后,kprobe调用后处理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
#ifdef CONFIG_X86
printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
p->addr, regs->msr);
#endif
}
/*在pre-handler或post-handler中的任何指令或者kprobe单步执行的被探测指令产生了例外时,会调用fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
p->addr, trapnr);
/* 不处理错误时应该返回*/
return 0;
}
/*初始化内核模块*/
static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;
ret = register_kprobe(&kp); /*注册kprobe*/
if (ret < 0) {
printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
return ret;
}
printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
Systemtap调试
(1)Systemtap原理
Systemtap是一个基于kprobe调试内核的开源软件。调试者只需要写一些脚本,通过Systemtap提供的命令行接口对正在运行的内核进行诊断调试,不需要修改或插入调试代码、重新编译内核、安装内核和重启动等工作,使内核调试变得简单容易。Systemtap调试过程与在gdb调试器中用断点命令行调试类似。
Systemtap用类似于awk语言的脚本语言编写调试脚本,该脚本命名事件并给这些事件指定处理例程。只要指定的事件发生,Linux内核将运行对应的处理例程。
有几种类型的事件,如:进入或退出一个函数,一个定时器超时或整个systemtap会话开始或停止。处理例程是一系列脚本语言语句指定事件发生时所做的工作,包括从事件上下文提取数据,存储它们进入内部变量或打印结果。
Systemtap的运行过程如图2所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。
(2)stap程序
stap程序是Systemtap工具的前端,它接受用systemtap脚本语言编写的探测指令,翻译这些指令到C语言代码,编译C代码产生并装载内核模块到正运行的Linux内核,执行请求的跟踪或探测函数。用户可在一个命名文件中提供脚本或从命令行中提供调试语句。
命令stap的用法列出如下:
stap [ OPTIONS ] FILENAME [ ARGUMENTS ]
stap [ OPTIONS ] - [ ARGUMENTS ]
stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]
stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]
选项[ OPTIONS ]说明如下:
-h 显示帮助信息。
-V 显示版本信息。
-k 在所有操作完成后,保留临时目录。对于检查产生的C代码或重使用编译的内核对象来说,这是有用的。
-u 非优化编译模式。.
-w 关闭警告信息。
-b 让内核到用户数据传输使用bulk模式。
-t 收集时间信息:探测执行的次数、每个探测花费的平均时间量。
-sNUM 内核到用户数据传输使用NUM MB 的缓冲区。当多个处理器工作在bulk模式时,这是单个处理器的缓冲区大小。
-p NUM Systemtap在通过NUM个步骤后停止。步骤数为1-5: parse, elaborate, translate, compile, run。
-I DIR 添加tapset库(用于翻译C代码的函数集)搜索目录。
-D NAME=VALUE 添加C语言宏定义给内核模块Makefile,用于重写有限的参数。
-R DIR 在给定的目录查找Systemtap运行源代码。
-r RELEASE 为给定的内核发布版本RELEASE而不是当前运行内核编译内核模块。
-m MODULE 给编译产生的内核模块命名MODULE,替代缺省下的随机命名。产生的内核模块被拷贝到当前目录。
-o FILE 发送标准输出到命名文件FILE。在bulk模式,每个CPU的文件名将用"FILE_CPU序号"表示。
-c CMD 开始探测,运行CMD,当CMD完成时退出。
-x PID 设置target()𤣵到PID。这允许脚本作为指定进程的过滤器。
-l PROBE 代替运行一个探测脚本,它仅列出所有匹配给定模式PROBE可用的探测点,模式PROBE可用通配符。
--kmap[=FILE] 指定符号文件,缺省文件为/boot/System.map-VER-SION。探测的函数的地址和名字需要通过内核或内核模块的符号表解析。如果内核编译时没有调试信息或者探测是在没有调试信息的汇编语言言论的,这将是有用的。
--ignore-vmlinux 忽略vmlinux文件。
(3)Systemtap脚本语法
Systemtap脚本语法类似于C语言,它使用了三种数据类型:整数(integers)、字符串(strings)和关联数组(associative Arrays)。它有与C语言一样的控制结构。Systemtap脚本语法详细内容请参考《Systemtap tutorial》。
Systemtap脚本由探测点(probe)和探测输出函数组成。每个Systemtap脚本至少定义一个探测点。函数是探测点的处理例程。
#!/usr/bin/env stap #Systemtap脚本的标志
#
# 显示在最后5秒内调用最后10个系统调用
display the top 10 syscalls called in last 5 seconds
#
global syscalls #定义全局变量
function print_top () { #定义函数
cnt=0 #局部变量
log ("SYSCALL\t\t\t\tCOUNT") #打印表头标题“SYSCALL COUNT”
foreach ([name] in syscalls-) { #查询每个系统调用的计数值
printf("%-20s\t\t%5d\n",name, syscalls[name]) #按格式打印
if (cnt++ == 10)
break
}
printf("--------------------------------------\n")
delete syscalls #删除全局变量
}
probe syscall.* { #在系统调用探测点
syscalls[probefunc()]++ #系统调用计数
}
probe timer.ms(5000) {
print_top () #调用函数
}
kdb内核调试器
Kdb(Kernel Debug)是SGI公司开发的遵循GPL的内建Linux内核调试工具。标准的Linux内核不包括kdb,需要从ftp://oss.sgi.com/www/projects/kdb/download/ix86 下载对应标准版本内核的kdb补丁,对标准内核打补丁,然后,编译打过补丁的内核代码。目前kdb支持包括x86(IA32)、IA64和MIPS在内的体系结构。
Kdb调试器是Linux内核的一部分,提供了检查内存和数据结构的方法。通过附加命令,它可以格式化显示给定地址或ID的基本系统数据结构。kdb当前的命令集可以完全控制内核的操作,包括单步运行一个处理器、在指定的指令执行处理暂停、在访问或修改指定虚拟内存的位置暂停、在输入-输出地址空间对一个寄存器访问处暂停、通过进程ID跟踪任务、指令反汇编等。
安装kdb
标准内核不包含kdb,因此,用户需要先下载kdb补丁,如:kdb-v4.4-2.6.24-x86-2.bz2,接着,应打补丁、配置、编译和安装内核。
(1)打补丁
下载和解压缩补丁,将补丁打进标准内核中。方法如下:
$ upzip kdb-v4.4-2.6.24-x86-2.bz2
$cp kdb-v4.4-2.6.24-x86-2 linux-2.6.24/
$ cd linux-2.6.24
$ patch -p1 < kdb-v4.4-2.6.24-x86-2.bz
$ make xconfig
(2)配置新内核
运行make xconfig,在配置界面上选择CONFIG_KDB选项,为了更好地调试,建议用户从配置界面上选择CONFIG_FRAME_POINTER选项,尽管该选项使用了格外的寄存器并产生稍慢一些的内核。
(3)编译与安装新内核
按下面步骤重新编译和安装新内核:
#make
#make install
#make modules_install
使用kdb调试命令
运行支持kdb的内核后,在控制台上按下 Pause(或 Break)键将启动调试。当内核发生 oop或到达某个断点时,也会启动 kdb。kdb提示符如下所示:
Entering kdb (current=0xc03b0000,pid 0)on processor 0 due to Keyboard Entry
[0]kdb>
在kdb提示符下,用户可以输入kdb命令,详细的kdb命令使用说明请参考man kdb文档,一些常见的命令说明如表1.
表1 常见kdb命令说明
命令 | 命令说明 |
' | 命令可以用于显示所有kdb命令。 |
bp | 设置或显示一个断点。 |
bph | 设置一个硬件断点。 |
bc | 清除一个断点。 |
bl | 列出所有当前断点。 |
bt | 显示当前进程的堆栈跟踪情况。 |
go | 退出调试器并重启内核运行。 |
Id | 反汇编指令。 |
md | 显示指定地址内容。 |
mds | 以符号形式显示内存。 |
mm | 修改内存。 |
reboot | 立即重启机器。 |
rd | 显示寄存器内容。 |
ss | 单步执行(一次一条指令)。 |
ssb | 单步执行CPU直到到达一分支。 |
下面以调试scull驱动程序为例简单说明kdb的使用方法:
假定scull驱动程序内核模块已装载入内核,先在驱动程序的函数scull_read 中设置一个断点,方法如下:
[1]kdb> bp scull_read
Instruction(i) BP #0 at 0xc8833514 (scull_read) is enabled on cpu 1
[1]kdb> go
命令bp在函数scull_read 开始处设置了一个断点,接着,命令go退出调试器,重启内核运行。内核下一次进入函数scull_read 时暂停运行。产生如下的状态:
Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515
Instruction(i) breakpoint #0 at 0xc8833514
scull_read+0x1: movl %esp,%ebp
[0]kdb>
kdb当前scull_read断点位置。可用命令bt查看堆栈跟踪记录,检查函数调用层次树,方法如下:
[0]kdb> bt
EBP EIP Function(args)
0xc3109c5c 0xc8833515 scull_read+0x1
0xc3109fbc 0xfc458b10 scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000,
0x1000, 0x804ad78)
0xbffffc88 0xc010bec0 system_call
[0]kdb>
再可用命令mds显示指定内存的数据,如:查询 scull_devices 指针的值方法如下:
[0]kdb> mds scull_devices 1
c8836104: c4c125c0 ....
上面命令查看指针scull_devices所指位置的一个双字(4个字节)数据,表示设备结构数组的起始地址为c4c125c0。再用mds查看设备结构的数据,方法如下:
[0]kdb> mds c4c125c0
c4c125c0: c3785000 ....
c4c125c4: 00000000 ....
c4c125c8: 00000fa0 ....
c4c125cc: 000003e8 ....
c4c125d0: 0000009a ....
c4c125d4: 00000000 ....
c4c125d8: 00000000 ....
c4c125dc: 00000001 ....
上面8行分别对应结构Scull_Dev的8个成员。再与数据结构Scull_Dev的定义相对照,可知这8个数据的含义。
还可以使用命令mm修改数据。例如:将结构Scull_Dev的某一成员值设置为0x50,方法如下:
[0]kdb> mm c4c125d0 0x50
0xc4c125d0 = 0x50
kgdb
kgdb调试原理
调试器GNU gdb主要用于调试用户级程序,通过串口线或网络将两台计算机以主机/目标机(host machine/target machine)方式连接时,gdb还可用于调试linux内核。这种方式需要给内核打进包含kgdb驱动程序在内的补丁。
kgdb是Linux内核的源代码级调试器,与gdb配合使用可以调试Linux内核。在Linux内核的kgdb配合下,内核开发者可以用类似于调试应用程序的方式通过gdb调试内核,可以方便以使用gdb的命令在内核代码放置断点、单步调试内核和观察内核变量值等。
kgdb进行源码级内核调试的原理图如图1所示。在两台计算中机,一台用作开发计算机,称为主机或开发机;一台用作测试计算机,称为目标机或测试机。两台计算机可通过串行线或以太网进行通信。内核在测试机上调试,gdb在开发机上运行,gdb通过串行线用null modem与调试的内核通信。两台计算机也可以使用一台计算机上的两个虚拟机进行替代。
图1 kgdb进行源码级内核调试原理图
目前,kgdb支持i386, x86_64, ppc, arm, mips和ia64等处理器构架,开发机和测试机可用串行线或以太网进行连接通信。
kgdb补丁将下面的内容加入到内核代码中:
●gdb stub - gdb stub("树桩")是调试器的核心,它处理来自开发机上gdb的请求。当测试机运行了帯有kgdb的内核时,gdb stub控制测试机中所有的处理器。
●对出错处理例程的修改- 当一个不期望的错误发生时,内核将控制传递给kgdb调试器。不含有kgdb的内核在出现不可预测错误时会崩溃(panic),通过对出错处理的修改,kgdb允许开发者分析不可预测的出错。
●串行通信-该部件通过内核的串行驱动程序,为内核中的stub提供接口,负责在串行连接线上发送和接收数据,还负责处理开发机上gdb发送的处理控制断点请求。
建立kdbg联机调试的方法
下面说明建立kdbg联机调试的步骤:
(1)软件建立和应用kgdb补丁
1)下载Linux内核源代码:linux-2.6.15.5.tar.bz2。
2)下载与内核版本对应的kgdb补丁:linux-2.6.15.5-kgdb-2.4.tar.bz2。
3)解压缩软件包,方法如下:
cd ${BASE_DIR}
tar -jxvf linux-2.6.15.5.tar.bz2
cd ${BASE_DIR}/linux-2.6.15.5
tar -jxvf linux-2.6.15.5-kgdb-2.4.tar.bz2
在${BASE_DIR}/linux-2.6.15.5目录中,给Linux内核打kgdb补丁,方法如下:
patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/core-lite.patch
patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/i386.patch
(2)在开发机上编译内核
1)在${BASE_DIR}/linux-2.6.15.5/Makefile,设置EXTRAVERSION = -kgdb。
2)运行命令make xconfig或make oldconfig,出现如图2所示的内核配置界面。在配置界面中,为目标机硬件选择合适的选项;在"Kernel hacking"条目下,选择kgdb的选项。
图2 在Linux内核配置界面中与kgdb相关的配置选项
3)运行make bzImage编译内核。
4)将编译的内核从开发机上传送到目标机上,拷贝内核映像${BASE_DIR}/linux-2.6.15.5/arch/i386/boot/bzImage到目标机/boot/vmlinuz-2.6.15.5-kgdb。然后,再拷贝映射文件${BASE_DIR}/linux-2.6.15.5/System.map到目标机/boot/System.map-2.6.15.5-kgdb。再如下建立符号链接:
ln -s /boot/vmlinuz-2.6.15.5-kgdb /boot/vmlinuz
ln -s /boot/System.map-2.6.15.5-kgdb /boot/System.map
5)在目标机上编辑文件/boot/grub/grub.conf, 在该文件加入含有kgdb的内核条目,方法如下:
title Linux-2.6.15.5-kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdbwait
(3)在开发机上开始调试会话
1)在启动目标机后,它将等待开发机连接,显示下面的消息:
Waiting for connection from remote gdb...
2)用命令cd ${BASE_DIR}/linux-2.6.15.5进入目录linux-2.6.15.5目录。
3)用root用户登录设置调试会话波特率,方法如下:
<root#> gdb ./vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
breakpoint () at kernel/kgdb.c:1212
1212 atomic_set(&kgdb_setting_breakpoint, 0);
warning: shared library handler failed to enable breakpoint
(gdb)
4)在开发机上输入调试命令
此时,gdb已连接到目标机上的内核,目标机上的内核正等待接收命令进行测试。输入命令(gdb) c(表示继续运行)时,目标机系统正常启动,在配置内核时,如果开发机选择了通过gdb输出控制台消息,则控制台log消息会从gdb上显示。
由gdb连接到测试内核,如果测试内核发生内核崩溃(kernel panic),它将首先将控制权转移给gdb,以让gdb分析崩溃原因。
(4)使用kgdb以太网接口
kgdb还可能通过以太网接口调试内核,用以太网接口建立连接的步骤说明如下:
1)添加下面的行到grub条目中:
kgdboe=@10.0.0.6/,@10.0.0.3/ (that's kgdboe=@LOCAL-IP/,@REMOTE-IP/)
# Sample grub.conf which will by default boot the kgdb enabled kernel
title Linux-2.6.15.5-kgdb(eth)
root (hd0,0)
kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdboe=@10.0.0.6/,@10.0.0.3/
console=ttyS0,115200
2)接着在gdb中用下面的命令开始调试会话:
(gdb) ./vmlinux
(gdb) target remote udp:HOSTNAME:6443
调试内核模块
内核可加载模块的调试具有其特殊性,内核模块中各段的地址在模块加载进内核后才最终确定的,开发机的gdb无法得到各种符号地址信息。因此,用户需要使用特殊版本的gdb,可以检测到内核模块的装载和卸载。另外,还需要将内核模块的符号装载到gdb中,这样,gdb才能解析到符号。
(1)准备检测内核模块装载和卸载代码的gdb派生版本
此步骤在开发机上完成。
安装在开发机上的gdb应含有内核模块调试特征,用户需要安装含有检测内核模块装载和卸载代码的gdb派生版本,该版本gdb派生于标准的gdb。用户可以从网址http://kgdb.linsyssoft.com/downloads.htm下载gdb-6.4-kgdb-2.4.tar.bz2,然后,编译安装生成gdb派生版本。或者下载gdbmod-2.4.bz2,解压缩后得到可执行的gdb派生版本。
在测试机上不需要特别的安装,内核模块可以出现在根文件系统或一个ramdisk中。
(2)装载内核模块符号到gdb中
此步骤在开发机上完成。
在开发机上,用户应装载内核模块的符号到gdb,让gdb调试时可以解析到二进制代码对应的符号。
首先,内核模块编译时应打开调试信息。然后,用下面方法设置内核、调试接口和定位内核模块位置:
#cd /usr/src/linux 2.6.13
#gdbmod 2.4 vmlinux
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
breakpoint () at gdbstub.c:1153
1153 }
(gdb)set solib search path /usr/linux 2.6.13/drivers/net
一旦kgdb通知一个内核模块装载时,gdb必须能定位模块文件。因此,用户需要用命令"set solib-search-path"设置内核模块文件所在的路径。
(3)插入内核模块到内核
插入内核模块到内核,方法 如下:
# insmod mymodule.ko
到此,已装载了内核模块符号,内核模块可以像正常的内核代码一样调试了。
样例:调试内核模块test
1)编写内核模块
首先,在开发机上编写简单的内核模块test,代码如下:
void test_func()
{
printk("test_func\n");
printk("aaaaaaaaaaa\n");
}
int test_init()
{
printk("test_init_module\n");
return 0;
}
void test_exit()
{
printk("test_cleanup_module\n");
}
module_init(test_init);
module_exit(test_exit);
2)编译安装内核模块
接着,编译内核模块,并将内核模块拷贝到测试机上。方法如下:
#cd /root/mymodule
#gcc -D__KERNEL__ -DMODULE -I/usr/src/linux-2.6.15/kernel/include -O -Wall -g -c -o test.ko test.c
#scp test.ko root@192.168.1.130:/root
3)开始调试
装载内核符号到gdb中,设置内核模块所在路径,方法如下:
# gdbmod vmlinux
(gdb) set solib-search-path /root/mymodule
执行命令rmt,进入测试机调试,方法如下:
(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005 atomic_set(&kgdb_setting_breakpoint, 0);
在内核模块初始化处设置断点。查内核源码可知,内核模块初始化函数init在module.c文件函数sys_init_module函数中的mod->init处调用,对应行号为2168(根据不同版本的内核,行号可能不同)。设置断点方法如下:
(gdb) b module.c:2168
Breakpoint 1 at 0xc011cd83: file module.c, line 2168.
让测试机上的内核继续运行,方法如下:
(gdb) c
Continuing.
[New Thread 1352]
[Switching to Thread 1352]
测试机用命令"insmod test.ko"执行插入内核模块操作时,开发机会在断点处被暂停,暂停时显示如下
Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
mod_user=0x80904d8) at module.c:2168
2168 ret = mod->init();
用step命令进入内核模块test的函数init,方法如下:
(gdb) step
test_init () at test.c:12
12 printk("test_init_module\n");
(gdb) n
15 }
(gdb)
对内核模块的非init函数调试时,由于测试机上已插入模块,模块的符号也已加载,只需要直接需要调试的代码处设置断点。如:在函数test_func处设置断点的方法如下:
(gdb)bt test_func
调试内核
用gdb调试内核类型于调试应用程序进程,kgdb支持gdb的执行控制命令、栈跟踪和线程分析等。但kgdb不支持watchpoint,kgdb通过gdb宏来执行watchpoint。
调试内核的命令说明如下:
(1)停止内核执行
用户在gdb终端中按下Ctrl + C键,gdb将发送停止消息给kgdb stub,kgdb stub控制内核的运行,并与gdb通信。
(2)继续内核运行
gdb命令"(gdb) c"告诉kgdb stub继续内核运行,直到遇到一个断点,或者gdb执行Ctrl + C,或其他原因,内核运行才停顿下来。
(3)断点
gdb断点(Breakpoints)用于在一个函数或代码行处暂停内核运行,设置断点命令如:"(gdb) b module.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:1878
Cannot access memory at address 0x8e000
除非栈帧数作为命令backtrace的参数外,gdb仅在栈跟踪走出了可访问地址空间时才停止打印栈的信息。上面例子中,函数调用层次次序从上到下为:ret_from_intr, do_IRQ, handle_IRQ_event, gdb_interrupt。
放置一个断点在函数ext2_readlink,并访问一个符号链接,以便运行到该断点。 设置断点方法如下:
(gdb) br ext2_readlink
Breakpoint 2 at 0xc0158a05: file symlink.c, line 25.
(gdb) c
Continuing.
在测试机上运行命令"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) delete
Delete all breakpoints? (y or n) y
(gdb) c
Continuing.
(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:1160
1160 }
(gdb) break __down
Breakpoint 1 at 0xc0105a43: file semaphore.c, line 62.
(gdb) c
Continuing.
为了让程序运行到断点处,在目标机上运行"man lilo"。程序会运行到断点,gdb会进入命令行模式。输入栈跟踪命令,显示如下:
Breakpoint 1, __down (sem=0xc7393f90) at semaphore.c:62
62 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:719
714 * do not have enough call-clobbered registers to hold all
715 * 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 }
721
722 /*
723 * sys_execve() executes a new program.
就像gdb显示的一样,函数sys_vfork调用函数do_fork,再看栈跟踪显示的第2帧,gdb显示它在文件semaphore.h中的行号是120,显示的行号虽然没有用但是正确的。查看该文件可得到确认,方法如下:
(gdb) list semaphore.h:118
113 */
114 static inline void down(struct semaphore * sem)
115 {
116 #if WAITQUEUE_DEBUG
117 CHECK_MAGIC(sem->__magic);
118 #endif
119
120 __asm__ __volatile__( <-----
121 "# atomic down operation\n\t"
122 LOCK "decl %0\n\t" /* --sem->count */
上述代码中,在箭头所指示语句处,得到的信息仅是它在do_fork的一个扩展的内联函数down中。gdb还打印了在do_fork中从下一个被调用的函数开始代码的绝对地址:0xc011433b。这里我们用脚本disasfun找出该地址所对应的代码行。命令"disasfun vmlinux do_fork"输出的部分结果显示如下:
if ((clone_flags & CLONE_VFORK) && (retval > 0))
c011431d: 8b 7d 08 mov 0x8(%ebp),%edi
c0114320: f7 c7 00 40 00 00 test $0x4000,%edi
c0114326: 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_DEBUG
CHECK_MAGIC(sem->__magic);
#endif
__asm__ __volatile__(
c011432e: 8b 4d d0 mov 0xffffffd0(%ebp),%ecx
c0114331: 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,info regi这样的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(PID 7)具有gdb id 8。为了分析内核线程8 ,我们指定线程9给gdb。gdb接着切换到该线程中,准备做更多的分析。
下面是分析线程的命令显示:
(gdb) thr 9
[Switching to thread 9 (thread 270)]
#0 schedule_timeout (timeout=-1011908724) at sched.c:439
439 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:1891
Cannot access memory at address 0x31494350
(gdb) info regi
eax 0xc38fdf7c -1013981316
ecx 0x86 134
edx 0xc0339f9c -1070358628
ebx 0x40f13 266003
esp 0xc3af7f74 0xc3af7f74
ebp 0xc3af7fa0 0xc3af7fa0
esi 0xc3af7f8c -1011908724
edi 0xc3af7fbc -1011908676
eip 0xc011346d 0xc011346d
eflags 0x86 134
cs 0x10 16
ss 0x18 24
ds 0x18 24
es 0x18 24
fs 0xffff 65535
gs 0xffff 65535
fctrl 0x0 0
fstat 0x0 0
ftag 0x0 0
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 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:439
439 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) ps
0 swapper
1 init
2 keventd
3 ksoftirqd_C
4 kswapd
5 bdflush
6 kupdated
8 khubd
270 eth0
328 portmap
383 syslogd
392 klogd
406 atd
420 crond
438 inetd
511 mingetty
512 mingetty
513 mingetty
514 mingetty
515 mingetty
516 mingetty
(gdb)
The psname macro can be used to get name of a thread when it's id is known.
(gdb) psname 8
8 khubd
(gdb) psname 7
(gdb)
(7)Watchpoints
(7)Watchpoints
kgdb stub使用x86处理器的调试特征支持硬件断点,这些断点不需要代码修改。它们使用调试寄存器。x86体系中的ia32处理器有4个硬件断点可用。每个硬件断点可以是下面三个类型之一:
执行断点 当代码在断点地址执行时,触发执行断点。由于硬件断点有限,建议通过gdb break命令使用软件断点,除非可避免修改代码。
写断点 当系统对在断点地址的内存位置进行写操作时,触发一个写断点。写断点可以放置可变长度的数据。写断点的长度指示为观察的数据类型长度,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
使用UML调试Linux内核
用户模式Linux(User Mode Linux,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 mode Linux,UML)已随Linux内核源代码一起发布,它存放于arch/um目录下。编译好UML的内核之后,可直接用gdb运行编译好的内核并进行调试。
UML原理
用户模式Linux(User mode Linux,UML)将Linux内核的一部分作为用户空间的进程运行,称为客户机内核。UML运行在基于Linux系统调用接口所实现的虚拟机。UML运行的方式如图1所示。UML像其他应用程序一样与一个"真实"的Linux内核(称为"主机内核")交互。应用程序还可运行在UML中,就像运行在一个正常的Linux内核下。
图1 UML在Linux系统中运行的位置
使用UML的优点列出如下:
如果UML崩溃,主机内核还将运行完好。
可以用非root用户运行UML。
可以像正常进程一样调试UML。
在不中断任何操作下与内核进行交互。
用UML作为测试新应用程序的"沙箱",用于测试可能有伤害的程序。
可以用UML安全地开发内核。
可以同时运行不同的发布版本。
由于UML基于以Linux系统调用接口实现的虚拟机,UML无法访问主机的硬件设备。因此,UML不适合于调试与硬件相关的驱动程序。
编译UML模式客户机Linux内核
(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
内核提供了配置选项用于内核调试,这些选项大部分在配置界面的kernel hacking菜单项中。一般需要选取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,带有用户友好的图形界面。
运行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中打开虚拟网络。如果运行下载的预编译的内核,则已打开虚拟网络。如果用户自己编译内核,则在配置界面上的"Network device 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 netmask 255.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,multicast port,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。使用选项"-unix socket"可指定不同的socket,"-hub"可将交换机后台变为集线器(Hub)。如果用户期望交换机后台连接到主机网络(允许UML访问通过主机访问外部的网络),可使用选项"-tap tap0"。
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)的任何网络设备,过滤器表达式(filter expression)与工具tcpdump使用的一样,option1为"promisc"或"nopromisc",控制pcap是否将主机接口设为"promiscuous"(混杂)模式;option2为"optimize "或"nooptimize",表示是否使用pcap表达式优化器。
一个设置pcap的样例列出如下:
eth0=pcap,eth0,tcp
eth1=pcap,eth0,!tcp
上述语句将引起在主机eth0上的UML eth0将所有的tcp发出,并且在主机eth0上的UML eth1将发出所有非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.250 up
如果正建立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 Block Device)块设备驱动程序的写拷贝(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:16 cow.debian
525832 -rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.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文件 存在的后备文件
创建UML的文件系统
如果根文件系统硬盘空间不够大,或者想使用不同于ext2的文件系统,用户就可能想创建和挂接新的UML文件系统,用户可以用如下方法创建UML的根文件系统:
(1)创建文件系统的文件
使用命令dd创建一个合适尺寸的空文件,用户可以创建稀疏文件,该文件直到实际使用时才分配硬盘空间。例如:下面的命令创建一个100M填满0的稀疏文件:
host% dd if=/dev/zero of=new_filesystem seek=100 count=1 bs=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=parent gdb-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的顶层目录运行该脚本,显示如下:
-
-
-
-
-
-
-
- GDB pid is 21903 ********
-
-
-
-
-
-
Start UML as: ./linux <kernel switches> 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 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"...
(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, process 1
0xa00f4221 in __kill ()
(UML gdb) c
Continuing.
此时,当用户用insmod插入内核模块,显示列出如下:
-
-
- Module hostfs loaded ***
-
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) at syscall_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 of bounds>,
archdata_end = 0xe5890000 <Address 0xe5890000 out of bounds>,
kernel_data = 0xf689c35d <Address 0xf689c35d out of bounds>}
>> Finished loading symbols for hostfs ...
(2)手动调试内核模块
在调试器中启动内核,并用insmod或modprobe装载内核模块。在gdb中执行下面命令:
(UML gdb) p module_list
这是已装载进内核的内核模块列表,通常用户期望的内核模块在module_lis中。如果不在,就进入下一个链接,查看name域,直到找到用户调试的内核模块。获取该结构的地址,并加上module.size_of_struct值,gdb可帮助获取该值,方法如下:
(UML gdb) printf "%#x\n", (int)module_list module_list->size_of_struct
从内核模块开始处的偏移偶尔会改变,因此,应检查init和cleanup的地址,方法如下:
(UML gdb) add-symbol-file /path/to/module/on/host that_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 /* __ASSEMBLY__ */
#define BUGFLAG_WARNING (1<<0)
#endif /* CONFIG_GENERIC_BUG */
#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 /* !CONFIG_BUG */
#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 lock validator)可以在死锁发生前检测到死锁,即使是很少发生的死锁。它将每个自旋锁与一个键值相关,相似的锁仅处理一次。加锁时,查看所有已获取的锁,并确信在其他上下文中没有已获取的锁,在新获取锁之后被获取。解锁时,确信正被解开的锁在已获取锁的顶部。
.
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上。
驱动程序的调试
一. 打印: prink, 自制proc文件
UBOOT传入console=ttySAC0 console=tty1
1. 内核处理UBOOT传入的参数
console_setup
add_preferred_console // 我想用名为"ttySAC0"的控制台,先记录下来
2. 硬件驱动的入口函数里:
drivers/serial/s3c2410.c
register_console(&s3c24xx_serial_console);
3. printk
vprintk
/* Emit the output into the temporary buffer */
// 先把输出信息放入临时BUFFER
vscnprintf
// Copy the output into log_buf.
// 把临时BUFFER里的数据稍作处理,再写入log_buf
// 比如printk("abc")会得到"<4>abc", 再写入log_buf
// 可以用dmesg命令把log_buf里的数据打印出来重现内核的输出信息
// 调用硬件的write函数输出
release_console_sem();
call_console_drivers(_con_start, _log_end);
// 从log_buf得到数据,算出打印级别
_call_console_drivers(start_print, cur_index, msg_level);
// 如果可以级别够格打印
if ((msg_log_level < console_loglevel
__call_console_drivers
con->write(con, &LOG_BUF(start), end - start);
二. 根据内核打印的段错误信息分析
oops信息 : 单词oops的含义是“惊讶”,当内核出错时(比如访问非法地址),打印出来的信息被称为oops信息。
a). 作为模块:
1,根据PC值,找到导致错误的指令。
pc = 0x00000000 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。
2,假设它的加载的驱动程序引入的错误。那又怎么确定是哪一个驱动程序?
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数
从这些信息里找到一个相近的地址, 这个地址<=0xbf000018
比如找到了:
bf000000 t first_drv_open [first_drv]
3. 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D lcd.ko > lcd.dis
在dis文件里找到first_drv_open
first_drv.dis文件里 insmod后
00000000 : bf000000 t first_drv_open [first_drv]
00000018 pc = bf000018
18: e5923000 ldr r3, [r2] //r2的值在下面可以找到,是56000050
此时,要通过汇编语言来找到对应的c语言的语句。考验汇编能力的时候。
./firstdrvtest on
//1,一段文本描述信息
Unable to handle kernel paging request at virtual address 56000050
内核使用56000050来访问时发生了错误
pgd = c3eb0000
[56000050] *pgd=00000000
//2,oops信息的序号,#1,表示是第1次。
Internal error: Oops: 5 [#1]
//3,内核中加载的模块的名称
Modules linked in: first_drv
//4,发送错误时,CPU的序号,对于单处理器系统,序号为0。
CPU: 0 Not tainted (2.6.22.6 #1)
//5,PC就是发生错误时,指令的地址。
//大多时候,PC值只会给出一个地址,不会指示说是在哪个函数里面。
PC is at first_drv_open+0x18(该指令的偏移)/0x3c(该函数的总大小) [first_drv]
PC就是发生错误的指令的地址
大多时候,PC值只会给出一个地址,不到指示说是在哪个函数里
//__init_begin = c0008000, PC=__init_begin+0x3fff8000=
//6,LR寄存器的值。
LR is at chrdev_open+0x14c/0x164
LR寄存器的值
//7,发送错误时,CPU各个寄存器的值。
pc = 0xbf000018
pc : [] lr : [] psr: a0000013
sp : c3c7be88 ip : c3c7be98 fp : c3c7be94
r10: 00000000 r9 : c3c7a000 r8 : c049abc0
r7 : 00000000 r6 : 00000000 r5 : c3e740c0 r4 : c06d41e0
r3 : bf000000 r2 : 56000050 r1 : bf000964 r0 : 00000000
执行这条导致错误的指令时各个寄存器的值
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33eb0000 DAC: 00000015
//8,发生错误时,当前进程是它,并不是说发生错误的是这个进程
Process firstdrvtest (pid: 777, stack limit = 0xc3c7a258)
//发生错误时当前进程的名称是firstdrvtest
//9,栈信息
Stack: (0xc3c7be88 to 0xc3c7c000)
be80: c3c7bebc c3c7be98 c008d888 bf000010 00000000 c049abc0
bea0: c3e740c0 c008d73c c0474e20 c3e766a8 c3c7bee4 c3c7bec0 c0089e48 c008d74c
bec0: c049abc0 c3c7bf04 00000003 ffffff9c c002c044 c3d10000 c3c7befc c3c7bee8
bee0: c0089f64 c0089d58 00000000 00000002 c3c7bf68 c3c7bf00 c0089fb8 c0089f40
bf00: c3c7bf04 c3e766a8 c0474e20 00000000 00000000 c3eb1000 00000101 00000001
bf20: 00000000 c3c7a000 c04a7468 c04a7460 ffffffe8 c3d10000 c3c7bf68 c3c7bf48
bf40: c008a16c c009fc70 00000003 00000000 c049abc0 00000002 bec1fee0 c3c7bf94
bf60: c3c7bf6c c008a2f4 c0089f88 00008520 bec1fed4 0000860c 00008670 00000005
bf80: c002c044 4013365c c3c7bfa4 c3c7bf98 c008a3a8 c008a2b0 00000000 c3c7bfa8
bfa0: c002bea0 c008a394 bec1fed4 0000860c 00008720 00000002 bec1fee0 00000001
bfc0: bec1fed4 0000860c 00008670 00000002 00008520 00000000 4013365c bec1fea8
bfe0: 00000000 bec1fe84 0000266c 400c98e0 60000010 00008720 00000000 00000000
//10,栈回溯信息,可以从中看出函数调用关系:从最后一个函数 sys_init_module 开始,向上可以找到函数调用的关系。
//可以通过内核配置信息 make menuconfig 来指定是否输出 栈回溯信息。
Backtrace: (回溯)
[] (first_drv_open+0x0/0x3c [first_drv]) from [] (chrdev_open+0x14c/0x164)
[] (chrdev_open+0x0/0x164) from [] (__dentry_open+0x100/0x1e8)
r8:c3e766a8 r7:c0474e20 r6:c008d73c r5:c3e740c0 r4:c049abc0
[] (__dentry_open+0x0/0x1e8) from [] (nameidata_to_filp+0x34/0x48)
[] (nameidata_to_filp+0x0/0x48) from [] (do_filp_open+0x40/0x48)
r4:00000002
[] (do_filp_open+0x0/0x48) from [] (do_sys_open+0x54/0xe4)
r5:bec1fee0 r4:00000002
[] (do_sys_open+0x0/0xe4) from [] (sys_open+0x24/0x28)
[] (sys_open+0x0/0x28) from [] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f1024 e3a00000 e5912000 (e5923000)
Segmentation fault
#
b). 编入内核
Modules linked in:
CPU: 0 Not tainted (2.6.22.6 #2)
PC is at first_drv_open+0x18/0x3c
LR is at chrdev_open+0x14c/0x164
pc : [] lr : [] psr: a0000013
sp : c3a03e88 ip : c3a03e98 fp : c3a03e94
r10: 00000000 r9 : c3a02000 r8 : c03f3c60
r7 : 00000000 r6 : 00000000 r5 : c38a0c50 r4 : c3c1e780
r3 : c014e6a8 r2 : 56000050 r1 : c031a47c r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 339f0000 DAC: 00000015
Process firstdrvtest (pid: 750, stack limit = 0xc3a02258)
1. 根据pc值确定该指令属于内核还是外加的模块
pc = c014e6c0 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。
2. 反汇编内核: arm-linux-objdump -D vmlinux > vmlinux.dis
/*
vmlinux是未压缩的内核,vmlinux 是ELF文件,即编译出来的最原始的文件。用于kernel-debug,产生system.map符号表,不能用于直接加载,不可以作为启动内核。只是启动过程中的中间媒体
vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制
*/
first_drv.dis文件中:搜c014e6c0
c014e6a8 :
c014e6a8: e1a0c00d mov ip, sp
c014e6ac: e92dd800 stmdb sp!, {fp, ip, lr, pc}
c014e6b0: e24cb004 sub fp, ip, #4 ; 0x4
c014e6b4: e59f1024 ldr r1, [pc, #36] ; c014e6e0 <.text+0x1276e0>
c014e6b8: e3a00000 mov r0, #0 ; 0x0
c014e6bc: e5912000 ldr r2, [r1]
c014e6c0: e5923000 ldr r3, [r2] // 在此出错 r2=56000050,在下面可以找到
c).根据栈信息分析函数调用过程(作为模块 或者 编入内核都可,下面的实验是作为模块)
# ./firstdrvtest on
Unable to handle kernel paging request at virtual address 56000050
pgd = c3e78000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1]
Modules linked in: first_drv
CPU: 0 Not tainted (2.6.22.6 #48)
PC is at first_drv_open+0x18/0x3c [first_drv]
LR is at chrdev_open+0x14c/0x164
pc : [] lr : [] psr: a0000013
1 根据PC确定出错位置,是在内核,还是在模块中,前面有详细的分析System.map
bf000018 属于 insmod的模块
bf000000 t first_drv_open [first_drv]
2 确定它属于哪个函数
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数
3 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D first_drv.ko > first_drv.dis
在dis文件里找到first_drv_open
first_drv.dis文件里 insmod后
00000000 : bf000000 t first_drv_open [first_drv]
00000018 pc = bf000018
first_drv.dis文件中:
c014e6a8 :
c014e6a8: e1a0c00d mov ip, sp
c014e6ac: e92dd800 stmdb sp!, {fp, ip, lr, pc}
//从这里可以看见 first_drv_open函数的栈信息,保存有4*4个字节
sp : c3e69e88 ip : c3e69e98 fp : c3e69e94
r10: 00000000 r9 : c3e68000 r8 : c0490620
r7 : 00000000 r6 : 00000000 r5 : c3e320a0 r4 : c06a8300
r3 : bf000000 r2 : 56000050 r1 : bf000964 r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33e78000 DAC: 00000015
Process firstdrvtest (pid: 752, stack limit = 0xc3e68258)
可以仅根据栈信息,来确定函数的调用关系。栈就是一块内存
分析如下 :
Stack: (0xc3e69e88 to 0xc3e6a000)
9e80: c3e69ebc c3e69e98 c008c888 bf000010 00000000 c0490620
fp ip 返回地址=lr pc
栈信息开始部分的4个数据 是first_drv_open 的 栈 chrdev_open is sp
first_drv_open执行完之后,返回地址lr=c008c888,再在 vmlinux.dis 中找到 调用者的函数。
4个数据后面的数据,就是它的调用者的函数的栈空间中的数据。
9ea0: c3e320a0 c008c73c c0465e20 c3e36cb4 c3e69ee4 c3e69ec0 c0088e48 c008c74c
lr=c0088e48
9ec0: c0490620 c3e69f04 00000003 ffffff9c c002b044 c06e0000 c3e69efc c3e69ee8
__dentry_open 的栈
9ee0: c0088f64 c0088d58 00000000 00000002 c3e69f68 c3e69f00 c0088fb8 c0088f40
lr=c0088f64 nameidata_to_filp的栈 lr=c0088fb8
9f00: c3e69f04 c3e36cb4 c0465e20 00000000 00000000 c3e79000 00000101 00000001
do_filp_open的栈
9f20: 00000000 c3e68000 c04c1468 c04c1460 ffffffe8 c06e0000 c3e69f68 c3e69f48
9f40: c008916c c009ec70 00000003 00000000 c0490620 00000002 be94eee0 c3e69f94
9f60: c3e69f6c c00892f4 c0088f88 00008520 be94eed4 0000860c 00008670 00000005
lr=c00892f4 do_sys_open的栈
9f80: c002b044 4013365c c3e69fa4 c3e69f98 c00893a8 c00892b0 00000000 c3e69fa8
lr=c00893a8 sys_open的栈
9fa0: c002aea0 c0089394 be94eed4 0000860c 00008720 00000002 be94eee0 00000001
lc=002aea0 ret_fast_syscall的栈
9fc0: be94eed4 0000860c 00008670 00000002 00008520 00000000 4013365c be94eea8
9fe0: 00000000 be94ee84 0000266c 400c98e0 60000010 00008720 00000000 00000000
ret_fast_syscall()函数是被谁调用的,我们这里还不需要详细了解,只要知道是应用程序是通过swi中断来加入内核的。
注意 : 上面的信息,从下往上,从大地址到小地址方向。是从栈底开始,往栈顶方向打印栈中的值。也是函数调用的方向。
三. 修改内核来定位系统僵死问题
1,\kernel-2.6.13\arch\arm\kernel\irq.c //修改这个内核文件
修改这个文件的原因 : 只要系统还在运行,即使某个驱动程序卡死了,系统时钟中断是绝对不会停歇的。
所以,可以在系统中断代码中加入一些调试代码,来帮助我们找到bug。
/*
* do_IRQ handles all hardware IRQ's. Decoded IRQs should not
* come via this function. Instead, they should provide their
* own 'handler'
*/
asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irqdesc *desc = irq_desc + irq;
static pid_t pre_pid;
static int count = 0;
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
if(irq == 30) /*系统时钟中断*/
{
/*如果10秒之内,都是同一个进程在运行,就打印卡死信息。*/
/*静态局部变量:
只有在这个函数中能访问,但是生命周期是和全局变量差不多的,函数退出之后变量还在,
而且只在第一次进入的时候做初始化,以后会跳过初始化语句,保留原来的值*/
if(pre_pid == current->pid) /*当前进程 等于 之前记录下来的进程号*/
{
count++;
}
else /*当前进程 不等于 之前记录下来的进程号*/
{
count = 0;
pre_pid = current->pid; /*把新的当前进程记录下来。*/
}
if(count == 10*HZ) /*累计达到10秒的时候*/
{
count = 0;
/*明确是在哪个进程导致卡死的,明确PC值*/
printk("asm_do_IRQ==>s3c2410_timer_interrupt : pid=[%d],
task_name=[%s], PC=[0x%08x]\n", current->pid, current->comm,
regs->ARM_pc);
}
}
irq_enter();
spin_lock(&irq_controller_lock);
desc->handle(irq, desc, regs);
/*
* Now re-run any pending interrupts.
*/
if (!list_empty(&irq_pending))
do_pending_irqs(regs);
irq_finish(irq);
spin_unlock(&irq_controller_lock);
irq_exit();
}
重新编译内核 make clean;make
重新启动这个新的内核
2, 在这个驱动文件中加入死循环,来模拟系统僵死问题 \nfs_2.6.13\wxc\driver\chardriver\led\leddriver2.c
static int leddriver2_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
//printk("DEVICE:mydriver2_ioctl Called!\n");
printk("DEVICE:cmd=[%d], arg=[%ld]\n", cmd, arg);
while(1); //故意加的
if(cmd == 0) //所有的灯全亮2s
{
....
}
return 0;
}
启动待测试的驱动程序
# insmod leddriver2.ko
[kernel/sys.c][notifier_call_chain][175]
[kernel/sys.c][notifier_call_chain][187]
DEVICE:leddriver2_init
DEVICE:register leddriver2 OK! Major = [253]
# mknod /dev/leddriver2 c 253 0
[root@EmbedSky ko]# ls -lrt /dev/led*
crw-r--r-- 1 root root 253, 0 Jan 1 00:03 /dev/leddriver2
启动应用程序
# ./leddrivertest2 0 3
APP:open fd=[3]
APP:led_no=[0] time=[3]
DEVICE:cmd=[0], arg=[3] //会发现驱动程序卡死在这里了,无论怎么也退出不了,下面打印出来了很多关键信息。
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
PC=0xbf000044
3,定位僵死发生的代码的具体位置
3.1), PC=0xbf000044
从这些信息里找到一个相近的地址, 这个地址<=0xbf000044
前面有分析 可知 地址 0xbf000044 不属于内核地址空间,属于外加的内核。
在开发板目录下 :
# cat /proc/kallsyms >> kallsyms.txt
在这个文件下面,查找 地址 0xbf000044 大概在什么函数
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2] // 对于中断, pc-4=0xbf000044 才是发生中断瞬间的地址,说明发生中断的驱动程序是 : leddriver2.ko。
bf002000 t leddriver2_init [leddriver2]
bf00004c t leddriver2_exit [leddriver2]
c481e178 ? __mod_license202 [leddriver2]
c481e184 ? __mod_description201 [leddriver2]
c481e1a4 ? __mod_author200 [leddriver2]
bf0008a0 b $d [leddriver2]
bf0008a4 b MYDRIVER_Major [leddriver2]
bf000698 d leddriver2_fops [leddriver2]
bf000698 d $d [leddriver2]
bf000028 t leddriver2_ioctl [leddriver2] //bf000028 是与 0xbf000044 最近的地址。说明 : 发生中断的驱动程序是 : leddriver2.ko。
bf000000 t leddriver2_open [leddriver2] //中断很有可能发生在 leddriver2_ioctl 函数中。
bf000014 t leddriver2_release [leddriver2]
bf000704 d GPBDAT_HIGH [leddriver2]
bf000714 d GPBDAT_LOW [leddriver2]
bf000724 d GPBUP_UPEN [leddriver2]
bf000734 d GPBCON_OUTP [leddriver2]
bf000744 d GPBCON_CLEAN [leddriver2]
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2]
bf002000 t $a [leddriver2]
bf002094 t $d [leddriver2]
bf00004c t $a [leddriver2]
bf000080 t $d [leddriver2]
3.2), 找到 leddriver2.ko
在PC上反汇编它:
在ubuntu目录下 :
# arm-linux-objdump -D leddriver2.ko > leddriver2.dis //PATH没有,就用下面一行
#
/home/wangxc/linux/toolchain/crosstools_3.4.1_softfloat/arm-linux/gcc-3.4.1-glibc-2.3.3/bin/arm-linux-objdump
-D leddriver2.ko > leddriver2.dis
在dis文件里找到 leddriver2_ioctl
00000028 : //insmod后 上面的地址 bf000028 对应在这里
28: e1a0c00d mov ip, sp
2c: e92dd800 stmdb sp!, {fp, ip, lr, pc}
30: e24cb004 sub fp, ip, #4 ; 0x4
34: e59f000c ldr r0, [pc, #12] ; 48 <.text+0x48>
38: e1a01002 mov r1, r2
3c: e1a02003 mov r2, r3
40: ebfffffe bl 40 // 对于中断, pc-4才是发生中断瞬间的地址,所以僵死的代码在这里
44: ea00000f b 88 //所以 0xbf000044 在这里
48: 00000000 andeq r0, r0, r0
Disassembly of section .init.text:
3.3), 分析汇编代码,找到对应的c语言代码
40: ebfffffe bl 40 //bl自己跳转到自己,就是一个死循环,
所以,c语言代码中的就是死循环的语句。
在驱动源码leddriver2_ioctl函数中寻找,可以找到 while(1);语句,说明僵死就发生在这里。
内核调试方法
目录[-]
· 一些前言
· 作者前言
· 知识从哪里来
· 为什么撰写本文档
· 源码阅读的陷阱
· 代码调试的陷阱
· 原理理解的陷阱
· 建立调试环境
· 中文环境设置
· pdf乱码的解决
· 建立编译环境
· 安装交叉编译工具
· 什么是EABI
· qemu的使用
· 安装与使用
· 利用qemu
· 利用skyeye
· 快速试玩
· 基于串口
· 基于网口
· gdb基础
· 基本命令
· gdb之gui
· gdb技巧
· gdb宏
· 参考资料
· gdb宏的使用
· 实例
· 链表遍历类
· 功能增强类
· 用户手册
· AT&T汇编格式
· 内联汇编
· 调用链的形成
· 完整的调用过程
· C难点的汇编解释
· 例1
· 例2
· 例3
· 例4
· 优化级别的影响
· 优化选项
· 例子
· 用户手册
· 壮观的标准
· 别名的烦恼
· 条件执行
· 调用链的形成
· 完整的调用过程
· 调用链回溯的实现
· 源码浏览工具
· 调用图生成工具
· 优缺点
· 安装wine
· 安装SI
· SI的设置
· SI的使用
· global
· 优缺点
· 命令选项
· 使用
· 建立索引
· 快捷键的使用
· kscope
· lxr
· 理想调用链
· 函数指针调用
· 调用链的层次
· 非理想调用链
· 调用树与调用图
· 调用树的定义
· 调用树的作用
· 调用树的分类
· 调用树的显示
· 调用树的拼接
· 调用图
· 穿越盲区
· 穿越gdb的盲区
· 进程切换
· 中断异常
· 系统调用
· 函数指针
· 查看函数的参数
· 工程方法
· 二叉断点
· 给调用指令下断点
· 网站
· 调试相关子系统
· kgdb源码分析
· sysrq
· oprofile
· kprobes
· 驱动分析
· 载入模块符号
· 中断处理过程
· 虚拟地址空间
· 用户层的观察窗
· 理解设备模型
· 面向对象的实现
· 设备模型的分层
· 外围支持机制
· sysfs
· hotplug
· 文件系统
· strace
· ltrace
· MEMWATCH
· YAMD
· 补丁提交相关文档
· 多补丁发送工具
· git使用
· 内核git库
· 书籍
· 子系统官方网站
· 参考文章
· 私人备忘
大海里的鱼有很多,而我们需要的是鱼钩一只
本文档由大家一起自由编写,修改和扩充,sniper负责维护。引用外来的文章要注明作者和来处。本文档所有命令都是在ubuntu/debian下的操 作。选取的内核源码从文档开始编写时最新的内核版本–2.6.26开始,而且会随着linux的更新而不断更换新的版本。所以文档的内容可能前后不一致。 相信大家有能力克服这个问题。
本文档的字符图示在linux环境下显示正常,在window下显示有细微的错乱。
本文档唯一的更新网址是:http://wiki.zh-kernel.org/sniper 转载请保留此网址。
有任何建议请发邮件:s3c24xx@gmail.com
有任何问题请到邮件列表提问:http://zh-kernel.org/mailman/listinfo/linux-kernel
一些和内核调试分析有关的小工具放在:
http://code.google.com/p/root-kit/
一些前言
作者前言
一个人默默地敲打这篇文章也有段时间了。在这个过程里,没有收到任何的赞誉,也没接到任何的板砖,没有任何的反馈。就这么敲打着,修理着。但是本人从没怀疑这篇文档的价值,这是因为,本人就是这篇文档的亲身收益者。在这里把它“无私”奉献出来,乃是出于对于某类同道者锲而不舍孜孜以求的“德性”的认 同和“同情”,你的痛苦我表示感同身受,你的迷茫我愿意一起分担。一定有人能从个文档受益,这便已让我知足。其实,写这个文档并非是件苦差,而是字字都是有感而发的,不吐不快的结果。这里的句句都是本人教训和经验的记录。
谈到调试器,世上存在两种截然不同的看法。其中一种,是超级解霸的作者,他认为“程序不是写出来的,好程序绝对是调试出来的”。对于这个观点,本人持着极不认同的态度。而第二种相反观点的人,便是linux之父linus了。他认为调试器只会“误人子弟”,只会导致人们迷于表象而不去真正理解源码本身。并以此为由,长期没把kgdb内置到内核中。对于调试器调试bug会引入错误的修正这个观点,我认为还是有点道理的。但是他以此为由而不把它集合到内核中,这个做法我就认为是毫无道理了。因为linus本人就说过:“我只使用GDB,而且我总是并不把它作为调试器来使用,只是将其作为一个可以用来分析程序的分解器来使用。”既然他可以这样做,为什么就认定他人使用gdb的目的一定就是用来调试bug而不是另有所用呢?本人之所以这样说,这是因为本人正 也是使用gdb主要是用来辅助分析内核代码而不是主要用来调试错误的。这也正就是本文的主题。
世上从不缺少解决问题的答案,缺少的是解决问题的方法。现在,linux的世界里已经不缺少牛书了,将尽一千页一本的满载答案的砖头书接踵而来,但 是渐渐地发现,看书看到后面就忘了前面,回到前面有忘了后面,甚至一个章节还没看完,那个子系统已经被完全重写了。慢慢地,就会怀疑“我是不是真的变老了?真的不行了?”但是我们从没想过:“凭什么我们就如此受制于人?他就能搞懂,而我就不行呢?”。其实,我们需要的是一种重其意而忘其形的根本之道,需 要的是一种兵来将挡,火来水淹的通用解决方法。而绝不是淹没于牛人们的结论中。否则,遇到一个新的问题,就只能埋怨牛人的书还不够厚,以至于没把你需要的东西也包括进去了。牛人一定有一套牛方法,而他在书中不详说,我不认为是他故意“留一手”,而是认为这是对自身觉得习以为常的事物的一种疏忽。牛人的研究 结果其实不是最重要的,他的研究方法和手段才是最重要的事情。而我,也渐渐地发现,调试器能带给我们很多有用的提示,使得我们能不断的寻找到思考的灵感和方向,也使得学习变得非常的有趣性和有目的性。我想,利用调试器辅助源码分析,是不是正是很多牛人正在做的而没有说出来的事情呢?无论答案如何,本人还是 觉得,调试器是个好东西,不要轻易把它搁置在一旁。虽然很多高人也许已经是深安此道,甚至已经不需要它的提示了,但是它依然有益于我等功力尚浅的人。把这种经验和技巧记录下来,让需要这项技巧的人少化时间去摸索,这绝对不是一件坏事。
正是因为这个原因,随着文档慢慢地变大,也更加的觉得文档的题目起得有点不恰当了,题目起作“内核动态分析指南”更恰当点。文档的主旨是利用调试器动态分析内核,调试错误只是这个过程的副产品罢了。不过,这个新的名字实在是不够现在名字“刺眼”,所以也就没有启用它。
说了这么多的废话和出格的话,无非是有两个目的:这个文章慢慢的变得这么长了,如果没有半句的“人”话,没有半句的现实世界中的语句。那估计本人不是变成了机器人,阅读的人也会变成了机器人。顺便借这段文字交交朋友。另一个目的呢,是说不应拘束于工具,工具是死的,人是活的。如果某些工具确能带给我 们某些有益的提示,我们就可以去尝试它,取起优点而舍其糟粕。
引用的原文:
Linus 谈调试器和内核如何发展: http://www.bitscn.com/linux/kernel/200604/7493.html
知识从哪里来
1. 永远不要忘记的三大帮助命令
· XXX -h(xxx–help)
· man -a XXX
· info XXX
2. 如何安装帮助文档
· $ sudo synaptic界面出来后,在“组别”->“文档”选取你要的文档进行安装
· 或$apt-cache search Documentation | grep XXX搜索需要的文档进行安装
3. 从软件/工具的官方网站阅读/下载文档
4. 从irc获取帮助 irc.freenode.net
5. 从邮件列表获取帮助 mailist http://lkml.org/ http://marc.info/
6. 发行版社区文档或社区 https://help.ubuntu.com/community/ http://wiki.ubuntu.org.cn/
7. 利用google搜索文档或阅读他人文章
8. 利用google搜索lkml
http://www.google.cn/advanced_search?hl=zh-CN 网域那里填上lkml.org
9. 获取内核文档
· 源码本身
· 源码中的注释
· 内核源码附带的文档 Documentation
· 相关的教科书
· 论文 免费论文引擎 http://citeseerx.ist.psu.edu/
· 内核子系统的官方网站
· 获取内核源码目录Documentation/DocBook/下已经编译好的书籍
找到最新版本的文档
$ apt-cache search linux-doc
安装最新的文档
$ sudo apt-get installlinux-doc-2.6.24
阅读Documentation/DocBook/下已经编译好的书籍(html格式)
$ firefox/usr/share/doc/linux-doc-2.6.24/html/index.html
10. 买书
11. 书籍最后面的参考书目
12. 文章末尾的参考文章
13. 电子书搜索网站
emule: 只要知道书名,windows下用emule基本可以找到所有的英文版电子书。
但在linux不行,可能是我的设置问题。
http://rapidshare.com/index.html
为什么撰写本文档
todo:学习方法,学习曲线,参考书籍的特点和不足,本文档的任务
内核学习曲线
1.只读书不看源码
参考书籍:Linux KernelDevelopment
2.参考源码读书(读书为主)
参考书籍:understandingthe linux kernel
3.参考书读源码(看源码为主)
参考书籍:情景分析
4.只看源码不/少读书(提交补丁为主)
参考:lkml,main-tree,mm-tree
linux内核分析方法:
按分析的对象分:
1.代码: 分析的对象是源代码
2.数据: 分析的对象是内核运行时产生的数据
按观察对象的状态分:
1.静态: 观察的目标对象是静止不动的
2.动态: 观察的目标对象是动态变化的
所以综合地看,分析方法的种类有:
1.静态代码:
最原始的方式,阅读源代码
2.动态代码:
利用某些工具或手段,动态分析源代码。又分为
a. 利用lxr,cscope, source insight等工具交叉索引源代码
b. 利用git,web-git通过阅读增量patch等形式观察源码的进化
c. 利用调试器跟随内核的运行动态观察内核正在运行的代码片段
3.静态数据:
观察的对象是内核在运行时产生或收集汇总出来的数据。又分为
a. 代码中printk语句打印出来的内核信息
b. 系统出错产生的oops,panic信息
c. 借助systemtap等类似工具提取的内核数据汇总
4.动态数据:
借助内核调试器实时观察内核不断产生的数据
可见内核调试器是最强大的内核分析工具,但它也不是“全功能”的工具。
1. 主要地,本文档聚焦于描述如何利用gdb对内核进行源码级别和汇编级别的观察和调试。
而这种调试的目的有两个:
· 确定bug产生的引入点。这部分内容放于本文档第一部分。
· 配合源码阅读工具(source insight,kscope等),观察内核实时运行的状况,观察内核数据的产生和变化,以及观察各个函数的动态调用关系,从而以一种精确的动态的和验证性的方式来理解内核运作的原理。这部分内容放于本文档第二部分
前者是调试器应用的主要价值,而后者却是本文档的兴趣所在。
2. 因为需要观察用户层和内核层的交互,演示调试工具的全面功能等原因,本文档内容不完全局限于内核层。
3. 另外,为了提供内核调试知识的全面叙述,我们对其他调试工具,其他调试的问题比如检测内存泄露等内容,也会进行说明。此部分内容放于本文档的第三部分。
为什么需要汇编级调试
· 逆向工程的需要
例子1:NT 内核的进程调度分析笔记 http://www.whitecell.org/list.php?id=11
例子2: NT 下动态切换进程分析笔记 http://www.whitecell.org/list.php?id=13
在windows的世界里,内核源码和具体原理是不公开的。但很多牛人就凭一个破烂调试器阅读反汇编代码就能得到内部真相,可见调试器汇编级调试威力之大。但是在linux是源码公开的情况下,就没必要干那样的辛苦活了。但是因为以下原因,汇编级调试还是必要的。
· 汇编比C语言更低层
有时(比如代码优化)情况下,因为C代码经过了编译器的处理,调试器在c源码调试这个级别下给出的信息是无法理解的,甚至看起来是错误的。但是如果直接对 调试器给出的反汇编代码进行分析,就不会受到那类问题的束缚。也就是说,进行汇编级别的调试能最大程度的利用调试器的功能。
· 汇编是C语义的解释
当你对某句C语言不是很理解时,看看编译器是怎么想的,是个很不错的办法。
· 能锻炼汇编源码的阅读能力
另一方面,内核中本来存在很多汇编源代码,进行汇编级调试也是锻炼阅读汇编源码能力的最有效方法。
当然,汇编级调试虽然强大,但代价也是很昂贵。和源码级调试相比,分析汇编代码花的时间要多上几十倍。所以,在源码公开的情况下,应该以源码级调试为主,特殊情况下才需要汇编级调试。
***第一部分:基础知识***
总纲:内核世界的陷阱
也是阅读理解其他任何大型代码会遇到的问题。下面各节的内容都是围绕这些小项展开的。如果有的内容不知所云,先看后面内容,再回头看这里。
[先从其他地方复制过来,等待充实]
源码阅读的陷阱
源码不但是越来越大,更是越来越“***”了。“***”到了就是借助源码交叉索引工具也有它索引不到的地方。所以目前,即使是从源码阅读的角度而不是从调试的角度,只利用阅读工具不借助调试工具的话,源码都无法阅读。
源码“***”到源码解析工具都无法解析的因素有:
1. 汇编源码包括内嵌汇编 可能无法被你的源码阅读工具所解析
2. 汇编代码和C代码之间的调用关系 无法被被源码阅读工具解析
3. 利用函数指针的函数调用 无法被被源码阅读工具解析
4. 宏“假函数” 可能无法被被源码阅读工具解析(SI不能解析,lxr能)
比如page_buffers()。定义是:
#define page_buffers(page) \
({ \
BUG_ON(!PagePrivate(page)); \
((structbuffer_head *)page_private(page)); \
})
5. 利用宏在编译时动态生成的函数体 无法被被源码阅读工具解析
比如fs/buffer.c中有一大批类似函数。比如buffer_unwritten()
定义在buffer_head.h
82 #define BUFFER_FNS(bit, name) \
..省略
91 static inline int buffer_##name(conststruct buffer_head *bh) \
92 { \
93 return test_bit(BH_##bit, &(bh)->b_state); \
94 }
..
130 BUFFER_FNS(Unwritten, unwritten)
这类函数一般是短小的内嵌函数,用gdb调试时都看不出来。只能靠字符搜索再加上一点机灵。
6. 函数/变量的某类c扩展属性标记, 可能导致该函数/变量无法被被源码阅读工具解析
比如static struct vfsmount *bd_mnt __read_mostly;中的bd_mnt
7. 其他语种的保留关键字,可能无法被你的源码阅读工具所解析
如默认配置的SI无法解析struct class,当然,这个问题和内核无关。
但是借助调试器,就能直接而轻易地解决上述源码解析工具难以解决的问题。
代码调试的陷阱
搭建调试环境
gdb调试器的陷阱
1. 宏“假函数”
2. 内嵌函数
3. 代码优化
4. 汇编码
5. 进程切换
6. 中断处理
7. 系统调用
原理理解的陷阱
0. 链接器脚本和make语法
下面这些杂七杂八的文件对内核整体原理的理解起着决定性的作用。
内核中的链接脚本
linux-2.6$ find ./ -name"*lds*"
内核中的重要宏文件
module_param* macros
include/linux/moduleparam.h
*__initcall Macros
include/linux/init.h
内核中的汇编文件
linux-2.6$ find ./ -name "*.S"
内核中的Makefile
linux-2.6$ find ./ -name"Makefile"
内核中的配置文件
linux-2.6$ find ./ -name"*config*"
1. C与汇编代码的相互调用
2. 各子系统间的接口互动
3. 内核的设计思想及其代码编写和运行形式
a) 基于对象的思想
例子:文件系统,设备模型
b) “发布—订阅”模型
例子:notificationchain
建立调试环境
发行版的选择和安装
为什么选debian
[如题] http://www.debian.org/ http://www.emdebian.org/
为什么本人选择debian?因为:引用内容来之www.debian.org
“Debian 计划 是一个致力于创建一个自由操作系统的合作组织。...屁话省略...屁话..N多屁话之后: 当然,人们真正需要的是应用软件,也就是帮助他们完成工作的程序: 从文档编辑,到电子商务,到游戏娱乐,到软件开发。Debian 带来了超过 18733 个 软件包 (为了能在您的机器上轻松的安装,这些软件包都已经被编译包装为一种方便的格式) —这些全部都是 自由 软件。”
原因终于看到了,选择debian是因为本人比较懒,比较笨。而debian正好迎合了我这种人的需求。
1. 它”带来了超过 18733个 软件包”。18733这个数目非常不直观,而且或许是N年前的数据了。我们可以到debian的ftp看看,现在它可供安装的软件和工具达到了5个DVD的容量。难以想象,在这5个DVD容量的工具库中,还会找不到我所想要的东西。
2. debian有一个非常出名的安装包管理机制。你需要做的就是,打开“立新得”软件,然后在一个小方框里写上你需要东西的相关信息,然后再点点一个叫做“搜索”的小方块。接着,debian就会在它5个DVD大的工具库中寻找你想要的工具。在结果返回后,选择好你的工具,再点点一个叫做“应用”的小方块, 过一会,就可以使用你的工具了。
再也没有了“缺少什么什么包”的烦人提示了,一切都这么简单,又这么强大。这,正是我想要的。
debian与ubuntu
[两者区别,版本外号,支持社区,source list等] 1. ubuntu的易用性比debian要好。尤其是中文支持,还有ubuntu国内有活跃的社区。 2. 虽然ubuntu是基于debian的,apt 软件库也能获取到debian的软件,但它毕竟是不同的系统环境,理念不同,对于一些偏门或太旧或太新的软件时,ubuntu往往不支持,安装不了。比 如,gcc-3.4-arm-linux-gnu这个包,发行时间已久,ubuntu下安装不了,但在debian下则可以。http://www.ubuntu.com/community/ubuntustory/debian
如不特别说明,本文档所有命令都是在ubuntuHardy Heron8.04版本 和debian testing版本下的操作。
从0安装debian
[如果想领教古典linux相对于windows的特色,请安装一次debian吧。尽管和以前比,已经很智能了。但安装了debian,选了中文环境, 发现汉字都是歪歪倒倒的。而且没有汉字输入法,装了汉字输入法后,却用不了。不知道是我笨还是程序有bug.所以不得不用英文写下本烂文,怕把安装过程给 忘了。需要翻译回中文]
How to install andconfigure a debian system from zero
1.install the systemwith one CD
Download CD iso filefrom debian official website, and burn it into a CD. Note that, we can justdownload the first CD iso but not DVDs or the whole serials of CDs, because thefirst CD has already contained all the basis components of dedian system andmany other most common applications. We can use the first CD to install debiansystem, and then to install some other needed programs from it if needed. Inthis way, you can save much time spent on touching many inrelatived things.
2.installapplication & tool from CD
ou can install somecommon apllications from the CD with the following commnad: apt-get installexpected-application. Why can we do that without any more configuration? Why isit not need to has a ability to access internet? Well, Let’slook at the file named sourse.list which idenifying where to get software’s pakage?? deb cdrom:[Debian GNU/Linux testing _Lenny_ - OfficialSnapshot i386 CD Binary-1 20080605-15:01]/ lenny main It means that system tryto get somethig from your CD, so obviously that you can get some the mostcommon but not all the tools available in debian official apllicationrepository.
3.try to access theinternet
Thank to the firstCD, we can do that easily. Fist, install the tool ppp contained in CD and its’configuration tool pppoeconfig. All these steps are described in file ADSL(PPPOE)接入指南.txt
4.search any usefulinformation through the internet
now, we have built abase debian system, but it is too simple. I want to do some some thing, forexample, to chat with some other people with pidgin, but it is not contained inthe first CD, which just downloaded by you. And you may want to search somehelps with google,etc. Just to do it, google is a most useful tool.
5.search theinternet updating source
I think you have getmuch thing through the google. But the most important thing is to get a availableupdate source for your system, and change the source.list–thatis /etc/apt/source.list. Now, I have got a good one, and it seems good. Don’t forget to turn on the security entry in the orgion filesource.list. That file looks like following after my updataion:
#deb cdrom:[Debian GNU/Linux testing_Lenny_ - Official Snapshot i386 CD Binary-1 20080605-15:01]/ lenny main
deb http://ftp.debian.org/debian/ lennymain contrib non-free
deb http://security.debian.org/lenny/updates main
deb-src http://security.debian.org/lenny/updates main
You should note thatthe internet address is debian office’s, but It takessome while to get it. And my searching tool is google. :) Oh, we shoul run acommand to update the new configuration to system before using it, don’t ferget: apt-get update
6.get helpfrom IRC
Well, we havealready been able to get some applications or tools from internet with commandapte-get or wget,etc.. But I think the first thing to do is to get and installa very valuable tool named pidgin which can bring you into IRC world.Because Many experiance and kind person live in channel #debian ofirc.freenode.net. You can get help from it very quickly. How to configurepidgin? Sorry, I don’t like to answer such a problem , please just to googleit or try it by yourselft. I am not so kind as some guys livingin IRC : )
7.get and installsynaptic
If you ever usedubuntu, you should agree that synaptic is good tool to update you system. Itcan save you much time of searching tools, typing commnad, or managing thedownloaded tools. But Unfortunately, such a important tool is not installed inthe default system, and it is not contained in the first CD. So, We can just toget it with command“apt-get install synaptic”. Afterdoing that successfully, I don’t want to type thatcommand anymore. It’s so tedious to me.
8.get more toolswith the help of synaptic
synaptic is my GODin the linux world. Without it, I will become crazy. But now, I have owned it,so I can fly very freely in the internet sky. Just to search any tools and toupdate your system. And now, the CD used to install debian can be discarded, ifyou will never reinstall or rescure the system with it in future.
Now, the sun hasraise up, and you have found the road to reback to civilization. Why? Just toask your google and synaptic. :)
debian重要命令
[来源]《APT and Dpkg 快速参考表》 http://i18n.linux.net.cn/others/APT_and_Dpkg.php
Apt 不止是apt-get
http://www.erwinwang.com/node/10
中文环境设置
debian的键盘设置更改
默认安装的debian,键盘的设置可能有问题。比如“|”打不出来。值得一提的是,这个设置甚至是和qemu的monitor模式相关联的。也就是说,qemu下有的字符也打不出来。如果有这个问题,按下面步骤设置
System→Preferences→Keyboard→Layouts
然后通过“Add”增加China,并设置它为默认,或者同时把其他的删除掉。
英文Locale下使用中文输入法
说明,中文环境比英文环境有很多缺点。比如编译时编译器的提示都给汉化了,有如,minicom的中文汉化界面是错乱的,而且minicom无法设置。本 人一般是英文环境+中文输入法。先安装好好中文环境,系统中就有了中文输入法和其他一些和中文有关的东西。然后转到英文环境下,按照下面做法更改scim 的配置文件即可。
编辑/etc/gtk-2.0/gtk.immodules(如果存在的话) 或者 /usr/lib/gtk-2.0/2.10.0/immodule-files.d/libgtk2.0-0.immodules 文件,在xim 的 local 增加 en 也就是说:
"xim" "X InputMethod" "gtk20" "/usr/share/locale" "ko:ja:th:zh"
改成:
"xim" "X InputMethod" "gtk20" "/usr/share/locale""en:ko:ja:th:zh"
注意,一定要重启一下机器。
pdf乱码的解决
$sudo apt-get installxpdf-chinese-simplified xpdf-chinese-traditional poppler-data
参考:
http://wiki.ubuntu.org.cn/PDF%E6%96%87%E6%A1%A3%E7%9A%84%E4%B9%B1%E7%A0%81%E9%97%AE%E9%A2%98
建立编译环境
$ sudo apt-get install build-essentialautoconf automake1.9 cvs subversion libncurses5-dev git rar unrar p7zip-fullcabextract
其余的根据出错的提示,利用“立新得”搜索,然后进行安装。没有“立新得”界面程序的可以在终端下利用以下命令来搜索和安装。
$ sudo apt-get update
$ apt-cache search XXX
$ sudo apt-get install XXX
双硬盘系统切换设置的grub设置,私人备忘用
title Microsoft Windows XP Professional
root (hd1,0)
savedefault
makeactive
map (hd0) (hd1)
map (hd1) (hd0)
chainloader +1
安装交叉编译工具
交叉编译工具下载网址
下面是几个交叉编译工具下载网址,需要手动安装时,对比一下编译器的名称可以找到合适的下载地址。debian维护有自己的已经打包成.deb形式安装包,在debian软件库中。
http://www.codesourcery.com/gnu_toolchains/arm/download.html
(据说是arm公司推荐的)
Download Sourcery G++ Lite Edition forARM
Target OS Download
EABI Sourcery G++ Lite 2008q1-126
All versions...
uClinux SourceryG++ Lite 2008q1-152
All versions...
GNU/Linux Sourcery G++ Lite 2008q1-126
All versions...
SymbianOS Sourcery G++ Lite 2008q1-126
All versions...
到底是选EABI还是GNU/LINUX呢?应该是后者....
点GNU/LINUX的连接进去,可看到
Download MD5 Checksum
IA32 GNU/Linux Installer 93eee13a08dd739811cd9b9b3e2b3212
IA32 Windows Installer fac5b0cee1d9639c9f15e018e6d272ad
Documentation
Title Format
Assembler (PDF) PDF
Binary Utilities (PDF) PDF
C Library (GLIBC) (PDF) PDF
Compiler (PDF) PDF
Debugger (PDF) PDF
Getting Started Guide (PDF) PDF
Linker (PDF) PDF
Preprocessor (PDF) PDF
Profiler (PDF) PDF
Advanced Packages
Expert users may prefer packages inthese formats.
Download MD5 Checksum
IA32 GNU/Linux TAR 4f11b0fa881864f220ab1bd84666108b
IA32 Windows TAR ed6d25fd68301e728a1fba4cd5cb913f
Source TAR 2db28fb2aa80134e7d34d42b7039d866
名字标识不是很明显,进去看才知道。比如,IA32 GNU/Linux Installer对应的安装包
名字叫arm-2008q1-126-arm-none-linux-gnueabi.bin
为什么有个none?迷茫中..
---------------------------------
http://ftp.snapgear.org:9981/pub/snapgear/tools/arm-linux/
[DIR] Parent Directory 30-Sep-2003 15:44 -
[ ] arm-linux-tools-20031127.tar.gz 26-Nov-2007 16:56 141M
[ ] arm-linux-tools-20051123.tar.gz 24-Nov-2005 00:50 228M
[ ] arm-linux-tools-20061213.tar.gz 13-Dec-2006 13:31 230M
[ ] arm-linux-tools-20070808.tar.gz 30-Nov-2007 03:21 271M
[ ] binutils-2.16.tar.gz 16-Nov-2005 15:44 15.6M
[ ] binutils-2.17.tar.gz 06-Dec-2007 10:24 17.4M
[ ] build-arm-linux-3.4.4 02-Aug-2006 14:32 6k
[ ] build-arm-linux-4.2.1 30-Jul-2008 10:13 7k
[ ] elf2flt-20060707.tar.gz 17-Jan-2008 22:23 101k
[ ] elf2flt-20060708.tar.gz 30-Jul-2008 10:14 110k
[ ] gcc-3.4.4.tar.bz2 16-Nov-2005 15:39 26.3M
[ ] gcc-4.2.1.tar.bz2 06-Dec-2007 10:11 42.0M
[ ] genext2fs-1.3.tar.gz 03-Sep-2003 10:23 19k
[ ] glibc-2.3.3.tar.gz 16-Nov-2005 15:49 16.7M
[ ] glibc-2.3.6.tar.gz 06-Dec-2007 10:39 17.9M
[ ] glibc-linuxthreads-2.3.3.tar.gz 16-Nov-2005 15:49 303k
[ ] glibc-linuxthreads-2.3.6.tar.gz 06-Dec-2007 10:39 320k
--------------------------
http://www.handhelds.org/download/projects/toolchain/
[DIR] Parent Directory -
[ ] README 28-Jul-2004 17:37 788
[DIR] archive/ 28-Jul-2004 17:34 -
[ ] arm-linux-gcc-3.3.2.tar.bz2 03-Nov-2003 10:23 71M
[ ] arm-linux-gcc-3.4.1.tar.bz2 29-Jul-2004 14:01 41M
[DIR] beta/ 28-Jul-200417:36 -
[ ] crosstool-0.27-gcc3.4.1.tar.gz 28-Jul-2004 17:21 2.0M
[ ] gcc-build-cross-3.3 31-Oct-200315:43 5.1K
[DIR] jacques/ 24-Jul-2001 18:45 -
[ ] kernel-headers-sa-2.4.19-rmk6-pxa1-hh5.tar.gz 12-Mar-2003 17:42 4.7M
[DIR] monmotha/ 13-Aug-2002 17:54 -
[DIR] osx/ 14-Dec-2003 11:45 -
[DIR] pb/ 22-Nov-2002 20:10 -
[DIR] source/ 18-Mar-2004 16:12 -
------------------------------------
http://ftp.arm.linux.org.uk/pub/armlinux/toolchain/
[DIR] Parent Directory -
[ ] Oerlikon-DevKit-XScalev2.tar.gz 07-Feb-2003 22:30 3.7K
[ ] cross-2.95.3.tar.bz2 20-Jul-2001 21:12 35M
[ ] cross-3.0.tar.bz2 20-Jul-2001 22:27 39M
[ ] cross-3.2.tar.bz2 23-Aug-2002 11:04 81M
[ ] cross-3.2.tar.gz 23-Aug-2002 10:01 93M
[DIR] src-2.95.3/ 14-Jan-2002 17:52 -
[DIR] src-3.2/ 23-Aug-2002 10:53 -
--------------------------------------------
http://linux.omap.com/pub/toolchain/
[DIR] Parent Directory -
[ ] obsolete-gcc-3.3.2.t..> 15-May-2004 12:18 76M
---------------------------
http://www.uclinux.org/pub/uClinux/arm-elf-tools/
To install the Linux binaries, login asroot and run "sh ./XXX-elf-tools-20030314.sh".
m68k-elf-20030314/arm-elf-20030314
Get the m68k binaries or the ARM binaries. The source is here.
m68k-elf-20020410/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.
m68k-elf-20020218/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.
m68k/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.
You can also get Bernhard Kuhn's RPMs here.
m68k-elf-20010716
Get the binaries here and the source from here.
m68k-elf-20010712
Get the binaries here and the source from here.
m68k-elf-20010610
Get the binaries here and the source from here.
m68k-elf-20010228
The binaries are in two files, the compilers and the g++ headers. Thesource is here.
安装arm-linux-gnueabi-XXX工具集
debian有自己维护的一套交叉编译工具集
[参考]http://www.emdebian.org/tools/crosstools.html
工具库: http://www.emdebian.org/debian/pool/main/
步骤:
1. 往/etc/apt/sources.list文件加入下面软件源
deb http://buildd.emdebian.org/debian/unstable main
deb-srchttp://buildd.emdebian.org/debian/ unstable main
deb http://buildd.emdebian.org/debian/testing main
deb-srchttp://buildd.emdebian.org/debian/ testing main
然后:
安装 emdebian-archive-keyring package
$ sudo apt-get installemdebian-archive-keyring
更新
$ sudo apt-get update
2. 安装交叉编译器
$ sudo apt-get install libc6-armel-crosslibc6-dev-armel-cross binutils-arm-linux-gnueabi gcc-4.3-arm-linux-gnueabig++-4.3-arm-linux-gnueabi
注意,在ubuntu8.04下,只能安装4.2版。把上面文字中的4.3全部换为4.2即可。
3. 安装交叉调试器
$sudo apt-get installgdb-arm-linux-gnueabi
注意:
a. 安装时使用名称:gdb-arm-linux-gnueabi,调用时使用命令名是:arm-linux-gnueabi-gdb
b. ubuntu下,arm-linux-gnueabi-gdb和gdb有冲突。
解决方法:
需要使用arm-linux-gnueabi-gdb时先卸载gdb,记下卸载gdb时与gdb一起被卸载的软件名,然后安装arm-linux- gnueabi-gdb。 想换回gdb时,在反操作。apt-install remove arm-linux-gnueabi-gdb 然后apt-get install gdb以及之前和gdb一起被卸载包。可以写个脚本自动完成这些操作。本人环境下的脚本是:
脚本1.install-armgdb.sh
#! /bin/sh
sudo apt-get remove gdb
sudo apt-get install gdb-arm-linux-gnueabi
脚本2. install-gdb.sh
#! /bin/sh
sudo apt-get removegdb-arm-linux-gnueabi
sudo apt-get install apport apport-gtkapport-qt bug-buddy cgdb gdb python-apport xxgdb
什么是EABI
答: 来自AAPCS
ABI:Application Binary Interface:
1).The specifications to which an executable must conform in order to execute in aspecific execution environment. For example, the Linux ABI for the ARMArchitecture.
2).A particular aspect of the specifications to which independently producedrelocatable files must conform in order to be statically linkable andexecutable. For example, the C++ ABI for the ARM Architecture, the Run-time ABIfor the ARM Architecture, the C Library ABI for the ARM Architecture.
ARM-based …based on the ARM architecture…
EABI:An ABI suited to the needs of embedded (sometimes called free standing)applications.
参考:
ABI/EABI/OABI http://blog.csdn.net/hongjiujing/archive/2008/07/21/2686556.aspx
Re: 关于kernelARM_EABI http://zh-kernel.org/pipermail/linux-kernel/2008-January/002793.html
Why ARM’sEABI matters http://www.linuxdevices.com/articles/AT5920399313.html
Why switch toEABI? http://www.applieddata.net/forums/topic.asp?TOPIC_ID=2305
ArmEabiPort http://wiki.debian.org/ArmEabiPort
安装arm-elf-XXX 工具集
注:arm-elf-XXX 工具集是用于uclinux的
1. 依据要求搜索下载相应的arm-elf-tools安装包。比如arm-elf-tools-20030315.sh
2. 安装:$ ./arm-elf-tools-20030315.sh
3. 如果,该安装包年代过老,比如arm-elf-tools-20030315.sh,会出现下面的错误提示 “tail: 无法打开“ 43” 读取数据: 没有那个文件或目录。”。 这时需要修改安装包源码。方法:vi arm-elf-tools-20030315.sh, 搜索tail,在它后面加 -n .比如 把tail ${SKIP} ${SCRIPT} | gunzip |tar xvf -改成如下:tail -n ${SKIP} ${SCRIPT} | gunzip | tarxvf -
4.如何卸载已安装的arm-elf-tools? 答,重新安装一次,注意看终端提示。或直接viarm-elf-tools-20030315.sh,看脚本的内容,
bin工具集的使用
[该怎么称呼这类工具?待详述]
arm-elf-addr2line arm-elf-elf2flt arm-elf-gdb arm-elf-objdump arm-elf-size
arm-elf-ar arm-elf-flthdr arm-elf-ld arm-elf-protoize arm-elf-strings
arm-elf-as arm-elf-g++ arm-elf-ld.real arm-elf-ranlib arm-elf-strip
arm-elf-c++ arm-elf-gasp arm-elf-nm arm-elf-readelf arm-elf-unprotoize
arm-elf-c++filt arm-elf-gcc arm-elf-objcopy arm-elf-run
arm-linux-gnueabi-addr2line arm-linux-gnueabi-g++ arm-linux-gnueabi-gprof arm-linux-gnueabi-readelf
arm-linux-gnueabi-ar arm-linux-gnueabi-g++-4.2 arm-linux-gnueabi-ld arm-linux-gnueabi-size
arm-linux-gnueabi-as arm-linux-gnueabi-gcc arm-linux-gnueabi-nm arm-linux-gnueabi-strings
arm-linux-gnueabi-c++filt arm-linux-gnueabi-gcc-4.2 arm-linux-gnueabi-objcopy arm-linux-gnueabi-strip
arm-linux-gnueabi-cpp arm-linux-gnueabi-gdb arm-linux-gnueabi-objdump
arm-linux-gnueabi-cpp-4.2 arm-linux-gnueabi-gdbtui arm-linux-gnueabi-ranlib
如何获取这些工具的命令选项? 看章节“知识从哪里来” 一般是用命 xxxxxx–help就能得到简单的命令选项列表
下载arm-linux-gnueabi-手册地址 http://www.codesourcery.com/gnu_toolchains/arm/portal/release324
然后搜索”arm”,便能找到处理器相关的特殊命令选项
arm-linux-gnueabi-gcc
查看arm处理器相关的编译选项
$ viarch/arm/Makefile
阅读Makefile文件,并联系源码根目录下的.config文件,便能知道arm-linux-gnueabi-gcc用了哪些编译选项。再到手册中查找,便能知道这些选项是干什么用的,但手册中说的不是很详细。另外查找有用解释的方法的是,利用make ARCH=armCROSS_COMPILE=arm-linux-gnueabi- menuconfig,找到与命令选项有关联的CONFIG_XXX的菜单项,看它的帮助说明.比如
$ vi arch/arm/Makefile
....
ifeq ($(CONFIG_AEABI),y)
CFLAGS_ABI :=-mabi=aapcs-linux -mno-thumb-interwork
else
CFLAGS_ABI :=$(callcc-option,-mapcs-32,-mabi=apcs-gnu) $(call cc-option,-mno-thumb-interwork,)
endif
..
再查看CONFIG_AEABI的帮助文档 $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig 找到CONFIG_AEABI相关的菜单,看它的帮助文档,便能知道选项-mabi=aapcs-linux-mno-thumb-interwork的整体效果怎样的。
┌───────────────────── Use the ARM EABI to compile the kernel──────────────────────┐
│ CONFIG_AEABI: │
│ │
│ This option allows for the kernel to be compiled using the latest │
│ ARM ABI (aka EABI). This isonly useful if you are using a user │
│ space environment that is also compiled with EABI. │
│ │
│ Since there are major incompatibilities between the legacy ABIand │
│ EABI, especially with regard to structure member alignment, this │
│ option also changes the kernel syscall calling convention to │
│ disambiguate both ABIs and allow for backward compatibilitysupport │
│ (selected with CONFIG_OABI_COMPAT). │
│ │
│ To use this you need GCC version 4.0.0 or later. │
│ │
│ Symbol: AEABI [=n] │
│ Prompt: Use the ARM EABI to compile the kernel │
│ Defined atarch/arm/Kconfig:554 │
│ Location: │
│ -> Kernel Features
arm-linux-gnueabi-gcc的主要编译选项有如下几个。但是在编译内核时,这些选项是不需要手工去写的,而是通过make menuconfig生成包含了编译选项配置信息的.config文件。在make编译内核时,再利用Makefile文件中的规则结合.config文 件提取出那些选项。
太多了,手册吧
arm-linux-gnueabi-gdb
注意它的默认选项设置
$ arm-linux-gnueabi-gdb
(gdb) show arm
abi: The current ARM ABI is "auto" (currently "APCS").
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently"fpa").
(gdb)
但是,如果如果在命令后有参数vmlinux的话,它会自动识别出内核的abi,从而自动设置了gdb的abi。比如,在编译内核时,如果选了CONFIG_AEABI,则gdb的提示如下
$ arm-linux-gnueabi-gdb vmlinux
...
(gdb) show arm
abi: The current ARM ABI is "auto" (currently"AAPCS"). <--注意
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently"softvfp").
qemu的使用
参考手册
http://bellard.org/qemu/user-doc.html
http://wiki.debian.org.tw/index.php/QEMU
http://www.h7.dion.ne.jp/~qemu-win/
邮件列表
http://lists.gnu.org/archive/html/qemu-devel/
参考文章
“QEMU安装使用全攻略” http://forum.ubuntu.org.cn/viewtopic.php?p=248267&sid=f4e95025bdaf6a24a218315d03ad9933
[补充命令]引用自http://bbs.chinaunix.net/viewthread.php?tid=779540
安装过程中,要求换盘:
在qemu中按ctrl+alt+2切换到qemu monitor模式输入?或help可以查看可用命令及使用说明。
(在其他版本的qemu中,运行qemu加载OS后,这个shell就会自动变成qemu monitor模式)
change device filename -- change aremovable media
看来它就是用来换盘的了 : change cdrom /rhel4/EL_disc2.iso
切换回安装界面ctrl+alt+1
monitor下还有几个常用的命令:
savevm filename 将整个虚拟机当前状态保存起来
loadvm filename 恢复 (最初我没用change换盘时,就是先savevm->重新运行qemu->loadvm)
sendkey keys 向VM中发送按键,例如你想在虚拟机里切换到另一个终端,按下了ctrl-alt-F2
不幸的是,切换的却是你的主系统,所以就需要用 sendkey了 sendkeyctrl-alt-f2
还有其他几个命令,自己看看啦。
经过N久终于装好了,现在可以启动试试:
[root@LFS distro]#qemu redhat.img-enable-audio -user-net -m 64
-user-net 相当于VMware的nat,主系统可以上,虚拟机就可以
-m 64 使用64M内存,缺省下使用128M
ctrl-alt-f 全屏
ctrl-alt 主机/虚拟机鼠标切换
qemu还有一些其他参数,输入qemu可以查看其相关说明
initrd.img的原理与制作
[扩展,原理,相关命令。下面的skyeye可能需要这部分知识]
“Linux2.6 内核的 Initrd 机制解析” http://www.ibm.com/developerworks/cn/linux/l-k26initrd/
“Introducinginitramfs, a new model for initial RAM disks” http://www.linuxdevices.com/articles/AT4017834659.html
””深入理解 Linux 2.6 的 initramfs 機制 (上)“ http://blog.linux.org.tw/~jserv/archives/001954.html
MKINITRAMFS http://www.manpage.org/cgi-bin/man/man2html?8+mkinitramfs
安装与使用
$ sudo apt-get install initramfs-tools
$ mkinitramfs /lib/modules/2.6.26/ -oinitrd.img-2.6.26
x86虚拟调试环境的建立
参考
“debugging-linux-kernel-without-kgdb” http://memyselfandtaco.blogspot.com/2008/06/debugging-linux-kernel-without-kgdb.html
“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html
“透過虛擬化技術體驗 kgdb (1)” http://blog.linux.org.tw/~jserv/archives/002045.html
基于qemu和内核内置kgdb
缺点:相对于下节的“基于qemu和qemu内置gdbstub”,这个方法配置麻烦。
优点:真机远程调试时只能使用内置kgdb这个方法。
[等待扩展,,,,]
终极参考
“Using kgdb and thekgdb Internals” http://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/index.html
参考文章
“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html
基于qemu和qemu内置gdbstub
· 参考文章
“Debugging LinuxKernel Without KGDB Patch (Qemu + GDB)” http://memyselfandtaco.blogspot.com/2008/06/debugging-linux-kernel-without-kgdb.html
· 优缺点
优点:相对上节,优点是操作简单,几乎不需要什么配置
缺点:真机的远程调试,就只能利用内核的内置kgdb了
说明:
如果长时间调试固定版本的内核,采取下面的把调试用内核安装的虚拟机内部就可以了。但是如果是要频繁地更换新内核或修改被调试内核,就需要采取把内核挂在虚拟机外部的形式。也就是用 -kernel在虚拟机外面挂个内核, 再利用-append传递起内核启动参数等。[待研究]
[太概过了,待扩展...]
· 调试用内核的安装过程:
1. 利用qemu安装一个系统.
2. 在真机中配置并编译一个用于安装到虚拟系统中的新内核,注意配置时的选择
*配置和启动
1. 内核选项
同时,为了能在系统运行时中断系统并出发远程 gdb,必须打开内核 Magic Sys-Rq键选项:[后记,没实验去掉会怎样,估计没影响]
CONFIG_MAGIC_SYSRQ=y
打开内核符号调试:
CONFIG_DEBUG_INFO=y
3. 在真机下编译好虚拟机新内核的源码
4. 结束qemu,用以下命令在真机上挂载虚拟硬盘。然后把编译好的整个源码目录都拷贝到挂载好的虚拟硬盘上(真机上保留一份源码)。
$ sudo mount -o loop,offset=32256debian.img /mnt
拷贝完后,在真机上卸载虚拟硬盘
$ sudo umount /mnt
5.启动虚拟机,进入旧系统,在新内核源码根目录下用以下命令给qemu的虚拟系统安装一个新的内核
拷贝模块
$ make modules_install
安装内核
$ make install
制作initrd.img
$ cd /boot
$ mkinitramfs /lib/modules/2.6.26/ -oinitrd.img-2.6.26
检查/boot/grub/menu.lst文件内容是否妥当
6.用以下命令重启虚拟系统,并选择进入新系统,确认新系统是否安装成功。
$ shutdown -r now
· 调试:
1. 在真机新内核源码目录下建立一个文件 .gdbinit内容是
target remote localhost:1234
b start_kernel
#c
注意我把c注释掉是因为ddd和gdb有切换的需要。见”gdb技巧”
2. 用以下命令启动虚拟机
qemu -hda debian.img -cdrom../debian-testing-i386-CD-1.iso -m 500 -S -s
3. 在真机新内核源码目录下运行
gdb ./vmlinux
[实验记录]
实验过了,.config中不选择kgdb,利用qemu照样能调试。也不能调试start_kernel以前的代码。比如head_32.S中的代码。
CONFIG_HAVE_ARCH_KGDB=y
# CONFIG_KGDB is not set
但是不知CONFIG_HAVE_ARCH_KGDB是在menuconfig菜单的哪里。想试试把这项去了qemu还能不能调试。
经测试,取消CONFIG_HAVE_ARCH_KGDB后,qemu也能进行调试。情况不变。看来qemu能完全脱离内核中的kgdb就能调试内核。
· 调试截图
步骤2:
XXX@ubuntu:/new/myqemu/debian-x86$ qemu-hda debian.img -cdrom ../debian-testing-i386-CD-1.iso -m 500 -S -s
步骤3:
由下图我们注意到:“基于qemu和qemu内置gdbstub”这个方法的调试,最早只能从函数start_kernel () 开始进行。
内核在start_kernel ()之前的初始化过程就无法观察了。这就是这个方法的最大缺点。但下节利用skyeye调试arm-linux的
方法就可以从第一个机器指令开始进行。
XXX@ubuntu:/storage/myqemu/new/linux-2.6.26$gdb ./vmlinux
GNU gdb 6.8-debian
Copyright (C) 2008 Free SoftwareFoundation, Inc.
License GPLv3+: GNU GPL version 3 orlater <http://gnu.org/licenses/gpl.html>
This is free software: you are free tochange and redistribute it.
There is NO WARRANTY, to the extentpermitted by law. Type "showcopying"
and "show warranty" fordetails.
This GDB was configured as"i486-linux-gnu"...
0x0000fff0 in ?? ()
Breakpoint 1 at 0xc037f5ca: fileinit/main.c, line 535.
(gdb) c
Continuing.
Breakpoint 1, start_kernel () atinit/main.c:535
535 {
(gdb)
调试示意图:
给sys_read下断点
(gdb) b sys_read
Breakpoint 2 at 0xc017585e: filefs/read_write.c, line 360.
(gdb)
用快捷键 ctrl+x+2打开tui,并按c继续运行,而后拦截到sys_read
┌──fs/read_write.c────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│354 { │
│355 struct file*file; │
│356 ssize_t ret =-EBADF; │
│357 intfput_needed; │
│358 │
│359 file =fget_light(fd, &fput_needed); │
B+>│360 if (file) { │
│361 loff_tpos = file_pos_read(file); │
│362 ret =vfs_read(file, buf, count, &pos); │
│363 file_pos_write(file, pos); │
│364 fput_light(file, fput_needed); │
│365 } │
│366 │
│367 return ret; │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc017585a <sys_read> push %ebp │
│0xc017585b <sys_read+1> mov %esp,%ebp │
│0xc017585d <sys_read+3> push %esi │
B+>│0xc017585e<sys_read+4> mov $0xfffffff7,%esi │
│0xc0175863 <sys_read+9> push %ebx │
│0xc0175864 <sys_read+10> sub $0xc,%esp │
│0xc0175867 <sys_read+13> mov 0x8(%ebp),%eax │
│0xc017586a <sys_read+16> lea -0xc(%ebp),%edx │
│0xc017586d <sys_read+19> call 0xc0175f65<fget_light> │
│0xc0175872 <sys_read+24> test %eax,%eax │
│0xc0175874 <sys_read+26> mov %eax,%ebx │
│0xc0175876 <sys_read+28> je 0xc01758b1<sys_read+87> │
│0xc0175878 <sys_read+30> mov 0x24(%ebx),%edx │
│0xc017587b <sys_read+33> mov 0x20(%eax),%eax │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: sys_read Line: 360 PC: 0xc017585e
(gdb) c
Continuing.
Breakpoint 2, sys_read (fd=3,buf=0xbfc781a4 "", count=512) at fs/read_write.c:360
(gdb)
arm虚拟调试环境的建立
利用qemu
利用qemu安装debian linux
目标:
本节在qemu虚拟机上安装一个基于arm的“桌面“系统,可以有X桌面,该虚拟系统能利用apt-get从debian的软件库下载数不完的用交 叉编译已经编译好的arm下的程序和工具。除了虚拟处理器是arm外,简直就是PC机。可以进行应用程序的本机(在虚拟机内进行)调试。但是,本人装的时 候,如果选了安装桌面环境,内核就启动失败,好像是提示文件系统出错。[成功的麻烦把过程贴出来]
过程是:
Debian on anemulated ARM machine http://www.aurel32.net/info/debian_arm_qemu.php
下面是过程的提炼步骤,方便查看。
1.创建虚拟硬盘
$ qemu-img create -fqcow hda.img 40G
2.下载必要文件
$ wgethttp://people.debian.org/~aurel32/arm-versatile/vmlinuz-2.6.18-6-versatile
$ wgethttp://people.debian.org/~aurel32/arm-versatile/initrd.img-2.6.18-6-versatile
$ wgethttp://ftp.de.debian.org/debian/dists/etch/main/installer-arm/current/images/rpc/netboot/initrd.gz
2.安装系统
qemu-system-arm -M versatilepb -kernelvmlinuz-2.6.18-6-versatile -initrd initrd.gz -hda hda.img -append"root=/dev/ram"
在安装过程中,为了节省时间,在这步choose a mirror of the debian archive
选http回车 ;
debian archive mirror country 选taiwan 回车;
debian archive mirror 选ftp.tw.debian.org
安装好基本系统后,不要选择安装Desktop environment
安装完成后,它提示你把光盘拿掉并重启系统时,终止掉qemu。并用下一步的命令启动qemu.不要回车,否则又重新安装。
3. 第一次启动系统
$ qemu-system-arm -M versatilepb -kernelvmlinuz-2.6.18-6-versatile -initrd initrd.img-2.6.18-6-versatile -hda hda.img-append "root=/dev/sda1"
4. 把旧的内核,intrd.img制作工具安装到虚拟机的系统内(操作在虚拟机内)
$ apt-get install initramfs-tools
$ wget http://people.debian.org/~aurel32/arm-versatile/linux-image-2.6.18-6-versatile_2.6.18.dfsg.1-18etch1+versatile_arm.deb
$ su -c "dpkg -ilinux-image-2.6.18-6-versatile_2.6.18.dfsg.1-18etch1+versatile_arm.deb"
5.其他更多的玩法请看原文http://www.aurel32.net/info/debian_arm_qemu.php
参考:
Debian ARM Linux onQemu
http://909ers.apl.washington.edu/~dushaw/ARM/#SYSTEM
Running Linux forARM processors under QEMU
http://iomem.com/index.php?archives/2-Running-Linux-for-ARM-processors-under-QEMU.html&serendipity[entrypage]=2
Debian on anemulated ARM machine
http://www.aurel32.net/info/debian_arm_qemu.php
利用qemu安装能进行内核调试的系统
[暂时没法子,期待扩展。下面这个例子可以,但没尝试。估计这个方法与下节的利用skyeye的方法相比,没有优势。因为这个方法可能也是不能进行全程调试。但是下面网站的资料还是有一定参考价值的。]
使用qemu-jk2410做為學習環境:
另外:看看下面这个站点,
Firmware Linux: http://landley.net/code/firmware/
利用skyeye
skyeye虚拟机的内核调试
相对于利用qemu的方式,用skyeye虚拟机调试内核有个很重要的
优点是:
调试可以从第一条机器指令开始。这对研究系统启动过程提供了极大的便利。
skyeye的安装与使用
该文非常好,好像没啥要扩充的
SkyEye硬件模拟平台,第二部分:安装与使用
http://www.ibm.com/developerworks/cn/linux/l-skyeye/part2/
SkyEye UserManual http://www.skyeye.org/wiki/UserManual
http://skyeye.wiki.sourceforge.net/
参考文档:
Linux-2.6.20 on XXXplatform
http://skyeye.wiki.sourceforge.net/Linux
uClinux-dist-20070130on XXX platform
http://skyeye.wiki.sourceforge.net/uClinux
http://www.linuxfans.org/bbs/thread-182101-1-1.html
安装:
1. 安装主程序
在ubuntu系统能进行在线安装,但版本是v1.2,不是最新的
$sudo apt-get install skyeye
2. 测试套件
测试套件下载后解压开即可
地址:http://sourceforge.net/project/showfiles.php?group_id=85554
快速试玩
目的:
尽可能快的成功运行一个arm linux虚拟机。如果您化了很长时间也无法编译出一个能运行的内核,或写不出一个恰当的skyeye.conf时,在你的热情受到打击之前,我想这节是你急需的。
操作步骤:
1.依照上节说明安装好主程序,下载并解压好测试套件
2.进入测试套件的目录 skyeye-testsuite-1.2.5/linux/s3c2410/s3c2410x-2.6.14
可以看到有三个文件initrd.imgskyeye.conf vmlinux
3.运行虚拟机
$skyeye -e vmlinux
注意下面的提示,说明平时要注意在启动命令前加上sudo
NOTICE: you should be root at first !!!
NOTICE: you should inmod linux kernelnet driver tun.o!!!
NOTICE: if you don't make device node,you should do commands:
NOTICE: mkdir /dev/net; mknod /dev/net/tun c 10 200
NOTICE: now the net simulation function cannot support!!!
NOTICE: Please read SkyEye.README andtry again!!!
4.可以看到,一个2.6.14版本的linux跑起来了,还带有一个lcd.
快速配置能调试的环境
参考:
http://skyeye.wiki.sourceforge.net/linux_2_6_17_lubbock
环境条件:
1. ubuntu hardy 8.04
2. 安装了debian提供的交叉编译工具套件 arm-linux-gnueabi- (4.2版本)
目标:
这小节能得到基于pxa平台(类似s3c2410,也基于arm核心)的linux2.6.20内核的虚拟系统,具备调试功能。相比“基于qemu 和qemu内置gdbstub”该节,利用skyeye的调试有那节所没有的优点:调试时可以从内核运行的第一条指令开始[这就是模拟硬件调试?]。
参考手册:
XScale PXA250开发手册 http://soft.laogu.com/download/intelpxa250.pdf
ARMv5 体系结构参考手册 http://www.arm.com/community/university/eulaarmarm.html
操作步骤:
1. 下载linux-2.6.20(由于交叉编译器太新,如果利用linux-2.6.17则编译不过)
2. 修改文件include/asm-arm/arch-pxa/memory.h第18行
#define PHYS_OFFSET UL(0xa0000000)
为
#define PHYS_OFFSET UL(0xc0000000)
3. 下载内核配置选项,放置于linux-2.6.20源码的根目录下 http://skyeye.wiki.sourceforge.net/space/showimage/skyeye_2.6.17_lubbock.config
这个下载好的配置文件已经帮我们做了的两件事
首先,在block device菜单下配置了ramdisk和initrd的支持
其次,把内核原来的启动参数改为
root=/dev/ram0 console=ttyS0initrd=0xc0800000,0x00800000 rw mem=64M
4. 把下载到的skyeye_2.6.17_lubbock.config更名为.config
5. 编译内核
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi-
6. 创建文件 skyeye.conf,内容如下:
cpu: pxa25x
mach: pxa_lubbock
mem_bank: map=I, type=RW,addr=0x40000000, size=0x0c000000
mem_bank: map=M, type=RW,addr=0xc0000000, size=0x00800000
mem_bank: map=M, type=RW,addr=0xc0800000, size=0x00800000, file=./initrd.img
mem_bank: map=M, type=RW,addr=0xc1000000, size=0x00800000
mem_bank: map=M, type=RW,addr=0xc1800000, size=0x02800000
7. 从skyeye的测试套件中拷贝initrd.img到linux-2.6.20源码根目录下。该initrd.img的路径是:
skyeye-testsuite-1.2.5/linux/pxa/2.6.x/
8. 运行内核看看,在linux-2.6.20源码根目录下运行下面的命令。可以看到,内核成功运行
sudo skyeye -e vmlinux
调试:
1. 在linux-2.6.20源码根目录下运行命令:
sudo skyeye -d -e vmlinux
2. 在源码根目录下新开一个终端,并运行:
arm-linux-gnueabi-gdb ./vmlinux
gdb界面出来后
(gdb) target remote:12345
之后可以看到,下断点,查看汇编等一切调试功能和x86下都一样。
3. ddd下如何调用arm-linux-gnueabi-gdb ?答
$ ddd --debugger arm-linux-gnueabi-gdb./vmlinux
为s3c2410配置2.6.26内核
[启动过程中有若干错误提示,但内核能启动成功并运行。有待研究]
目标:
得到一个基于s3c2410cpu的2.6.26最新稳定内核的虚拟系统,能进行全程的内核调试,即调试能从第一条机器指令开始进行。
参考:
http://skyeye.wiki.sourceforge.net/Linux
http://www.linuxfans.org/bbs/thread-182101-1-1.html
环境条件:
1. ubuntu hardy 8.04
2. 安装了debian提供的交叉编译工具套件 arm-linux-gnueabi- (4.2版本)
操作步骤:
1.依据“安装交叉编译工具”这节,安装好交叉编译工具
2.修改源码
将include/asm-arm/arch-s3c2410/map.h里的
#define S3C2410_CS6 (0x30000000)
改为
#define S3C2410_CS6 (0xc0000000)
将include/asm-arm/arch-s3c2410/memory.h里的
#define PHYS_OFFSET UL(0x30000000)
改为
#define PHYS_OFFSET UL(0xc0000000)
3.把默认.config替换为s3c2410版本
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi- s3c2410_defconfig
3.修改配置文件
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi- menuconfig
进入[Device Driver] ->[ Character Driver] -> [SerialDriver]等菜单下 ,
取消8250/16550 and compatible serial support的选择
4.修改内核启动命令
在Boot option --> Default kernel command string里输入
mem=32M console=ttySAC0 root=/dev/raminitrd=0xc0800000,0x00800000 ramdisk_size=2048 rw
5.编译
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi-
6.从skyeye的测试套件中拷贝相应的文件initrd.img和skyeye.conf到linux-2.6.26源码根目录下。这两个文件的位于
skyeye-testsuite-1.25/linux/s3c2410/s3c2410x-2.6.14/中
7.启动虚拟机
XXX@ubuntu:~/dt/linux-2.6.26$ sudoskyeye -e vmlinux
8.启动完成后那激动人心的logo如下
Welcome to
_ _____ __ __ _ _
/ \ / __ \ / \_/ \ || |_|
/ _ \ | | | | //\ /\ \ | | _ ____ _ _ _ _
/ /_\ \ | |__| | / / \_/ \\| | | | _ \| | | |\ \/ /
//___\ \ | |__\ \ | | | || |___ | ||_| | |_| |/ \
/_/ \_\| | \_\|_| |_||_____||_|_| |_|\____|\_/\_/
ARMLinux for Skyeye
For further information please check:
http://www.skyeye.org/
BusyBox v1.4.1 (2007-02-10 01:19:06 CST)Built-in shell (ash)
Enter 'help' for a list of built-incommands.
/bin/ash: can't access tty; job controlturned off
/ $ uname -a
Linux skyeye 2.6.26 #2 Sun Oct 519:56:57 CST 2008 armv4tl unknown
/ $
调试:
1. 在linux-2.6.26源码根目录下新建文件”.gdbinit”,内容是:
(gdb) target remote:12345
2. 在linux-2.6.26源码根目录下命令:
sudo skyeye -d -e vmlinux
3. 在源码根目录下新开一个终端,并运行:
arm-linux-gnueabi-gdb ./vmlinux
之后可以看到,下断点,查看汇编等一切调试功能和x86下都一样。
4. ddd下如何调用arm-linux-gnueabi-gdb ?答
$ ddd --debugger arm-linux-gnueabi-gdb./vmlinux
截图:
步骤2:
XXX@ubuntu:~/桌面/test/linux-2.6.26_s3c2410$ sudo skyeye -d -e vmlinux
big_endian is false.
arch: arm
cpu info: armv4, arm920t, 41009200,ff00fff0, 2
mach info: name s3c2410x, mach_init addr0x805f030
lcd_mod:1
dbct info: Note: DBCT not compiled in.This option will be ignored
uart_mod:0, desc_in:, desc_out:,converter:
SKYEYE: use arm920t mmu ops
Loaded RAM ./initrd.img
start addr is set to 0xc0008000 by execfile.
debugmode= 1, filename = skyeye.conf,server TCP port is 12345
------------------------
步骤3:
fqh@ubuntu:~/桌面/test/linux-2.6.26_s3c2410$ arm-linux-gnueabi-gdb vmlinux
GNU gdb 6.8-debian
Copyright (C) 2008 Free SoftwareFoundation, Inc.
License GPLv3+: GNU GPL version 3 orlater <http://gnu.org/licenses/gpl.html>
This is free software: you are free tochange and redistribute it.
There is NO WARRANTY, to the extentpermitted by law. Type "showcopying"
and "show warranty" fordetails.
This GDB was configured as"--host=i486-linux-gnu --target=arm-linux-gnueabi"...
stext () at arch/arm/kernel/head.S:80
80 msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE@ ensure svc mode
Current language: auto; currently asm
(gdb) source extendinstr //载入辅助的gdb宏
--------------
用快捷键 ctrl+x+2打开tui模式后的图示,可看到调试是从第一条指令开始的。这对研究系统启动过程提供了极大的便利。
┌──arch/arm/kernel/head.S────────────────────────────────────────────────────────────────────────────┐
>│80 msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE@ ensure svc mode │
│81 @ and irqs disabled │
│82 mrc p15, 0, r9, c0, c0 @ get processor id │
│83 bl __lookup_processor_type @ r5=procinfo r9=cpuid │
│84 movs r10, r5 @ invalid processor(r5=0)? │
│85 beq __error_p @ yes, error 'p' │
│86 bl __lookup_machine_type @ r5=machinfo │
│87 movs r8, r5 @ invalid machine(r5=0)? │
│88 beq __error_a @ yes, error 'a' │
│89 bl __vet_atags │
│90 bl __create_page_tables │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
>│0xc0008000 <stext> msr CPSR_c, #211 ; 0xd3 │
│0xc0008004 <stext+4> mrc 15, 0, r9, cr0, cr0,{0} │
│0xc0008008 <stext+8> bl 0xc00082f8<__lookup_processor_type> │
│0xc000800c <stext+12> movs r10, r5 │
│0xc0008010 <stext+16> beq 0xc0008190<__error_p> │
│0xc0008014 <stext+20> bl 0xc0008358<__lookup_machine_type> │
│0xc0008018 <stext+24> movs r8, r5 │
│0xc000801c <stext+28> beq 0xc00081e8<__error_a> │
│0xc0008020 <stext+32> bl 0xc00083a0<__vet_atags> │
│0xc0008024 <stext+36> bl 0xc0008078<__create_page_tables> │
│0xc0008028 <stext+40> ldr sp, [pc, #240] ; 0xc0008120 <__switch_data> │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: stext Line: 80 PC: 0xc0008000
(gdb) b sys_read //下断点
Breakpoint 1 at 0xc008cc4c: filefs/read_write.c, line 354.
(gdb) c
----------------
调试示意图
效果可能与你机器上看到的不一样。这个例子中,每个gdb单步指令都会自动显示backtrace。这是因为本人使用了章节“gdb宏”中的extendinstr宏。
┌──include/asm/thread_info.h──────────────────────────────────────────────────────────────────────────────────────────────┐
│91 */ │
│92 static inline structthread_info *current_thread_info(void) __attribute_const__; │
│93 │
│94 static inline structthread_info *current_thread_info(void) │
│95 { │
│96 register unsignedlong sp asm ("sp"); │
>│97 return(struct thread_info *)(sp & ~(THREAD_SIZE - 1)); │
│98 } │
│99 │
│100 /* thread informationallocation */ │
│101 #ifdefCONFIG_DEBUG_STACK_USAGE │
│102 #definealloc_thread_info(tsk) \ │
│103 ((structthread_info *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, \ │
│104 THREAD_SIZE_ORDER)) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│0xc008d480 <fget_light> mov r12, sp │
│0xc008d484 <fget_light+4> push {r11, r12, lr, pc} │
│0xc008d488 <fget_light+8> sub r11, r12, #4 ; 0x4 │
│0xc008d48c <fget_light+12> bic r3, sp, #8128 ; 0x1fc0 │
>│0xc008d490 <fget_light+16> bic r3, r3, #63 ; 0x3f │
│0xc008d494 <fget_light+20> ldr r3, [r3, #12] │
│0xc008d498 <fget_light+24> mov r12, #0 ; 0x0 │
│0xc008d49c <fget_light+28> ldr r2, [r3, #560] │
│0xc008d4a0 <fget_light+32> str r12, [r1] │
│0xc008d4a4 <fget_light+36> ldr r3, [r2] │
│0xc008d4a8 <fget_light+40> cmp r3, #1 ; 0x1 │
│0xc008d4ac <fget_light+44> bne 0xc008d4d0<fget_light+80> │
│0xc008d4b0 <fget_light+48> ldr r2, [r2, #4] │
│0xc008d4b4 <fget_light+52> ldr r3, [r2] │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: fget_light Line: 97 PC: 0xc008d490
Program received signal SIGHUP, Hangup.
0xc008d490 in fget_light (fd=1, fput_needed=0xc1c17ed4)at include/asm/thread_info.h:97
-------------------
#0 0xc008d490 in fget_light (fd=1, fput_needed=0xc1c17ed4) atinclude/asm/thread_info.h:97
#1 0xc008cc5c in sys_read (fd=1, buf=0xc1196800 "", count=512) atfs/read_write.c:359
#2 0xc000ac7c in rd_load_image (from=0xc02b43bc "/initrd.image")at init/do_mounts_rd.c:108
#3 0xc000bbe8 in initrd_load () at init/do_mounts_initrd.c:121
#4 0xc00094c0 in prepare_namespace () at init/do_mounts.c:384
#5 0xc0008a9c in kernel_init (unused=<value optimized out>) atinit/main.c:878
#6 0xc0048484 in sys_waitid (which=<value optimized out>,upid=-1044283692, infop=0x0, options=0, ru=Cannot access memory at
address 0x4
) at kernel/exit.c:1689
Backtrace stopped: previous frame innerto this frame (corrupt stack?)
(gdb)
使用最新的skyeye
1. 新版本的改进
在ubuntu下利用在线安装命令所安装的skyeye是旧的版本,新版本修正了旧版本的一些小问题。比如,旧版本在调试时会出现下面一些烦人的小提示。
Can't send signals to this remotesystem. SIGHUP not sent.
Program received signal SIGHUP, Hangup.
但是,两个版本并不是完全兼容的,主要是skyeye.conf的处理上。不过,幸好这些都是很容易解决的问题。
2. 新版本的安装
http://sourceforge.net/project/showfiles.php?group_id=85554
到上面的网站下载最新版本,目前是skyeye-1.2.6_rc1。解压后用下面命令编译就可以了
$./configure
$ make STATIC=1
然后把在源码根目录下生成的skyeye拷到内核目录下运行即可。这样系统中的老版本skyeye还照样可以使用。
sudo ./skyeye -d -e vmlinux
3. 新老版本的兼容问题
主要是skyeye.conf的格式识别上。老版本要求load_address,load_address_mask不能写在skyeye.conf文件内部,只能用-l选项指定。如果运行老版本时提示skyeye.conf出错,你就得去查查那里,并手动修改处理一下即可。
arm开发板调试环境的建立
基于串口
为qq2440平台移植2.6.26或更新内核,并建立kgdb调试环境
进行中...
[移植中的一些零碎的笔记]
1.内核版本
使用linus的git,但是已知2.6.25中arm已经支持kgdb了。
XXX@ubuntu:/storage/linus-git/linux-2.6$git-describe
v2.6.27-rc9-2-g85ba94b
2.
arm体系的默认配置文件在
arch/arm/configs
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi- s3c2410_defconfig
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi- menuconfig
选取以下选现
CONFIG_DEBUG_INFO=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
make ARCH=armCROSS_COMPILE=arm-linux-gnueabi-
移植环境
windows:硬盘安装的真实系统(XP)
ubuntu: 运行在windows下的vmware虚拟机中
qq2440开发板:真实开发板,IP是192.168.1.230
第一天:(完成)
熟悉开发板,PC机,虚拟机的网络互连
理解内核启动过程
开发板与PC机(XP)PING不通的原因有
1. PC机开着防火
2. PC机上的VMWARE的网络设置有问题(先卸载确认)
3. 安全类软件造成,比如卡巴司机(先卸载,不行重装系统)
ubuntu的网络配置分两种情况,一种是平时上网用的,一种是和开发板通讯用的。
平时使用虚拟机ubuntu上网的配置:
连接方式选出NAT: used to share the host's IP address
虚拟系统启动后,桌面右上角的
wiredconnection->properties->configuration选automatic configuration(DHCP)
开发板挂载ubuntu虚拟系统中的nfs
1.虚拟机本身的网络设置不用动
2.虚拟系统如ubuntu的网卡设置改为桥接
edit virtual machinesettings->virtual machine setting->hardware->ethernet
->bridged:connected directly to thephysical network
3.虚拟系统启动后,桌面右上角的manual network configuration要改.
点左键->network settings->wiredconnection->properties:enable roaming mode不选,
connection settings
configuration:static IP address
IP address:192.168.1.111 与PC机IP,开发板IP同个网段
subnet mask:255.255.255.0
gateway address:空
PC机网络信息:
Ethernet adapter 本地连接:
Connection-specific DNS Suffix .:
IP Address. . . . . . . . . . . . : 192.168.1.100
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . :
开发板的网络信息:
[root@(none) /]# ifconfig
eth0 Link encap:Ethernet HWaddr08:00:3E:26:0A:5B
inet addr:192.168.1.230 Bcast:192.168.1.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1011 errors:0 dropped:0 overruns:0 frame:0
TX packets:610 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:111858 (109.2 KiB) TXbytes:57276 (55.9 KiB)
Interrupt:53 Base address:0x300
windows打开ubuntu中的samba共享目录的方法
\\192.168.1.111
ubuntu中nfs服务的安装和启用
$ sudo apt-get install nfs-common
$ sudo apt-get install nfs-kernel-server
$ sudo vi /etc/exports
/new/root_nfs *(rw,sync)
$ sudo /etc/init.d/nfs-kernel-serverstart
4. 檢查
$ showmount -e localhost
开发板挂载nfs成功后可看到显示结果是
All mount points on localhost:
192.168.1.230:/new/root_nfs
开发板挂载ubuntu中的nfs
(此时运行的文件系统还是在开发板上)
mount -t nfs -o nolock192.168.1.111:/new/root_nfs /tmp/fuck
192.168.1.111:ubuntu的IP
/tmp/fuck:开发板中的挂载点
[root@(none) /]# mount -t nfs -o nolock192.168.1.111:/new/root_nfs /tmp/fuck
[root@(none) /]# cd /tmp/fuck/
[root@(none) fuck]# ls
bin lib proc usr
dev linuxrc sbin var
etc mnt shanghaitan.mp3 www
home opt tmp
-----
通过nfs启动开发板
(挂载的文件系统是在ubuntu虚拟系统上)
下面文字来自于:Embedded Linux Primer: A Practical,Real-World Approach
ip=192.168.1.139:192.168.1.1:192.168.1.1:255.255.255.0:coyote1:eth0:off
ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<PROTO>
Here, client-ip is the target's IPaddress; server-ip is the address of the NFS server; gw-ip is the gateway(router), in case the server-ip is on a different subnet; and netmask definesthe class of IP addressing. hostname is a string that is passed as the targethostname; device is the Linux device name, such as eth0; and PROTO defines theprotocol used to obtain initial IP parameters.
本人的实际操作的命令参数是:
param set linux_cmd_line"console=ttySAC0 root=/dev/nfs nfsroot=192.168.1.111:/new/root_nfsip=192.168.1.130:192.168.1.111:192.168.1.111:255.255.255.0:sbc2440.arm9.net:eth0:off"
注意把编辑器的换行功能去掉后,再复制上面的命令。
192.168.1.130是开发板的IP,系统启动后,用ifconfig就会显示这个IP地址。可以随意设置,当然要满足和PC机,ubuntu的IP在同个网段,而且不能冲突的先前条件。
130:192.168.1.111:nfs的server,也就是ubuntu的IP
按住空格健重启开发板,出现:
+---------------------------------------------+
| S3C2440A USB Downloader ver R0.03 2004Jan |
+---------------------------------------------+
USB: IN_ENDPOINT:1 OUT_ENDPOINT:3
FORMAT:<ADDR(DATA):4>+<SIZE(n+10):4>+<DATA:n>+<CS:2>
NOTE: Power off/on or press the resetbutton for 1 sec
in order to get a valid USB device address.
NAND device: Manufacture ID: 0xec, ChipID: 0x76 (Samsung K9D1208V0M)
Found saved vivi parameters.
Press Return to start the LINUX/Wincenow, any other key for vivi
type "help" for help.
Supervivi> menu
##### FriendlyARM BIOS for 2440 #####
[x] bon part 0 320k 2368k
[v] Download vivi
[k] Download linux kernel
[y] Download root_yaffs image
[c] Download root_cramfs image
[n] Download Nboot
[e] Download Eboot
[i] Download WinCE NK.nb0
[w] Download WinCE NK.bin
[d] Download & Run
[f] Format the nand flash
[p] Partition for Linux
[b] Boot the system
[s] Set the boot parameters
[t] Print the TOC struct of wince
[q] Goto shell of vivi
Enter your selection: s //<--
##### Parameter Menu #####
[r] Reset parameter table to defaulttable
[s] Set parameter
[v] View the parameter table
[w] Write the parameter table to flashmemeory
[q] Quit
Enter your selection: s //<--
Enter the parameter's name(mach_type,media_type, linux_cmd_line, etc): linux_cmd_line
Enter the parameter's value(if the valuecontains space, enclose it with "): "console=ttySAC0 root=/dev/nfsnfsroot=192.168.1.111:/new/root_nfsip=192.168.1.130:192.168.1.111:192.168.1.111:255.255.255.0:sbc2440.arm9.net:eth0:off"
Change linux command line to"console=ttySAC0 root=/dev/nfs nfsroot=192.168.1.111:/new/root_nfsip=192.168.1.130:192.168.1.111:192.168.1.111:255.255.255.0:sbc2440.arm9.net:eth0:off"
##### Parameter Menu #####
[r] Reset parameter table to defaulttable
[s] Set parameter
[v] View the parameter table
[w] Write the parameter table to flashmemeory
[q] Quit
Enter your selection: w //<--
Found block size = 0x0000c000
Erasing... ... done
Writing... ... done
Written 49152 bytes
Saved vivi private data
第二天:(完成)
文件系统制作
理解系统启动过程
先实验在skyeye下能不能成功,学习一下文件系统的制作。而后再下载到开发板实验
dd if=/dev/zero of=./test.image bs=1kcount=8192
块大小单位:1k,8120块,8M
mke2fs ./test.image
格式化
mkdir fuckroot
tar -xzvf root_mini.tgz
sudo mount -o loop test.image./fuckroot/
cp -r root_mini/* fuckroot/
sudo umount fuckroot/
可以将文件系统映像压缩后再使用:
gzip -v9 test.image > test.image.gz
本人这个文件系统解压后的大小是6.4M,制作成8M大的test.image,压缩成test.image.gz后只有2.9M大。
但是利用skyeye启动时,解压花的时间比较长。
命令行中的ramdisk_size太小,修改.
mem=32M console=ttySAC0 root=/dev/raminitrd=0xc0800000,0x00800000 ramdisk_size=8192 rw initcall_debug
ramdisk_size=N
This parameter tells the RAM disk driverto set up RAM disks of N k size.
问题,文件系统没创建console设备节点:
RAMDISK: Loading 8192KiB [1 disk] intoram disk... done.
VFS: Mounted root (ext2 filesystem).
Freeing init memory: 132K
Warning: unable to open an initialconsole.
创建rootfs过程中,在/dev目录下手动创建如下节点:
mknod -m 660 null c 1 3
mknod -m 660 console c 5 1
结果:
VFS: Mounted root (ext2 filesystem).
Freeing init memory: 132K
hwclock: Could not access RTC: No suchfile or directory
mknod: /dev/pts/0: No such file ordirectory
mount: Mounting none on /tmp failed:Invalid argument
mount: Mounting none on /var failed:Invalid argument
/etc/init.d/rcS: /etc/init.d/rcS: 44:cannot create /dev/vc/0: Directory nonexistent
/etc/init.d/rcS: /etc/init.d/rcS: 45:cannot create /dev/vc/0: Directory nonexistent
/etc/rc.d/init.d/httpd: /etc/rc.d/init.d/httpd:16: /sbin/boa: not found
/etc/init.d/rcS: /etc/init.d/rcS: 48:cannot create /dev/vc/0: Directory nonexistent
/etc/init.d/rcS: /etc/init.d/rcS: 49:cannot create /dev/vc/0: Directory nonexistent
/etc/rc.d/init.d/leds:/etc/rc.d/init.d/leds: 16: /etc/init.d/rcS: /etc/init.d/rcS: 52: cannot create/dev/vc/0: Directory nonexistent
/etc/init.d/rcS: /etc/init.d/rcS: 53:cannot create /dev/vc/0: Directory nonexistent
/sbin/led-player: not found
SIOCSIFADDR: No such device
SIOCGIFFLAGS: No such device
/etc/init.d/rcS: /etc/init.d/rcS: 59:/sbin/madplay: not found
Please press Enter to activate thisconsole.
-sh: can't access tty; job controlturned off
id: unknown uid 0
[@FriendlyARM /]# ls
bin home lost+found sbin var
dev lib mnt tmp www
etc linuxrc proc usr
[@FriendlyARM /dev]# ls
console dsp fb0 mixer null sda1 tty1 video0
还有一堆提示,但总算系统能跑了。
现在我的心头大患是udev的问题,因为2.6.26内核中没有devfs了。但有下面这篇文章参考
udev轻松上路
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=embedded&Number=628054&page=0&view=collapsed&sb=5&o=0&fpart=
第三天:(完成)
移植内核2.6.27-rc9到qq2440开发板,实现基本功能,能挂载板上文件系统.
步骤:
1.使用vivi修改mach_type参数
2.修改时钟频率
3.修改源码正确分区
4.禁止nand的ECC校验
分述:
问题1.表现
UncompressingLinux.................................................................................................................done, booting the kernel.
Error: unrecognized/unsupported machineID (r1 = 0x0000030e).
Available machine support:
ID (hex) NAME
000000c1 SMDK2410
0000015b IPAQ-H1940
0000039f Acer-N35
00000290 Acer-N30
0000014b Simtec-BAST
000002a8 Nex Vision - Otom 1.1
00000400 AML_M5900
000001db Thorcom-VR1000
00000454 QT2410
000003fe SMDK2413
000003f1 SMDK2412
00000377 S3C2413
00000474 VSTMS
000002de Simtec-Anubis
0000034a Simtec-OSIRIS
00000250 IPAQ-RX3715
0000016a SMDK2440
000002a9 NexVision - Nexcoder 2440
0000043c SMDK2443
Please check your kernel config and/orbootloader.
解决方法:
##### Parameter Menu #####
[r] Reset parameter table to defaulttable
[s] Set parameter
[v] View the parameter table
[w] Write the parameter table to flashmemeory
[q] Quit
Enter your selection: s
Enter the parameter's name(mach_type,media_type, linux_cmd_line, etc): mach_type
Enter the parameter's value(if the valuecontains space, enclose it with "): 362 //<---
Change 'mach_type' value.0x0000030e(782) to 0x0000016a(362)
问题2.表现
UncompressingLinux.................................................................................................................done, booting the kernel.
8?'·{e#???;?·7'0??3G?#?G'?乱码
解决方法:
static void __init smdk2440_map_io(void)
{
s3c24xx_init_io(smdk2440_iodesc,ARRAY_SIZE(smdk2440_iodesc));
s3c24xx_init_clocks(12000000);//修改处,原为16934400
s3c24xx_init_uarts(smdk2440_uartcfgs,ARRAY_SIZE(smdk2440_uartcfgs));
}
问题3.表现
VFS: Cannot open root device"mtdblock2" or unknown-block(31,2)
Please append a correct"root=" boot option; here are the available partitions:
1f00 16 mtdblock0 (driver?)
1f01 2048 mtdblock1 (driver?)
1f02 4096 mtdblock2 (driver?)
1f03 2048 mtdblock3 (driver?)
1f04 4096 mtdblock4 (driver?)
1f05 10240 mtdblock5 (driver?)
1f06 24576 mtdblock6 (driver?)
1f07 16384 mtdblock7 (driver?)
Kernel panic - not syncing: VFS: Unableto mount root fs on unknown-block(31,2)
解决:
依据nand分区修改源码:
static struct mtd_partitionsmdk_default_nand_part[] = {
[0]= {
.name = "vivi",
.size = 0x00030000,
.offset = 0,
},
[1]= {
.name = "kernel",
.offset= 0x00050000,
.size = 0x00200000,
},
[2]= {
.name = "root",
.offset= 0x00250000,
.size = 0x03dac000,
},
};
问题4.表现
Kernel panic - not syncing: VFS: Unableto mount root fs on unknown-block(31,2)
导致上面panic的原因是没有禁止Flash ECC校验
解决:
s3c2410_nand_init_chip()
..
if(set->disable_ecc)
chip->ecc.mode = NAND_ECC_NONE;
chip->ecc.mode = NAND_ECC_NONE;//<-在函数最后加上
启动信息:
Copy linux kernel from 0x00050000 to0x30008000, size = 0x00200000 ... done
zImage magic = 0x016f2818
Setup linux parameters at 0x30000100
linux command line is: "noinitrdroot=/dev/mtdblock2 init=/linuxrc console=ttySAC0"
MACH_TYPE = 362
NOW, Booting Linux......
UncompressingLinux.................................................................................................................done, booting the kernel.
Linux version 2.6.27-rc9(fqh@ubuntu-sniper) (gcc version 4.2.4 (Debian 4.2.4-3)) #8 Sat Oct 11 03:17:21CST 2008
CPU: ARM920T [41129200] revision 0(ARMv4T), cr=c0007177
Machine: SMDK2440
ATAG_INITRD is deprecated; please updateyour bootloader.
Memory policy: ECC disabled, Data cachewriteback
CPU S3C2440A (id 0x32440001)
S3C244X: core 405.000 MHz, memory101.250 MHz, peripheral 50.625 MHz
S3C24XX Clocks, (c) 2004 SimtecElectronics
CLOCK: Slow mode (1.500 MHz), fast, MPLLon, UPLL on
CPU0: D VIVT write-back cache
CPU0: I cache: 16384 bytes,associativity 64, 32 byte lines, 8 sets
CPU0: D cache: 16384 bytes,associativity 64, 32 byte lines, 8 sets
Built 1 zonelists in Zone order,mobility grouping on. Total pages: 16256
Kernel command line: noinitrd root=/dev/mtdblock2init=/linuxrc console=ttySAC0
irq: clearing pending ext status00000200
irq: clearing subpending status 00000002
PID hash table entries: 256 (order: 8,1024 bytes)
timer tcon=00000000, tcnt a4ca, tcfg00000200,00000000, usec 00001e57
Console: colour dummy device 80x30
console [ttySAC0] enabled
Dentry cache hash table entries: 8192(order: 3, 32768 bytes)
Inode-cache hash table entries: 4096(order: 2, 16384 bytes)
Memory: 64MB = 64MB total
Memory: 61140KB available (3224K code,335K data, 144K init)
Calibrating delay loop... 201.93BogoMIPS (lpj=504832)
Mount-cache hash table entries: 512
CPU: Testing write buffer coherency: ok
net_namespace: 440 bytes
NET: Registered protocol family 16
S3C2410 Power Management, (c) 2004Simtec Electronics
S3C2440: Initialising architecture
S3C2440: IRQ Support
S3C24XX DMA Driver, (c) 2003-2004,2006Simtec Electronics
DMA channel 0 at c4800000, irq 33
DMA channel 1 at c4800040, irq 34
DMA channel 2 at c4800080, irq 35
DMA channel 3 at c48000c0, irq 36
S3C244X: Clock Support, DVS off
SCSI subsystem initialized
usbcore: registered new interface driverusbfs
usbcore: registered new interface driverhub
usbcore: registered new device driverusb
NET: Registered protocol family 2
IP route cache hash table entries: 1024(order: 0, 4096 bytes)
TCP established hash table entries: 2048(order: 2, 16384 bytes)
TCP bind hash table entries: 2048(order: 1, 8192 bytes)
TCP: Hash tables configured (established2048 bind 2048)
TCP reno registered
NET: Registered protocol family 1
NetWinder Floating Point Emulator V0.97(extended precision)
JFFS2 version 2.2. (NAND) (SUMMARY) © 2001-2006 Red Hat, Inc.
msgmni has been set to 119
io scheduler noop registered
io scheduler anticipatory registered(default)
io scheduler deadline registered
io scheduler cfq registered
Console: switching to colour framebuffer device 30x40
fb0: s3c2410fb frame buffer device
lp: driver loaded but no devices found
ppdev: user-space parallel port driver
Serial: 8250/16550 driver4 ports, IRQsharing enabled
s3c2440-uart.0: s3c2410_serial0 at MMIO0x50000000 (irq = 70) is a S3C2440
s3c2440-uart.1: s3c2410_serial1 at MMIO0x50004000 (irq = 73) is a S3C2440
s3c2440-uart.2: s3c2410_serial2 at MMIO0x50008000 (irq = 76) is a S3C2440
brd: module loaded
loop: module loaded
dm9000 Ethernet Driver, V1.31
Uniform Multi-Platform E-IDE driver
Driver 'sd' needs updating - please usebus_type methods
S3C24XX NAND Driver, (c) 2004 SimtecElectronics
s3c2440-nand s3c2440-nand: Tacls=3, 29nsTwrph0=7 69ns, Twrph1=3 29ns
NAND device: Manufacturer ID: 0xec, ChipID: 0x76 (Samsung NAND 64MiB 3,3V 8-bit)
NAND_ECC_NONE selected by board driver.This is not recommended !!
Scanning device for bad blocks
Bad eraseblock 562 at 0x008c8000
Bad eraseblock 566 at 0x008d8000
Creating 3 MTD partitions on "NAND64MiB 3,3V 8-bit":
0x00000000-0x00030000 : "vivi"
0x00050000-0x00250000 :"kernel"
0x00250000-0x03ffc000 : "root"
usbmon: debugfs is not available
s3c2410-ohci s3c2410-ohci: S3C24XX OHCI
s3c2410-ohci s3c2410-ohci: new USB busregistered, assigned bus number 1
s3c2410-ohci s3c2410-ohci: irq 42, iomem 0x49000000
usb usb1: configuration #1 chosen from 1choice
hub 1-0:1.0: USB hub found
hub 1-0:1.0: 2 ports detected
usbcore: registered new interface driverlibusual
usbcore: registered new interface driverusbserial
usbserial: USB Serial support registeredfor generic
usbcore: registered new interface driverusbserial_generic
usbserial: USB Serial Driver core
usbserial: USB Serial support registeredfor FTDI USB Serial Device
usbcore: registered new interface driverftdi_sio
ftdi_sio: v1.4.3:USB FTDI SerialConverters Driver
usbserial: USB Serial support registeredfor pl2303
usbcore: registered new interface driverpl2303
pl2303: Prolific PL2303 USB to serialadaptor driver
mice: PS/2 mouse device common for allmice
S3C24XX RTC, (c) 2004,2006 SimtecElectronics
s3c2440-i2c s3c2440-i2c: slave address0x10
s3c2440-i2c s3c2440-i2c: bus frequencyset to 98 KHz
s3c2440-i2c s3c2440-i2c: i2c-0: S3C I2Cadapter
S3C2410 Watchdog Timer, (c) 2004 SimtecElectronics
s3c2410-wdt s3c2410-wdt: watchdoginactive, reset disabled, irq enabled
TCP cubic registered
NET: Registered protocol family 17
RPC: Registered udp transport module.
RPC: Registered tcp transport module.
drivers/rtc/hctosys.c: unable to openrtc device (rtc0)
VFS: Mounted root (cramfs filesystem)readonly.
Freeing init memory: 144K
hwclock: Could not access RTC: No suchfile or directory
mknod: /dev/pts/0: Read-only file system
ln: /dev/video0: Read-only file system
ln: /dev/fb0: Read-only file system
ln: /dev/tty1: Read-only file system
ln: /dev/dsp: Read-only file system
ln: /dev/mixer: Read-only file system
ln: /dev/sda1: Read-only file system
/etc/init.d/rcS: /etc/init.d/rcS: 44:cannot create /dev/vc/0: Read-only file system
/etc/init.d/rcS: /etc/init.d/rcS: 45:cannot create /dev/vc/0: Read-only file system
/etc/rc.d/init.d/httpd:/etc/rc.d/init.d/httpd: 16: /sbin/boa: not found
/etc/init.d/rcS: /etc/init.d/rcS: 48:cannot create /dev/vc/0: Read-only file system
/etc/init.d/rcS: /etc/init.d/rcS: 49:cannot create /dev/vc/0: Read-only file system
/etc/rc.d/init.d/leds:/etc/rc.d/init.d/leds: 16: /sbin/led-player: not found
/etc/init.d/rcS: /etc/init.d/rcS: 52:cannot create /dev/vc/0: Read-only file system
/etc/init.d/rcS: /etc/init.d/rcS: 53:cannot create /dev/vc/0: Read-only file system
SIOCSIFADDR: No such device
SIOCGIFFLAGS: No such device
/etc/init.d/rcS: /etc/init.d/rcS: 59:/sbin/madplay: not found
Please press Enter to activate thisconsole.
-sh: can't access tty; job controlturned off
id: unknown uid 0
[@FriendlyARM /]# uname -a
Linux FriendlyARM 2.6.27-rc9 #8 Sat Oct11 03:17:21 CST 2008 armv4tl unknown
[@FriendlyARM /]#
第四天:
实现XP下虚拟机中的ubuntu利用gdb通过串口调试开发板上的2.6.27-rc9内核
问题,开发板只有一个串口,给gdb占用了,怎么操作开发板?
第五天:
实现硬盘安装的ubuntu系统利用gdb通过串口调试开发板上的内核。
第六天:
移植cs8900a网卡驱动。
实现开发板从硬盘ubuntu的nfs启动。
实现硬盘安装的ubuntu系统利用gdb通过网口调试开发板上的内核。
参考:
ubuntu8.04+skyeye1.2.4搭建linux2.6.24+s3c2410的模拟arm-linux开发环境
http://www.google.cn/search?complete=1&hl=zh-CN&newwindow=1&client=firefox-a&rls=org.mozilla:zh-CN:official&hs=R4b&q=cs8900+s3c2440+%E9%A9%B1%E5%8A%A8&start=20&sa=N
http://blog.chinaunix.net/u2/72751/showart_1130655.html
http://www.akae.cn/bbs/redirect.php?tid=6929&goto=lastpost
基于网口
gdb基础
基本命令
推荐这篇,内容很全: gdb 使用手册 http://blog.chinaunix.net/u/11240/showart.php?id=340632
终极参考: Debugging withGDB http://sourceware.org/gdb/current/onlinedocs/gdb.html#SEC_Top
gdb之gui
网址:
cgdb:http://cgdb.sourceforge.net/
kgdb:http://www.kdbg.org/screenshot.php
ddd:http://www.gnu.org/software/ddd/
insight:http://sourceware.org/insight/
这些工具在ubuntu下都有编译好的.deb安装包,利用“立新得”就直接搜索然后在线安装。
这篇短文是我的浅陋之见,我接触这些gui的时间也不久。错误难免。虚拟机:qemu
内核内置kgdb
developer machine: 运行gdb
除了只用命令行gdb外,还可以用gdb的gui,有
1.cgdb 缺点:界面简陋,自动化程度低,只是把terminal分为两部分,上面部分显示源码,下面打命令。由于没有显示反汇编的窗体,不适合要求使用到 stepi命令的场合。优点:运行快,锻炼手指头. 最大的优点是,它有完美的代码着色功能。其他几款调试器中都没有。
2.ddd: 缺点:与kdbg相比,界面凌乱。优点:代码显示效果比kdbg好,c和反汇编代码分开在两个窗口。 可以随时暂停程序的运行。data windows 这个功能非常强大灵活。提示 ddd –tty 2>/dev/null ./vmlinux ; remotetarget localhost:1234
3. kdbg: 缺点:功能比ddd弱。字体太小,c和反汇编代码交错显示,反汇编代码折叠隐藏在C代码之间,要显示反汇编代码要手动展开,不可忍受。太过界面化,居然找 不到是在哪里手动打gdb命令。致命缺点是,内核跑起来后,如果没有断点拦截,就没法把内核的运行暂停下来,kdbg成了没事姥,源码窗口的显示不更新。 另一个致命缺点是,如果没有源码只有二进制文件,虽然可以下断点,但无法显示反汇编代码,没意义。据说kdbg是用来调试kde程序的,实际上也能调试内 核。优点:窗口可以整合到一块,稳定。有变化的寄存器会显示红色。提示 kdbg -r localhost:1234 ./vmlinux
4. insight: 和ddd都是基于TCL/TK,比较相似。优点:源码显示功能最强,可以选择C和反汇编代码分开和交叉显示。可以选择反汇编代码使用intel还是 at&t格式。可以列出当前有哪些源文件,当前文件有哪些函数。变化的寄存器有改变颜色的功能,ddd则没有。缺点:和ddd一样,小窗口无法整 合到到窗口中,但比ddd差的是,主窗口最大化后小窗口无法保持置顶。相对ddd的大劣势是没有一个强大的data windows。感觉界面比ddd强大,但灵活性比ddd差点。对于调试内核来说,还有一个和kdbg相同的大缺点,内核只能通过断点暂停运行,而ddd 下还可以用ctrl+c暂停内核。另外它有个SB错误,显示backtrace的窗口,标题居然是stack. 提示: insight ./vmlinux
5. xxgdb: 古董级别。没事干的时候可以玩玩
6. 其实,gdb自带了一个基于curses的gui。启动方式是gdbtuixxx; 或者在gdb启动之后用命令layout启动gui。很好用,可以至多同时显示三个分窗口。要是代码有着色功能就好了。
针对内核调试的总结:
1. kdbg不适合调试内核
3. 如果想复习gdb强大的命令,选cgdb或纯gdb。
4. 如果想学习汇编,insight是不二选择。
5 如果倾向于把调试器当作浏览器使用,作为source insight等工具的辅助工具,在内核运行中拦截函数,分析函数的调用关系,不需要反汇编的话,则cgdb是不错的选择 .(source insight等源码分析工具有个共同的缺点,因为体系和内核配置不同,一个函数有很多的定义,借助调试器可以在内核运行的时候找出实际调用的那个)
6.insight和ddd很接近,各有千秋。但如果侧重于追溯数据结构体间的联系,ddd更好一点,因为它有data window,它的强项是数据和数据结构关系分析并用图像方式显示出来(What is DDD? Data Display Debugger)。如果侧重于分析汇编指令是怎么在cpu中跑的,推荐用insight,因为它汇编代码显示功能更细致。
7.可惜目前在ubuntu8.04下,ddd+qemu组合用来调试驱动时有bug:驱动函数被拦截时如果正在qemu的系统下操作,鼠标就会冻结在qemu的屏幕中。其实调试单个驱动,用gdb就足够了。ddd等gui一般用来调试理解内核原理。
gdb技巧
另外有用的命令 ptype,whatis
----
更多相关技巧:
1. 获取structpage结构的大小
(gdb) p mem_map
$80 = (struct page *) 0xc1000000
(gdb) p mem_map+1
$81 = (struct page *) 0xc1000020
(gdb) p/x 0xc1000020 - 0xc1000000
$82 = 0×20
2.
打印前从指针mem_map所指起的5个page结构体
(gdb) p *mem_map@5
$83 = {{flags = 1024, _count = {counter= 1}, {_mapcount = {counter = -1}, {inuse = 65535, objects = 65535}}, {{private= 0, mapping = 0×0}, ptl =…
用ddd的图形显示命令是 (gdb) graph display *mem_map@5
参考 p *array@len
@的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中
3.
每运行一次stepi/next等命令后显示下一步要将要运行的反汇编指令
(gdb) display/i $pc
6: x/i $pc
0xc0144fb6<init_cgroup_root+22>: mov %esp,%ebp
(gdb) stepi
6: x/i $pc
0xc0144fb8 <init_cgroup_root+24>: mov %edx,0×44(%eax)
提示:display的管理:
undisplay deletedisplay disable display enable display info display
4.使结构体的显示更漂亮
(gdb) show print pretty
Prettyprinting of structures is on.
(gdb) set print pretty off
(gdb) p *init_task->group_info
$12 = {ngroups = 0, usage = {counter =14}, small_block = {0 <repeats 32 times>}, nblocks = 0, blocks =0xc0355530}
(gdb) set print pretty on
(gdb) p *init_task->group_info
$13 = {
ngroups = 0,
usage = {
counter = 14
},
small_block = {0 <repeats 32times>},
nblocks = 0,
blocks = 0xc0355530
}
(注:6.7.条来自http://techcenter.dicder.com/2006/0906/content_173.html)
5. 使用自定义命令。
(gdb) define nid
Type commands for definition of “nid”.
End with a line saying just “end”.
>ni
>disassemble $pc $pc+16
>end
6. 纯gdb的多窗口显示 GUI调试器可以同时打开多个小窗口,分别显示寄存器、汇编和源代码等。在gdb里也可以做到,但同时最多只能显示两个窗口,试了一下也很方便的。基本命令如下:
a) `layout src’ 仅显示源代码窗口。
b) `layout asm’ 仅显示汇编代码窗口。
c) `layout split’ 显示源代码和汇编代码窗口。
d) `layout regs’ 显示寄存器和源代码窗口,或者寄存器和汇编代码窗口。
e) `layout next` 和`layout prev’ 切换窗口。
f) ctrl + L 刷新屏幕。
g) `C-x 1′ 单窗口模式。
h) `C-x 2′ 双窗口模式。
i) `C-x a’ 回到传统模式。
7. 字符gdb中,如何在每执行一次next命令后都自动显示backtrace的内容 这个问题实际是如何一次执行多条命令。用自定义命令解决
(gdb) define nbt
Type commands for definition of “nbt”.
End with a line saying just “end”.
>next
>bt
>end
(gdb) nbt
#0 early_cpu_init () at arch/x86/kernel/cpu/common.c:626
#1 0xc0384ca9 in setup_arch (cmdline_p=0xc0379fe8)
at arch/x86/kernel/setup_32.c:765
#2 0xc037f62e in start_kernel () at init/main.c:564
#3 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#4 0×00000000 in ?? ()
(gdb)
8. gdb在TUI模式下如何把光标焦点炸转移到command窗口,以便能用上下箭头键能快速翻出历史指令?
实际是转换“active”窗口。
C-x o: ctrl+x,接着放开这两个键,然后在按o(不需要+ctrl)
关于TUI更多信息:
http://sourceware.org/gdb/current/onlinedocs/gdb_23.html#SEC236
还有组合键
C-x C-a
C-x a
C-x A 退出TUI模式
C-x 1 只用一个窗口
C-x 2 用两个窗口,按多次会有不同两个窗口的组合形式
C-x o active 窗口转移
C-x s 进入和退出TUI SingleKey模式
注:C-x o多次使用相当于依次执行以下命令
focus src 转移焦点到源码窗口。
focus asm
focus regs
focus cmd
TUI模式还有以下专用命令
info win
layout next
layout prev
layout src
layout asm
layout split
layout regs
focus next
refresh
tui reg float
tui reg general
tui reg next
tui reg system
update
winheight name +count
winheight name -count
tabset nchars
9. 如何在子函数调用和退出时都暂停运行 watch $ebp
10. 如何获取结构体中特定域的相对偏移量,比如struct stak_struct中lock_depth的相对偏移量?
(gdb) p/x &(*(struct task_struct*)0).lock_depth
$7 = 0x14
11. 如何能够交换使用ddd与gdb,也就是说使用ddd调试时,想换回使用纯gdb,同时保证启用gdb后保证“调试上下文”没任何变化?
只要.gdbinit 文件没包含 c, next..等等能驱动gdb继续调试的命令就可以。
12. 如何通过函数名确定所在的源文件
(gdb) info line vfs_mkdir
Line 2131 of "fs/namei.c"starts at address 0xc017c048 <vfs_mkdir> and ends at 0xc017c052<vfs_mkdir+10>.
13. 由汇编指令地址确定该指令所对应源码的所在行(注:一行c语言一般对应几行汇编指令)
info line *xxxxxxx(xxx是汇编指令地址)
14. 如何快速定位函数中某句C语句对应汇编指令的开始地址。比如以下 [内容太大,准备移到其他位置]
2130 intvfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
.......
2145 DQUOT_INIT(dir);
2146 error= dir->i_op->mkdir(dir, dentry, mode);//<-我们想确定这句语句的汇编指令开始地址,注意它在源文件中的行数
2147 if(!error)
2148 fsnotify_mkdir(dir,dentry);
2149 returnerror;
2150 }
首先,通过函数名查询对应的源文件
(gdb) info line vfs_mkdir
Line 2131 of "fs/namei.c"starts at address 0xc017c048 <vfs_mkdir> and ends at 0xc017c052<vfs_mkdir+10>.
然后,利用info line 源文件:目标语句的行数 就能查询到
(gdb) info line fs/namei.c:2146
Line 2146 of "fs/namei.c"starts at address 0xc017c0ee <vfs_mkdir+166> and ends at 0xc017c0fe<vfs_mkdir+182>.
验证一下
(gdb) disass 0xc017c0ee
Dump of assembler code for functionvfs_mkdir:
0xc017c048 <vfs_mkdir+0>: push %ebp
.....
0xc017c0e4 <vfs_mkdir+156>: mov 0x24(%eax),%ecx
0xc017c0e7 <vfs_mkdir+159>: or $0xffffffff,%edx
0xc017c0ea <vfs_mkdir+162>: mov %esi,%eax
0xc017c0ec <vfs_mkdir+164>: call *(%ecx)
0xc017c0ee <vfs_mkdir+166>: mov 0x98(%esi),%ebx //
0xc017c0f4 <vfs_mkdir+172>: mov %edi,%edx //参数 dentry -> %edx
0xc017c0f6 <vfs_mkdir+174>: mov %esi,%eax //参数dir -> %eax
0xc017c0f8 <vfs_mkdir+176>: mov -0x10(%ebp),%ecx //参数mode -> %ecx
0xc017c0fb <vfs_mkdir+179>: call *0x14(%ebx) //dir->i_op->mkdir(dir, dentry, mode)
0xc017c0fe <vfs_mkdir+182>: test %eax,%eax //判断返回值(error = dir->i_op->mkdir(dir,dentry, mode);)
0xc017c100 <vfs_mkdir+184>: mov %eax,%ebx //保存返回值
0xc017c102 <vfs_mkdir+186>: jne 0xc017c15d <vfs_mkdir+277> //如果返回值 != 0,也就是mkdir失败,跳到最后返回。成功则继续
0xc017c104 <vfs_mkdir+188>: testb $0x4,0x11c(%esi) //内联函数fsnotify_mkdir及子函数->inode_dir_notify在这里展开
//static inline voidinode_dir_notify(struct inode *inode, unsigned long event)
//{
// if (inode->i_dnotify_mask &(event)) <-注意这里判断位,刚好对应testb $0x4,0x11c(%esi)
0xc017c10b <vfs_mkdir+195>: je 0xc017c119 <vfs_mkdir+209>
.....
0xc017c15d <vfs_mkdir+277>: lea -0xc(%ebp),%esp
0xc017c160 <vfs_mkdir+280>: mov %ebx,%eax
我们通过mkdir参数个数,及testb 指令基本判定我们的猜测没错。也就是说vfs_mkdir函数中dir→i_op→mkdir的实际调用是在0xc017c0fb <vfs_mkdir+179>: call *0×14(%ebx)
15. 下断点的形式
1. b 函数名
2. b *指令地址
3. b 源码:行数
(gdb) b fs/namei.c:2146
Breakpoint 9 at 0xc017c0ee: filefs/namei.c, line 2146.
16. 陷入循环语句后,想自动运行到循环语句结束:
u
17. 重复当前的gdb指令
按enter键即可
gdb宏
本小节意义:为了方便把调试内容复制出来,而又需要一定的功能,本人经常使用的工具是gdb的tui。所以gdb宏的使用更是成了不可缺少的辅助手段。比如extendinstr宏,能实时显示调用链的情况,相当于实现了ddd的backtrace分窗口。其他宏的作用就不说了。
参考资料
kgdb官方的gdb宏 http://kgdb.linsyssoft.com/downloads.htm
“Fun with strace andthe GDB Debugger” http://www.ibm.com/developerworks/aix/library/au-unix-strace.html
“GNU ProjectDebugger: More fun with GDB” http://www.ibm.com/developerworks/aix/library/au-gdb.html
“14.3.4. UsefulKernel gdb Macros” from “EmbeddedLinux Primer” http://book.opensourceproject.org.cn/embedded/embeddedprime/
gdb宏的使用
假设要使用下节的lsmod,该gdb宏能列举内核中的模块。 在内核源码目录下建立一个新文件lsmod,内容见下节。
装载宏
(gdb) source lsmod
查看说明
(gdb) help lsmod
list module struct's address, textaddress and their module name
使用
(gdb) lsmod
(gdb) lsmod
Address text Module
0xE014DDA0 0xE014D000 nls_iso8859_1
0xE0169AE0 0xE0164000 isofs
0xE014BA20 0xE0148000 zlib_inflate
0xE0161FE0 0xE0152000 udf
.....
0xE0012DE0 0xE000B000 processor
0xE0008EA0 0xE0008000 fan
0xE00223E0 0xE0020000 thermal_sys
----end----
(gdb)
(gdb)
我们查看一下processor模块结构体的内容
(gdb) p *(struct module *)0xE0012DE0
$10 = {
state = MODULE_STATE_LIVE,
list = {
next = 0xe0008ea4,
prev = 0xe0018984
},
name = "processor", '\0' <repeats 50 times>,
mkobj = {
kobj = {
name = 0xd5910ba0 "processor",
kref = {
refcount = {
counter = 3
}
},
entry = {
next = 0xe00189d0,
...
...
为了方便查看该结构中指针域所指向的结构体,可在ddd下用以下命令打开数据图形然后展开查看
(gdb) graph display *(struct module*)0xE0012DE0
实例
给出的例子都在2.6.26内核上上测试通过。
链表遍历类
宏名: lsmod(有小bug,饭后再看)
作用: 列举内核模块的名称及对应模块结构体的地址,以及text段的地址[todo,导出.bss,.data地址]
define lsmod
printf"Address\t\ttext\t\tModule\n"
set $m=(struct list_head *)&modules
set $done=0
#获取结构体内特定域的相对偏移,见"gdb技巧"
set $offset=&(*(struct module *)0).list
while ( !$done )
set $mp=(struct module *)((char*)$m->next - (char *)$offset)
printf"0x%X\t0x%X\t%s\n", $mp, $mp->module_core,$mp->name
if ( $mp->list->next ==&modules)
set $done=1
end
set $m=$m->next
end
printf "----end----\n"
end
document lsmod
list module struct's address, textaddress and their module name
end
效果如下
(gdb) lsmod
Address text Module
0xE014DDA0 0xE014D000 nls_iso8859_1
0xE0169AE0 0xE0164000 isofs
0xE014BA20 0xE0148000 zlib_inflate
0xE0161FE0 0xE0152000 udf
.....
0xE001BEA0 0xE001A000 8390
0xE017EEC0 0xE016C000 ide_core
0xE0018980 0xE0015000 thermal
0xE0012DE0 0xE000B000 processor
0xE0008EA0 0xE0008000 fan
0xE00223E0 0xE0020000 thermal_sys
----end----
宏名: psusr,pskern
作用: 列举所有task的结构地址,状态,PID,PPID,comm。
psusr,只列举用户层可见的进程;pskern,列举内核层可见的所有进程。
define __show_state
if ($arg0->state == 0)
printf "running\t\t"
else
if ($arg0->state == 1)
printf "sleeping\t"
else
if ($arg0->state ==2)
printf"disksleep\t"
else
if($arg0->state == 4)
printf"zombie\t"
else
if($arg0->state == 8)
printf "stopped\t"
else
if ($arg0->state == 16)
printf "wpaging\t"
else
printf"%d\t\t", $arg0->state
end
end
end
end
end
end
end
document __show_state
internel macro, don't call it by hand
end
define psusr
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
set $init_t = &init_task
set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while ($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
printf"0x%08X\t", $next_t
__show_state$next_t
printf"%d\t%d\t%d\t%s\n", \
$next_t->uid,$next_t->pid, \
$next_t->parent->pid,$next_t->comm
set $next_t=(char *)($next_t->tasks.next) -$tasks_off
end
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
printf "----end----\n"
end
document psusr
print information for all tasks, but notincluding thread members.
This command looks like "ps-aux" in userspace.
end
define pskern
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
set $init_t = &init_task
printf "0x%08X\t", $init_t
__show_state $init_t
printf "%d\t%d\t%d\t%s\n", \
$init_t->uid,$init_t->pid, \
$init_t->parent->pid,$init_t->comm
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group.next)
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
printf"0x%08X\t", $next_t
__show_state$next_t
printf"%d\t%d\t%d\t%s\n", \
$next_t->uid,$next_t->pid, \
$next_t->parent->pid,$next_t->comm
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
printf"0x%08X\t", $next_th
__show_state$next_th
printf"%d\t%d\t%d\t%s\n", \
$next_th->uid,$next_th->pid, \
$next_th->parent->pid,$next_th->comm
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set $next_t=(char *)($next_t->tasks.next) -$tasks_off
end
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
printf "----end----\n"
end
document pskern
print infor for all tasks viewed inkernel, including all thread members
and swapper(PID==0).
end
效果如下
(gdb) source ps
(gdb) psusr
address state uid pid ppid comm
0xDC43F8A0 sleeping 0 1 0 init
0xDC43F490 sleeping 0 2 0 kthreadd
0xDC43F080 sleeping 0 3 2 migration/0
0xDC43EC70 sleeping 0 4 2 ksoftirqd/0
0xDC43E860 sleeping 0 5 2 watchdog/0
.....
0xDC44E060 sleeping 0 1707 1 acpid
0xD8AE6100 sleeping 104 1716 1 dbus-daemon
0xDC46ECD0 sleeping 0 1739 1 cupsd
0xDC45E080 sleeping 101 2009 1 exim4
0xD5A6C0E0 sleeping 0 2026 1 inetd
0xD5A6CD10 sleeping 0 2034 1 dhcdbd
0xDBD45160 sleeping 105 2044 1 hald
0xDBD45570 sleeping 0 2045 2044 hald-runner
....
address state uid pid ppid comm
----end----
宏名: lssp
作用: 列举超级块地址及其s_id域
define lssp
printf "address\t\ts_id\n"
set $sb_lh=(struct list_head *)&super_blocks
#获取结构体内特定域的相对偏移,见"gdb技巧"
set $offset=&(*(struct super_block *)0).s_list
set $sbp=(struct super_block *)((char *)$sb_lh->next - (char*)$offset)
while ( &$sbp->s_list != $sb_lh )
printf"0x%08X\t%s\n", $sbp, $sbp->s_id
set $sbp=(struct super_block*)((char *)$sbp->s_list.next - (char *)$offset)
end
printf "----end----\n"
end
document lssp
List the super_block and their startaddresses
end
效果
(gdb) lssp
address s_id
0xDC40DC00 sysfs
0xDC40DA00 rootfs
0xDC40D800 bdev
0xDC40D400 proc
0xDC41B200 sockfs
0xDC431C00 debugfs
0xDC486600 pipefs
0xDC486000 anon_inodefs
0xD58C5A00 tmpfs
0xD58C5200 inotifyfs
0xD8C09800 devpts
0xD8C09600 hugetlbfs
0xD8C09400 mqueue
0xD590E000 tmpfs
0xD59E4C00 hda1
0xD5908A00 tmpfs
0xD7753200 tmpfs
0xDBD66400 hdc
----end----
功能增强类
宏名: eih, lih, ooi
作用: 克服时钟中断干扰与中断无关的目标代码的调试(X86下适用),解释请看“工程方法”
说明: 使用gdb或ddd时,进入中断后用finish命令的话常常是要么无法返回被中断的原指令处后停住,而是继续运行,要么是会进入到另一个时钟中断中;但是好像在insight下没这个问题。使用这个gdb宏可以解决该问题。
define eih
b common_interrupt
b native_iret
end
document eih
eih: early interrupt hacking, breakcommon_interrupt and native_iret
end
define lih
b apic_timer_interrupt
b irq_return
end
document lih
lih: late interrupt hacking, breakapic_timer_interrupt and irq_return
end
define ooi
c
stepi
end
document ooi
ooi: out of interrupt, return to theinstruction interrupted by interrupt handler
end
宏名: extendinstr
作用: 扩展指令集。配合gdb自带的tui使用,能代替ddd等界面工具的部分功能。
说明: 指令开头:s→step,si→stepi,n→next,ni→nexti,中间bt→bt,末尾i→info args && info local
define inar
printf "-----args start----\n"
info args
end
define inlo
printf "-----localstart----\n"
info local
end
define btl
printf "-------------------\n"
bt
end
define sibt
stepi
btl
end
define sbt
step
btl
end
define nibt
nexti
btl
end
define nbt
next
btl
end
define sibti
inar
inlo
stepi
btl
end
define sbti
inar
inlo
step
btl
end
define nibti
inar
inlo
nexti
btl
end
define nbti
inar
inlo
next
btl
end
效果
宏名: quick
作用: 超级快捷键。gdb的快捷键并没用用尽所有的按键。我们可以利用空余的按键定义自己的命令。方便起见,我只是利用自定义命令简单的实现该该功能,而不是自定义快捷键。可以根据自己偏好来定义。
说明: 这个宏是配合前面的宏ooi和宏extendinstr使用的。这样,如果调试时进入了时钟中断,按a+enter就可以瞬间返回;q+enter–>sibt; z+enter–>finish。
define a
ooi
end
define q
sibt
end
define z
finish
end
宏名:bttnobp,btt,psusr,pskern,trapinfo,btpid,dmesg
内核文档gdbmacros.txt 的gdb宏的升级版本,还修正了一个bug,已在2.6.26下测试。
如果你运行这个脚本有错误,那说明你的内核版本太低了,请运行内核源码中原文件的宏。
本人这个文件的补丁还在提交的过程中。
能提供non-running进程的backtrace功能,还实现了dmesg。
说明bttnobp没在!CONFIG_FRAME_POINTER的配置下测试过,但是估计结果很不可靠,
因为条件判断太宽大了。
#
# This file contains a few gdb macros(user defined commands) to extract
# useful information from kernelcrashdump (kdump) like stack traces of
# all the processes or a particularprocess and trapinfo.
#
# These macros can be used by copyingthis file in .gdbinit (put in home
# directory or current directory) or byinvoking gdb command with
# --command=<command-file-name>option
#
# Credits:
# Alexander Nyberg<alexn@telia.com>
# V Srivatsa <vatsa@in.ibm.com>
# Maneesh Soni<maneesh@in.ibm.com>
#
define __show_state
if ($arg0->state == 0)
printf "running\t\t"
else
if ($arg0->state == 1)
printf"sleeping\t"
else
if ($arg0->state ==2)
printf"disksleep\t"
else
if($arg0->state == 4)
printf"zombie\t"
else
if($arg0->state == 8)
printf "stopped\t"
else
if($arg0->state == 16)
printf "wpaging\t"
else
printf "%d\t\t",$arg0->state
end
end
end
end
end
end
end
document __show_state
internel macro, don't call it by hand
end
define psusr
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
set $init_t = &init_task
set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while ($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
printf"0x%08X\t", $next_t
__show_state$next_t
printf"%d\t%d\t%d\t%s\n", \
$next_t->uid,$next_t->pid, \
$next_t->parent->pid,$next_t->comm
set $next_t=(char *)($next_t->tasks.next) -$tasks_off
end
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
printf "----end----\n"
end
document psusr
print information for all tasks, but notincluding thread members.
This command looks like "ps-aux" in userspace.
end
define pskern
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
set $init_t = &init_task
printf "0x%08X\t", $init_t
__show_state $init_t
printf "%d\t%d\t%d\t%s\n", \
$init_t->uid,$init_t->pid, \
$init_t->parent->pid,$init_t->comm
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group.next)
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
printf"0x%08X\t", $next_t
__show_state$next_t
printf"%d\t%d\t%d\t%s\n", \
$next_t->uid,$next_t->pid, \
$next_t->parent->pid,$next_t->comm
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
printf"0x%08X\t", $next_th
__show_state$next_th
printf"%d\t%d\t%d\t%s\n", \
$next_th->uid,$next_th->pid, \
$next_th->parent->pid,$next_th->comm
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set $next_t=(char *)($next_t->tasks.next) -$tasks_off
end
printf "address\t\tstate\t\tuid\tpid\tppid\tcomm\n"
printf "----end----\n"
end
document pskern
print infor for all tasks viewed inkernel, including all thread members
and swapper(PID==0).
end
define __prinfo_nobp
printf "\npid %d; addr:0x%08x; comm %s:\n", \
$arg0.pid, $arg0, $arg0.comm
printf "=====================================\n"
set var $stackp = $arg0.thread.sp
set var $stack_top = ($stackp & ~4095) + 4096
while ($stackp < $stack_top)
if (*($stackp) > _stext&& *($stackp) < _sinittext)
info symbol *($stackp)
end
set $stackp += 4
end
end
document __prinfo_nobp
internal macro, don't call it by hand.
end
define bttnobp
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group.next)
set$init_t=&init_task
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
__prinfo_nobp$next_t
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
__prinfo_nobp$next_th
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set$next_t=(char *)($next_t->tasks.next) - $tasks_off
end
end
document bttnobp
dumpall thread stack traces on a kernel compiled with !CONFIG_FRAME_POINTER
end
define __prinfo
printf "\npid %d; addr:0x%08x; comm %s:\n", \
$arg0.pid, $arg0, $arg0.comm
printf "=====================================\n"
set var $stackp = $arg0.thread.sp
set var $stack_top = ($stackp & ~4095) + 4096
set var $stack_bot = ($stackp & ~4095)
set $stackp = *($stackp)
while (($stackp < $stack_top) && ($stackp > $stack_bot))
set var $addr = *($stackp + 4)
info symbol $addr
set $stackp = *($stackp)
end
end
document __prinfo
internal macro, don't call it by hand.
end
define btt
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group.next)
set$init_t=&init_task
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
__prinfo$next_t
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
__prinfo$next_th
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set$next_t=(char *)($next_t->tasks.next) - $tasks_off
end
end
document btt
dumpall thread stack traces on a kernel compiled with CONFIG_FRAME_POINTER
end
define btpid
setvar $pid = $arg0
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group)
set$init_t=&init_task
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
setvar $pid_task = 0
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
if($next_t.pid == $pid)
set$pid_task = $next_t
end
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
if($next_th.pid == $pid)
set$pid_task = $next_th
end
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set$next_t=(char *)($next_t->tasks.next) - $tasks_off
end
__prinfo$pid_task
end
document btpid
backtraceof pid
end
define trapinfo
setvar $pid = $arg0
set$tasks_off=((size_t)&((struct task_struct *)0)->tasks)
set$thread_off=((size_t)&((struct task_struct *)0)->thread_group.next)
set$init_t=&init_task
set$next_t=(((char *)($init_t->tasks).next) - $tasks_off)
setvar $pid_task = 0
while($next_t != $init_t)
set$next_t=(struct task_struct *)$next_t
if($next_t.pid == $pid)
set$pid_task = $next_t
end
set$next_th=(((char *)$next_t->thread_group.next) - $thread_off)
while($next_th != $next_t)
set$next_th=(struct task_struct *)$next_th
if($next_th.pid == $pid)
set$pid_task = $next_th
end
set$next_th=(((char *)$next_th->thread_group.next) - $thread_off)
end
set$next_t=(char *)($next_t->tasks.next) - $tasks_off
end
printf"Trapno %ld, cr2 0x%lx, error_code %ld\n", $pid_task.thread.trap_no,\
$pid_task.thread.cr2,$pid_task.thread.error_code
end
document trapinfo
Runinfo threads and lookup pid of thread #1
'trapinfo<pid>' will tell you by which trap & possibly
addressthe kernel panicked.
end
define dmesg
set$i = 0
set$end_idx = (log_end - 1) & (log_buf_len - 1)
while($i < logged_chars)
set$idx = (log_end - 1 - logged_chars + $i) & (log_buf_len - 1)
if($idx + 100 <= $end_idx) || \
($end_idx <= $idx && $idx + 100< log_buf_len)
printf"%.100s", &log_buf[$idx]
set$i = $i + 100
else
printf"%c", log_buf[$idx]
set$i = $i + 1
end
end
end
document dmesg
printthe kernel ring buffer
end
宏名:vmap, lsvmaps,lsmod, lsmodsects, lsallmodsects
说明:没测试,待更新
来源 http://jeanmarc.saffroy.free.fr/kdump2gdb/
# Copyright Jean-Marc Saffroy<saffroy@gmail.com> 2006
# This program is free software,distributed under the terms of the
# GNU General Public License version 2.
# a few useful(?) macros for x86-64 VMMhacks
# useful constants
set $PAGE_SIZE = (1<<12)
set $__PHYSICAL_MASK = (1 << 46)-1
set $PTE_MASK = ~($PAGE_SIZE-1) &$__PHYSICAL_MASK
set $__PAGE_OFFSET = 0xffff810000000000
set $_PAGE_PSE = 0x80
define vmap
set$addr = (long)$arg0
#index in each of the 4 levels of page directories
set$pgd = $addr >> 39 & (1<<9)-1
set$pud = $addr >> 30 & (1<<9)-1
set$pmd = $addr >> 21 & (1<<9)-1
set$pte = $addr >> 12 & (1<<9)-1
#offset in page
set$off = $addr & (1<<12)-1
#printf"%03x %03x %03x %03x %03x\n", $pgd, $pud, $pmd, $pte, $off
set$pgd_off = (pgd_t *) &init_level4_pgt + $pgd
#printf"pgd_off: %lx pgd: %lx\n", $pgd_off, (long)$pgd_off->pgd
set$pgd_page = ((long)$pgd_off->pgd & $PTE_MASK) + $__PAGE_OFFSET
#printf"pgd_page: %lx\n", $pgd_page
set$pud_off = ((pud_t *) $pgd_page) + $pud
#printf"pud_off: %lx pud: %lx\n", $pud_off, (long)$pud_off->pud
set$pud_page = ((long)$pud_off->pud & $PTE_MASK) + $__PAGE_OFFSET
#printf"pud_page: %lx\n", $pud_page
set$pmd_off = ((pmd_t *) $pud_page) + $pmd
#printf"pmd_off: %lx pmd: %lx\n", $pmd_off, (long)$pmd_off->pmd
set$pmd_page = ((long)$pmd_off->pmd & $PTE_MASK) + $__PAGE_OFFSET
#printf"pmd_page: %lx\n", $pmd_page
if((long)$pmd_off->pmd & $_PAGE_PSE) != 0
#printf"PSE page! "
set$paddr = $pmd_page + ($addr & (1<<21)-1)
else
set$pte_off = ((pte_t *) $pmd_page) + $pte
#printf"pte_off: %lx pte: %lx\n", $pte_off, (long)$pte_off->pte
set$pte_page = ((long)$pte_off->pte & $PTE_MASK) + $__PAGE_OFFSET
#printf"pte_page: %lx\n", $pte_page
set$paddr = $pte_page + $off
end
#printf"remapped physical addr: %lx\n", $paddr
printf"%lx -> %lx\n", $addr, $paddr
end
document vmap
Usage: vmap <address>
Convert a kernel remapped virtualaddress to an identity-mapped address.
end
define lsvmaps
set$map = vmlist
set$gcount = 0
while$map != 0
if$map->pages != 0
set$vaddr = (long)$map->addr
set$count = (long)$map->size / $PAGE_SIZE
set$gcount = $gcount + $count -1
while$count > 1
vmap$vaddr
set$vaddr = $vaddr + $PAGE_SIZE
set$count = $count - 1
end
end
set$map = $map->next
end
printf"page count: %d\n", $gcount
end
document lsvmaps
List all kernel remapped pages (vmallocregions) and corresponding identity-mapped pages.
end
define lsmod
set $mod = modules.next
printf "struct module size name\n"
while $mod != &modules
set $m = (struct module *)((char*)$mod-(char*)(&((structmodule*)0)->list))
printf "0x%lx % 8d%s\n", $m, $m->core_size, $m->name
set$mod = $mod->next
end
end
document lsmod
List loaded kernel modules.
end
define lsmodsects
set$mod = (struct module *)$arg0
printf"add-symbol-file %s.ko 0x%lx ", $mod->name,$mod->sect_attrs->attrs[0].address
set$i = 1
while$mod->sect_attrs->grp->attrs[$i] != 0
printf"-s %s ", (char*)$mod->sect_attrs->attrs[$i].name
printf"0x%lx ", $mod->sect_attrs->attrs[$i].address
set$i = $i + 1
end
printf"\n"
end
document lsmodsects
Usage: lsmodsects <address of structmodule>
Prints "add-symbol-file..."command to load sections of the given module.
end
define lsallmodsects
set $mdl = modules.next
while $mdl != &modules
set $m = (struct module *)((char*)$mdl-(char*)(&((structmodule*)0)->list))
lsmodsects$m
set$mdl = $mdl->next
end
end
document lsallmodsects
Calls lsmodsects on all modules.
end
汇编基础--X86篇
注意:某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。
用户手册
Intel® 64 and IA-32Architectures Software Developer’s Manuals
http://www.intel.com/products/processor/manuals/index.htm
AT&T汇编格式
参考
“AT&T汇编语言与GCC内嵌汇编简介” http://blog.chinaunix.net/u2/73528/showart_1110874.html
[杂类文章]
“Linux Assembly andDisassembly an Introduction” http://www.milw0rm.com/papers/47
内联汇编
GCC-Inline-Assembly-HOWTO http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
汇编与C函数的相互调用
调用链形成和参数传递
参考文章 [多如牛毛]
“Guide: FunctionCalling Conventions” http://www.delorie.com/djgpp/doc/ug/asm/calling.html
“Intel x86Function-call Conventions - Assembly View” http://www.unixwiz.net/techtips/win32-callconv-asm.html
“C Function CallConventions and the Stack” http://www.cs.umbc.edu/~chang/cs313.s02/stack.shtml
“The C CallingConvention and the 8086: Using the Stack Frame” http://www.et.byu.edu/groups/ece425web/stable/labs/StackFrame.html
“C Function CallingConvention” http://adamw-dev.blogspot.com/2007/05/c-function-calling-convention.html
“C函数调用在GNU汇编中的实现” http://www.unixresources.net/linux/clf/cpu/archive/00/00/59/75/597564.html
“函数调用的几个概念:_stdcall,_cdecl....” http://blog.chinaunix.net/u2/67530/showart_601750.html
“Callingconventions(调用规则)” http://www.bobd.cn/itschool/Program/delphi/200612/itschool_12084.html
[扩展,简要说明原理。并用实例解析]
x86终极参考
CHAPTER 6 PROCEDURECALLS, INTERRUPTS, AND EXCEPTIONS of
IA-32 Intel_Architecture Software Developer’s Manual Volume 1_ BasicArchitecture.pdf http://download.intel.com/design/processor/manuals/253665.pdf
寄存器的角色与保护
· 寄存器的角色
1. %esp: 栈指针
指向栈的顶端,也就是指向栈的最后一个正在使用的元素。%esp的值隐式地受到几个机器指令的影响,比如push,pop,call,ret等。
2. %ebp: 基址指针
指向当前栈的基地址,有时也称为“帧指针”。与%esp不同的是,它必须显式地进行操作才能改变值。
3. %eip: 指令指针
保存着下一个被执行机器指令的地址。当CPU执行call指令时,%eip的值自动被保存到栈中。还有,任何一个“jump”跳转指令都会直接地改变%eip
· 两条规则
1. gcc要求在函数调用的前后,寄存器%ebx,%esi,%edi,%ebp,%esp,%ds, %es,%ss的值保持不变。所以被调用函数如果需要修改这些寄存器的值,被调用函数必须负责对它们进行保护。[后三个??]
2. gcc规定在函数调用的前后,寄存器%eax,%edx,%ecx的值可以改变。所以调用函数如果需要防止子函数破坏这三个寄存器的值,调用者必须在函数调用前自己负责保护它们。
我们注意到,是保护,不一定是保存。如果确认没用到某寄存器,那么该寄存器就不需要一定要有一个先保存到栈而后再恢复原值的过程。
这两条规则实际是定义了对系统资源使用的权限和义务。
第一条规则,是银行和借贷者的关系。有人向银行借了几千万,结果赌博全输光了。还钱的期限到了,银行的行长对借贷者说“没事,你回家吧。几千万而已,我拿我工资给你垫上”。我想这样的事决不会发生,行长一个电话110过去,借贷者一天后就把钱还清了。所以,这里,调用函数是银行行长,子函数是借贷者。
第二条规则,则是老爸和儿子的关系了。儿子对老爸说“老爸,解我100去买球鞋,我明天还你”。结果,第二天,老爸没钱吃饭了,问儿子“还钱”。儿 子说“昨晚逛街碰到一个美女,请了一顿,把钱化光了”。老爸没法子,总不能把儿子绳以正法吧。怪只能怪自己事前没防这招咯。所以,这里,调用函数是老爸,子函数是儿子你。
· 返回值
1. Integers (of any size up to 32 bits) and pointers are returned in the%eax register.
2.Floating point values are returned in the 387 top-of-stack register, st(0).
3. Return values of type long long int are returned in %edx:%eax (themost significant word in %edx and the least significant in %eax).
4. Returning a structure is complicated and rarely useful; try to avoidit. (Note that this is different from returning a pointer to a structure.)
5. If your function returns void (e.g. no value), the contents of theseregisters are not used.
调用链的形成
· 应用层实例解析
我们回头看看“寄存器的角色”这一小节,很快就能明白调用链的形成的本质。
调用链包含两方面的内容
1.返回地址的保存与恢复
2.旧栈帧的保存与恢复
因为在普通的调用形式中(call调用),返回地址的保存与恢复是由处理器机制本身保证的,不需人工维护。调用指令call的执行自动将call指令之下 的指令地址压入栈中,被调用函数返回时,ret指令的执行会重新将返回地址从栈弹出传送到pc中。要求下面分析旧栈帧的保存与恢复。
旧栈帧的保存与恢复,无非就是要解决两大问题:
1. 建立新栈帧 这一步很简单,栈帧无非有两个头,底端和顶端。%esp指向栈的顶端,而%esp是不需要手工维护的,随着push,pop等指令,它自己就在改变自己。 那么又怎么建立栈帧的底端呢?我们知道,栈底(也就是基址)是由%ebp指定的,在一个栈帧的整个生命周期里,%ebp的值都不变,也就是说,赋个合适的 值给它就完事。怎么赋值就是问题所在了。我们知道,%esp指向栈中最后一个被使用的元素。所以,当我们正在使用(我们认为的)第一个元素时,把%esp 的值赋给%ebp,%ebp不就是指向栈的基址了吗?
2. 保护旧栈帧的信息 同样的问题,保护旧栈帧的信息,就是保存旧栈帧指向底端和顶端的指针值,也就是旧%ebp,%esbp的值。当函数调用指令刚执行完,马上就要保护作案现 场了。首先,push%ebp,这句就把旧栈帧的基地址保存在栈的顶端。此时,%esp指向的内存地址中,就放着旧栈帧的基地址的值。但是还不够啊,%esp是个不可靠的东 西,它经常在变化,必须把这个地址放到一个不会隐式变化的寄存器中。于是选择了%ebp。mov %esp %ebp.这样,%ebp指向的内存地址中,就放着旧栈帧的基地址的值。这就解放了%esp,可以用%esp来动态指向新栈帧的顶端了。按照定 义,%ebp所指向的地址是新栈帧的底端,也就是新栈帧的第一个元素,也就是说新栈帧第一个元素的值是旧栈帧基址。
但是注意,%ebp指向的地址再加4bytes的地址上,存放的是被调用函数的返回地址。在执行call指令时,call指令后面的那个指令的地址(也就是被调用函数的返回地址)被自动隐式地放到了栈中。
当子函数返回时,再按照上面文字进行逆操作,就能恢复旧栈帧的信息。
#include <stdio.h>
void func()
{}
void funb()
{
func();
}
void funa()
{
funb();
}
int main()
{
funa();
}
-------
08048344 <func>:
#include <stdio.h>
void func()
{}
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 5d pop %ebp
8048348: c3 ret
08048349 <funb>:
void funb()
{
8048349: 55 push %ebp
804834a: 89 e5 mov %esp,%ebp
func();
804834c: e8 f3 ff ff ff call 8048344 <func>
}
8048351: 5d pop %ebp
8048352: c3 ret
08048353 <funa>:
void funa()
{
8048353: 55 push %ebp
8048354: 89 e5 mov %esp,%ebp
funb();
8048356: e8 ee ff ff ff call 8048349 <funb>
}
804835b: 5d pop %ebp
804835c: c3 ret
0804835d <main>:
int main()
{
804835d: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048361: 83 e4 f0 and $0xfffffff0,%esp
8048364: ff 71 fc pushl -0x4(%ecx)
8048367: 55 push %ebp
8048368: 89 e5 mov %esp,%ebp
804836a: 51 push %ecx
funa();
804836b: e8 e3 ff ff ff call 8048353 <funa>
}
8048370: 59 pop %ecx
8048371: 5d pop %ebp
8048372: 8d 61 fc lea -0x4(%ecx),%esp
8048375: c3 ret
8048376: 90 nop
8048377: 90 nop
8048378: 90 nop
8048379: 90 nop
804837a: 90 nop
804837b: 90 nop
804837c: 90 nop
804837d: 90 nop
804837e: 90 nop
804837f: 90 nop
func被调用后内存如下
| |
| | | | hight
| | | |
| +--------------\ |
+---+ main's %ebp |\ |
+-> +--------------+ --funa's frame |
| | ret to funa | / |
| +--------------+X |
+---+ funa's %ebp | \ |
+-->+--------------+ ---funb's frame |
| | ret to funb | / |
| +--------------+ |
+---+ funb's %ebp |<---func'sframe | low
%esp--> +--------------+<---- %ebp v
| |
| |
| |
| |
| |
· 内核层实例解析
栈帧结构与参数传递
· 栈元素引用的就近原则
为了说明就近原则,我们先看看典型和全面的栈帧是怎样的。函数caller调用子函数callee所形成的栈帧。
1. 从被调用的子函数callee来看,获取caller的传递的实参,以及建立自身本地变量时,因为内存地址都靠近栈帧的基址,所以这两种引用都是利用%ebp加上偏移量的形式。
2. 相反,主函数在调用子函数前,在为子函数准备实参时,因为实参位于栈帧末端,所以对实参的引用都是利用%esp加上偏移量的形式(没画出来)
caller's frame pointer
|
| | |
| | |
| | |
| +-------------------+
| | caller saved |
| | registers |
| | %eax,%ecx,%edx |
| | (as needed) |
| +-------------------+
| | argument #3 | [%ebp+16]
| +-------------------+
| | argument #2 | [%ebp+12]
| +-------------------+
| | argument #1 | [%ebp+8]
| +-------------------+
| | return address |
| +-------------------+ -----
+-----+ caller's %ebp |<---%ebp \
+-------------------+ \
| local var #1 | [%ebp-4] \
+-------------------+ |
| local var #2 | [%ebp-8] |
+-------------------+ |
| temporary | |
| storage | |
+-------------------+
| callee saved | callee stack frame
| registers | |
| %ebx,%esi,%edi | |
| (as needed) | |
+-------------------+ |
| | |
| | |
| | /
| |<----%esp /
|
caller:调用者 callee:被调用者
完整的调用过程
函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。
· 函数调用前调用者的动作
1.%eax,%edx,%ecx入栈(可选)
2.子函数的参数入栈
· 函数调用 call callee
call机器指令,原子性自动地完成了两种任务.
1.%eip入栈,保存了callee函数的返回地址
2.callee的函数地址传递到%eip.
所以下一指令就从callee函数的第一指令开始运行。控制权转移给callee
· 函数调用后被调用者的动作
1.保存caller栈帧基址 push %ebp
2.建立callee栈帧基址 mov %esp,%ebp
3.分配本地变量和临时存储的空间 sub $XXX, %esp
4.本地变量赋值
5.%ebx,%esi,%edi入栈(可选)
· 调用返回前被调用者的动作
1.%ebx,%esi,%edi还原(出栈,可选)
2.释放本地变量和临时存储的栈空间mov %ebp,%esp
3.还原caller栈帧的基址 pop %ebp
或者2.3.步用一条元语指令完成 leave
4.调用返回 ret
该指令把存放于栈的返回地址取出(出栈),存放到%eip中。下一指令就从callcallee指令的下一指令开始运行。控制权返回给caller
· 调用返回后调用者的动作
1.释放存放callee参数的栈空间 add $XXX, %esp
2.转移%eax的值(子函数的返回值,可选)
3.还原%eax,%edx,%ecx(出栈,可选)
· 应用层实例解析
应用层参数的传入: 用户层参数的传递是利用栈来完成的。函数右边的参数先入栈,位于栈的高地址。反之,函数左边的参数后入栈,位于栈的低地址。
例子请看 “C难点的汇编解释”
· 内核层实例解析
内核层参数的传入: 混合使用寄存器和栈来传递参数。当参数个数不多于3个时,参数从左到右依次传递到%eax, %edx, %ecx.当参数个数多于3时,从第4个起的其余参数通过栈传递。同样,函数右边的参数先入栈,位于栈的高地址。反之,函数左边的参数后入栈,位于栈的低地址。
· 系统调用实例解析
系统调用的参数传递:[以后再看]
C库函数
ssize_t read(int fd, void *buf, size_t count);
000b6a30 <__read>:
b6a30: 65 83 3d 0c 00 00 00 cmpl $0x0,%gs:0xc
b6a37: 00
b6a38: 75 1d jne b6a57 <__read+0x27>
b6a3a: 53 push %ebx
b6a3b: 8b 54 24 10 mov 0x10(%esp),%edx //count
b6a3f: 8b 4c 24 0c mov 0xc(%esp),%ecx //buf
b6a43: 8b 5c 24 08 mov 0x8(%esp),%ebx //fd
b6a47: b8 03 00 00 00 mov $0x3,%eax //系统调用号
b6a4c: cd 80 int $0x80
b6a4e: 5b pop %ebx
b6a4f: 3d 01 f0 ff ff cmp $0xfffff001,%eax
b6a54: 73 2d jae b6a83 <__read+0x53>
b6a56: c3 ret
b6a57: e8 14 ae 01 00 call d1870 <pthread_exit+0x110>
b6a5c: 50 push %eax
b6a5d: 53 push %ebx
b6a5e: 8b 54 24 14 mov 0x14(%esp),%edx
b6a62: 8b 4c 24 10 mov 0x10(%esp),%ecx
b6a66: 8b 5c 24 0c mov 0xc(%esp),%ebx
b6a6a: b8 03 00 00 00 mov $0x3,%eax
b6a6f: cd 80 int $0x80
b6a71: 5b pop %ebx
b6a72: 87 04 24 xchg %eax,(%esp)
b6a75: e8 c6 ad 01 00 call d1840 <pthread_exit+0xe0>
b6a7a: 58 pop %eax
b6a7b: 3d 01 f0 ff ff cmp $0xfffff001,%eax
b6a80: 73 01 jae b6a83 <__read+0x53>
b6a82: c3 ret
b6a83: e8 8e 5a 04 00 call fc516 <__frame_state_for+0xb96>
b6a88: 81 c1 6c e5 0700 add $0x7e56c,%ecx
b6a8e: 8b 89 e0 ff ffff mov -0x20(%ecx),%ecx
b6a94: 31 d2 xor %edx,%edx
b6a96: 29 c2 sub %eax,%edx
b6a98: 65 03 0d 00 00 0000 add %gs:0x0,%ecx
b6a9f: 89 11 mov %edx,(%ecx)
b6aa1: 83 c8 ff or $0xffffffff,%eax
b6aa4: eb dc jmp b6a82 <__read+0x52>
b6aa6: 90 nop
调用号#define __NR_read 3
(gdb) disass sys_read
Dump of assembler code for functionsys_read:
0xc017585a <sys_read+0>: push %ebp
0xc017585b <sys_read+1>: mov %esp,%ebp
0xc017585d <sys_read+3>: push %esi
0xc017585e <sys_read+4>: mov $0xfffffff7,%esi
0xc0175863 <sys_read+9>: push %ebx
0xc0175864 <sys_read+10>: sub $0xc,%esp
0xc0175867 <sys_read+13>: mov 0x8(%ebp),%eax
0xc017586a <sys_read+16>: lea -0xc(%ebp),%edx
0xc017586d <sys_read+19>: call 0xc0175f65 <fget_light>
0xc0175872 <sys_read+24>: test %eax,%eax
0xc0175874 <sys_read+26>: mov %eax,%ebx
0xc0175876 <sys_read+28>: je 0xc01758b1 <sys_read+87>
0xc0175878 <sys_read+30>: mov 0x24(%ebx),%edx
0xc017587b <sys_read+33>: mov 0x20(%eax),%eax
0xc017587e <sys_read+36>: mov 0x10(%ebp),%ecx
0xc0175881 <sys_read+39>: mov %edx,-0x10(%ebp)
0xc0175884 <sys_read+42>: mov 0xc(%ebp),%edx
0xc0175887 <sys_read+45>: mov %eax,-0x14(%ebp)
0xc017588a <sys_read+48>: lea -0x14(%ebp),%eax
0xc017588d <sys_read+51>: push %eax
0xc017588e <sys_read+52>: mov %ebx,%eax
0xc0175890 <sys_read+54>: call 0xc01753c1 <vfs_read>
0xc0175895 <sys_read+59>: mov -0x10(%ebp),%edx
0xc0175898 <sys_read+62>: mov %eax,%esi
0xc017589a <sys_read+64>: mov -0x14(%ebp),%eax
0xc017589d <sys_read+67>: mov %edx,0x24(%ebx)
0xc01758a0 <sys_read+70>: mov %eax,0x20(%ebx)
0xc01758a3 <sys_read+73>: cmpl $0x0,-0xc(%ebp)
0xc01758a7 <sys_read+77>: pop %eax
0xc01758a8 <sys_read+78>: je 0xc01758b1 <sys_read+87>
0xc01758aa <sys_read+80>: mov %ebx,%eax
0xc01758ac <sys_read+82>: call 0xc0175eae <fput>
0xc01758b1 <sys_read+87>: lea -0x8(%ebp),%esp
0xc01758b4 <sys_read+90>: mov %esi,%eax
0xc01758b6 <sys_read+92>: pop %ebx
0xc01758b7 <sys_read+93>: pop %esi
0xc01758b8 <sys_read+94>: pop %ebp
0xc01758b9 <sys_read+95>: ret
End of assembler dump.
(gdb) list fget_light
313 * holds a refcnt to that file. That check has to be done at fget() only
314 * and a flag is returned to be passed to the corresponding fput_light().
315 * There must not be a cloning between an fget_light/fput_light pair.
316 */
317 structfile *fget_light(unsigned int fd, int *fput_needed)
来自2.6.11
378 #define_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
379 type5,arg5,type6,arg6) \
380 type name (type1 arg1,type2arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) \
381 { \
382 long __res; \
383 __asm__ volatile ("push %%ebp ;movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" \
384 : "=a" (__res) \
385 : "i"(__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
386 "d"((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), \
387 "0" ((long)(arg6))); \
388 __syscall_return(type,__res); \
389 }
调用链回溯的代码实现
内核中(x86)对调用链的回溯的代码实现在文件dumpstack_32.c文件中。主要函数是dump_trace和print_context_stack.
待解释
C难点的汇编解释
例1
if ... else if
这个例子有人看来也许是非常非常地简单,但就这个例子,有的人还真给我考”倒”了。他的回话是“还真没见过这样子的代码”。但是,这样的代码在内核中比比皆是,比如后面附上的函数代码 do_path_lookup。如果对if ... else if 理解有偏差,对内核代码的逻辑理解根本就是差以千里。
#include <stdio.h>
int main()
{
int i = 1;
int j = 2;
if (i == 1)
printf("i,ok\n");
else if (j == 2)
printf("j,ok\n");
return 0;
}
这个例子,有人会疑问为什么”j,ok”没打印出来。现在我们分析下它的汇编代码
08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 14 sub $0x14,%esp //建立本地变量栈空间,以及子函数实参栈空间
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量i赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量j赋值
8048393: 83 7d f8 01 cmpl $0x1,-0x8(%ebp) //i和1比较
8048397: 75 0e jne 80483a7 <main+0x33> //如果i-1不等0,跳到地址80483a7执行。否则继续执行下面指令
8048399: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个参数入栈,它的栈空间之前已经建好。
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483a0: e8 2f ff ff ff call 80482d4 <puts@plt> //调用printf
80483a5: eb 12 jmp 80483b9 <main+0x45> //printf返回后,接着执行这个指令,将跳到地址80483b9继续运行
80483a7: 83 7d f4 02 cmpl $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9 <main+0x45>
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4 <puts@plt>
80483b9: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483be: 83 c4 14 add $0x14,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483c1: 59 pop %ecx //恢复保存的旧%ecx的值
80483c2: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c3: 8d 61 fc lea -0x4(%ecx),%esp
80483c6: c3 ret
经过上面的汇编代码分析,可见c代码块
else if (j == 2)
printf("j,ok\n");
对应的汇编代码是:
80483a7: 83 7d f4 02 cmpl $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9 <main+0x45>
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4 <puts@plt>
上面的代码指令根本就没有机会运行。
结论,一个if ... else if..else..
if (判断语句1)
代码块1
else if (判断语句2)
代码块2;
else if ....
..
else 代码块N;
语句块1,2..N的运行机会是一种互斥的关系。当然它们的“机会优先级”是不一样的。语句块1,2..N只有一个有被运行的机会,如果没有else甚至可能没有一个语句块能被运行。
内核代码实例
static int do_path_lookup(int dfd, constchar *name,
unsignedint flags, struct nameidata *nd)
{
intretval = 0;
intfput_needed;
structfile *file;
structfs_struct *fs = current->fs;
nd->last_type= LAST_ROOT; /* if there are only slashes... */
nd->flags= flags;
nd->depth= 0;
if(*name=='/') {
read_lock(&fs->lock);
if(fs->altroot.dentry && !(nd->flags & LOOKUP_NOALT)) {
nd->path= fs->altroot;
path_get(&fs->altroot);
read_unlock(&fs->lock);
if(__emul_lookup_dentry(name,nd))
gotoout; /* found in altroot */
read_lock(&fs->lock);
}
nd->path= fs->root;
path_get(&fs->root);
read_unlock(&fs->lock);
}else if (dfd == AT_FDCWD) {
read_lock(&fs->lock);
nd->path= fs->pwd;
path_get(&fs->pwd);
read_unlock(&fs->lock);
}else {
structdentry *dentry;
file= fget_light(dfd, &fput_needed);
retval= -EBADF;
if(!file)
gotoout_fail;
dentry= file->f_path.dentry;
retval= -ENOTDIR;
if(!S_ISDIR(dentry->d_inode->i_mode))
gotofput_fail;
retval= file_permission(file, MAY_EXEC);
if(retval)
gotofput_fail;
nd->path= file->f_path;
path_get(&file->f_path);
fput_light(file,fput_needed);
}
retval= path_walk(name, nd);
out:
if(unlikely(!retval && !audit_dummy_context() &&nd->path.dentry &&
nd->path.dentry->d_inode))
audit_inode(name,nd->path.dentry);
out_fail:
returnretval;
fput_fail:
fput_light(file,fput_needed);
gotoout_fail;
}
例2
短路逻辑算法。
这样的例子在内核代码中也是非常地多,一般用在短的函数或宏中。
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
if (a || ++b)
printf("%d\n", b);
return 0;
}
这个例子,有人会疑问为什么b的值没有变化,还是为2。现在我们分析下它的汇编代码
08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 24 sub $0x24,%esp //创建本地变量和子函数实参的栈空间(实际上没全部使用到)
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量a赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量b赋值
8048393: 83 7d f8 00 cmpl $0x0,-0x8(%ebp) //变量a和0比较,其实就是判断“表达式 a”是不是为假
8048397: 75 0a jne 80483a3 <main+0x2f> //a-0如果不等0,也就是a为真时就跳到地址80483a3执行。
//已经知道a==1,表达式a为真,所以将跳到地址80483a3执行
8048399: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6 <main+0x42>
80483a3: 8b 45 f4 mov -0xc(%ebp),%eax //把变量b的值放到临时寄存器%eax
80483a6: 89 44 24 04 mov %eax,0x4(%esp) //接着把它作为printf函数第二个实参入栈,
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483aa: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个实参入栈。记得X86下用户层的子函数参数
//是保存到栈的,而且是从右到左依次入栈
80483b1: e8 22 ff ff ff call 80482d8 <printf@plt> //调用printf函数
80483b6: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483bb: 83 c4 24 add $0x24,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483be: 59 pop %ecx //恢复保存的旧%ecx的值
80483bf: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c0: 8d 61 fc lea -0x4(%ecx),%esp
80483c3: c3 ret
分析可见C语句 if (a || ++b)中的++b对应的汇编码是
8048399: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6 <main+0x42>
可是因为a==1,表达式a已经为真,++b这个语句,也就是上面的汇编码,根本就没运行。所以变量b的值没有自增,还是保持为2。
结论
表达式 a, b
a || b: 如果a为真,b就不管;如果运行到b,a必已是假
a && b: 如果a为假,b就不管;如果运行到b,a必已是真
内核代码实例
static struct char_device_struct *
__register_chrdev_region(unsigned intmajor, unsigned int baseminor,
int minorct, const char *name)
{
......
i= major_to_index(major);
for(cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct> baseminor))))
break;
.....
}
例3
自增自减
自增自减,以及增减的前后问题。这类代码在内核数不胜数。理解稍有偏差,就会产生“边界问题”,或者在条件判断时理解出错。
#include <stdio.h>
int main()
{
int i = -1;
if (!i++) {
printf("inner: %d\n", i);
}
printf("outer: %d\n", i);
return 0;
}
汇编代码
08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp
8048381: 51 push %ecx
8048382: 83 ec 24 sub $0x24,%esp
8048385: c7 45 f8 ff ff ff ff movl $0xffffffff,-0x8(%ebp)
804838c: 83 45 f8 01 addl $0x1,-0x8(%ebp)
8048390: 83 7d f8 01 cmpl $0x1,-0x8(%ebp)
8048394: 75 13 jne 80483a9 <main+0x35>
8048396: 8b 45 f8 mov -0x8(%ebp),%eax
8048399: 89 44 24 04 mov %eax,0x4(%esp)
804839d: c7 04 24 90 84 04 08 movl $0x8048490,(%esp)
80483a4: e8 2f ff ff ff call 80482d8 <printf@plt>
80483a9: 8b 45 f8 mov -0x8(%ebp),%eax
80483ac: 89 44 24 04 mov %eax,0x4(%esp)
80483b0: c7 04 24 9b 84 04 08 movl $0x804849b,(%esp)
80483b7: e8 1c ff ff ff call 80482d8 <printf@plt>
80483bc: b8 00 00 00 00 mov $0x0,%eax
80483c1: 83 c4 24 add $0x24,%esp
80483c4: 59 pop %ecx
80483c5: 5d pop %ebp
80483c6: 8d 61 fc lea -0x4(%ecx),%esp
80483c9: c3 ret
80483ca: 90 nop
内核代码实例
int platform_add_devices(structplatform_device **devs, int num)
{
inti, ret = 0;
for(i = 0; i < num; i++) {
ret= platform_device_register(devs[i]);
if(ret) {
while(--i >= 0) /*没错,devs[i]没注册成功的话,从devs[i-1]起反注册*/
platform_device_unregister(devs[i]);
break;
}
}
returnret;
}
例4
函数指针
解释在“穿越交叉索引工具的盲区”→函数指针
#include <stdio.h>
int main()
{
int myfunc(int a, int b)
{
int c = a + b;
printf("%d\n", c);
return 0;
}
int (*funa)(int, int) = myfunc;
int (*funb)(int, int) = &myfunc;
int (*func)(int, int) = (int (*)(int, int))myfunc;
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);
myfunc(1, 2);
funa(3, 4);
funb(5, 6);
func(7, 8);
fund(9, 10);
return 0;
}
编译:
$ gcc -g -Wall fuk.c //注意,没任何警告
int main()
{
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
....省略
int (*funa)(int, int) = myfunc;
8048385: c7 45 f8 13 84 04 08 movl $0x8048413,-0x8(%ebp)
int (*funb)(int, int) = &myfunc;
804838c: c7 45 f4 13 84 04 08 movl $0x8048413,-0xc(%ebp)
int (*func)(int, int) = (int (*)(int, int))myfunc;
8048393: c7 45 f0 13 84 04 08 movl $0x8048413,-0x10(%ebp)
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);
804839a: c7 45 ec 13 84 04 08 movl $0x8048413,-0x14(%ebp)
myfunc(1, 2);
...省略
funa(3, 4);
80483b5: c7 44 24 04 04 00 00 movl $0x4,0x4(%esp)
80483bc: 00
80483bd: c7 04 24 03 00 00 00 movl $0x3,(%esp)
80483c4: 8b 45 f8 mov -0x8(%ebp),%eax
80483c7: ff d0 call *%eax
funb(5, 6);
....省略,funb, func,fund汇编码和funa完全相同
return 0;
8048405: b8 00 00 00 00 mov $0x0,%eax
}
804840a: 83 c4 24 add $0x24,%esp
...省略
08048413 <myfunc.1933>:
#include <stdio.h>
int main()
{
int myfunc(int a, int b)
{
8048413: 55 push %ebp
.....省略
}
xxx@ubuntu:~/dt/test$ gdb a.out
GNU gdb 6.8-debian
...
(gdb) list
1 #include<stdio.h>
......
17 funa(3,4);
....
20
(gdb) b 17
(gdb) r
Starting program: /home/xxx/桌面/test/a.out
Breakpoint 1, main () at fuck.c:17
17 funa(3,4);
(gdb) display/i $pc
1: x/i $pc
0x80483b5 <main+65>: movl $0x4,0x4(%esp)
(gdb) stepi
0x080483bd 17 funa(3,4);
1: x/i $pc
0x80483bd <main+73>: movl $0x3,(%esp)
(gdb)
0x080483c4 17 funa(3,4);
1: x/i $pc
0x80483c4 <main+80>: mov -0x8(%ebp),%eax
(gdb)
0x080483c7 17 funa(3,4);
1: x/i $pc
0x80483c7 <main+83>: call *%eax
(gdb) p/x $eax
$4 = 0x8048413
(gdb) info line *0x8048413
Line 6 of "fuck.c" starts ataddress 0x8048413 <myfunc> and ends at 0x8048419 <myfunc+6>.
(gdb)
其他例子
#include <stdio.h>
int main()
{
staticconst char *operstates[] = {
"unknown",
"notpresent",/* currently unused */
"down",
"lowerlayerdown",
"testing",/* currently unused */
"dormant",
"up"
};
enum{
IF_OPER_UNKNOWN,
IF_OPER_NOTPRESENT,
IF_OPER_DOWN,
IF_OPER_LOWERLAYERDOWN,
IF_OPER_TESTING,
IF_OPER_DORMANT,
IF_OPER_UP,
};
enum {
OPER_UNKNOWN = 1,
OPER_NOTPRESENT,
OPER_DOWN,
OPER_LOWERLAYERDOWN,
OPER_TESTING,
OPER_DORMANT,
OPER_UP,
};
enum {
UNKNOWN,
NOTPRESENT = 6,
DOWN,
LOWERLAYERDOWN,
TESTING,
DORMANT,
UP,
};
printf("%d\n",sizeof(operstates));
printf("%d\n",sizeof(operstates[0]));
printf("%d\n",IF_OPER_DOWN);
printf("%d\n",OPER_DOWN);
printf("%d\n",UNKNOWN);
printf("%d\n",DOWN);
}
优化级别的影响
这部分内容有点偏题,没必要这么钻牛角尖。但是为了说明“调试用的代码和实际运行的代码是不一样”的这个事实以及因为代码优化导致的“非理想状态”的调用链问题(见“内核初窥”),有必要用观察一个实例,以便有个直观的印象。
首先应该知道,有没有指定调试选项-g(–debug),在相同优先级下生成的代码都是一样的。差别只是,指定-g后,多生成了一个调试表。
优化选项
下面文字来自“ARM 系列应用技术完全手册”
使用-Onum选择编译器的优化级别。优化级别分别有
· -O0:除一些简单的代码编号外,关闭所有优化,该选项可提供最直接的优化信息。
· -O1:关闭严重影响调试效果的优化功能。使用该编译选项,编译器会移除程序中未使用到的内联函数和静态函数。如果于–debug(也就是-g)一起使用,该选项可以在较好的代码密度下,给出最佳调试视图。
· -O2:生成充分优化代码。如果与–debug一起使用,调试效果可能不令人满意,因为对目标代码到源代码的映射可能因为代码优化而发生变化。如果不生成调试表,这是默认优化级别。
· -O3:最高优化级别。使用该优化级别,使生成的代码在时间和空间上寻求平衡。
例子
#include <stdio.h>
int add(int a, int b)
{
return(a + b);
}
void funa()
{
inta = 3 + 4;
intb;
printf("%d\n",a);
b = add(5,6);
printf("%d\n",b);
}
int main()
{
intm = 1 + 2;
printf("%d\n",m);
funa();
}
$ gcc -g -O0 src.c (或者不指定优化选项: gcc -g src.c,编译出的机器码一样)
$ objdump -d a.out
得到一个结论:如果指定了-g而没指定优化等级,那么默认优化等级是最低的-O0
08048374 <add>:
8048374: 55 push %ebp
8048375: 89e5 mov %esp,%ebp
8048377: 8b45 0c mov 0xc(%ebp),%eax
804837a: 0345 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret
0804837f <funa>:
804837f: 55 push %ebp
8048380: 89e5 mov %esp,%ebp //保存旧栈帧,建立新栈帧
8048382: 83ec 18 sub $0x18,%esp //分配栈帧空间,注意分配了$0x18
8048385: c745 fc 07 00 00 00 movl $0x7,-0x4(%ebp) //-0x4(%ebp)是本地变量a的地址,int a = 3 + 4;
//注意编译器已经完成了计算
804838c: 8b45 fc mov -0x4(%ebp),%eax //a放到临时寄存器%eax
804838f: 8944 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
8048393: c704 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
804839a: e839 ff ff ff call 80482d8<printf@plt> //printf("%d\n",a);
804839f: c744 24 04 06 00 00 movl $0x6,0x4(%esp) //add(5,6);第二个参数入栈
80483a6: 00
80483a7: c704 24 05 00 00 00 movl $0x5,(%esp) //add(5,6);第一个参数入栈
80483ae: e8c1 ff ff ff call 8048374<add> //调用add
80483b3: 8945 f8 mov %eax,-0x8(%ebp) //-0x8(%ebp)是本地变量b的地址,b = add(5,6);
80483b6: 8b45 f8 mov -0x8(%ebp),%eax //b放到临时寄存器%eax
80483b9: 8944 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
80483bd: c704 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
80483c4: e80f ff ff ff call 80482d8 <printf@plt> //printf("%d\n", b);
80483c9: c9 leave //撤销新栈帧空间
80483ca: c3 ret //funa返回
080483cb <main>:
80483cb: 8d4c 24 04 lea 0x4(%esp),%ecx
80483cf: 83e4 f0 and $0xfffffff0,%esp
80483d2: ff71 fc pushl -0x4(%ecx)
80483d5: 55 push %ebp
80483d6: 89e5 mov %esp,%ebp
80483d8: 51 push %ecx
80483d9: 83ec 24 sub $0x24,%esp
80483dc: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%ebp)
80483e3: 8b45 f8 mov -0x8(%ebp),%eax
80483e6: 8944 24 04 mov %eax,0x4(%esp)
80483ea: c704 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f1: e8e2 fe ff ff call 80482d8<printf@plt>
80483f6: e884 ff ff ff call 804837f<funa>
80483fb: 83c4 24 add $0x24,%esp
80483fe: 59 pop %ecx
80483ff: 5d pop %ebp
8048400: 8d61 fc lea -0x4(%ecx),%esp
8048403: c3 ret
$ gcc -g -O1 src.c
$ objdump -d a.out
08048374 <add>:
8048374: 55 push %ebp
8048375: 89e5 mov %esp,%ebp
8048377: 8b45 0c mov 0xc(%ebp),%eax
804837a: 0345 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret
0804837f <funa>: //funa与-O0相比,没有了向本地变量a,b赋值的过程。
//代码量少了,分配的栈帧空间也小了。
804837f: 55 push %ebp
8048380: 89e5 mov %esp,%ebp
8048382: 83ec 08 sub $0x8,%esp //分配栈帧空间,注意分配了$0x8,比-O0下小了
8048385: c744 24 04 07 00 00 movl $0x7,0x4(%esp) //printf("%d\n", a);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量a赋值的过程。
804838c: 00
804838d: c704 24 c0 84 04 08 movl $0x80484c0,(%esp)
8048394: e83f ff ff ff call 80482d8 <printf@plt> //printf("%d\n", a);
8048399: c744 24 04 06 00 00 movl $0x6,0x4(%esp)
80483a0: 00
80483a1: c704 24 05 00 00 00 movl $0x5,(%esp)
80483a8: e8c7 ff ff ff call 8048374<add> //add(5,6);
80483ad: 8944 24 04 mov %eax,0x4(%esp) //add的返回结果作为printf("%d\n", b);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量b赋值的过程。
80483b1: c704 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483b8: e81b ff ff ff call 80482d8<printf@plt> //printf("%d\n",b);
80483bd: c9 leave
80483be: c3 ret
080483bf <main>:
80483bf: 8d4c 24 04 lea 0x4(%esp),%ecx
80483c3: 83e4 f0 and $0xfffffff0,%esp
80483c6: ff71 fc pushl -0x4(%ecx)
80483c9: 55 push %ebp
80483ca: 89e5 mov %esp,%ebp
80483cc: 51 push %ecx
80483cd: 83ec 14 sub $0x14,%esp
80483d0: c744 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d7: 00
80483d8: c704 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483df: e8f4 fe ff ff call 80482d8<printf@plt>
80483e4: e896 ff ff ff call 804837f<funa>
80483e9: 83c4 14 add $0x14,%esp
80483ec: 59 pop %ecx
80483ed: 5d pop %ebp
80483ee: 8d61 fc lea -0x4(%ecx),%esp
80483f1: c3 ret
$ gcc -g -O2 src.c
$ objdump -d a.out
我们应该知道,如果没有指定-g和优化选项,那么默认的优化等级就是-O2
08048380 <add>:
8048380: 55 push %ebp
8048381: 89e5 mov %esp,%ebp
8048383: 8b45 0c mov 0xc(%ebp),%eax
8048386: 0345 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d74 26 00 lea 0x0(%esi),%esi
08048390 <funa>:
8048390: 55 push %ebp
8048391: 89e5 mov %esp,%ebp
8048393: 83ec 08 sub $0x8,%esp
8048396: c744 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c704 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483a5: e82e ff ff ff call 80482d8 <printf@plt>
80483aa: c744 24 04 06 00 00 movl $0x6,0x4(%esp)
80483b1: 00
80483b2: c704 24 05 00 00 00 movl $0x5,(%esp)
80483b9: e8c2 ff ff ff call 8048380<add>
80483be: c704 24 d0 84 04 08 movl $0x80484d0,(%esp) //第二个参数入栈
80483c5: 8944 24 04 mov %eax,0x4(%esp) //第一个参数入栈。注意和-O1相比,参数在栈帧空间的位置没变,
//但是入栈指令的执行顺序有变。
80483c9: e80a ff ff ff call 80482d8<printf@plt> //printf("%d\n",b);
80483ce: c9 leave
80483cf: c3 ret
080483d0 <main>:
80483d0: 8d4c 24 04 lea 0x4(%esp),%ecx
80483d4: 83e4 f0 and $0xfffffff0,%esp
80483d7: ff71 fc pushl -0x4(%ecx)
80483da: 55 push %ebp
80483db: 89e5 mov %esp,%ebp
80483dd: 51 push %ecx
80483de: 83ec 14 sub $0x14,%esp
80483e1: c744 24 04 03 00 00 movl $0x3,0x4(%esp)
80483e8: 00
80483e9: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f0: e8e3 fe ff ff call 80482d8<printf@plt>
80483f5: e896 ff ff ff call 8048390<funa>
80483fa: 83c4 14 add $0x14,%esp
80483fd: 59 pop %ecx
80483fe: 5d pop %ebp
80483ff: 8d61 fc lea -0x4(%ecx),%esp
8048402: c3 ret
$ gcc -g -O3 src.c
$ objdump -d a.out
048380 <add>:
8048380: 55 push %ebp
8048381: 89e5 mov %esp,%ebp
8048383: 8b45 0c mov 0xc(%ebp),%eax
8048386: 0345 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d74 26 00 lea 0x0(%esi),%esi
08048390 <funa>: //与-O2相比,对函数add()的调用被编译器优化消失
8048390: 55 push %ebp
8048391: 89e5 mov %esp,%ebp
8048393: 83ec 08 sub $0x8,%esp
8048396: c744 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c704 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483a5: e82e ff ff ff call 80482d8<printf@plt>
80483aa: c744 24 04 0b 00 00 movl $0xb,0x4(%esp) //注意,与-O2相比,b = add(5,6);被优化掉了。
//之前应该有个优化为内联函数的过程,但因为add函数
//太简单,被直接计算了结果。(猜想)
//编译器直接计算出它的结果$0xb,也就是11
80483b1: 00
80483b2: c704 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483b9: e81a ff ff ff call 80482d8<printf@plt> //printf("%d\n",b);
80483be: c9 leave
80483bf: c3 ret
080483c0 <main>:
80483c0: 8d4c 24 04 lea 0x4(%esp),%ecx
80483c4: 83e4 f0 and $0xfffffff0,%esp
80483c7: ff71 fc pushl -0x4(%ecx)
80483ca: 55 push %ebp
80483cb: 89e5 mov %esp,%ebp
80483cd: 51 push %ecx
80483ce: 83ec 14 sub $0x14,%esp
80483d1: c744 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d8: 00
80483d9: c704 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483e0: e8f3 fe ff ff call 80482d8<printf@plt>
80483e5: c744 24 04 07 00 00 movl $0x7,0x4(%esp)
80483ec: 00
80483ed: c704 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483f4: e8df fe ff ff call 80482d8<printf@plt>
80483f9: c744 24 04 0b 00 00 movl $0xb,0x4(%esp)
8048400: 00
8048401: c704 24 e0 84 04 08 movl $0x80484e0,(%esp)
8048408: e8cb fe ff ff call 80482d8<printf@plt>
804840d: 83c4 14 add $0x14,%esp
8048410: 59 pop %ecx
8048411: 5d pop %ebp
8048412: 8d61 fc lea -0x4(%ecx),%esp
8048415: c3 ret
汇编基础--ARM篇
说明:
1. 部分内容和X86的重复,重复部分请参考X86的内容。
2. 某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。
用户手册
ARM7TDMI TechnicalReference Manual
ARM920T TechnicalReference Manual
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.home/index.html
指令速查 http://www.arm.com/pdfs/QRC0001H_rvct_v2.1_arm.pdf
调用链形成和参数传递
注意:arm体系过程调用的文字说明部分,都是依据AAPCS标准。
壮观的标准
参考:
AAPCS
Procedure CallStandard for the ARM Architecture
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042b/IHI0042B_aapcs.pdf
终于在“ARM ProcedureCall Standard”中找到了答案
PCS Procedure Call Standard.
AAPCS Procedure Call Standard for the ARM Architecture (this standard).
APCS ARM Procedure Call Standard (obsolete).
TPCS Thumb Procedure Call Standard (obsolete).
ATPCS ARM-Thumb Procedure Call Standard (precursor to this standard).
PIC, PID Position-independent code,position-independent data.
下面的标准已过时
APCS
ARM Procedure CallStandard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BGBGFIDA.html
Using the ARMProcedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Chdbceig.html
APCS 简介http://www.bsdmap.com/UNIX_html/ARM/apcsintro.html#01
TPCS
Thumb Procedure CallStandard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BCEEAHAF.html
Using the ThumbProcedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Cihdbchi.html
ATPCS
About the ARM-ThumbProcedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/Bcffcieh.html
别名的烦恼
arm体系的函数调用标准换了好几个版本,对寄存器的别名也是不一样。不同的调试器,或者它在不同的选项下,对同一个寄存器可能就有多种称呼。又或者你在调试器下看到的名称和书籍上的不一样。所以,又必要知道这些寄存器各自都有哪些别名。
我们运行下命令
$ arm-linux-gnueabi-objdump --help
....省略
The following ARM specific disassembleroptions are supported for use with
the -M switch:
reg-names-special-atpcs Selectspecial register names used in the ATPCS
reg-names-atpcs Selectregister names used in the ATPCS
reg-names-apcs Selectregister names used in the APCS
reg-names-std Selectregister names used in ARM's ISA documentation
reg-names-gcc Selectregister names used by GCC
reg-names-raw Selectraw register names
force-thumb Assumeall insns are Thumb insns
no-force-thumb Examinepreceeding label to determine an insn's type
我们下载它的源码打开看看
$ sudo apt-getsource binutils-arm-linux-gnueabi
完成后,在下载目录下多了几个东东,其中有一个文件夹binutils-2.18.1~cvs20080103,这是debian对官方binutils进行过修改的源码。在里面搜索文件arm-dis.c,该文件中有以下这个数组。
就是不同标准下各个寄存器的不同别名。
static const arm_regname regnames[] =
{
{ "raw" , "Select raw register names",
{ "r0", "r1", "r2", "r3","r4", "r5", "r6", "r7", "r8","r9", "r10", "r11", "r12","r13", "r14", "r15"}},
{ "gcc", "Selectregister names used by GCC",
{ "r0", "r1", "r2", "r3","r4", "r5", "r6", "r7", "r8","r9", "sl", "fp", "ip", "sp", "lr", "pc"}},
{ "std", "Selectregister names used in ARM's ISA documentation",
{ "r0", "r1", "r2", "r3","r4", "r5", "r6", "r7", "r8","r9", "r10", "r11", "r12","sp", "lr", "pc" }},
{ "apcs", "Select register names used in the APCS",
{ "a1", "a2", "a3", "a4","v1", "v2", "v3", "v4", "v5","v6", "sl", "fp", "ip", "sp", "lr", "pc" }},
{ "atpcs", "Select register names used in theATPCS",
{ "a1", "a2", "a3", "a4","v1", "v2", "v3", "v4", "v5","v6", "v7", "v8", "IP", "SP", "LR", "PC"}},
{ "special-atpcs", "Select special register names used inthe ATPCS",
{ "a1", "a2", "a3", "a4","v1", "v2", "v3", "WR", "v5","SB", "SL", "FP", "IP", "SP", "LR", "PC"}},
};
但是可以看到,该列表并没有包含AAPCS标准,AAPCS标准对 r9又引入了一个别名 TR,这样AAPCS下,r9使用了三个别名v6, SB, TR。选用哪个
别名,是依赖于不同平台的选择。
[扩展,简要说明原理。并用实例解析]
寄存器的角色与保护
· 寄存器的角色(AAPCS标准)
寄存器 |
可选寄存器名 |
特殊寄存器名 |
在函数调用中的角色 |
r15 |
PC |
The Program Counter. |
|
r14 |
LR |
The Link Register. |
|
r13 |
SP |
The Stack Pointer. |
|
r12 |
IP |
The Intra-Procedure-call scratch register. |
|
r11 |
v8 |
Variable-register 8. |
|
r10 |
v7 |
Variable-register 7. |
|
r9 |
v6/SB/TR |
Platform register. The meaning of this register is defined by the platform standard |
|
r8 |
v5 |
Variable-register 5. |
|
r7 |
v4 |
Variable register 4. |
|
r6 |
v3 |
Variable register 3. |
|
r5 |
v2 |
Variable register 2. |
|
r4 |
v1 |
Variable register 1. |
|
r3 |
a4 |
Argument / scratch register 4. |
|
r2 |
a3 |
Argument / scratch register 3. |
|
r1 |
a2 |
Argument / result / scratch register 2. |
|
r0 |
a1 |
Argument / result / scratch register 1. |
前四个寄存器r0-r3 (a1-a4)用于传递参数给子函数或从函数中返回结果值。他们也可用于在一个函数中保存寄存器的值(但是,一般只用在子函数调用中)。
寄存器r12 (IP) 可在函数以及该函数调用的任何子函数中被链接器用作临时寄存器。它也可以在函数调用中用于保存寄存器的值。
寄存器r9的角色是平台相关的。虚拟系统可能赋予该寄存器任何角色,因此必须说明它的用法。比如,在位置无关数据模型中它可以指定为static base(SB),或者在带有本地线程存储的环境中指定它为threadregister(TR)。该寄存器的使用可能要求在所有调用过程前后,它保存的值必须不变。在一个不需要这样特殊寄存器的虚拟平台上,r9可以指定为新 增的callee-saved variableregister,v6.
通常,寄存器r4-r8, r10 和 r11 (v1-v5, v7 和 v8)用于保存函数的本地变量。这些寄存器中,只有v1-v4能被整个thumb指令集一致地使用,但是AAPCS并没有规定Thumb代码只能使用这些寄存器。
子函数必须保护寄存器r4-r8,r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。
在所有的函数调用标准中,寄存器r12-r15都扮演特殊的角色。依据这些角色,它们被标注为IP, SP, LR 和 PC。
寄存器CPSR的属性(省)
· 寄存器保护规则
子函数必须保护寄存器r4-r8,r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。子函数调用
· 子函数调用
ARM 和Thumb指令集都有一个函数调用指令元语,BL,它执行branch-with-link操作。BL的执行效果是把紧跟程序计数器的下一个值--也就是返回地址--传送到链接寄存器(LR),然后把目标地址传送到程序寄存器(PC)中。如果 BL指令是在Thumb状态下执行的,链接寄存器的Bit 0就设置为1;如果是在ARM状态下执行的,则设置为0。执行的结果是,把控制权转给目标地址,并把存放在LR中的返回地址作为附加的参数传递给了被调用的函数。
当返回地址装载到PC时,控制就返回给跟随BL后面的指令。
子函数调用可以由具有下面效果的任何指令序列完成:
LR[31:1] ← 返回地址
LR[0] ← 返回地址的代码类型 (0 ARM, 1 Thumb)
PC ← 子函数地址
...
返回地址:
例如,在ARM状态中,调用由r4指定了地址的子函数
do:
MOVLR, PC
BX r4
...
注意,相同的指令序列在Thumb状态中将不能工作,因为设置LR的指令并没有拷贝Thumb 状态标志位到LR[0]中。
在ARM V5架构中,ARM 和 Thumb指令集都提供了BLX指令,它将调用由一个寄存器指定了地址的子函数,并正确地设置返回地址为程序计数器的下一个值。
条件执行
操作码[31:28] |
助记符扩展 |
解释 |
用于执行的标志位状态 |
0000 |
EQ |
相等/等于0 |
Z置位 |
0001 |
NE |
不等 |
Z清0 |
0010 |
CS/HS |
进位/无符号数高于或等于 |
C置位 |
0011 |
CC/LO |
无进位/无符号数小于 |
C清0 |
0100 |
MI |
负数 |
N置位 |
0101 |
PL |
正数或0 |
N清0 |
0110 |
VS |
溢出 |
V置位 |
0111 |
VC |
未溢出 |
V清0 |
1000 |
HI |
无符号数高于 |
C置位,Z清0 |
1001 |
LS |
无符号数小于或等于 |
C清0,Z置位 |
1010 |
GE |
有符号数大于或等于 |
N等于V |
1011 |
LT |
有符号数小于 |
N不等于V |
1100 |
GT |
有符号数大于 |
Z清0且N等于V |
1101 |
LE |
有符号数小于或等于 |
Z置位且N不等于V |
1110 |
AL |
总是 |
任何状态 |
1111 |
NV |
从不(未使用) |
无 |
调用链的形成
注意对比ARM和X86在调用链形成的类似和区别之处。
区别,首先在寄存器的名称和角色的差异。
1. X86中寄存器%eip指向的是下一个将要执行的指令。在ARM中也有个类似别名的寄存器ip。但这个寄存器ip的作用并不是指向的是下一个将要执行的指 令。在ARM中,寄存器pc才是起着X86中寄存器%eip的角色,也就是包含下一个将要执行指令的地址。而ARM中的ip寄存器,作用比较自由,类似干 杂工的人,一般用于临时寄存器。[扩展,引用权威手册的话]
2. X86中,返回地址是直接保存在栈中的。但是ARM不一样了,它寄存器比X86多得多,财大气粗,所以,返回地址保存在了专用的寄存器lr(link register)中。但是,不要以为把返回地址放到专用的寄存器中会省事,其实反而多事了。因为,在调用函数刚执行完调用语句之时,lr保存的是子函数 的返回地址,而指令控制权转移到了子函数后,子函数照样可能调用自己的子函数,依次需要使用lr。所以自然也就有了lr的值的保存与恢复的问题,解决方法 还是要靠压栈解决。(参考下面的内容)
3. 我们知道,描述栈帧就是描述栈帧的基地址和顶端地址。在X86中,用专用的寄存器%ebp保存栈基址,也就是base pointer;%esp保存栈顶端地址,也就是stack pointer。在ARM中,也有专用的寄存器保存栈顶端地址,就是SP(stack pointer的简称)。但是,在保存栈基址这方面,依据最新的AAPCS标准,ARM就很吝啬了,没有一个保存栈基址的专用寄存器。又不过呢,在 APCS和ATPCS标准中,有fp寄存器用于保存帧指针(frame pointer,也就是X86的base pointer)。在现在的编译器,可以看到,还是依照惯例把fp用于保存帧指针。既然如此,当然也有个入栈保存恢复的问题。
调用链包含两方面的内容,和X86类似
1.返回地址的保存与恢复
由调用函数在执行调用指令时把子函数的返回地址传送进连接寄存器lr中,指令控制权转交给子函数后,再由子函数负责把上层函数的lr(也就是子函数的返回地址)保存到栈中。然后子函数在返回前的最后时刻,再负责把lr的保存值从栈弹回到lr中,从而恢复了上层函数的lr。这时还没完事,子函数在执行 返回指令时,由返回指令把lr的值传送到寄存器pc(Program Counter),从而导致接下来的指令是从子函数的返回地址开始运行。这样,指令控制权就返回给了调用函数。
我们应当注意到,ARM中调用指令也是多种多样的。有b,bl,bx,bxl。如果调用指令是不带连接的指令,比如b,bx,这时就要人工给lr赋 值。不过为了简便,我不再区分这两类指令,而把实现跳转和连接以及可能的换态这些功能的整个指令序列为“调用指令”,相关区别参考指令手册。在ARM中, 返回指令和调用指令都是同一套的。而X86,调用用call,返回用ret。
2.旧栈帧的保存与恢复
对比X86栈帧的保存与恢复的方式,ARM的更加简单直接。就是直接把上一栈帧的帧指针(frame pointer,也就是栈帧基地址)以及栈顶端指针sp(stack pointer)压入栈中。子函数返回时,在执行返回指令之前的最后关头才从栈弹出fp和sp的值,从而恢复旧栈帧。这个过程真的没有遗漏了吗?我们看 下,上面的步骤保证了调用函数的栈帧不被破坏,但是子函数自己的栈帧却没有建立起来呢。首先是帧指针需要人赋值。这个情形和X86非常相似。子函数在使用 栈帧之前,把上层函数的栈顶端指针sp赋给一个临时寄存器ip,然后在旧fp的值被压栈保存之后,把ip的值减去4,再赋给帧指针寄存器fp,此时,fp 就指向了新栈帧的基址。这是因为,新栈帧基地址刚好位于旧栈帧栈顶之下,地址低了4字节。其次,子函数栈帧的栈顶指针sp也是要考虑的,根据压栈指令的不 同,sp可能不需要人工维护,也可能需要人工维护[有疑问...????]。
我们还注意到,在X86中,子函数的栈帧的底端(也就是%ebp所指的内存位置)存放着上一层栈帧的基址指针(旧%ebp)的值,一层层下去,这样 就形成回溯的链条。那么,在ARM之下,也是靠子函数的栈帧的底端提供回溯的能力的吗?当然不是。实际上子函数的栈帧的基址位置存放的是什么,这无所谓 的。
[疑问???如果旧fp保存在新栈帧中的位置不是固定的,那么调试器是如何做到栈帧回溯的呢?]
根据AAPCS标准的规定,子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。注意,它用的字眼是“保护”,而不是“保存”。
· 应用层实例解析
#include <stdio.h>
void func()
{}
void funb()
{
func();
}
void funa()
{
funb();
}
int main()
{
funa();
}
-----------
000083b0 <func>:
#include <stdio.h>
void func()
{}
83b0: e1a0c00d mov ip, sp
83b4: e92dd800 push {fp, ip, lr, pc}
83b8: e24cb004 sub fp, ip, #4 ; 0x4
83bc: e24bd00c sub sp, fp, #12 ; 0xc
83c0: e89d6800 ldm sp, {fp, sp, lr}
83c4: e12fff1e bx lr
000083c8 <funb>:
void funb()
{
83c8: e1a0c00d mov ip, sp
83cc: e92dd800 push {fp, ip, lr, pc}
83d0: e24cb004 sub fp, ip, #4 ; 0x4
func();
83d4: ebfffff5 bl 83b0 <func>
}
83d8: e24bd00c sub sp, fp, #12 ; 0xc
83dc: e89d6800 ldm sp, {fp, sp, lr}
83e0: e12fff1e bx lr
000083e4 <funa>:
void funa()
{
83e4: e1a0c00d mov ip, sp
83e8: e92dd800 push {fp, ip, lr, pc}
83ec: e24cb004 sub fp, ip, #4 ; 0x4
funb();
83f0: ebfffff4 bl 83c8 <funb>
}
83f4: e24bd00c sub sp, fp, #12 ; 0xc
83f8: e89d6800 ldm sp, {fp, sp, lr}
83fc: e12fff1e bx lr
00008400 <main>:
int main()
{
8400: e1a0c00d mov ip, sp
8404: e92dd800 push {fp, ip, lr, pc}
8408: e24cb004 sub fp, ip, #4 ; 0x4
funa();
840c: ebfffff4 bl 83e4 <funa>
}
8410: e24bd00c sub sp, fp, #12 ; 0xc
8414: e89d6800 ldm sp, {fp, sp, lr}
8418: e12fff1e bx lr
· 内核层实例解析
栈帧结构与参数传递
[1.栈:栈对齐,栈限制。2.参数传递:variadic函数,nonvariadic函数。3.结果的返回 4.互交代码(ARM-Thumbinterworking)]
栈帧示意图
+------------------------------ + ---------
| Register Save Area | |
+------------------------------ + |
| Locals andTemporaries | |
+------------------------------+
| alloca() Locals | Caller's Frame
+------------------------------ +
| Incoming Args Past FourWords | |
+------------------------------ + ---------
| First Four Words OfArgs | |
Frame Pointer--> +------------------------------ + |
| Register Save Area | |
+------------------------------ + Current Frame
| Locals andTemporaries |
+------------------------------ + |
| alloca() Locals | |
+------------------------------ + |
| Outgoing Args PastFour Words | |
Stack Pointer---> +------------------------------ + ---------
完整的调用过程
函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。
· 函数调用前调用者的动作
· 函数调用 call callee
· 函数调用后被调用者的动作
· 调用返回前被调用者的动作
· 调用返回后调用者的动作
· 应用层实例解析
· 内核层实例解析
调用链回溯的实现
arm体系对调用链的回溯的代码实现主要在
arch/arm/kernel/traps.c 和arch/arm/lib/backtrace.S.其中核心函数是backtrace.S中的__backtrace函数。
待解释
---/*
* linux/arch/arm/lib/backtrace.S
*
* Copyright (C) 1995, 1996 Russell King
*
*This program is free software; you can redistribute it and/or modify
*it under the terms of the GNU General Public License version 2 as
*published by the Free Software Foundation.
*
*27/03/03 Ian Molton Clean up CONFIG_CPU
*
*/
#include <linux/linkage.h>
#include <asm/assembler.h>
.text
@ fp is 0 or stack frame
#define frame r4
#define sv_fp r5
#define sv_pc r6
#define mask r7
#define offset r8
ENTRY(__backtrace)
mov r1, #0x10
mov r0, fp
ENTRY(c_backtrace)
#if !defined(CONFIG_FRAME_POINTER) ||!defined(CONFIG_PRINTK)
mov pc, lr
ENDPROC(__backtrace)
ENDPROC(c_backtrace)
#else
stmfd sp!, {r4 - r8, lr} @ Save an extra register so we have a location...
movs frame, r0 @if frame pointer is zero
beq no_frame @we have no stack frames
tst r1, #0x10 @26 or 32-bit mode?
moveq mask, #0xfc000003 @ mask for 26-bit
movne mask, #0 @mask for 32-bit
1: stmfd sp!, {pc} @calculate offset of PC stored
ldr r0, [sp], #4 @ by stmfd for this CPU
adr r1, 1b
sub offset, r0, r1
/*
*Stack frame layout:
* optionally saved caller registers (r4 - r10)
* saved fp
* saved sp
* saved lr
* frame => saved pc
* optionally saved arguments (r0 - r3)
*saved sp => <next word>
*
*Functions start with the following code sequence:
* mov ip, sp
* stmfd sp!, {r0 - r3}(optional)
*corrected pc => stmfd sp!, {..., fp,ip, lr, pc}
*/
for_each_frame: tst frame, mask @ Check for address exceptions
bne no_frame
1001: ldr sv_pc, [frame, #0] @ get saved pc
1002: ldr sv_fp, [frame, #-12] @ get saved fp
sub sv_pc, sv_pc, offset @ Correct PC for prefetching
bic sv_pc, sv_pc, mask @ mask PC/LR for the mode
1003: ldr r2, [sv_pc, #-4] @ if stmfd sp!, {args} exists,
ldr r3, .Ldsi+4 @ adjust saved 'pc' back one
teq r3, r2, lsr #10 @ instruction
subne r0, sv_pc, #4 @ allow for mov
subeq r0, sv_pc, #8 @ allow for mov + stmia
ldr r1, [frame, #-4] @ get saved lr
mov r2, frame
bic r1, r1, mask @ mask PC/LR for the mode
bl dump_backtrace_entry
ldr r1, [sv_pc, #-4] @ if stmfd sp!, {args} exists,
ldr r3, .Ldsi+4
teq r3, r1, lsr #10
ldreq r0, [frame, #-8] @ get sp
subeq r0, r0, #4 @point at the last arg
bleq .Ldumpstm @dump saved registers
1004: ldr r1, [sv_pc, #0] @ if stmfd sp!, {..., fp, ip, lr, pc}
ldr r3, .Ldsi @instruction exists,
teq r3, r1, lsr #10
subeq r0, frame, #16
bleq .Ldumpstm @dump saved registers
teq sv_fp, #0 @zero saved fp means
beq no_frame @no further frames
cmp sv_fp, frame @ next frame must be
mov frame, sv_fp @ above the current frame
bhi for_each_frame
1006: adr r0, .Lbad
mov r1, frame
bl printk
no_frame: ldmfd sp!, {r4 - r8,pc}
ENDPROC(__backtrace)
ENDPROC(c_backtrace)
.section__ex_table,"a"
.align 3
.long 1001b, 1006b
.long 1002b, 1006b
.long 1003b, 1006b
.long 1004b, 1006b
.previous
#define instr r4
#define reg r5
#define stack r6
.Ldumpstm: stmfd sp!, {instr, reg,stack, r7, lr}
mov stack, r0
mov instr, r1
mov reg, #10
mov r7, #0
1: mov r3, #1
tst instr, r3, lsl reg
beq 2f
add r7, r7, #1
teq r7, #6
moveq r7, #1
moveq r1, #'\n'
movne r1, #' '
ldr r3, [stack], #-4
mov r2, reg
adr r0, .Lfp
bl printk
2: subs reg, reg, #1
bpl 1b
teq r7, #0
adrne r0, .Lcr
blne printk
ldmfd sp!, {instr, reg, stack, r7, pc}
.Lfp: .asciz "%cr%d:%08x"
.Lcr: .asciz "\n"
.Lbad: .asciz "Backtrace aborted due to bad framepointer <%p>\n"
.align
.Ldsi: .word 0xe92dd800 >> 10 @ stmfd sp!, {... fp, ip, lr, pc}
.word 0xe92d0000 >> 10 @ stmfd sp!, {}
#endif
源码浏览工具
本节意义: 内核源码的代码量越来越大,不借助源码交叉索引工具根本是无法阅读了。一定要熟练灵活掌握此类工具的使用
调用图生成工具
1.CodeViz
官网:
http://www.csn.ul.ie/~mel/projects/codeviz/
安装使用:
CodeViz —— 一款分析C_C++源代码中函数调用关系的调用图生成工具.pdf
http://linux.chinaunix.net/bbs/thread-1031921-1-1.html
用CodeViz产生函数调用图
http://barry-popy.blog.sohu.com/31629163.html
分析函数调用关系图(callgraph)的几种方法
http://blog.csdn.net/Solstice/archive/2005/09/24/488865.aspx
用CodeViz绘制函数调用关系图(call graph)
http://blog.csdn.net/Solstice/archive/2005/09/22/486788.aspx
2.ncc
find + grep
对于源码的阅读工具,一般是选取后面提到的某种源码索引工具,再和find以及grep“高低搭配”一起来使用。
1.命令选项
2.正则表达式
Regular ExpressionHOWTO: http://www.amk.ca/python/howto/regex/
正则表达式之道: http://net.pku.edu.cn/~yhf/tao_regexps_zh.html
wine + SI
wine + sourceinsight
优缺点
优点: SI的特点是有图形界面,操作和浏览特别方便快捷。特别是它的“函数调用树”的图形显示功能,以及分窗口自动显示函数,变量等定义的功能。
缺点: 不能解析汇编源文件。
安装wine
在ubuntu/debian下用以下命令就能在线安装wine
$ sudo apt-get install wine
安装好后,就能看到wine的快捷菜单被添加到了任务栏的“应用程序”中。
安装SI
wine安装好后,就可以像在windows一样去安装使用SI了。安装完成后,SI的快捷菜单被添加到“应用程序”→“wine”→“programs”→“source insight3”中。以后用快捷菜单就能启动SI
SI的设置
字体,颜色就不说了。现在加入 无名小卒 大侠发现的一个有用设置。
preferences→display→ trim long path names with ellipses. 去掉该选项的选择。这样就能直接在上下两个分窗口的标题栏上看到一个源文件的全路径。如果不去掉的话,对于长路径它会用...的形式来表示路径的一部分。
SI的使用
可以乱点乱试一下,它能提供很多的功能。其中一些经常要到的功能有 查找符号;函数调用的函数,被调用的函数;以及调用关系的多层展开显示;字符串搜索等。
global
[待玩] http://www.gnu.org/software/global/
Source-Navigator
[待玩] http://sourcenav.sourceforge.net/
安装:
在ubuntu下可以在线安装
$ sudo apt-get install sourcenav
运行:
$ snavigator
vim + cscope/ctags
参考:
cscope的官方教程 “The Vim/Cscope tutorial”:
http://cscope.sourceforge.net/cscope_vim_tutorial.html
对应的中文翻译: http://www.gracecode.com/Archive/Display/316
http://www.lupaworld.com/?uid-151392-action-viewspace-itemid-106656
http://dev.21tx.com/2007/02/21/10252.html
优缺点
优点: 本人感觉在终端下看源码比较舒服。
缺点: 没有一个实时显示函数/变量定义的分窗口。也不能直接显示“调用树”,但有其他小工具可以实现该功能。也许vim高手能解决这些问题。
安装cscope/ctags
ubuntu/debian下用以下命令就能在线安装
$ sudo apt-get install cscope ctags
命令选项
在终端下可以用 man info –help等形式查看cscope/ctags的手册
在vim下查看手册的方式是
:help cscope
和
:help ctags
1. 以下是cscope建立索引文件用到的一些选项
-R: 在生成索引文件时,搜索子目录树中的代码
-b: 只生成索引文件,不进入cscope的界面
-q: 生成cscope.in.out和cscope.po.out文件,加快cscope的索引速度
-k: 在生成索引文件时,不搜索/usr/include目录
-i: 如果保存文件列表的文件名不是cscope.files时,需要加此选项告诉cscope到哪儿去找源文件列表。可以使用“-”,表示由标准输入获得文件列表。
-I dir: 在-I选项指出的目录中查找头文件
-u: 扫描所有文件,重新生成交叉索引文件
-C: 在搜索时忽略大小写
-P path: 在以相对路径表示的文件前加上的path,这样,你不用切换到你数据库文件所在的目录也可以使用它了。
2. 在vim下利用:cscope find <关键字> 命令的选项有
s: 查找C语言符号,即查找函数名、宏、枚举值等出现的地方
g: 查找函数、宏、枚举等定义的位置,类似ctags所提供的功能
d: 查找本函数调用的函数
c: 查找调用本函数的函数
t: 查找指定的字符串
e: 查找egrep模式,相当于egrep功能,但查找速度快多了
f: 查找并打开文件,类似vim的find功能
i: 查找包含本文件的文
使用
建立索引
[可能要修改]
用以下命令先产生一个文件列表,然后让cscope为这个列表中的每个文件都生成索引。在这里,我们只关注.h, .c, .S文件,所以只对他们进行索引。可以根据自己需求进行更改。接着我们用-bq选项利用cscope生成索引。选项意义见上节。同时也生成ctags索 引。
#!/bin/sh
find . -name "*.h" -o -name"*.c" -o -name "*.S" > cscope.files
cscope -bkq -i cscope.files
ctags -R
利用vim浏览源码
切换到内核源码的目录上,运行vim,然后在vim下导入索引
$vim
:cscope add cscope.out
然后就可以在vim下调用“:cscope find <关键字>”来查找函数的定义,函数调用的函数以及被调用函数等
“:cscope find <关键字>” 可以缩写为 “:cs f <关键字>”
比如以下命令用来查找sys_read的定义
:cs f g sys_read
“cs f”的其他命令选项请看上节
快捷键的使用
ctrl + t : 退回
ctrl + ] : 进入光标处的变量/函数的定义处
kscope
kscope是cscope的图形前端工具。在ubuntu下可以在线安装。它的界面上和操作上与source insight都比较类似。但是目前它对cpu的占用很大,不是很好。但是它和cscope相比,有一个很大的优点是:可以图形显示“函数调用树”,甚至这个功能比SI还强大。
$sudo apt-get install kscope
lxr
1. 优缺点
优点:本身好像没什么特别的优点。但是有专门提供这种服务的网站,上面有很多不同系统的不同版本源码
缺点:在本机上配置运行的话,配置麻烦。如果是浏览lxr站点的方式,速度比较慢。
2. lxr官方: http://lxr.linux.no/
特点是可以浏览历史上linux所有版本的源码,可以看到它的演化过程。
3. 其他系统的源码 http://fxr.watson.org/
估计超一流的内核开发人员,可能会经常访问此类站点。因为他需要借鉴其他系统的设计思想。
SI等与gdb的特点
在源码阅读的功能上:
1. SI等适合“面读”,也就是读一个代码段,并且提供更舒适的阅读辅助手段。SI适合分析函数全面的逻辑。
2. gdb适合“线读”,也就是以追踪调用链的方式深入阅读,并且提供了数据分析的调试功能。适合分析特定情况下的函数逻辑表现。
调用链、调用树和调用图
为了能使用调试器,必须理解函数调用链在调试器级别的表现形式。但是,因为存在内嵌函数和代码优化等原因,调试器的表现形式和源码浏览器下的表现形式是不一样的。它们两者的信息显示可能存在“错位”的现象。本节的目的就是为了磨合调试器和交叉索引工具之间的代沟。
为了简化问题的描述,在实际分析前,先将知识点分解介绍一下。
理想调用链
下面我给出一个处于“理想状态”的经典backtrace(backtrace的意思是“回溯”,依照它的作用来说,也就是本人说的调用链)。所谓“理想 状态的”的backtrace是指,可以利用内核源码交叉索引工具,依据gdb给出的这个backtrace,从frame0开始一级级往后最追溯,能够一直追溯到最前面的frame N,而且追溯的过程中,没有出现多出来的连接frameN和frame(N-1)的“过渡”frame.
注意其中的两个条件:1.能够 2.不多出。但是,在现实的世界里,往往没这么美好。源码浏览工具往往要么“不能”,要么“多出”。造成前者的原因在于源码浏览工具的局限性,造成后者的是内嵌函数以及代码优化。详细情况可看下节的分析。
追溯的方法对于sourceinsight来说就是:打开”relation window”→选中要被追溯的函数→右键→选“view relation”→选“referenced by functions”,这样就能显示出调用了被选函数的函数来。
我们拿下面这个“理想状态”的backtrace分析一下
(gdb) bt
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
#1 0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) atlib/kobject.c:149
#2 0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) atlib/kobject.c:282
#3 0xc01de972 in kobject_create () at lib/kobject.c:619
#4 0xc01def53 in kobject_create_and_add (name=0xdc40abe4 "",parent=0xc035b9dc) at lib/kobject.c:641
#5 0xc0393b04 in mnt_init () at fs/namespace.c:2333
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
理想状态下的backtrace各个域的含义是(注意,在非理想状态的backtrace中,这些含义往往对不上号)
#frameN的编号 frame(N-1)的返回地址(注:fram0没有这项) in frameN所处的函数(该函数的参数...) at该函数所处的源文件 : frameN函数内对frame(N-1)函数的调用语句在源文件中所处的行数
我们看下
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
它说明frame0时,kref_init正要运行。传入的参数是0xdc40abe4。函数kref_init从源文件lib/kref.c第33行开始。 在gdb下调用shell来查看源文件
(gdb) shell vi lib/kref.c
vi 出来后打命令:set nu可看到
31 */
32 void kref_init(struct kref *kref)
33 {
34 kref_set(kref, 1);
35 }
36
我们再看看frame0这一瞬间是不是“kref_init正要运行”。应该知道,“正要运行”和“正要被调用”是两个不同的概念。前者来说,到了下一个指令,代码的控制权就会交给了被调用的函数;而后者,到了下一个指令,代码的控制权还在调用者手里,
(gdb) f 0
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
33 {
(gdb) info registers
....
edi 0x0 0
eip 0xc01df520 0xc01df520 <kref_init> //<-注意eip是下一个将要运行的指令地址
eflags 0x282 [SF IF ]
....
(gdb) disass kref_init
Dump of assembler code for functionkref_init:
0xc01df520 <kref_init+0>: push %ebp //对比上面,eip指向这里
0xc01df521 <kref_init+1>: mov %esp,%ebp
...
0xc01df52f <kref_init+15>: ret
End of assembler dump.
(gdb)
可见,kobject_init_internal的调用指令call已经执行完毕,到了frame0时,下一个指令“将要运行”函数kref_init。
再看看
#1 0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) atlib/kobject.c:149
frameN与frame(N-1)之间是调用的关系,前者调用了后者。也就是说,frame1的kobject_init_internal调用 frame0的kref_init,并且kref_init函数返回后,将返回到地址0xc01de8be继续执行。0xc01de8be就在 kobject_init_internal的体内,函数kobject_init_internal中调用kref_init的C语句位于lib/kobject.c的149行。
查看一下kobject_init_internal的反汇编码
(gdb) disass kobject_init_internal
Dump of assembler code for functionkobject_init_internal:
0xc01de8ac<kobject_init_internal+0>: push %ebp
0xc01de8ad<kobject_init_internal+1>: test %eax,%eax
0xc01de8af<kobject_init_internal+3>: mov %esp,%ebp
0xc01de8b1<kobject_init_internal+5>: push %ebx
0xc01de8b2<kobject_init_internal+6>: mov %eax,%ebx
0xc01de8b4<kobject_init_internal+8>: je 0xc01de8d3<kobject_init_internal+39>
0xc01de8b6<kobject_init_internal+10>: lea 0x4(%eax),%eax
0xc01de8b9<kobject_init_internal+13>: call 0xc01df520 <kref_init>
0xc01de8be<kobject_init_internal+18>: lea 0x8(%ebx),%eax //注意这个地址0xc01de8be是kref_init的返回地址
0xc01de8c1<kobject_init_internal+21>: mov %eax,0x8(%ebx)
再看看lib/kobject.c,看看最后的那个行数的意义
145 static voidkobject_init_internal(struct kobject *kobj)
146 {
147 if (!kobj)
148 return;
149 kref_init(&kobj->kref); //注意kobject_init_internal调用子函数kref_init的C语句位于行数149
150 INIT_LIST_HEAD(&kobj->entry);
151 kobj->state_in_sysfs = 0;
152 kobj->state_add_uevent_sent = 0;
153 kobj->state_remove_uevent_sent = 0;
154 kobj->state_initialized = 1;
155 }
在验证一下
#2 0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) atlib/kobject.c:282
看看kobject_init的反汇编码
(gdb) disass kobject_init
Dump of assembler code for functionkobject_init:
0xc01de8f3 <kobject_init+0>: push %ebp
........
0xc01de923 <kobject_init+48>: call 0xc01de8ac <kobject_init_internal>
0xc01de928 <kobject_init+53>: mov %esi,0x18(%ebx) //注意这个地址0xc01de928是kobject_init_internal的返回地址
......
0xc01de94b <kobject_init+88>: pop %ebp
0xc01de94c <kobject_init+89>: ret
End of assembler dump.
看看看看lib/kobject.c,看看最后的那个行数的意义
263 void kobject_init(struct kobject *kobj,struct kobj_type *ktype)
264 {
265 char *err_str;
.......
282 kobject_init_internal(kobj); ////注意kobject_init调用子函数kobject_init_internal的C语句位于行数282
283 kobj->ktype = ktype;
......
287 printk(KERN_ERR "kobject (%p):%s\n", kobj, err_str);
288 dump_stack
通过这两个例子,可见最初的猜想是正确的。
函数指针调用
本小节意义: 在利用SI等工具查看函数调用链时,遇到的一个最多的问题是函数指针的调用。所以把该小节内容移到这里来,为下小节的叙述作铺垫。SI等交叉索引工具不能在父函数内部解析出这种调用关系。
我们经常碰到这种情况:如果内核中函数A是通过函数指针调用函数B,那么源码交叉索引工具(如sourceinsight, kscope等)就无法通过函数B的名称回溯到上层函数A。这是因为在函数A内部对函数B的调用并不是通过函数B的名称,而是利用指向函数B代码块的指针(函数指针)。
要想解决这个问题,方法有两种:
1. 利用字符串搜索功能:
搜索函数指针的变量名。如果已经知道的是子函数,想找出通过指针调用它的所有上层父函数:利用子函数的函数名进行搜索,就能找到所有相应的函数指针变量赋值的语句。然后搜索该函数指针变量就能得到所有可能调用该函数的上层父函数。相反,如果是已经知道父函数,想知道该父函数体内的一个函数指针可能会 调用哪些子函数,可以搜索该函数指针变量(一般在该变量名前加个点号“.”),这样可以搜索出所有给该函数指针变量赋值的语句,从而找出所有可能的子函数。
当然,既然是字符串搜索,搜索结果中会夹带其他没用的信息,这需要进一步的筛选。这个方法能搜索出依赖某函数指针变量的所有调用关系。
2. 利用调试工具:
在目标函数处下断点。调试器器会实时拦截该函数的调用,然后用bt命令就能看到整个调用链。
这个方法得到的只是一个特定的具体调用关系。可能还有其他很多的潜在调用路径。
然而,我们研究的目标并不满足于知道调用链。下面我们观察函数究竟是怎样利用函数指针调用子函数的。[待整理]
2130 int vfs_mkdir(struct inode *dir,struct dentry *dentry, int mode)
2131 {
2132 int error = may_create(dir, dentry,NULL);
2133
2134 if (error)
2135 return error;
2136
2137 if (!dir->i_op ||!dir->i_op->mkdir)
2138 return -EPERM;
2139
2140 mode &= (S_IRWXUGO|S_ISVTX);
2141 error = security_inode_mkdir(dir,dentry, mode);
2142 if (error)
2143 return error;
2144
2145 DQUOT_INIT(dir);
2146 error = dir->i_op->mkdir(dir,dentry, mode);
2147 if (!error)
2148 fsnotify_mkdir(dir, dentry);
2149 return error;
2150 }
对源码文件下断点
(gdb) b fs/namei.c:2146
Breakpoint 9 at 0xc017c0ee: filefs/namei.c, line 2146.
问题一:
动态分析call *0x14(%ebx)是怎么回事,函数指针
-------------------------
┌──Register group:general───────────────────────────────────────────────────────────────────────────────────────────────────┐
│eax 0xdc20b0a8 -601837400 ecx 0x1ed 493 │
│edx 0xdb9526c0 -610982208 ebx 0xe01c87d4 -535001132 │
│esp 0xd8c5bf1c 0xd8c5bf1c ebp 0xd8c5bf34 0xd8c5bf34 │
│esi 0xdc20b0a8 -601837400 edi 0xdb9526c0 -610982208 │
│eip 0xc017c0fb 0xc017c0fb <vfs_mkdir+179> eflags 0x200246 [ PF ZF IF ID ] │
│cs 0x60 96 ss 0x68 104 │
│ds 0x7b 123 es 0x7b 123 │
│fs 0xd8 216 gs 0x33 51 │
│ │
│ │
│ │
│ │
│ │
│ │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc017c0ea <vfs_mkdir+162> mov %esi,%eax │
│0xc017c0ec <vfs_mkdir+164> call *(%ecx) │
B+ │0xc017c0ee <vfs_mkdir+166> mov 0x98(%esi),%ebx │
│0xc017c0f4 <vfs_mkdir+172> mov %edi,%edx │
│0xc017c0f6 <vfs_mkdir+174> mov %esi,%eax │
│0xc017c0f8 <vfs_mkdir+176> mov -0x10(%ebp),%ecx │
>│0xc017c0fb <vfs_mkdir+179> call *0x14(%ebx) │
│0xc017c0fe <vfs_mkdir+182> test %eax,%eax │
│0xc017c100 <vfs_mkdir+184> mov %eax,%ebx │
│0xc017c102 <vfs_mkdir+186> jne 0xc017c15d<vfs_mkdir+277> │
│0xc017c104 <vfs_mkdir+188> testb $0x4,0x11c(%esi) │
│0xc017c10b <vfs_mkdir+195> je 0xc017c119<vfs_mkdir+209> │
│0xc017c10d <vfs_mkdir+197> mov $0x4,%edx │
│0xc017c112 <vfs_mkdir+202> mov %esi,%eax │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: vfs_mkdir Line: 2146 PC: 0xc017c0fb
i_state = 1,
dirtied_when = 0,
i_flags = 0,
i_writecount = {
counter = 0
},
i_security = 0x0,
i_private = 0x0
}
(gdb) p/x $ebx
$20 = 0xe01c87d4
(gdb) p/x $ebx+0x14
$21 = 0xe01c87e8
(gdb) p &sfs_dir_inode_ops
$13 = (struct inode_operations *)0xe01c87d4
(gdb) p/x *(int * )0xe01c87d4@10
$18 = {0xe01c75b1, 0xe01c7677,0xc018d3f0, 0xc018cc91, 0xe01c75dd, 0xe01c75c0, 0xc018d441, 0xe01c7510,0xc018d474, 0x0}
(gdb) disass sfs_mkdir
Dump of assembler code for functionsfs_mkdir:
0xe01c75c0 <sfs_mkdir+0>: push %ebp //<-
0xe01c75c1 <sfs_mkdir+1>: or $0x40,%ch
0xe01c75c4 <sfs_mkdir+4>: mov %esp,%ebp
0xe01c75c6 <sfs_mkdir+6>: push %ebx
0xe01c75c7 <sfs_mkdir+7>: mov %eax,%ebx
0xe01c75c9 <sfs_mkdir+9>: push $0x0
0xe01c75cb <sfs_mkdir+11>: call 0xe01c7510 <sfs_mknod>
0xe01c75d0 <sfs_mkdir+16>: pop %edx
0xe01c75d1 <sfs_mkdir+17>: test %eax,%eax
0xe01c75d3 <sfs_mkdir+19>: jne 0xe01c75d8 <sfs_mkdir+24>
0xe01c75d5 <sfs_mkdir+21>: incl 0x28(%ebx)
0xe01c75d8 <sfs_mkdir+24>: mov -0x4(%ebp),%ebx
0xe01c75db <sfs_mkdir+27>: leave
0xe01c75dc <sfs_mkdir+28>: ret
End of assembler dump.
(gdb) p/x *0xe01c87e8
$9 = 0xe01c75c0 // <-sfs_mkdir的地址
(gdb)
struct inode_operations {
int(*create) (struct inode *,struct dentry *,int, struct nameidata *);
structdentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
int(*link) (struct dentry *,struct inode *,struct dentry *);
int(*unlink) (struct inode *,struct dentry *);
int(*symlink) (struct inode *,struct dentry *,const char *);
int(*mkdir) (struct inode *,struct dentry *,int);
......
};
struct inode_operationssfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};
-----------------------------------------------------------------------
0xc017c0fb <vfs_mkdir+179> call *0x14(%ebx) 为什么要加 * ?
call *0x14(%ebx) ==
push %eip
mov 0x14(%ebx) %eip
注意call与mov指令语义的区别
mov 0x14(%ebx) %eax; 把存放在地址0x14(%ebx)中的32位数据拷贝到%eax
mov %eax 0x14(%ebx); 把%eax的值拷贝到地址0x14(%ebx)指向的内存中
call 0x14(%ebx) : 结果是跳到地址0x14(%ebx)继续执行(当然对于本例来说,该地址指向的并不是目标代码段)
call *0x14(%ebx) : 取出存放在地址0x14(%ebx)中的32位数据,把该数据作为目标地址,跳到该地址继续执行。
mov $0xe01c75c9 %eax ; 0xe01c75c9被认为是立即数,前面有$。没有mov 0xe01c75c9 %eax这种形式
call 0xe01c75c9 ;0xe01c75c9被认为是地址。没有call $0xe01c75c9这种形式。
注意,也没有call %eax等形式(假设%eax放着目标地址)。需用 call *%eax,同样,*%eax表示从%eax获取地址值
| - | | -- |
| - | | -- | 4. callsfs_mkdir == call 0xe01c75c0
| - | | -- |
+--------------------+ <-------> | -- | 3. 0xe01c75c0== fetch from 0xe01c87e8
| --- | | -- | *0x14(%ebx)
+--------------------+ +---------------+
| init (*mkdir)(..) +--+ | 0xe01c75c0 | 2. 0xe01c87e8 == calculate 0x14(%ebx)
0x14(%ebx)--->+--------------------+ | +---------------+
| ... | | | 0xe01c75dd |
+--------------------+ | +---------------+
| ... | | | 0xc018cc91 |
+--------------------+ | +---------------+
| ... | | | 0xc018d3f0 |
+--------------------+ | +---------------+ 1. 0xe01c87d4 == fetchfrom %ebx
| ... | | | 0xe01c7677 |
+--------------------+ | +---------------+ +------------+
| int (*create)(..) | | | 0xe01c75b1 | 0xe01c87d4 | 0xe01c87d4 |
%ebx------->+--------------------+ | +---------------+ +------------+
structinode_operations | contents address register %ebx
sfs_dir_inode_ops |
| call *0x14(%ebx)的过程
|
+-----------------------------+
|
static int sfs_mkdir(..) |
0xe01c75c0 <sfs_mkdir+0>: +-> push %ebp
0xe01c75c1 <sfs_mkdir+1>: or $0x40,%ch
0xe01c75c4<sfs_mkdir+4>: mov %esp,%ebp
0xe01c75c6 <sfs_mkdir+6>: push %ebx
0xe01c75c7 <sfs_mkdir+7>: mov %eax,%ebx
0xe01c75c9 <sfs_mkdir+9>: push $0x0
0xe01c75cb <sfs_mkdir+11>: call 0xe01c7510<sfs_mknod>
0xe01c75d0<sfs_mkdir+16>: pop %edx
0xe01c75d1 <sfs_mkdir+17>: test %eax,%eax
0xe01c75d3 <sfs_mkdir+19>: jne 0xe01c75d8<sfs_mkdir+24>
0xe01c75d5 <sfs_mkdir+21>: incl 0x28(%ebx)
0xe01c75d8 <sfs_mkdir+24>: mov -0x4(%ebp),%ebx
0xe01c75db <sfs_mkdir+27>: leave
0xe01c75dc <sfs_mkdir+28>: ret
address contents
---------------------------------------------------------------------------
问题二:
下面的dir->i_op->mkdir(),为什么不是dir.i_op.mkidr. .和 -> 有什么区别
一般得,有一个结构体变量a,其中a有一个域b,想取得b的值,一般用a.b;
而如果a是一个指向结构体的指针变量,取域b的值一般用a->b.
static int sfs_mkdir(struct inode * dir,struct dentry * dentry, int mode)
{
....
}
2130 int vfs_mkdir(struct inode *dir,struct dentry *dentry, int mode)
2131 {
....
2146 error =dir->i_op->mkdir(dir, dentry, mode);
...
2150 }
struct inode {
...
const struct inode_operations *i_op;
...
};
struct inode_operations {
...
int(*mkdir) (struct inode *,struct dentry *,int);
...
};
dir: 取得(struct inode *)dir
dir->i_op: 取得(const struct inode_operations *)i_op
dir->i_op->mkdir: 取得(int (*) (struct inode *,struct dentry *,int))mkdir
dir->i_op->mkdir(dir, dentry,mode)也就是 函数指针变量名(参数...)
函数指针是一个指针,它向目标函数的代码块的第一个指令。
函数名的值等于该函数第一条指令的地址。
(gdb) p sfs_mkdir
$20 = {int (struct inode *, structdentry *, int)} 0xe01c75c0 <sfs_mkdir>
(gdb) p &sfs_mkdir
$21 = (int (*)(struct inode *, structdentry *, int)) 0xe01c75c0 <sfs_mkdir>
(gdb) p dir->i_op->mkdir
$18 = (int (*)(struct inode *, structdentry *, int)) 0xe01c75c0 <sfs_mkdir>
前者指明变量名/函数名的类型,后者是它的值
struct inode_operationssfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};
函数的两种调用形式:函数指针变量名(参数...) 函数名(参数...)
严格地说,从C语言的形式看来,前者通过函数指针变量名调用函数,后者通过函数名调用,是不同的。
但从汇编级代码看来,都是转化为指令call函数地址。是一样的。
引入了函数指针变量后,这个变量就可以动态地赋值,从而指向不同的函数体,实现某些特殊的功能。
我们再看下函数指针的赋值.mkdir = sfs_mkdir,
严格地说,mkdir和sfs_mkdir是类型不同的东西,但在编译时自动经过了类型转换。所以下面这些写法效果都一样
.mkdir = sfs_mkdir,
.mkdir = &sfs_mkdir,
.mkdir = (int (*)(struct inode *, struct dentry*, int))sfs_mkdir,
.mkdir = (int (*)(struct inode *, structdentry *, int))(&sfs_mkdir),
函数指针的运用:
假设有函数指针 a,要调用该指针指向的函数,有意思地是,又有两形式:
a(参数);
或者
(*a)(参数);
硬要理解的话,可以这样想:a之指向一个函数体的指针,按么(*a)自然就是得到了那个函数本身了。
(*a)(参数)这个形式就变成了普通的函数调用,这比直接利用函数指针的变量名进行调用更直观,
或者有人错误地认为本来就应该这样调用才正确。其实,这些形式上的东西是给人看的,
到了汇编级别的指令都是一样的。
linux使用第一种形式,而openbsd使用下面的第二种。linux不管在函数指针的赋值还是函数的调用,
都是采取最简洁的形式。
举个openbsd下的例子,可见它的形式不是很美观。
int
sobind(struct socket *so, struct mbuf*nam, struct proc *p)
{
ints = splsoftnet();
interror;
error= (*so->so_proto->pr_usrreq)(so, PRU_BIND, NULL, nam, NULL, p);
splx(s);
return(error);
}
其中结构体:
struct socket {
...
struct protosw *so_proto; /* protocol handle */
...
}
struct protosw {
...
int (*pr_usrreq)(struct socket *, int, structmbuf *,
struct mbuf *, struct mbuf *, struct proc*);
...}
可见,下面这句
(*so->so_proto->pr_usrreq)(so,PRU_BIND, NULL, nam, NULL, p);
最后变成
(*pr_usrreq)(so, PRU_BIND, NULL, nam,NULL, p);
---
例子
#include <stdio.h>
int main()
{
int myfunc(int a)
{
printf("%d\n", a);
return 0;
}
int (*funa)(int) = myfunc;
int (*funb)(int) = &myfunc;
int (*func)(int) = (int (*)(int))myfunc;
int (*fund)(int) = (int (*)(int))(&myfunc);
myfunc(1);
funa(2);
funb(3);
func(4);
fund(5);
(*funa)(2);
(*funb)(3);
(*func)(4);
(*fund)(5);
return 0;
}
那几个函数调用的代码部分的反汇编码如下,可见在汇编级别,是一样的指令。
08048374 <main>:
8048374: 8d4c 24 04 lea 0x4(%esp),%ecx
8048378: 83e4 f0 and $0xfffffff0,%esp
804837b: ff71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89e5 mov %esp,%ebp
8048381: 51 push %ecx
8048382: 83ec 14 sub $0x14,%esp
8048385: c745 f8 1b 84 04 08 movl $0x804841b,-0x8(%ebp)
804838c: c745 f4 1b 84 04 08 movl $0x804841b,-0xc(%ebp)
8048393: c745 f0 1b 84 04 08 movl $0x804841b,-0x10(%ebp)
804839a: c745 ec 1b 84 04 08 movl $0x804841b,-0x14(%ebp)
80483a1: c704 24 01 00 00 00 movl $0x1,(%esp)
80483a8: e86e 00 00 00 call 804841b<myfunc.1932>
80483ad: c704 24 02 00 00 00 movl $0x2,(%esp)
80483b4: 8b45 f8 mov -0x8(%ebp),%eax
80483b7: ffd0 call *%eax
80483b9: c704 24 03 00 00 00 movl $0x3,(%esp)
80483c0: 8b45 f4 mov -0xc(%ebp),%eax
80483c3: ff d0 call *%eax
80483c5: c704 24 04 00 00 00 movl $0x4,(%esp)
80483cc: 8b45 f0 mov -0x10(%ebp),%eax
80483cf: ffd0 call *%eax
80483d1: c704 24 05 00 00 00 movl $0x5,(%esp)
80483d8: 8b45 ec mov -0x14(%ebp),%eax
80483db: ffd0 call *%eax
80483dd: c704 24 02 00 00 00 movl $0x2,(%esp)
80483e4: 8b45 f8 mov -0x8(%ebp),%eax
80483e7: ffd0 call *%eax
80483e9: c704 24 03 00 00 00 movl $0x3,(%esp)
80483f0: 8b45 f4 mov -0xc(%ebp),%eax
80483f3: ffd0 call *%eax
80483f5: c704 24 04 00 00 00 movl $0x4,(%esp)
80483fc: 8b45 f0 mov -0x10(%ebp),%eax
80483ff: ffd0 call *%eax
8048401: c704 24 05 00 00 00 movl $0x5,(%esp)
8048408: 8b45 ec mov -0x14(%ebp),%eax
804840b: ffd0 call *%eax
调用链的层次
1. 人观念层次
2. 交叉解析器层次
2. c调用层次
3. 编译器(机器码静态)层次
4. 运行时(机器码动态)层次,也叫调试器层次
很明显,前面所讲的“理想状态”的backtrace就是指在交叉解析器层次下和在调试器层次下的表现相同的调用链。
非理想调用链
任务:
从一个断点开始,从后向前推导,分析出ramfs注册函数的调用过程。同时,观察调试器的优点和局限性。
ramfs文件系统的注册函数是register_filesystem(&ramfs_fs_type)。为了更快定位,在上层函数init_ramfs_fs下断点。而后在gdb下得到的调用链是
(gdb) bt
#0 register_filesystem (fs=0xc03595cc) at fs/filesystems.c:68
#1 0xc0394594 in init_ramfs_fs () at fs/ramfs/inode.c:213
#2 0xc037f473 in kernel_init (unused=<value optimized out>) atinit/main.c:708
#3 0xc010463f in kernel_thread_helper () at arch/x86/kernel/entry_32.S:1013
我们注意到:
1. 这个backtrace包含的函数只有4个,实际上并非如此。经过分析,它实际上(用C的观点看)调用链如下所示,这是为什么呢?
start_kernel→rest_init→kernel_thread→kernel_thread_helper→call %ebx (即call kernel_init)→do_basic_setup→do_initcalls→do_one_initcall→result = fn() (即call init_ramfs_fs)→register_filesystem
2. backtrace推溯到kernel_thread_helper后就再没下文了。又是什么使得调试器变成了瞎子,无法看得再远了呢?
欲见其详,且听下回分解
[下面准备材料]
kernel_init对do_basic_setup的调用被优化成内联函数
do_basic_setup对do_initcalls的调用被优化成内联函数
do_initcalls对do_one_initcall的调用被优化成内联函数
有三层的非内联函数都被被优化成内联函数,整个代码被优化的乱七八糟。
838 static int __init kernel_init(void *unused)
839 {
.....
864 cpuset_init_smp();
865
866 do_basic_setup();
867
.......
887 return 0;
888 }
static void __init do_basic_setup(void)
{
/*drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();
init_irq_proc();
do_initcalls();
}
741 static void __initdo_initcalls(void)
742 {
743 initcall_t *call;
744
745 for (call = __initcall_start; call< __initcall_end; call++)
746 do_one_initcall(*call);
747
748 /* Make sure there is no pending stufffrom the initcall sequence */
749 flush_scheduled_work();
750 }
static void __init do_one_initcall(initcall_tfn)
{
intcount = preempt_count();
ktime_tt0, t1, delta;
charmsgbuf[64];
intresult;
if(initcall_debug) {
print_fn_descriptor_symbol("calling %s\n", fn);
t0= ktime_get();
}
result= fn();
if(initcall_debug) {
....
}
static inline voidprint_fn_descriptor_symbol(const char *fmt, void *addr)
{
#if defined(CONFIG_IA64) ||defined(CONFIG_PPC64)
addr= *(void **)addr;
#endif
print_symbol(fmt,(unsigned long)addr);
}
(gdb) disass kernel_init
Dump of assembler code for functionkernel_init:
0xc037f349 <kernel_init+0>: push %ebp
0xc037f34a <kernel_init+1>: mov %esp,%ebp
0xc037f34c <kernel_init+3>: push %edi
0xc037f34d <kernel_init+4>: push %esi
......
0xc037f413 <kernel_init+202>: call 0xc0391454 <cpuset_init_smp>
0xc037f418 <kernel_init+207>: call 0xc0390081 <init_workqueues> //<-do_basic_setup被优化成内联函数,在这里开始展开
0xc037f41d <kernel_init+212>: call 0xc039004e <usermodehelper_init>
0xc037f422 <kernel_init+217>: call 0xc039b7d1 <driver_init>
0xc037f427 <kernel_init+222>: call 0xc0153e18 <init_irq_proc>
0xc037f42c <kernel_init+227>: movl $0xc03aa470,-0x5c(%ebp) //do_initcalls被优化成内联函数,在这里开始展开
0xc037f433 <kernel_init+234>: pop %eax
0xc037f434 <kernel_init+235>: pop %edx
0xc037f435 <kernel_init+236>: jmp 0xc037f559 <kernel_init+528>
0xc037f43a <kernel_init+241>: mov -0x5c(%ebp),%eax //do_one_initcall被优化成内联函数,在这里开始展开
0xc037f43d <kernel_init+244>: mov (%eax),%eax
0xc037f43f <kernel_init+246>: mov %eax,-0x58(%ebp)
0xc037f442 <kernel_init+249>: mov %esp,%eax
0xc037f444 <kernel_init+251>: and $0xffffe000,%eax
0xc037f449 <kernel_init+256>: mov 0x14(%eax),%eax
0xc037f44c <kernel_init+259>: cmpl $0x0,0xc03a1820
0xc037f453 <kernel_init+266>: mov %eax,-0x54(%ebp)
0xc037f456 <kernel_init+269>: je 0xc037f470 <kernel_init+295>
0xc037f458 <kernel_init+271>: mov -0x58(%ebp),%edx //内联函数print_fn_descriptor_symbol在这里开始展开
0xc037f45b <kernel_init+274>: mov $0xc030d1be,%eax
0xc037f460 <kernel_init+279>: call 0xc013f598 <__print_symbol>//内联函数print_fn_descriptor_symbo的展开结束
0xc037f465 <kernel_init+284>: call 0xc013352f <ktime_get>
0xc037f46a <kernel_init+289>: mov %eax,-0x64(%ebp)
0xc037f46d <kernel_init+292>: mov %edx,-0x60(%ebp)
0xc037f470 <kernel_init+295>: call *-0x58(%ebp) //do_one_initcall中的调用语句result = fn();
.....
0xc037f553 <kernel_init+522>: pop %edi
0xc037f554 <kernel_init+523>: pop %eax
0xc037f555 <kernel_init+524>: addl $0x4,-0x5c(%ebp)
0xc037f559 <kernel_init+528>: cmpl $0xc03aa804,-0x5c(%ebp) //
0xc037f560 <kernel_init+535>: jb 0xc037f43a <kernel_init+241> //
如何在汇编码中定位内联(或被优化掉的非内联)函数
1.利用前后相关函数的提示
2.函数的前戏码定位函数的开始
3.注意跳转语句
4.利用调试器辅助定位(见gdb技巧)
调用树与调用图
[待充实]
调用树的定义
一个复杂的函数调用一定是调用了多个子函数,同时这些子函数又会调用若干“孙”函数,这样依次调用并依次返回到最初的父函数后,就形成了树状的调用关系,我们称之为“调用树”。
调用树的作用
函数调用树是比函数调用链更为复杂的观察对象。如果能够显示调用树,就可以对调用的整个过程有个直观的了解。
调用树的分类
函数调用树有两类:
1. 抽象调用树
也叫虚拟调用树。比如在源码中,父函数调用了子函数a, b, c。那么对这三个函数的调用逻辑都考虑进去,这就是“抽象调用”。抽象调用树能全面的描述了父函数的逻辑和代码开发员的意图。但是,在实际的环境中,这三个函数未必就全部会调用到。把在实际的具体情况下未调用的“潜在”调用关系去掉后,剩下的调用树就称为“具体调用树”。明显,具体调用树不能全面显示代码 开发员的意图,只是放映具体环境下函数的调用关系。
2. 具体调用树
也叫实时调用树。解释见上。
调用树的显示
1. 抽象调用树的显示
借助source insight等工具可以图形显示抽象调用树。
1. 具体调用树的显示
据本人的了解,目前gdb没有一个类似”bt”那样的能显示函数调用树的命令,但是借助gdb宏也许能够实现显示调用树的功能,这有待研究。不过,目前已经有个现成的调试工具可以显示调用树,它就是 systemtap.
效果如下:
[...]
0 klogd(1391):->sys_read
14 klogd(1391): ->fget_light
22 klogd(1391): <-fget_light
27 klogd(1391): ->vfs_read
35 klogd(1391): ->rw_verify_area
43 klogd(1391): <-rw_verify_area
49 klogd(1391): ->kmsg_read
0 sendmail(1696):->sys_read
17 sendmail(1696): ->fget_light
26 sendmail(1696): <-fget_light
34 sendmail(1696): ->vfs_read
44 sendmail(1696): ->rw_verify_area
52 sendmail(1696): <-rw_verify_area
58 sendmail(1696): ->proc_file_read
70 sendmail(1696): ->loadavg_read_proc
84 sendmail(1696): ->proc_calc_metrics
92 sendmail(1696): <-proc_calc_metrics
95 sendmail(1696): <-loadavg_read_proc
101 sendmail(1696): <-proc_file_read
106 sendmail(1696): ->dnotify_parent
115 sendmail(1696): <-dnotify_parent
119 sendmail(1696): ->inotify_dentry_parent_queue_event
127 sendmail(1696): <-inotify_dentry_parent_queue_event
133 sendmail(1696): ->inotify_inode_queue_event
141 sendmail(1696): <-inotify_inode_queue_event
146 sendmail(1696): <-vfs_read
151 sendmail(1696):<-sys_read
[...]
见于
http://sourceware.org/systemtap/wiki/WSCallGraph?highlight=1)
调用树的拼接
对于一个更***的函数调用来说,利用工具显示的抽象调用树和具体调用调用树可能是不完整的。比如,对于抽象调用树来说,它的显示工具是source insight。但是如果这个函数对某个子函数或在更下层的函数对下下层的函数调用是通过函数指针来调用的,那么source insight显示的调用树中就会漏掉通过函数指针调用的子函数,以及以子函数为根的子调用树。这是因为函数指针变量的赋值是发生在代码动态运行时的。 source insight无法利用静态的源码就捕捉到未来才出现东西,甚至它也无法在形式上解析出“那里存在一个利用函数指针的调用”。这就要通过阅读源码来找出这种调用关系。同时,可以利用调试器实时找出具体情况下是通过那个函数指针调用了哪个特定的下层函数。这样就能把漏掉的子调用树拼接到父调用树中。
可见,这些内容又回归到了调用链的内容。具体看前面。
调用图
各函数间的像蜘蛛网一样的调用关系的图形表示就是调用图了,显然它比调用树更复杂。
穿越盲区
本节意义:经过上面章节的叙述,利用源码交叉索引工具+调试器已经能解决大部分问题,但是因为调试器和交叉索引工具的各自局限性,依然会存在一些问题。本节尝试如何联合交叉索引工具以及调试器再加上人脑来解决各自的缺点。
[观察积累中,待扩展]
穿越gdb的盲区
进程切换
中断异常
系统调用
穿越交叉索引工具的盲区
函数指针
该小节内容移到了: 调用链的状态→函数指针调用
查看函数的参数
我们知道,一个函数的计算结果并不都是通过它的返回值返回的,有时会通过函数的参数返回真正感兴趣的数据。看内核源码的时候,如果调用链过长,涉及内容和数据结构过多的话,往往是看到最后都记不住函数的参数哪些是已经“初始化的”。
这也是交叉索引工具无法克服的先天弱点。它能动态索引源码,却无法动态查看数据。此时,可以利用gdb给目标函数下断点,而后可以用命令info args查看参数,另外命令info local可查看本地变量。当然在ddd下查看效果会更好。
内容简单,不展开了。
工程方法
二叉断点
实例 “什么/proc下无法创建目录?”
给调用指令下断点
如果对目标函数下断点后,受到很多骚扰,那么就转为在上层函数内对目标函数的调用指令处下断点。如果你已经进入了上层函数,对调用指令下断点,是更为精确的断点方法。
绕过时钟中断的干扰
有时我们调试的程序与中断无关的,但是由于时钟中断的异步到来,在调试过程中经常会自动进入时钟中断处理例程中,这严重干扰了我们的工作。用下面的方法可绕过时钟中断的干扰。
注:
使用GDB与QEMU调试内核时的问题分析: http://www.chinaitlab.com/linux/kernel/356774.html
关于qemu在单步指令时进入时钟中断的问题,上面给出链接给出了比较“深入”地探讨。这个问题涉及虚拟机本身,有人说是虚拟机相对于真机的固有缺陷,似乎很深奥,我没那个能力也没那个时间研究。但是我们应该知道,如果问题足够的复杂,以至于解决它要花费太高的代价,那么绕过这个问题是个更明智的解 决方法。
解决方法(手工)
1. 内核启动早期
事先下两个断点
b common_interrupt
b native_iret
自定义返回命令
(gdb) define ooi
Type commands for definition of"ooi".
End with a line saying just"end".
>c
>stepi
>end
一旦时钟中断产生,就会拦截在中断处理的通用入口common_interrupt,然后运行返回指令,就会“回到”被时钟中断打断的原指令处
ooi
2. 内核启动完毕
事先下两个断点
b apic_timer_interrupt
b irq_return
一旦时钟中断产生,就会拦截在中断处理例程apic_timer_interrupt,然后运行返回指令,就会“回到”被时钟中断打断的原指令处
ooi
分析记录,待整理
提示,分析异常和中断的处理过程比分析C代码更直观,因为源码本身是汇编码。
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│614 SAVE_ALL │
│615 TRACE_IRQS_OFF │
│616 movl%esp,%eax │
│617 call do_IRQ │
>│618 jmpret_from_intr │
│619 ENDPROC(common_interrupt) │
│620 CFI_ENDPROC │
│621 │
│622 #defineBUILD_INTERRUPT(name, nr) \ │
│623 ENTRY(name) \ │
│624 RING0_INT_FRAME; \ │
│625 pushl$~(nr); \ │
│626 CFI_ADJUST_CFA_OFFSET 4; \ │
│627 SAVE_ALL; \ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│0xc01043e1 <common_interrupt+17> mov %edx,%ds │
│0xc01043e3 <common_interrupt+19> mov %edx,%es │
│0xc01043e5 <common_interrupt+21> mov $0xd8,%edx │
│0xc01043ea <common_interrupt+26> mov %edx,%fs │
│0xc01043ec <common_interrupt+28> mov %esp,%eax │
│0xc01043ee <common_interrupt+30> call 0xc0106151 <do_IRQ> │
>│0xc01043f3 <common_interrupt+35> jmp 0xc01038dc <ret_from_exception> │
│0xc01043f8 <reschedule_interrupt> push $0xffffff03 │
│0xc01043fd <reschedule_interrupt+5> cld │
│0xc01043fe <reschedule_interrupt+6> push %fs │
│0xc0104400 <reschedule_interrupt+8> push %es │
│0xc0104401 <reschedule_interrupt+9> push %ds │
│0xc0104402 <reschedule_interrupt+10> push %eax │
│0xc0104403 <reschedule_interrupt+11> push %ebp │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In:common_interrupt Line: 618 PC: 0xc01043f3
(gdb)
(gdb)
(gdb) bt
#0 common_interrupt () at arch/x86/kernel/entry_32.S:618
#1 0x00000292 in ?? ()
#2 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") atinclude/linux/slab.h:266
#3 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3"rootfs", data=0x0) at fs/super.c:896
#4 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs",flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#5 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
(gdb) disass
(gdb)
----
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│401 cmpl$((SEGMENT_LDT << 8) | USER_RPL), %eax │
│402 CFI_REMEMBER_STATE │
│403 je ldt_ss # returning touser-space with LDT SS │
│404 restore_nocheck: │
│405 TRACE_IRQS_IRET │
│406 restore_nocheck_notrace: │
│407 RESTORE_REGS │
│408 addl $4, %esp # skiporig_eax/error_code │
│409 CFI_ADJUST_CFA_OFFSET -4 │
│410 irq_return: │
>│411 INTERRUPT_RETURN │
│412 .section.fixup,"ax" │
│413 ENTRY(iret_exc) │
│414 pushl $0 # no error code │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc0103a61 <restore_nocheck_notrace> pop %ebx │
│0xc0103a62 <restore_nocheck_notrace+1> pop %ecx │
│0xc0103a63 <restore_nocheck_notrace+2> pop %edx │
│0xc0103a64 <restore_nocheck_notrace+3> pop %esi │
│0xc0103a65 <restore_nocheck_notrace+4> pop %edi │
│0xc0103a66 <restore_nocheck_notrace+5> pop %ebp │
│0xc0103a67 <restore_nocheck_notrace+6> pop %eax │
│0xc0103a68 <restore_nocheck_notrace+7> pop %ds │
│0xc0103a69 <restore_nocheck_notrace+8> pop %es │
│0xc0103a6a <restore_nocheck_notrace+9> pop %fs │
│0xc0103a6c <restore_nocheck_notrace+11> add $0x4,%esp │
>│0xc0103a6f <irq_return> jmp *%cs:0xc0353b54 │
│0xc0103a76 <ldt_ss> lar 0x3c(%esp),%eax │
│0xc0103a7b <ldt_ss+5> jne 0xc0103a61<restore_nocheck_notrace> │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: irq_return Line: 411 PC: 0xc0103a6f
(gdb) stepi
0xc0103a64 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
0xc0103a65 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
0xc0103a66 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
Watchpoint 3: $ebp
Old value = (void *) 0xc0378000
New value = (void *) 0xc0379f4c
0xc0103a67 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
0xc0103a68 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
0xc0103a69 in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
0xc0103a6a in restore_nocheck_notrace ()at arch/x86/kernel/entry_32.S:407
restore_nocheck_notrace () atarch/x86/kernel/entry_32.S:408
irq_return () atarch/x86/kernel/entry_32.S:411
(gdb)
------------
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│864 jmpirq_return │
│865 CFI_ENDPROC │
│866 KPROBE_END(nmi) │
│867 │
│868 #ifdefCONFIG_PARAVIRT │
│869 ENTRY(native_iret) │
>│870 iret │
│871 .section __ex_table,"a" │
│872 .align 4 │
│873 .long native_iret, iret_exc │
│874 .previous │
│875 END(native_iret) │
│876 │
│877 ENTRY(native_irq_enable_syscall_ret) │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
>│0xc01045a8 <native_iret> iret │
│0xc01045a9 lea 0x0(%esi),%esi │
│0xc01045ac <native_irq_enable_syscall_ret> sti │
│0xc01045ad <native_irq_enable_syscall_ret+1> sysexit │
│0xc01045af nop │
│0xc01045b0 <overflow> push $0x0 │
│0xc01045b2 <overflow+2> push $0xc0105030 │
│0xc01045b7 <overflow+7> jmp 0xc02bfb80 <page_fault+8> │
│0xc01045bc <bounds> push $0x0 │
│0xc01045be <bounds+2> push $0xc0104fe3 │
│0xc01045c3 <bounds+7> jmp 0xc02bfb80 <page_fault+8> │
│0xc01045c8 <invalid_op> push $0x0 │
│0xc01045ca <invalid_op+2> push $0xc0104f6d │
│0xc01045cf <invalid_op+7> jmp 0xc02bfb80 <page_fault+8> │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: native_iret Line: 870 PC: 0xc01045a8
#9 0x00000000 in ?? ()
(gdb) stepi
native_iret () atarch/x86/kernel/entry_32.S:870
(gdb) bt
#0 native_iret () at arch/x86/kernel/entry_32.S:870
#1 0xc0172198 in kmem_cache_alloc (cachep=0xdc404ec0, flags=<valueoptimized out>) at include/asm/string_32.h:183
#2 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") atinclude/linux/slab.h:266
#3 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3"rootfs", data=0x0) at fs/super.c:896
#4 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs",flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#5 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
(gdb)
-----------------------------
┌──mm/slab.c──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│3478 if (unlikely((flags &__GFP_ZERO) && objp)) │
│3479 memset(objp, 0, obj_size(cachep)); │
│3480 │
│3481 return objp; │
│3482 } │
│3483 │
│3484 /* │
│3485 * Caller needs toacquire correct kmem_list's list_lock │
│3486 */ │
│3487 static voidfree_block(struct kmem_cache *cachep, void **objpp, int nr_objects, │
│3488 int node) │
│3489 { │
│3490 int i; │
│3491 struct kmem_list3*l3; │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc0172183 <kmem_cache_alloc+100> cmpw $0x0,-0x14(%ebp) │
│0xc0172188 <kmem_cache_alloc+105> jns 0xc01721a7 <kmem_cache_alloc+136> │
│0xc017218a <kmem_cache_alloc+107> test %edi,%edi │
│0xc017218c <kmem_cache_alloc+109> je 0xc01721a7 <kmem_cache_alloc+136> │
│0xc017218e <kmem_cache_alloc+111> mov 0x2c(%esi),%edx │
│0xc0172191 <kmem_cache_alloc+114> xor %eax,%eax │
│0xc0172193 <kmem_cache_alloc+116> mov %edx,%ecx │
│0xc0172195 <kmem_cache_alloc+118> shr $0x2,%ecx │
>│0xc0172198 <kmem_cache_alloc+121> rep stos %eax,%es:(%edi) │
│0xc017219a <kmem_cache_alloc+123> test $0x2,%dl │
│0xc017219d <kmem_cache_alloc+126> je 0xc01721a1 <kmem_cache_alloc+130> │
│0xc017219f <kmem_cache_alloc+128> stos %ax,%es:(%edi) │
│0xc01721a1 <kmem_cache_alloc+130> test $0x1,%dl │
│0xc01721a4 <kmem_cache_alloc+133> je 0xc01721a7 <kmem_cache_alloc+136> │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: kmem_cache_alloc Line: 183 PC: 0xc0172198
#7 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#8 0x00000000 in ?? ()
(gdb) bt
#0 0xc0172198 in kmem_cache_alloc (cachep=0xdc404ec0, flags=<valueoptimized out>) at include/asm/string_32.h:183
#1 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") atinclude/linux/slab.h:266
#2 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3"rootfs", data=0x0) at fs/super.c:896
#3 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs",flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#4 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#5 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#6 0xc037f868 in start_kernel () at init/main.c:666
#7 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#8 0x00000000 in ?? ()
(gdb) list
(gdb) disass
(gdb)
bug 与 OOPS
[主要研究定位bug的技巧,找出是哪条指令引发了panic似乎很容易。但要找出错误产生的源头似乎是门艺术了]
经过上面章节的叙述,本小节问题的解决已不成问题了。不再展开叙述。可以参考下面链接。
参考手册
“Using kgdb and thekgdb Internals” http://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/index.html
kgdb官网 http://kgdb.linsyssoft.com/
参考书籍(freeebsd)
“Debugging KernelProblems” http://www.google.cn/search?q=Debugging+Kernel+Problems&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:zh-CN:unofficial&client=firefox-a
“Chapter 10 KernelDebugging” http://www.freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/kerneldebug.html
参考书籍(linux)
Chapter 14. KernelDebugging Techniques of“Embedded Linux Primer: A Practical, Real-World Approach”
http://book.opensourceproject.org.cn/embedded/embeddedprime/
参考文章
“掌握 Linux 调试技术” http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html
“定位Oops的具体代码行” http://blog.chinaunix.net/u/12592/showart_1092733.html
“跟踪内核 oops” http://wiki.zh-kernel.org/doc/oops-tracing.txt
“例解Linux KernelDebug” http://blog.chinaunix.net/u/2108/showart_164703.html
“kernel debug的一些小手段” http://blog.chinaunix.net/u/12592/showart_499502.html
“Kernel DebuggingTechniques” http://www.linuxjournal.com/article/9252
[参考文章]有的已过时,而且深度不够。
网站
http://www.lkml.org/ 搜索bug
***第二部分:内核分析***
这部分的内容侧重于内核原理分析,其中涉及gdb调试器的内容不是很多,但它起的作用很关键,主要用于观察内核数据的生成及变化,在对源码理解有困惑时用于验证自己的猜想。另外,gdb一个很重要的功能是,拦截通过函数指针调用的函数,从而追溯整个调用链,交叉索引工具无法做到这点。
另外,调试内核时,利用gdb的“list 函数名”命令看到的C代码都是当前处理器当前配置下内核实际运行的函数版本:”disass 函数名”看到的都是处理器实际运行时的机器代码,也就是说define语句和inline函数已经被编译器处理了,而且编译器也完成了优化。所以,gdb 本身就是一种不可替代的源码浏览工具,它能筛选掉出实际运行的函数版本,又能呈现出实际运行的机器码。
调试相关子系统
kgdb源码分析
gdb远程串口协议
http://sourceware.org/gdb/current/onlinedocs/gdb_34.html#SEC706
http://www.huihoo.org/mirrors/pub/embed/document/debugger/ew_GDB_RSP.pdf
Jason Wessel的linux-2.6-kgdb.git
http://git.kernel.org/?p=linux/kernel/git/jwessel/linux-2.6-kgdb.git;a=summary
gdb调试模式
(gdb) set debug serial 1
(gdb) set debug remote 1
sysrq
oprofile
kprobes
驱动分析
[分析一个简单的驱动,观察函数调用流程。重点观察驱动与驱动模型,以及和系统内核的交互过程。比如,中断的整个生命周期。]
参考:
“Debugging kernelmodules” http://lwn.net/Articles/90913/
“Linux 系统内核的调试” http://www.ibm.com/developerworks/cn/linux/l-kdb/
“Linux 可加载内核模块剖析” http://www.ibm.com/developerworks/cn/linux/l-lkm/
“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html
“使用 /proc 文件系统来访问 Linux 内核的内容” http://www.ibm.com/developerworks/cn/linux/l-proc.html
如何查找出当前系统所安装模块驱动对应的源码,从而对其做些修改等实验?
提示:
1. lsmod 列出模块名
2. modinfo 模块名, 查看模块信息
3. 模块名,模块信息中的别名,模块的参数说明文字都可结合source insight查找该模块的源码文件;模块信息中的模块路径也可用来定位对应源码的路径以及相关的kconfig文件,从而获取更多相关信息。一般源码文 件的名称就是模块名或在模块名的基础上加上某些后缀,用模块名的方法查找不出时再利用其他信息查找。
4. 如果利用以上方法还找不到源文件,或者一个模块对应着几个源文件,可使用最后的必杀绝招。比如lsmod后得到一个sr_mod。我们用modinfosr_mod的得到它的已编译文件的路径是/lib/modules/2.6.24-19-generic/kernel/drivers/scsi/sr_mod.ko ;把它拷贝出来,并用命令objdump -d sr_mod.ko 查看它的机器码,就可以知道它使用了哪些函数,利用这些函数名就可以结合source insight搜索出源码了。
载入模块符号
首先,在虚拟系统上装入目标模块foo,然后到/sys/module/foo/sections/下查看目标模块的section偏移地址信息.
实例
debian:/sys/module/smplefs/sections# cat.text .data .bss
0xe01c7000
0xe01c864c
0xe01c8b20
然后,到真机的gdb下用add-symbol-file命令装载目标模块的符号信息 格式如下
add-symbol-file /path/to/module 0xe01c7000 \ # .text
-s .data 0xe01c864c \
-s.bss 0xe01c8b20
实例
(gdb) add-symbol-file test/day11/samplefs.ko 0xe01c7000 -s .data0xe01c864c -s .bss 0xe01c8b20
add symbol table from file"test/day11/samplefs.ko" at
.text_addr= 0xe01c7000
.data_addr= 0xe01c864c
.bss_addr= 0xe01c8b20
(y or n) y
Reading symbols from/storage/myqemu/new/linux-2.6.26/test/day11/samplefs.ko...done.
(gdb)
然后,余下的对模块的调试就类似对内核的调试了。
seq_file.c的分析
module.c的分析
中断处理过程
s3c24xx内存初始化分析
[从这节开始,侧重于利用kgdb和source insight理解内核原理] [网上好像没这个内容。只看源码的话,因为source insight不能解析汇编源文件,在汇编源码中定位到初始化的源头好像很难,利用调试器很容易做到这点]
虚拟地址空间
用户层的观察窗
[待充实]
3G~4G虚拟地址空间的用途。(来自于qemu虚拟机的dmesg启动信息,500m物理内存)
<4>Zone PFN ranges:
<4> DMA 0 -> 4096
<4> Normal 4096 -> 127984
<4> HighMem 127984 -> 127984
<6>virtual kernel memory layout:
<4> fixmap : 0xfff4c000 - 0xfffff000 ( 716kB)
<4> pkmap : 0xff800000 - 0xffc00000 (4096kB)
<4> vmalloc : 0xe0000000 - 0xff7fe000 ( 503 MB)
<4> lowmem : 0xc0000000 - 0xdf3f0000 ( 499MB)
<4> .init : 0xc037f000 - 0xc03bb000 ( 240 kB)
<4> .data : 0xc02c0875 - 0xc03773ac ( 730 kB)
<4> .text : 0xc0100000 - 0xc02c0875 (1794 kB)
3G~4G虚拟地址空间的用途。(来自于qemu虚拟机的dmesg启动信息,897m物理内存)
<4>Zone PFN ranges:
<4> DMA 0 -> 4096
<4> Normal 4096 -> 229376
<4> HighMem 229376 -> 229616
<6>virtual kernel memory layout:
<4> fixmap : 0xfff4c000 - 0xfffff000 ( 716kB)
<4> pkmap : 0xff800000 - 0xffc00000 (4096kB)
<4> vmalloc : 0xf8800000 - 0xff7fe000 ( 111 MB)
<4> lowmem : 0xc0000000 - 0xf8000000 ( 896MB)
<4> .init : 0xc037f000 - 0xc03bb000 ( 240 kB)
<4> .data : 0xc02c0875 - 0xc03773ac ( 730 kB)
<4> .text : 0xc0100000 - 0xc02c0875 (1794 kB)
3G~4G虚拟地址空间的用途。(来自真机的dmesg启动信息,3G物理内存)
[ 0.000000] Zone PFN ranges:
[ 0.000000] DMA 0 -> 4096
[ 0.000000] Normal 4096 -> 229376
[ 0.000000] HighMem 229376 -> 786416
[ 33.262853] virtual kernel memory layout:
[ 33.262854] fixmap : 0xfff4b000 - 0xfffff000 ( 720 kB)
[ 33.262855] pkmap : 0xff800000 - 0xffc00000 (4096 kB)
[ 33.262856] vmalloc :0xf8800000 - 0xff7fe000 ( 111 MB)
[ 33.262857] lowmem : 0xc0000000 - 0xf8000000 ( 896 MB)
[ 33.262858] .init :0xc0421000 - 0xc047d000 ( 368 kB)
[ 33.262859] .data :0xc03204c4 - 0xc041bdc4 (1006 kB)
[ 33.262861] .text :0xc0100000 - 0xc03204c4 (2177 kB)
top, 4G --->+-------------------+
| |
| malloc()'ed memory|
| interrupt stack | kernel
| data |
| text |
kernel, 3G--->+-------------------+
| |
| argv,envp |
| user stack |
| | |
| | |
| v |
| | user process
| ^ |
| | |
| | |
| heap |
| data |
| text |
user, 0G---> +-------------------+
Layout of virtual address space
我们验证一下用户空间的内容(上图的下部分)[未完,待续] 引用于http://linux.chinaunix.net/bbs/viewthread.php?tid=978491
查看进程的虚拟地址空间是如何使用的。
该文件有6列,分别为:
地址:库在进程里地址范围
权限:虚拟内存的权限,r=读,w=写,x=,s=共享,p=私有;
偏移量:库在进程里地址范围
设备:映像文件的主设备号和次设备号;
节点:映像文件的节点号;
路径:映像文件的路径
每项都与一个vm_area_struct结构成员对应,
----
struct vm_area_struct {
structmm_struct * vm_mm; /* The address space webelong to. */
unsignedlong vm_start; /* Our startaddress within vm_mm. */
unsignedlong vm_end; /* The firstbyte after our end address
within vm_mm. */
/*linked list of VM areas per task, sorted by address */
structvm_area_struct *vm_next;
pgprot_tvm_page_prot; /* Accesspermissions of this VMA. */
unsignedlong vm_flags; /* Flags, listedbelow. */
structrb_node vm_rb;
/*
* For areas with an address space and backingstore,
* linkage into the address_space->i_mmapprio tree, or
* linkage to the list of like vmas hanging offits node, or
* linkage of vma in theaddress_space->i_mmap_nonlinear list.
*/
union{
struct{
structlist_head list;
void*parent; /* aligns with prio_tree_nodeparent */
structvm_area_struct *head;
}vm_set;
structraw_prio_tree_node prio_tree_node;
}shared;
/*
* A file's MAP_PRIVATE vma can be in bothi_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be inan anon_vma list.
*/
structlist_head anon_vma_node; /* Serializedby anon_vma->lock */
structanon_vma *anon_vma; /* Serializedby page_table_lock */
/*Function pointers to deal with this struct. */
structvm_operations_struct * vm_ops;
/*Information about our backing store: */
unsignedlong vm_pgoff; /* Offset(within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
structfile * vm_file; /* File we mapto (can be NULL). */
void* vm_private_data; /* wasvm_pte (shared mem) */
unsignedlong vm_truncate_count;/* truncate_count or restart_addr */
#ifndef CONFIG_MMU
atomic_tvm_usage; /* refcount(VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
structmempolicy *vm_policy; /* NUMA policyfor the VMA */
#endif
[todo 换个简单的程序]
$ ps -aux | grep firefox
Warning: bad ps syntax, perhaps a bogus'-'? See http://procps.sf.net/faq.html
fqh 8230 4.7 2.5 205872 80024 ? Tl 14:54 0:19 /usr/lib/firefox-3.0.1/firefox
fqh 8313 0.0 0.0 3220 764 pts/1 R+ 15:01 0:00 grep firefox
(gdb) attach 8230
...
.....
Loaded symbols for/usr/lib/libflashsupport.so
Reading symbols from/usr/lib/libpulse.so.0...(no debugging symbols found)...done.
Loaded symbols for/usr/lib/libpulse.so.0
Reading symbols from/lib/libcap.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libcap.so.1
(no debugging symbols found)
0xb7f24410 in __kernel_vsyscall ()
(gdb) bt
#0 0xb7f24410 in __kernel_vsyscall ()
#1 0xb7d46c07 in poll () from /lib/tls/i686/cmov/libc.so.6
#2 0xb6b4e1c6 in ?? () from /usr/lib/libglib-2.0.so.0
#3 0xb6b4e74e in g_main_context_iteration () from /usr/lib/libglib-2.0.so.0
#4 0xb77ba87c in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#5 0xb77cf624 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#6 0xb77cfa6f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#7 0xb787ecd6 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#8 0xb784e31f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#9 0xb77cf75e in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#10 0xb765f122 in ?? () from/usr/lib/xulrunner-1.9.0.1/libxul.so
#11 0xb70b3a88 in XRE_main () from/usr/lib/xulrunner-1.9.0.1/libxul.so
#12 0x08049033 in ?? ()
#13 0xb7c90450 in __libc_start_main ()from /lib/tls/i686/cmov/libc.so.6
#14 0x08048cc1 in ?? ()
(gdb)
$ cat /proc/8230/maps
08048000-0804f000 r-xp 00000000 08:017022914 /usr/lib/firefox-3.0.1/firefox <-text,注意标志可读可执行不可写私有
0804f000-08050000 rw-p 00006000 08:017022914 /usr/lib/firefox-3.0.1/firefox <-data,注意标志可读可写不可执行
08050000-0abd4000 rw-p 08050000 00:000 [heap] <-heap,一共45.5多MB[todo:验证向上增长]可读可写不可执行
ae060000-ae063000 r-xp 00000000 08:016941098 /usr/lib/libflashsupport.so <-libflashsupport.so共享库的代码段,可读可执行不可写
ae063000-ae064000 rw-p 00002000 08:016941098 /usr/lib/libflashsupport.so <-libflashsupport.so共享库的数据段,可读可写不可执行
.....
..
b7f20000-b7f21000 rw-p 00001000 08:016942869 /usr/lib/libplds4.so.0d
b7f21000-b7f22000 r--p 00000000 08:016966184 /usr/lib/locale/zh_CN.utf8/LC_IDENTIFICATION
b7f22000-b7f24000 rw-p b7f22000 00:00 0
b7f24000-b7f25000 r-xp b7f24000 00:000 [vdso]
b7f25000-b7f3f000 r-xp 00000000 08:012326545 /lib/ld-2.7.so
b7f3f000-b7f41000 rw-p 00019000 08:012326545 /lib/ld-2.7.so
bfbcd000-bfc0a000 rw-p bffc3000 00:000 [stack] <-stack,不到0.24MB,可读可执行不可写[todo:验证向下增长]
[todo:验证argv,envp]
$
交互,从内核层分析
[扩展]
理解设备模型
[结合source insight分析一个内核子系统的原理。源码分析工具虽好,但却是个死的东西,不能实时观察数据的生成和变化。如果在内核运行的时候,搭配调试器来分析,这个过程一定很形象和有趣]
面向对象的实现
设备模型的分层
外围支持机制
sysfs
hotplug
文件系统
参考书籍:
UNIX FilesystemsEvolution, Design, and Implementation.pdf :
站点:
Ext4 (and Ext2/Ext3)Wiki: http://ext4.wiki.kernel.org/index.php/Main_Page
Ext4 Developmentproject: http://www.bullopensource.org/ext4/
ext2-devel maillistarchive: http://sourceforge.net/mailarchive/forum.php?forum=ext2-devel
参考文章:
“Linux Filesystemsin 21 days 45 minutes” http://us1.samba.org/samba/ftp/cifs-cvs/ols2006-fs-tutorial-smf.pdf
***第三部分:其他工具***
strace
· 作用:strace能拦截和记录应用程序发起的系统调用和它收到的信号。主要用于观察应用层和内核层的交互。
· 命令选项:查看,$strace –help 或$man strace 或 $info strace
· 实例
ltrace
· 作用:ltrace用于监控程序发起的库函数调用以及程序收到的信号。
SystemTap
· 动态收集Linux内核信息和性能数据
· 官方 http://sourceware.org/systemtap/
· 参考文章
http://www.ibm.com/developerworks/cn/linux/l-cn-systemtap3/index.html
http://www.ibm.com/developerworks/cn/linux/l-systemtap/index.html
http://sourceware.org/systemtap/tutorial/
http://sourceware.org/systemtap/wiki
ubuntu下的配置安装: http://sourceware.org/systemtap/wiki/SystemtapOnUbuntu
MEMWATCH
· 作用:跟踪程序中的内存泄漏和错误
YAMD
· 作用:查找 C 和 C++ 中动态的、与内存分配有关的问题
Magic SysRq
内核文档 sysrq.txt
linux内核测试指南 相关章节
http://wiki.zh-kernel.org/#%E6%96%87%E7%AB%A0
附录:社区交流相关
补丁提交相关文档
如何参与 Linux 内核开发
http://wiki.zh-kernel.org/doc/howto
Linux内核代码风格
http://wiki.zh-kernel.org/doc/codingstyle
Linux内核开发邮件客户端资料
http://wiki.zh-kernel.org/doc/email-clients.txt
Linux内核补丁提交注意事项
基于git的Gentoo中文文档开发流程
http://www.gentoo-cn.org/doc/zh_cn/git-howto.xml
mutt配置使用
http://hi.baidu.com/springtty/blog/item/e6b25ddbb52f51ddb7fd4805.html
http://www.kongove.cn/blog/?p=149
http://www.kongove.cn/blog/?p=229
http://www.kongove.cn/blog/?p=225
Linux 系统内核的调试
调试是软件开发过程中一个必不可少的环节,在 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机上执行:
echo hello > /dev/ttyS0
在target机上执行:
cat /dev/ttyS0
如果串口连接没问题的话在将在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文件,为其打开调试开关。方法如下:。
CFLAGS += -g
最容易出现的问题是找不到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有所帮助。
参考资料
[1] http://oss.sgi.com/projects/kdb/
[2] http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html
[3] http://www.ibm.com/developerworks/cn/linux/l-kdbug/
[4] http://www.ibm.com/developerworks/cn/linux/l-kprobes.html
[5] http://kgdb.linsyssoft.com/downloads.htm
[8] http://www.uclinux.org/pub/uClinux/dist/
[9] http://opensrc.sec.samsung.com/download/linux-2.6.9-hsc0.patch.gz
[11] http://user-mode-linux.sourceforge.net/
[12] http://www.ibm.com/developerworks/cn/linux/l-skyeye/part1/
[13] http://www.ibm.com/developerworks/cn/views/linux/tutorials.jsp?cv_doc_id=84978
参考文献
[1]Robert Love Linux kernel development机械工业出版社
[2]陈渝 源代码开发的嵌入式系统软件分析与实践 北京航空航天大学出版社
[3]Alessandro Rubini Linux device driver 2se Edition O'Reilly
[4]Jonathan Corbet Linux device driver 3rd Edition O'Reilly
[5]李善平 Linux内核源代码分析大全 机械工业出版社
内核模块调试方法
关键字: 内核模块调试方法
对于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合,所以内核代码无法轻易地放在调试器中执行,而且也很难跟踪。同样,要想复现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样也就破坏了可以用来跟踪它们的现场。
本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。
4.1 通过打印调试
最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用printf 显示监视信息。调试内核代码的时候,则可以用 printk 来完成相同的工作。
4.1.1 printk
在前面的章节中,我们只是简单假设 printk 工作起来和 printf 很类似。现在则是介绍它们之间一些不同点的时候了。
其中一个差别就是,通过附加不同日志级别(loglevel),或者说消息优先级,可让 printk根据这些级别所标示的严重程度,对消息进行分类。一般采用宏来指示日志级别,例如,KERN_INFO,我们在前面已经看到它被添加在一些打印语句的前面,它就是一个可以使用的消息日志级别。日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接在一起;这也就是为什么下面的例子中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子,一个是调试信息,一个是临界信息:
printk(KERN_DEBUG "Here I am: %s:%i/n", _ _FILE_ _, _ _LINE_ _);
printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);
在头文件 <linux/kernel.h> 中定义了 8 种可用的日志级别字符串。
KERN_EMERG
用于紧急事件消息,它们一般是系统崩溃之前提示的消息。
KERN_ALERT
用于需要立即采取动作的情况。
KERN_CRIT
临界状态,通常涉及严重的硬件或软件操作失败。
KERN_ERR
用于报告错误状态;设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。
KERN_WARNING
对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重问题。
KERN_NOTICE
有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。
KERN_INFO
提示性信息。很多驱动程序在启动的时候,以这个级别打印出它们找到的硬件信息。
KERN_DEBUG
用于调试信息。
每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从0到7,数值越小,优先级就越高。
没有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在文件 kernel/printk.c 中指定为一个整数值。在 Linux 的开发过程中,这个默认的级别值已经有过好几次变化,所以我们建议读者始终指定一个明确的级别。
根据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。如果优先级小于 console_loglevel 这个整数值的话,消息才能显示出来。如果系统同时运行了 klogd 和 syslogd,则无论 console_loglevel 为何值,内核消息都将追加到 /var/log/messages 中(否则的话,除此之外的处理方式就依赖于对 syslogd 的设置)。如果 klogd 没有运行,这些消息就不会传递到用户空间,这种情况下,就只好查看 /proc/kmsg 了。
变量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL,而且还可以通过sys_syslog 系统调用进行修改。调用 klogd 时可以指定 -c 开关选项来修改这个变量, klogd 的 man 手册页对此有详细说明。注意,要修改它的当前值,必须先杀掉 klogd,再加 -c选项重新启动它。此外,还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的源文件 miscprogs/setlevel.c 里找到这样的一段程序。新优先级被指定为一个 1 到 8 之间的整数值。如果值被设为 1,则只有级别为 0(KERN_EMERG) 的消息才能到达控制台;如果设为 8,则包括调试信息在内的所有消息都能显示出来。
如果在控制台上工作,而且常常遇到内核错误(参见本章后面的“调试系统故障”一节)的话,就有必要降低日志级别,因为出错处理代码会把 console_loglevel 增为它的最大数值,导致随后的所有消息都显示在控制台上。如果需要查看调试信息,就有必要提高日志级别;这在远程调试内核,并且在交互会话未使用文本控制台的情况下,是很有帮助的。
从2.1.31这个版本起,可以通过文本文件 /proc/sys/kernel/printk 来读取和修改控制台的日志级别。这个文件容纳了 4 个整数值。读者可能会对前面两个感兴趣:控制台的当前日志级别和默认日志级别。例如,在最近的这些内核版本中,可以通过简单地输入下面的命令使所有的内核消息得到显示:
# echo 8 > /proc/sys/kernel/printk
不过,如果仍在 2.0 版本下的话,就需要使用 setlevel 这样的工具了。
现在大家应该清楚为什么在 hello.c范例中使用 <1> 这些标记了,它们用来确保这些消息能在控制台上显示出来。
对于控制台日志策略,Linux考虑到了某些灵活性,也就是说,可以发送消息到一个指定的虚拟控制台(假如控制台是文本屏幕的话)。默认情况下,“控制台” 就是当前地虚拟终端。可以在任何一个控制台设备上调用 ioctl(TIOCLINUX),来指定接收消息的虚拟终端。下面的 setconsole 程序,可选择专门用来接收内核消息的控制台;这个程序必须由超级用户运行,在 misc-progs 目录里可以找到它。下面是程序的代码:
int main(int argc, char **argv)
{
char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */
if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
else {
fprintf(stderr, "%s: need a single arg/n",argv[0]); exit(1);
}
if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) { /* use stdin */
fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s/n",
argv[0], strerror(errno));
exit(1);
}
exit(0);
}
setconsole 使用了特殊的ioctl命令:TIOCLINUX ,这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX 时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字,接下去的字节所具有的功能则由这个子命令决定。在 setconsole 中,使用的子命令是 11,后面那个字节(存于bytes[1]中)标识虚拟控制台。关于 TIOCLINUX 的详尽描述可以在内核源码中的 drivers/char/tty_io.c 文件得到。
4.1.2 消息如何被记录
printk 函数将消息写到一个长度为
LOG_BUF_LEN(定义在 kernel/printk.c 中)字节的循环缓冲区中,然后唤醒任何正在等待消息的进程,即那些睡眠在
syslog 系统调用上的进程,或者读取 /proc/kmesg 的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对
/proc/kmesg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog
系统调用却能随意地返回日志数据,并保留这些数据以便其它进程也能使用。一般而言,读 /proc 文件要容易些,这使它成为 klogd 的默认方法。
手工读取内核消息时,在停止klogd之后,可以发现 /proc 文件很象一个FIFO,读进程会阻塞在里面以等待更多的数据。显然,如果已经有 klogd 或其它的进程正在读取相同的数据,就不能采用这种方法进行消息读取,因为会与这些进程发生竞争。
如果循环缓冲区填满了,printk就绕回缓冲区的开始处填写新数据,覆盖最陈旧的数据,于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处相比,这个问题可以忽略不计。例如,循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时覆盖那些不再会有人去读的旧数据,从而使内存的浪费减到最少。Linux消息处理方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。而这个方法的唯一缺点就是可能丢失某些数据。
klogd 运行时,会读取内核消息并将它们分发到 syslogd,syslogd 随后查看 /etc/syslog.conf ,找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分;这两者的允许值均定义在 <sys/syslog.h> 中。内核消息由 LOG_KERN 设施记录,并以 printk 中使用的优先级记录(例如,printk 中使用的 KERN_ERR对应于syslogd 中的 LOG_ERR)。如果没有运行 klogd,数据将保留在循环缓冲区中,直到某个进程读取或缓冲区溢出为止。
如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为 klogd 指定 -f (file) 选项,指示 klogd 将消息保存到某个特定的文件,或者修改 /etc/syslog.conf 来适应自己的需求。另一种可能的办法是采取强硬措施:杀掉klogd,而将消息详细地打印到空闲的虚拟终端上。*
注: 例如,使用下面的命令可设置 10 号终端用于消息的显示:
setlevel 8
setconsole 10
或者在一个未使用的 xterm 上执行cat /proc/kmesg来显示消息。
4.1.3 开启及关闭消息
在驱动程序开发的初期阶段,printk
对于调试和测试新代码是相当有帮助的。不过,当正式发布驱动程序时,就得删除这些打印语句,或至少让它们失效。不幸的是,你可能会发现这样的情况,在删除了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或是有人发现了一个
bug),这时,又希望至少把一部分消息重新开启。这两个问题可以通过几个办法解决,以便全局地开启或禁止消息,并能对个别消息进行开关控制。
我们在这里给出了一个编写 printk 调用的方法,可个别或全局地对它们进行开关;这个技巧是定义一个宏,在需要时,这个宏展开为一个printk(或printf)调用。
可以通过在宏名字中删减或增加一个字母,打开或关闭每一条打印语句。
编译前修改 CFLAGS 变量,则可以一次关闭所有消息。
同样的打印语句既可以用在内核态也可以用在用户态,因此,关于这些额外的信息,驱动和测试程序可以用同样的方法来进行管理。
下面这些来自 scull.h 的代码,就实现了这些功能。
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
# ifdef _ _KERNEL_ _
/* This one if debugging is on, and kernel space */
# define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt,
## args)
# else
/* This one for user space */
# define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
# endif
#else
# define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif
#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */
符号 PDEBUG 依赖于是否定义了SCULL_DEBUG,它能根据代码所运行的环境选择合适的方式显示信息:内核态运行时使用printk系统调用;用户态下则使用 libc调用fprintf,向标准错误设备进行输出。符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除。
为了进一步简化这个过程,可以在 Makefile加上下面几行:
# Comment/uncomment the following line to disable/enable debugging
DEBUG = y
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif
CFLAGS += $(DEBFLAGS)
本节所给出的宏依赖于gcc 对ANSI C预编译器的扩展,这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖并不是什么问题,因为内核对 gcc 特性的依赖更强。此外,Makefile依赖于 GNU 的make 版本;基于同样的道理,这也不是什么问题。
如果读者熟悉 C 预编译器,可以将上面的定义进行扩展,实现“调试级别”的概念,这需要定义一组不同的级别,并为每个级别赋一个整数(或位掩码),用以决定各个级别消息的详细程度。
但是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点,对读者来说,我们无法预知最合适的点在哪里。记住,预处理程序的条件语句(以及代码中的常量表达式)只在编译时执行,要再次打开或关闭消息必须重新编译。另一种方法就是使用C条件语句,它在运行时执行,因此可以在程序运行期间打开或关闭消息。这是个很好的功能,但每次代码执行时系统都要进行额外的处理,甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法接受的。
在很多情况下,本节提到的这些宏都已被证实是很有用的,仅有的缺点是每次开启和关闭消息显示时都要重新编译模块。
4.2 通过查询调试
上一节讲述了 printk 是如何工作的以及如何使用它,但还没谈到它的缺点。
由于 syslogd 会一直保持对其输出文件的同步刷新,每打印一行都会引起一次磁盘操作,因此大量使用 printk 会严重降低系统性能。从 syslogd 的角度来看,这样的处理是正确的。它试图把每件事情都记录到磁盘上,以防系统万一崩溃时,最后的记录信息能反应崩溃前的状况;然而,因处理调试信息而使系统性能减慢,是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日志文件的名字前面,前缀一个减号符解决。*
注: 这个减号是个“特殊”标记,避免 syslogd 在每次出现新信息时都去刷新磁盘文件,这些内容记述在 syslog.conf(5) 中,这个手册页很值得一读。
修改配置文件带来的问题在于,在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希望尽快把信息刷新到磁盘时,也是如此。如果不愿作这种持久性修改的话,另一个选择是运行一个非
klogd 程序(如前面介绍的cat /proc/kmesg),但这样并不能为通常的系统操作提供一个合适的环境。
多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生数据。实际上,每个 Unix 系统都提供了很多工具,用于获取系统信息,如:ps、netstat、vmstat等等。
驱动程序开发人员对系统进行查询时,可以采用两种主要的技术:在 /proc 文件系统中创建文件,或者使用驱动程序的 ioctl 方法。/proc 方式的另一个选择是使用 devfs,不过用于信息查找时,/proc 更为简单一些。
4.2.1 使用 /proc 文件系统
/proc
文件系统是一种特殊的、由程序创建的文件系统,内核使用它向外界输出信息。/proc
下面的每个文件都绑定于一个内核函数,这个函数在文件被读取时,动态地生成文件的“内容”。我们已经见到过这类文件的一些输出情况,例如,
/proc/modules 列出的是当前载入模块的列表。
Linux系统对/proc的使用很频繁。现代Linux系统中的很多工具都是通过 /proc 来获取它们的信息,例如 ps、top 和 uptime。有些设备驱动程序也通过 /proc 输出信息,你的驱动程序当然也可以这么做。因为 /proc 文件系统是动态的,所以驱动程序模块可以在任何时候添加或删除其中的文件项。
特征完全的 /proc 文件项相当复杂;在所有的这些特征当中,有一点要指出的是,这些 /proc 文件不仅可以用于读出数据,也可以用于写入数据。不过,大多数时候,/proc 文件项是只读文件。本节将只涉及简单的只读情形。如果有兴趣实现更为复杂的事情,读者可以先在这里了解基础知识,然后参考内核源码来建立完整的认识。
所有使用 /proc 的模块必须包含 <linux/proc_fs.h>,通过这个头文件定义正确的函数。
为创建一个只读 /proc 文件,驱动程序必须实现一个函数,用于在文件读取时生成数据。当某个进程读这个文件时(使用 read 系统调用),请求会通过两个不同接口的其中之一发送到驱动程序模块,使用哪个接口取决于注册情况。我们先把注册放到本节后面,先直接讲述读接口。
无论采用哪个接口,在这两种情况下,内核都会分配一页内存(也就是 PAGE_SIZE 个字节),驱动程序向这片内存写入将返回给用户空间的数据。
推荐的接口是 read_proc,不过还有一个名为 get_info 的老一点的接口。
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
参数表中的 page 指针指向将写入数据的缓冲区;start 被函数用来说明有意义的数据写在页面的什么位置(对此后面还将进一步谈到);offset 和 count 这两个参数与在 read 实现中的用法相同。eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data 参数是一个驱动程序特有的数据指针,可用于内部记录。*
注: 纵览全书,我们还会发现这样的一些指针;它们表示了这类处理中有关的“对象”,与C++ 中的同类处理有些相似。
这个函数可以在2.4内核中使用,如果使用我们的 sysdep.h 头文件,那么在2.2内核中也可以用这个函数。
int (*get_info)(char *page, char **start, off_t offset, int count);
get_info 是一个用来读取 /proc 文件的较老接口。所有的参数与 read_proc 中的对应参数用法相同。缺少的是报告到达文件尾的指针和由data 指针带来的面向对象风格。这个函数可以用在所有我们感兴趣的内核版本中(尽管在它 2.0 版本的实现中有一个额外未用的参数)。
这两个函数的返回值都是实际放入页面缓冲区的数据的字节数,这一点与 read 函数对其它类型文件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标记,而 start 的用法就有点复杂了。
对于 /proc 文件系统的用户扩展,其最初实现中的主要问题在于,数据传输只使用单个内存页面。这样就把用户文件的总体尺寸限制在了 4KB 以内(或者是适合于主机平台的其它值)。start 参数在这里就是用来实现大数据文件的,不过该参数可以被忽略。
如果 proc_read 函数不对 *start 指针进行设置(它最初为 NULL),内核就会假定 offset 参数被忽略,并且数据页包含了返回给用户空间的整个文件。反之,如果需要通过多个片段创建一个更大的文件,则可以把 *start 赋值为页面指针,因此调用者也就知道了新数据放在缓冲区的开始位置。当然,应该跳过前 offset 个字节的数据,因为这些数据已经在前面的调用中返回。
长久以来,关于 /proc 文件还有另一个主要问题,这也是 start 意图解决的一个问题。有时,在连续的 read 调用之间,内核数据结构的 ASCII 表述会发生变化,以至于读进程发现前后两次调用所获得的数据不一致。如果把 *start 设为一个小的整数值,调用程序可以利用它来增加 filp->f_pos 的值,而不依赖于返回的数据量,因此也就使 f_pos 成为read_proc 或 get_info 程序中的一个内部记录值。例如,如果 read_proc 函数从一个大的结构数组返回数据,并且这些结构的前 5 个已经在第一次调用中返回,那么可将 *start 设置为 5。下次调用中这个值将被作为偏移量;驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作“hack”,可以在 /fs/proc/generic.c 中看到。
现在我们来看个例子。下面是scull 设备 read_proc 函数的简单实现:
int scull_read_procmem(char *buf, char **start, off_t offset,
int count, int *eof, void *data)
{
int i, j, len = 0;
int limit = count - 80; /* Don't print more than this */
for (i = 0; i < scull_nr_devs && len <= limit; i++) {
Scull_Dev *d = &scull_devices[ i];
if (down_interruptible(&d->sem))
return -ERESTARTSYS;
len += sprintf(buf+len,"/nDevice %i: qset %i, q %i, sz %li/n",
i, d->qset, d->quantum, d->size);
for (; d && len <= limit; d = d->next) { /* scan the list */
len += sprintf(buf+len, " item at %p, qset at %p/n", d,
d->data);
if (d->data && !d->next) /* dump only the last item
- save space */
for (j = 0; j < d->qset; j++) {
if (d->data[j])
len += sprintf(buf+len," % 4i: %8p/n",
j,d->data[j]);
}
}
up(&scull_devices[ i].sem);
}
*eof = 1;
return len;
}
这是一个相当典型的 read_proc 实现。它假定决不会有这样的需求,即生成多于一页的数据,因此忽略了 start 和 offset 值。但是,小心不要超出缓冲区,以防万一。
使用 get_info 接口的 /proc 函数与上面说明的 read_proc 非常相似,除了没有最后的那两个参数。既然这样,则通过返回少于调用者预期的数据(也就是少于 count 参数),来提示已到达文件尾。
一旦定义好了一个 read_proc 函数,就需要把它与一个 /proc 文件项连接起来。依赖于将要支持的内核版本,有两种方法可以建立这样的连接。最容易的方法是简单地调用 create_proc_read_entry,但这只能用于2.4内核(如果使用我们的 sysdep.h 头文件,则也可用于 2.2 内核)。下面就是 scull 使用的调用,以 /proc/scullmem 的形式来提供 /proc 功能。
create_proc_read_entry("scullmem",
0 /* default mode */,
NULL /* parent dir */,
scull_read_procmem,
NULL /* client data */);
这个函数的参数表包括:/proc 文件项的名称、应用于该文件项的文件许可权限(0是个特殊值,会被转换为一个默认的、完全可读模式的掩码)、文件父目录的 proc_dir_entry 指针(我们使用 NULL 值使该文件项直接定位在 /proc 下)、指向 read_proc 的函数指针,以及将传递给 read_proc 函数的数据指针。
目录项指针(proc_dir_entry)可用来在 /proc 下创建完整的目录层次结构。不过请注意,将文件项置于 /proc 的子目录中有更为简单的方法,即把目录名称作为文件项名称的一部分――只要目录本身已经存在。例如,有个新的约定,要求设备驱动程序对应的 /proc 文件项应转移到子目录 driver/ 中;scull 可以简单地指定它的文件项名称为 driver/scullmem,从而把它的 /proc 文件放到这个子目录中。
当然,在模块卸载时,/proc 中的文件项也应被删除。 remove_proc_entry 就是用来撤消 create_proc_read_entry 所做工作的函数。
remove_proc_entry("scullmem", NULL /* parent dir */);
另一个创建 /proc 文件项的方法是,创建并初始化一个 proc_dir_entry 结构,并将该结构传递给函数 proc_register_dynamic (2.0 版本)或 proc_register(2.2 版本,如果结构中的索引节点号为0,该函数即认为是动态文件)。作为一个例子,当在2.0内核的头文件下进行编译时,考虑下面 scull 所使用的这些代码:
static int scull_get_info(char *buf, char **start, off_t offset,
int len, int unused)
{
int eof = 0;
return scull_read_procmem (buf, start, offset, len, &eof, NULL);
}
struct proc_dir_entry scull_proc_entry = {
namelen: 8,
name: "scullmem",
mode: S_IFREG | S_IRUGO,
nlink: 1,
get_info: scull_get_info,
};
static void scull_create_proc()
{
proc_register_dynamic(&proc_root, &scull_proc_entry);
}
static void scull_remove_proc()
{
proc_unregister(&proc_root, scull_proc_entry.low_ino);
}
代码声明了一个使用 get_info 接口的函数,并填写了一个 proc_dir_entry 结构,用于对文件系统进行注册。
这段代码借助sysdep.h 中宏定义的支持,提供了 2.0 和 2.4 内核之间的兼容性。因为 2.0 内核不支持 read_proc,它使用了 get_info 接口。如果对 #ifdef 作一些更多的处理,可以使这段代码在 2.2 内核中使用 read_proc,不过这样收益并不大。
4.2.2 ioctl 方法
ioctl是作用于文件描述符之上的一个系统调用,我们会在下一章介绍它的用法;它接收一个“命令”号,用以标识将执行的命令;以及另一个(可选的)参数,通常是个指针。
做为替代 /proc文件系统的方法,可以为调试设计若干ioctl命令。这些命令从驱动程序复制相关数据到用户空间,在用户空间中可以查看这些数据。
使用ioctl 获取信息比起 /proc 来要困难一些,因为需要另一个程序调用 ioctl 并显示结果。这个程序是必须编写并编译的,而且要和测试模块配合一致。从另一方面来说,相对实现 /proc 文件所需的工作,驱动程序的编码则更为容易些。
有时 ioctl 是获取信息的最好方法,因为它比起读 /proc 要快得多。如果在数据写到屏幕之前要完成某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外,ioctl 并不要求把数据分割成不超过一个内存页面的片断。
ioctl 方法的一个优点是,在结束调试之后,用来取得信息的这些命令仍可以保留在驱动程序中。/proc文件对任何查看这个目录的人都是可见的(很多人可能会纳闷 “这些奇怪的文件是用来做什么的”),然而与 /proc文件不同,未公开的 ioctl 命令通常都不会被注意到。此外,万一驱动程序有什么异常,这些命令仍然可以用来调试。唯一的缺点就是模块会稍微大一些。
4.3 通过监视调试
有时,通过监视用户空间中应用程序的运行情况,可以捕捉到一些小问题。监视程序同样也有助于确认驱动程序工作是否正常。例如,看到 scull 的 read 实现如何响应不同数据量的 read 请求后,我们就可以判断它是否工作正常。
有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数,插入打印语句,或者在 strace 状态下运行程序。在检查内核代码时,最后一项技术最值得关注,我们将在此对它进行讨论。
strace 命令是一个功能非常强大的工具,它可以显示程序所调用的所有系统调用。它不仅可以显示调用,而且还能显示调用参数,以及用符号方式表示的返回值。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。strace 有许多命令行选项;最为有用的是 -t,用来显示调用发生的时间;-T,显示调用所花费的时间; -e,限定被跟踪的调用类型;-o,将输出重定向到一个文件中。默认情况下,strace将跟踪信息打印到 stderr 上。
strace从内核中接收信息。这意味着一个程序无论是否按调试方式编译(用 gcc 的 -g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样,strace 也可以跟踪一个正在运行的进程。
跟踪信息通常用于生成错误报告,然后发给应用开发人员,但是它对内核编程人员来说也同样非常有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace 允许我们检查每次调用中输入和输出数据的一致性。
例如,下面的屏幕信息显示了 strace ls /dev > /dev/scull0 命令的最后几行:
[...]
open("/dev", O_RDONLY|O_NONBLOCK) = 4
fcntl(4, F_SETFD, FD_CLOEXEC) = 0
brk(0x8055000) = 0x8055000
lseek(4, 0, SEEK_CUR) = 0
getdents(4, /* 70 entries */, 3933) = 1260
[...]
getdents(4, /* 0 entries */, 3933) = 0
close(4) = 0
fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
ioctl(1, TCGETS, 0xbffffa5c) = -1 ENOTTY (Inappropriate ioctl
for device)
write(1, "MAKEDEV/natibm/naudio/naudio1/na"..., 4096) = 4000
write(1, "d2/nsdd3/nsdd4/nsdd5/nsdd6/nsdd7"..., 96) = 96
write(1, "4/nsde5/nsde6/nsde7/nsde8/nsde9/n"..., 3325) = 3325
close(1) = 0
_exit(0) = ?
很明显,ls 完成对目标目录的检索后,在首次对 write 的调用中,它试图写入 4KB 数据。很奇怪(对于 ls 来说),实际只写了4000个字节,接着它重试这一操作。然而,我们知道scull的 write 实现每次最多只写一个量子(scull 中设置的量子大小为4000个字节),所以我们所预期的就是这样的部分写入。经过几个步骤之后,每件工作都顺利通过,程序正常退出。
另一个例子,让我们来对 scull 设备进行读操作(使用 wc 命令):
[...]
open("/dev/scull0", O_RDONLY) = 4
fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
read(4, "MAKEDEV/natibm/naudio/naudio1/na"..., 16384) = 4000
read(4, "d2/nsdd3/nsdd4/nsdd5/nsdd6/nsdd7"..., 16384) = 3421
read(4, "", 16384) = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, " 7421 /dev/scull0/n", 20) = 20
close(4) = 0
_exit(0) = ?
正如所料,read 每次只能读取4000个字节,但数据总量与前面例子中写入的数量是相同的。与上面的写跟踪相对比,请读者注意本例中重试工作是如何组织的。为了快速读取数据,wc 已被优化了,因而它绕过了标准库,试图通过一次系统调用读取更多的数据。可以从跟踪的 read 行中看到 wc 每次均试图读取 16KB 数据。
Linux行家可以在 strace 的输出中发现很多有用信息。如果觉得这些符号过于拖累的话,则可以仅限于监视文件方法(open,read 等)是如何工作的。
就个人观点而言,我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的 perror 调用在用于调试时信息还不够详细,而 strace 能够确切查明系统调用的哪个参数引发了错误,这一点对调试是大有帮助的。
4.4 调试系统故障
即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就会产生系统故障。在出现这种情况时,获取尽可能多的信息对解决问题是至关重要的。
注意,“故障”不意味着“panic”。Linux 代码非常健壮(用术语讲即为鲁棒,robust),可以很好地响应大部分错误:故障通常会导致当前进程崩溃,而系统仍会继续运行。如果在进程上下文之外发生故障,或是系统的重要组成被损害时,系统才有可能 panic。但如果问题出在驱动程序中时,通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时,为进程上下文分配的一些内存可能会丢失;例如,由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而,由于内核在进程中止时会对已打开的设备调用 close 操作,驱动程序仍可以释放由 open 方法分配的资源。
我们已经说过,当内核行为异常时,会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息。尽管它们对于初学者来说相当晦涩,不过处理器在出错时转储出的这些数据包含了许多值得关注的信息,通常足以查明程序错误,而无需额外的测试。
4.4.1 oops消息
大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。
由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失效”的信号。如果地址非法,内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。
值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理。
oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的 printk 语句产生,就象前面“printk”一节所介绍的那样分发出来。
让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[<c48370c3>]
EFLAGS: 00010286
eax: ffffffea ebx: c2281a20 ecx: c48370c0 edx: c2281a40
esi: 4000c000 edi: 4000c000 ebp: c38adf8c esp: c38adf8c
ds: 0018 es: 0018 ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 /
0000010e
4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 /
4000c000
0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b /
00000004
Call Trace: [<c01356e6>] [<c010b860>]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
这个消息是通过对 faulty 模块的一个设备进行写操作而产生的,faulty 这个模块专为演示出错而编写。faulty.c 中 write 方法的实现很简单:
ssize_t faulty_write (struct file *filp, const char *buf, size_t count,
loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
正如读者所见,我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值,所以错误发生,内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中,faulty 模块还有更多有意思的错误状态。
char faulty_buf[1024];
ssize_t faulty_read (struct file *filp, char *buf, size_t count,
loff_t *pos)
{
int ret, ret2;
char stack_buf[4];
printk(KERN_DEBUG "read: buf %p, count %li/n", buf, (long)count);
/* the next line oopses with 2.0, but not with 2.2 and later */
ret = copy_to_user(buf, faulty_buf, count);
if (!ret) return count; /* we survived */
printk(KERN_DEBUG "didn't fail: retry/n");
/* For 2.2 and 2.4, let's try a buffer overflow */
sprintf(stack_buf, "1234567/n");
if (count > 8) count = 8; /* copy 8 bytes to the user */
ret2 = copy_to_user(buf, stack_buf, count);
if (!ret2) return count;
return ret2;
}
这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:
EIP: 0010:[<00000000>]
[...]
Call Trace: [<c010b860>]
Code: Bad EIP value.
用户处理 oops 消息的主要问题在于,我们很难从十六进制数值中看出什么内在的意义;为了使这些数据对程序员更有意义,需要把它们解析为符号。有两个工具可用来为开发人员完成这样的解析:klogd 和 ksymoops。前者只要运行就会自行进行符号解码;后者则需要用户有目的地调用。下面的讨论,使用了在我们第一个 oops 例子中通过使用NULL 指针而产生的出错信息。
使用 klogd
klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下,klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助。
当 faulty 的一个oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address /
00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[faulty:faulty_write+3/576]
EFLAGS: 00010286
eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00
esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c
ds: 0018 es: 0018 ss: 0018
Process cat (pid: 23413, stackpage=c2337000)
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 /
00000001
0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 /
0804d038
00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b /
00000004
Call Trace: [sys_write+214/256] [system_call+52/56]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
klogd 提供了大多数必要信息用于发现问题。在这个例子中,我们看到指令指针(EIP)正执行于函数 faulty_write 中,因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数的第3个字节上,而函数整体长度为 576 个字节。注意这些数值都是十进制的,而非十六进制。
然而,当错误发生在可装载模块中时,为了获取错误相关的有用信息,开发者还必须注意一些情况。klogd 在开始运行时装入所有可用符号,并随后使用这些符号。如果在 klogd 已经对自身初始化之后(一般在系统启动时),装载某个模块,那 klogd 将不会有这个模块的符号信息。强制 klogd取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd 进程,这种操作在时间顺序上,必须是在模块已经装入(或重新装载)之后,而在进行任何可能引起 oops 的处理之前。
还可以在运行 klogd 时加上 -p 选项,这会使它在任何发现 oops 消息的时刻重新读入符号信息。不过,klogd 的man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息。而发生错误之后,所获得的信息可能是完全错误的了。
为了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本。通常这个文件在 /boot 中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot,或告知 klogd 到什么位置查看。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。
使用 ksymoops
有些时候,klogd
对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址,又要获得对应的符号,而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外,往往还需要更多的信息。对
klogd 来说,在出错期间被杀掉,也是常用的事情。在这些情况下,可以调用一个更为强大的 oops 分析器,ksymoops
就是这样的一个工具。
在 2.3 开发系列之前,ksymoops 是随内核源码一起发布的,位于 scripts 目录之下。它现在则在自己的FTP 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许还可以从 ftp://ftp.ocs.com.au/pub/ksymoops 站点上获取这个工具的升级版本。
为了取得最佳的工作状态,除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项:
System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。
模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块,以便获得它们的符号信息。如果未提供这个列表,ksymoops 会查看 /proc/modules。
在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。
当前正运行的内核映像的复本注意,ksymoops
需要的是一个直接的内核映像,而不是象 vmlinuz、zImage 或 bzImage
这样被大多数系统所使用的压缩版本。默认是不使用内核映像,因为大多数人都不会保存这样的一个内核。如果手边就有这样一个符合要求的内核的话,就应该采用
-v 选项告知 ksymoops 它的位置。
已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块,不过在开发中,几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。
虽然 ksymoops 会访问 /proc 中的文件来取得它所需的信息,但这样获得的结果是不可靠的。在 oops 发生和 ksymoops 运行的时间间隙中,系统几乎一定会重新启动,这样取自 /proc 的信息就可能与故障发生时的实际状态不符合。只要有可能,最好在引起 oops 发生之前,保存 /proc/modules 和 /proc/ksyms 的复本。
我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页,这是一个很好的资料文档。
这个工具命令行中的最后一个参数是 oops 消息的位置;如果缺少这个参数,ksymoops 会按Unix 的惯例去读取标准输入设备。运气好的话,消息可以从系统日志中重新恢复;在发生很严重的崩溃情况时,我们可能不得不将这些消息从屏幕上抄下来,然后再敲进去(除非用的是串口控制台,这对内核开发人员来说,是非常棒的工具)。
注意,当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱。如果 klogd 已经运行,而且 oops 发生后系统仍在运行,那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。
如果没有明确地提供全部的上述信息,ksymoops 会发出警告。对于载入模块未作符号定义这类的情况,它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。
ksymoops 的输出类似如下:
>>EIP; c48370c3 <[faulty]faulty_write+3/20> <=====
Trace; c01356e6 <sys_write+d6/100>
Trace; c010b860 <system_call+34/38>
Code; c48370c3 <[faulty]faulty_write+3/20>
00000000 <_EIP>:
Code; c48370c3 <[faulty]faulty_write+3/20> <=====
0: c7 05 00 00 00 movl $0x0,0x0 <=====
Code; c48370c8 <[faulty]faulty_write+8/20>
5: 00 00 00 00 00
Code; c48370cd <[faulty]faulty_write+d/20>
a: 31 c0 xorl %eax,%eax
Code; c48370cf <[faulty]faulty_write+f/20>
c: 89 ec movl %ebp,%esp
Code; c48370d1 <[faulty]faulty_write+11/20>
e: 5d popl %ebp
Code; c48370d2 <[faulty]faulty_write+12/20>
f: c3 ret
Code; c48370d3 <[faulty]faulty_write+13/20>
10: 8d b6 00 00 00 leal 0x0(%esi),%esi
Code; c48370d8 <[faulty]faulty_write+18/20>
15: 00
正如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd 所做的很相似,不过要更为准确,而且是十六进制形式的。可以注意到,faulty_write 函数的长度被正确地报告为 0x20个字节。这是因为 ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息。
而且在这个例子中,还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确切地判断发生了些什么事情;这里很明显,错误在于一个向 0 地址写入数据 0 的指令。
ksymoops 的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上,而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出 PC 的世界,我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了):
Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
// ____ //
"@'/ .. /`@"
/_| /_ _/ |_/
/_ _ _/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc /
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 /
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 /
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 /
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 /
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 /
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 /
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 /
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 /
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 <c0202000> /
30680005 01000000 01000000 01000000 01000000
请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。
下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息:
>>TPC; 0000000001000128 <[faulty].text.start+88/a0> <=====
>>O7; 0000000000457fb4 <sys_write+114/160>
>>I7; 0000000000410114 <linux_sparc_syscall+34/40>
Trace; 0000000000410114 <linux_sparc_syscall+34/40>
Trace; 000000007007cba4 <END_OF_CODE+6f07c40d/????>
Code; 000000000100011c <[faulty].text.start+7c/a0>
0000000000000000 <_TPC>:
Code; 000000000100011c <[faulty].text.start+7c/a0>
0: 01 00 00 00 nop
Code; 0000000001000120 <[faulty].text.start+80/a0>
4: 90 10 20 00 clr %o0 ! 0 <_TPC>
Code; 0000000001000124 <[faulty].text.start+84/a0>
8: 81 c3 e0 08 retl
Code; 0000000001000128 <[faulty].text.start+88/a0> <=====
c: c0 20 20 00 clr [ %g0 ] <=====
Code; 000000000100012c <[faulty].text.start+8c/a0>
10: 30 68 00 05 b,a %xcc, 24 <_TPC+0x24> /
0000000001000140 <[faulty]faulty_write+0/20>
Code; 0000000001000130 <[faulty].text.start+90/a0>
14: 01 00 00 00 nop
Code; 0000000001000134 <[faulty].text.start+94/a0>
18: 01 00 00 00 nop
Code; 0000000001000138 <[faulty].text.start+98/a0>
1c: 01 00 00 00 nop
Code; 000000000100013c <[faulty].text.start+9c/a0>
20: 01 00 00 00 nop
要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要这些信息,是因为 SPARC64 用户空间的本地结构是32位的)。本例中,使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。
读者可能会抱怨对调用的跟踪并没带回什么值得注意的信息;然而,SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中:07 和 I7 寄存器保存了最后调用的两个函数的指令指针,这就是它们出现在调用跟踪记录边上的原因。在这个例子中,我们可以看到,故障指令位于一个由 sys_write 调用的函数中。
要注意的是,无论平台/结构是怎样的一种配合情况,用来显示反汇编代码的格式与 objdump 程序所使用的格式是一样的。objdump 是个很强大的工具;如果想查看发生故障的完整函数,可以调用命令: objdump -d faulty.o(再次重申,对于 SPARC64 平台,需要使用特殊选项:--target elf64-sparc-architecture sparc:v9)。
关于 objdump 和它的命令行选项的更多信息,可以参阅这个命令的手册页帮助。
学习对 oops 消息进行解码,需要一定的实践经验,并且了解所使用的目标处理器,以及汇编语言的表达习惯等。这样的准备是值得的,因为花费在学习上的时间很快会得到回报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识,仍有必要花些时间对此进行学习,因为Unix 的语法与 Intel 的语法并不一样。(在 as 命令 infor 页的“i386-specific”一章中,对这种差异进行了很好的描述。)
4.4.2 系统挂起
尽管内核代码中的大多数错误仅会导致一个oops
消息,但有时它们则会将系统完全挂起。如果系统挂起了,任何消息都无法打印。例如,如果代码进入一个死循环,内核就会停止进行调度,系统不会再响应任何动作,包括
Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然;要么就是亡羊补牢,在发生挂起后调试代码。
通过在一些关键点上插入 schedule 调用可以防止死循环。schedule 函数(正如读者猜到的)会调用调度器,并因此允许其他进程“偷取”当然进程的CPU时间。如果该进程因驱动程序的错误而在内核空间陷入死循环,则可以在跟踪到这种情况之后,借助 schedule 调用杀掉这个进程。
当然,应该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule 允许其他进程开始运行。假设驱动程序进行了合适的锁定,这种重入通常还并不致于带来问题。不过,一定不要在驱动程序持有spinlock 的任何时候调用 schedule。
如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule 调用时,最好的方法是加入一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)。
有时系统看起来象挂起了,但其实并没有。例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况。运行专为探明此种情况而设计的程序,通过查看它的输出情况,可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新,就说明 scheduler 正在工作。如果没有使用图形显示,则可以运行一个程序让键盘LED闪烁,或不时地开关软驱马达,或不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免;可改为寻求 ioctl 命令 KDMKTONE ),来检查 scheduler 是否工作正常。O’Reilly FTP站点上可以找到一个例子(misc-progs/heartbeat.c),它会使键盘LED不断闪烁。
如果键盘不接收输入,最佳的处理方法是从网络登录到系统中,杀掉任何违例的进程,或是重新设置键盘(用 kdb_mode -a)。然而,如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起是由键盘死锁造成的也没有用了。如果是这样的情况,就应该配置一种可替代的输入设备,以便至少可以正常地重启系统。比起去按所谓的“大红钮”,在你的计算机上,通过替代的输入设备来关机或重启系统要更为容易些,而且它可以免去fsck 对磁盘的长时间扫描。
例如,这种替代输入设备可以是鼠标。1.10或更新版本的 gpm 鼠标服务器可以通过命令行选项支持类似的功能,不过仅限于文本模式。如果没有网络连接,并且以图形方式运行,则建议采用某些自定义的解决方案,比如,设置一个与串口线 DCD 针脚相连的开关,并编写一个查询 DCD 信号状态变化的脚本,用于从外界干预键盘已被死锁的系统。
对于上述情形,一个不可缺少的工具是“magic SysRq key”,2.2 和后期版本内核中,在其它体系结构上也可利用得到它。SysRq 魔法键是通过PC键盘上的 ALT 和 SysRq 组合键来激活的,在 SPARC 键盘上则是 ALT 和 Stop 组合键。连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种,这些动作如下:
r
在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式。
k
激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程,留下一个干净的终端。
s
对所有磁盘进行紧急同步。