精华内容
下载资源
问答
  • Linux内核模块编程

    千次阅读 2018-10-24 16:47:13
    Linux内核模块编程 (作者:Baron_wu 禁止转载) 首先,创建一个内核模块并插入Linux内核中。这是实验第一部分 首先查看当前内核模块使用情概况:lsmod Module:模块名 Size:模块大小 Used by:这些模块在哪被使用 ...

    Linux内核模块编程

    (作者:Baron_wu 禁止转载)
    首先,创建一个内核模块并插入Linux内核中。这是实验第一部分
    首先查看当前内核模块使用情概况:lsmod
    在这里插入图片描述
    Module:模块名
    Size:模块大小
    Used by:这些模块在哪被使用
    接下来编写一个simple.c的程序,当加载和卸载内核模块时给出适当的消息。
    代码如下:

     #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    
    /* This function is called when the module is loaded. */
    int simple_init(void)
    {
           printk(KERN_INFO "Loading Module\n");
    
           return 0;
    }
    
    /* This function is called when the module is removed. */
    void simple_exit(void) {
    	printk(KERN_INFO "Removing Module\n");
    }
    
    /* Macros for registering module entry and exit points. */
    module_init( simple_init );
    module_exit( simple_exit );
    
    MODULE_LICENSE("GPL");
    MODULE_DESCRIPTION("Simple Module");
    MODULE_AUTHOR("SGG");
    

    Simple_list (模块入口点) 返回0代表成功 其他值代表失败
    Simple_exit(模块退出点) 无返回值
    两个函数都没有参数
    接下来两个宏指令是注册模块的入口与出口
    Module_init()
    Module_exit()

    printk()

    注意入口点与出口点是如何调用系统函数printk的。Printk()是内核对应的printf(),输出传给内核加载模块,通过使用命令dmesg来查看他的内容。Printk()允许指定优先级标志。它的值在包含文件<linux/printk.h>中给出。
    在这个实例中KERN_INFO是优先的。它就像被定义的一个信号消息。
    最后三行属于软件许可信息,是内核模块开发的标准格式。
    Simple.c用Makefile这个文件来进行编译。在终端输入make进行编译。

    Makefile文件内容

    obj-m += simple.o
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    
    

    编译后将会生成多个文件, 文件simple.ko代表编译后生成的内核模块。
    首先在含有simple.c 和Makefile的文件目录下执行make命令(我的这两个文件在桌面上)
    在这里插入图片描述
    之后将会生成许多文件如下所示:
    在这里插入图片描述
    == Simple.ko是生成的内核模块 ==
    接下来便是加载和移除内核模块的演示了。
    首先在之前生成的编译模块的目录中执行命令:sudo insmod simple.ko 来加载内核模块
    在这里插入图片描述
    紧接着输入dmesg来查看当前内核加载情况。
    在这里插入图片描述
    在最后一行可以看到加载成功的消息。
    **之后要进行的便是移除内核模块的操作。**在终端输入命令:sudo rmmod simple
    在这里插入图片描述
    接着查看是否移除成功。输入命令:dmesg
    在这里插入图片描述
    可以看到已经成功移除。
    因为内核记录很快就会填满 所以定期清除缓冲区是有必要的。
    执行命令:sudo dmesg -c
    到此,Linux内核模块编程第一部分结束。

    展开全文
  • 调试linux内核模块

    千次阅读 2012-10-01 14:42:50
     最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核及内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只...







    1:前言:

        最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核及内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只需要重新编译2.6.26(或更高)内核即可.kgdb安装及模块调试过程也遇到不少问题,网上网下不断的搜索与探索,才算调通.现在记录下来,供朋友们参考.
        首先说一下,开始本打算安装kdb进行内核调试,后来听说kdb只能进行汇编级别的调试,所以放弃,改用kgdb.

    2: 系统环境:

    虚拟环境:   VMWare Workstation 5.5(英文版)
    操作系统:   CentOS-4.6-i386(原内核2.6.9,将会把内核升级至2.6.26)
    注:CentOS 是RedHat的一个社区版本.
         (由于我们采用的linux kernel 2.6.26已经集成kgdb,kgdb再不需要单独下载)

    3:系统的安装:

    在VMWare中新建一台计算机:

    • 点击 Next
    • 选中Custom 点击 Next
    • 选中 New-Workstation 5,点击Next
    • 选中Linux ,Version选中Other Linux 2.6.x kernel 点击Next
    • Virtual machine name 输入Client(Development) 点击Next
    • Processors 选中 One, 点击Next
    • Memory 输入256,点击Next
    • Network connection 选中Use network address translation(NAT) (选第一个貌似也可以) 点击Next
    • I/O adapter types
    • SCSI Adapters 选中默认的LSI Logic(这里如果你后面使用了SCSI格式的Disk,编译内核时需要添加相应的驱动,我选择的是IDE的硬盘,kernel默认就支持了)
    • Disk 选中Create a new virtual disk 点击Next
    • Virtual Disk Type 选中IDE,点击Next
    • Disk capacity Disk size 输入80G (下面的Allocate all disk space now不要选中,表示在真正使用才分配磁盘空间, Split disk into 2 GB files项,可不选,如果你的系统分区为fat32格式,最好选中) 点击Next.
    • Disk file ,输入Disk的名称,如:disk1.vmdk ,点击Finish.完成
    安装CentOS 4.6(过程略过)
    安装完成后,关闭计算机,然后Clone一台同样的计算机.步骤如下:
    • 点击VM->Clone
    • 选中默认的From current state,点击Next
    • 选中Create a full clone, 点击Next
    • Virtual Machine name 输入Server(Targe),将克隆的机器命令为目标机.
    说明一下,kgdb 需要两台计算机通过串口进行远程调试,两台计算机分别为:
    • Client(Development):开发机,也称客户机,将在该计算机上进行程序的开发,GDB将在本计算机上运行.用于输入命令控制Server(target)的运行.
    • Server(Target): 目标机,也称服务器,就是被调试的计算机,在Development机上开发好的内核模块程序将拷贝到Target上运行,其运行受到Development命令的控制.
    分别为两个系统增加一个串口,以" Output to named pipe "方式,其中:
    • Client端选择"this end is the client", "the other end is a virtual machine"
    • Server端选择"this end is the server", "the other end is a virtual machine"
    • 备注: 两个pipe的名称要相同,并且选中下面的Connect at power on,及Advanced里面的Yield CPU on poll
    以后的部分,Server上的操作与Client上的操作将以不同的背景色显示,输入的命令将以不同的字体颜色并带下划线显示.请注意:
    • Server(Target) 输入: cat /dev/ttyS0
      系统输出的信息: hello 
    • Client(Development) 输入: echo "hello" >/dev/ttuS0
    • 串口添加完成后,使用如果命令测试:
    • 在Server上cat /dev/ttyS0
    • 然后到Client上 echo "hello" > /dev/ttyS0
    • 这时回到Server上,如果能看到输入的hello,说明串口通讯正常.

     

    4:升级内核2.6.26(添加关于KGDB的选项)

          说明一下,这里是要升级Server的内核,因为kgdb是要Server上运行的,但是编译需要在Client完成(或者你也可以在Server上编译,之后再拷贝到Client上),因为调试时Client上的gdb会用到编译的内核及源代码.Client也需要升级,保证Client同Server上的内核一致,这样Client上编译的模块拿到Server上加载就不会有什么问题.

    首先下载kernel 2.6.26
          我习惯在windows上下载,然后共享,再到linux使用smbclient连接,拷贝到Linux上.你也可以直接在linux上下载. 
          smbclient用法 : smbclient //192.168.0.100/share -Uadministrator 回车后,会提示输入admin的密码.之后就可以通过get获取了 192.168.0.100是我在windows主机的IP,share为共享名,-U后面是用户名

    编译Kernel2.6.26

    • 进入Client(Development)系统,将linux-2.6.26.tar.bz2拷贝到/usr/src目录下:
    • cd /usr/src
    • tar jxvf linux-2.6.26.tar.bz2
    • ln -s linux-2.6.26 linux
    • cd linux
    • make menuconfig
      • File System --> 下面把ext3,ext2都编译进内核(就是把前面的M变成*)
      • Kernel Hacking --> 
              选中Compile the kernel with frame pointers
              选中KGDB:kernel debugging with remote gdb 
              并确认以下两项也是选中的(他们应该默认是选中的) 
              > kernel debugging 
              > Compile the kernel with debug info
      • 对于其它选项,请按实际情况,或你的要求定制.
      • 在其它网友的说明里面,会有Serial port number for KGDB等选项,但是我使用的版本未找到这些选项,所以忽略过.
      • 保存退出
    • make -j10 bzImage
      -j10表示使用10个线程进行编译.
    • make modules
      编译内核模块
    • make modules_install
      安装内核模块
    • make install 
      安装内核

     

    将Client系统中的linux-2.6.26整个目录同步到Server上.

    • 在Client系统上运行下列命令:
    • cd /usr/src/linux  
    • scp -r linux-2.6.26 root@Server(Target)IP:/usr/src/
           系统会提示输入root的密码,输入完了就会开始复制文件了,(这里要注意,如果原系统已经是2.6.26的内核,可以只拷贝arch/i386/boot/bzImage,及System.map文件到Server上,然后修改/boot/grub/grub.conf,但由于我是从2.6.9升级上来,所以需要将整个linux原代码目录拷贝到Server上进行升级)

     

    升级Srever系统的内核
    • 进入Server(Target)系统,usr/src目录:
    • ln -s linux-2.6.26 linux  
    • cd linux  
    • make modules_install
      安装内核模块
    • make install
      安装内核
    安装完成后,在/boot/目录下会有即个新添加的文件./boot/grub/grub.conf文件也会添加一个新的启动项,我的如下(行号不是文件的一部分):
     1 # grub.conf generated by anaconda
     2 #
     3 # Note that you do not have to rerun grub after making changes to this file
     4 # NOTICE:  You have a /boot partition.  This means that
     5 #          all kernel and initrd paths are relative to /boot/, eg.
     6 #          root (hd0,0)
     7 #          kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
     8 #          initrd /initrd-version.img
     9 #boot=/dev/hda
    10 default=0
    11 timeout=5
    12 splashimage=(hd0,0)/grub/splash.xpm.gz
    13 hiddenmenu
    14 title CentOS (2.6.26)
    15     root (hd0,0)
    16     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00
    17     initrd /initrd-2.6.26.img
    18 title CentOS-4 i386 (2.6.9-67.ELsmp)
    19     root (hd0,0)
    20     kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
    21     initrd /initrd-2.6.9-67.ELsmp.img
    
            注意里面的NOTICE说明,我的系统/boot是一个独立的分区,所以下面配置的文件路径都是相对于/boot/目录的,像 /vmlinuz-2.6.26,实际到它的绝对位置应该是/boot/vmlinuz-2.6.26. 在你的系统上请根据实际情况处理.  

    修改一下grub.conf,修改成下面这样:

     1 # grub.conf generated by anaconda
     2 #
     3 # Note that you do not have to rerun grub after making changes to this file
     4 # NOTICE:  You have a /boot partition.  This means that
     5 #          all kernel and initrd paths are relative to /boot/, eg.
     6 #          root (hd0,0)
     7 #          kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
     8 #          initrd /initrd-version.img
     9 #boot=/dev/hda
    10 default=0
    11 timeout=5
    12 splashimage=(hd0,0)/grub/splash.xpm.gz
    13 hiddenmenu
    14 title CentOS (2.6.26)
    15     root (hd0,0)
    16     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200
    17     initrd /initrd-2.6.26.img
    18 title CentOS (2.6.26) Wait...(kernel debug)
    19     root (hd0,0)
    20     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200 kgdbwait
    21     initrd /initrd-2.6.26.img
    22 title CentOS-4 i386 (2.6.9-67.ELsmp)
    23     root (hd0,0)
    24     kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
    25     initrd /initrd-2.6.9-67.ELsmp.img
    

    说明:

    • 第一个启动项在原来的基础上添加了kgdb的参数kgdboc=ttyS0,115200
      kgdboc 的意思是 kgdb over console,这里将kgdb连接的console设置为ttyS0,波特率为115200,如果不在内核启动项中配置该参数,可以在进入系统后执行命令: 
      echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc   
    • 第二个启动项,同第一个使用同一个内核,只是添加了kgdbwait参数
      kgdbwait 使 kernel 在启动过程中等待 gdb 的连接。

    我的启动菜单如下:
    • CentOS(2.6.26)
    • CentOS(2.6.26)Wait...(kernel debug)
    • CentOS-4 i386(2.6.9-67.ELsmp)
    调用内核模块,就选择第一个,如果要调试内核启动过程,选择第二个.

     

    5.内核调试

    重启Server,通过启动菜单第二项CentOS(2.6.26)Wait...(kernel debug)进入系统. 只到系统出现:

    	    kgdb: Registered I/O driver kgdboc
    	    kgdb: Waiting for connection from remote gdb 
     

     

    进入Client系统.

    • cd /usr/src/linux  
    • gdb vmlinux  
      启动gdb开始准备调试:输出大致如下:
      GNU gdb Red Hat Linux (6.3.0.0-1.153.el4rh)
      Copyright 2004 Free Software Foundation, Inc.
      GDB is free software, covered by the GNU General Public License, and you are
      welcome to change it and/or distribute copies of it under certain conditions.
      Type "show copying" to see the conditions.
      There is absolutely no warranty for GDB.  Type "show warranty" for details.
      This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db 
      library "/lib/tls/libthread_db.so.1" 
    • (gdb) set remotebaud 115200
    • (gdb) target remote /dev/ttyS0
      注意:有的文章讲:因为vmware的named piped不能被gdb直接使用,需要使用 socat -d -d /tmp/com_1 /dev/ttyS0转换,然后使用转换后的设备. socat需要自己下载安装. 但在我的环境中,直接使用并没有出错.
      执行该命令后输出如下:
      Remote debugging using /dev/ttyS0
      kgdb_breakpoint () at kernel/kgdb.c:1674
      1674		wmb(); /*Sync point after breakpoint */
      warning: shared library handler failed to enable breakpoint
      看到上面的内容说明已经连接成功,但Server上依然是假死状态,这时你可以像使用本地gdb一样设置断点(break),单步执行(step),或其它命令.
    • (gdb) cont
      继续执行,Server就继续下面的系统初始化了.

     

    系统启动完成后的内核调试:
        进入Server后,执行命令

    • echo g > /proc/sysrq-trigger 
      系统同样会中断,进入假死状态,等待远程gdb的连接.KGDB可能会输出如下信息:
      SysRq: GDB    
      上面的命令(echo g > /proc/sysrq-trigger)可以有一个快捷键(ALT-SysRq-G)代替,当然前提是你编译内核时需要选中相关选项,并且需要修改配置文件:/etc/sysctl.conf , 我用了一下,不太好用.因为有的桌面系统中PrintScreen/SysRq键是用于截屏的.所以还是直接用命令来的好! 
      我在~/.bashrc中添加了一句(添加完保存后,要执行source ~/.bashrc应用该配置):
      alias debug='echo g > /proc/sysrq-trigger'
      之后就可以直接输入debug来使内核进入调试状态.
    Server进入调试状态后,转换到Client系统,重复上面的步骤.

     

    6. Linux内核模块(设备驱动)的调试

    编写内核模块,及Makefile
          我使用的例子是Linux Device Driver 3中的例子scull. 你可以从这里下载LDD3的所有例子程序.

    • 进入Client系统,解压example.tar.gz,将其中的example/scull目录拷贝到/root/scull 
      然后执行:
    • cd /root/scull  
    • make  
      编译应该会出错,因为Linux Device Driver 3的例子程序是基于2.6.10内核的,请参靠下面的说明修改这些错误:
      编译scull驱动,完成后,scull目录应该多出了scull.ko及其它中间文件.
    • scp scull.ko root@targetIp:/root/
      将scull.ko模块文件拷贝到Server上.系统应该会提示输入密码:
      root@targetIp's password:
      输入正确密码后,应该能看到如下信息,表示复制成功了:
      scull.ko				100%  258k 258.0kb/s 00:00
    • 进入Server系统输入:
    • cd /root/  
    • insmod scull.ko  
      加载scull模块.
    • cat /sys/module/globalmem/sections/.text  
      显示scull模块的.text段地址.运行该命令后,会返回一个16进制的地址,如:
      0xd099a000
    • echo g > /proc/sysrq-trigger 
    现在Server系统变成等待状态.
    • 再次进入Client系统:
    • cd /usr/src/linux  
    • gdb vmlinux  
    • (gdb) set remotebaud 115200  
    • (gdb) target remote /dev/ttyS0
      Remote debugging using /dev/ttyS0
      kgdb_breakpoint () at kernel/kgdb.c:1674
      1674		wmb(); /*Sync point after breakpoint */
      warning: shared library handler failed to enable breakpoint
      出现上面的信息表示连接成功,但此时还不可以设置断点.我试了N次,现在设置端点后,无论Server上对scull做什么操作,Client上的gdb都不会停止.也有人说这是gdb的BUG,gdb无法正常解析内核模块的符号信息. 说是下载kgdb网站上的gdbmod可以解决该问题,但我试了之后发现根本没有用. 所以只能通过命令手动加载相关符号信息:
    • (gdb) add-symbol-file /root/scull/scull.ko 0xd099a000 
      注意: 0xd099a000地址是在上面获取的,命令输入完了,系统会有如下提示信息(第二行后面的y是自己输入的:
      add symbol table from file "/root/scull/scull.ko" at .text_addr = 0xd099a000
      (y or n)y
      Reading symbols from /root/scull/scull.ko...done.        
    • (gdb)break scull_write
      Breakpoint 1 at 0xd099a2d9: file /root/scull/main.c,line 338.
    • (gdb)break scull_read
      Breakpoint 2 at 0xd099a1a2: file /root/scull/main.c,line 294
    • (gdb)cont   
      Continuing
    Client上的工作暂停,回到Server系统上:
    • Server现在处于运行状态,在Server上运行下面的命令:
    • cat /proc/devices | grep "scull"
      查看scull模块分配的major.我的系统中返回如下:
      253 scull
      253 scullp
      253 sculla
    • mknod /dev/scull c 253 0
      253是刚才查询到的版本号.
    • echo "this is a test " > /dev/scull
      测试输入函数:scull_write. 该命令输入完成后,进程应该会停下来, 请切换到Client系统.
    • cat /dev/scull
      测试输出函数:scull_read. 该命令输入完成后,进程应该会停下来, 请切换到Client系统.
    • 回到Client系统,应该能看到gdb的输出信息:
      Breakpoint 1, scull_write (filp=0xce5870c0,buf=0xb7f44000 
            "this is a test/nias | /usr/bin/which --tty-only --read-alias 
            --show-dot --show-tilde'/n",count=15,f_pos=0xce5c5f9c)
        at /root/scull/main.c:338
      338   {    
      (gdb)_  
    • 现在就可以像调试本地程序一样调试scull.ko模块了.

    以同样的方法也可以调试其它函数.(初始化函数暂时没想到办法调试,因为使用这种方法需要先加载后,才可以进行调试)

     

    你可以自由使用和转载本文档,转载时请注明出处. (jie123108@163.com)

    如果可以,我想写一个脚本自动完成这个添加符号文件的过程,简化调试过程.









    1 开启虚拟机,虚拟机运行到 kgdb: Waiting for connection from remote gdb

    2. 在Host机上运行: socat tcp-listen:8888 /tmp/vbox2, 其中/tmp/vbox2为管道文件,它是目标机串口的重定向目的文件,socat将这个管道文件又重定向到tcp socket的8888端口。

    3. 开启一个新的虚拟终端,cd path/to/kernel/source/tree, 然后执行gdb ./vmlinux

    输出

    GNU gdb 6.8-debian

    Copyright (C) 2008 Free Software Foundation, Inc.

    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

    This is free software: you are free to change and redistribute it.

    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

    and "show warranty" for details.

    This GDB was configured as "i486-linux-gnu"...

    (gdb) set-remote

    set remote baud rate to 115200c/s

    set remote target to local tcp socket

    kgdb_breakpoint () at kernel/kgdb.c:1721

    1721        wmb(); /* Sync point after breakpoint */

    (gdb) c

    Continuing.

    目标机会一直启动,直到提示输入用户名密码。

    4. 进入目标机,输入用户名密码(推荐使用字符界面下的root用户),输入g命令,目标机被断下,控制移交到Host机中的gdb中。(目标机root的用户目录中的.bashrc中添加一行alias g='echo g>/proc/sysrq-trigger')

    5. 在Host机中的gdb中

    (gdb) set-mod-break

    set breakpoint in system module init function

    Breakpoint 1 at 0xc014bac5: file kernel/module.c, line 2288.

    (gdb) c

    Continuing.

    6. 在目标机中

    insmod klogger2.ko

    目标机再次断下,控制权移交Host机中的gdb

    7. 在Host机中的gdb中

    [New Thread 4693]

    [Switching to Thread 4693]

    Breakpoint 1, sys_init_module (umod=0x0, len=0, uargs=0x0)

    at kernel/module.c:2288

    2288        if (mod->init != NULL)

    (gdb) print-mod-segment

    Name:.note.gnu.build-id Address:0xdf977058

    Name:.text Address:0xdf975000

    Name:.rodata Address:0xdf977080

    Name:.rodata.str1.4 Address:0xdf9774b4

    Name:.rodata.str1.1 Address:0xdf977522

    Name:.parainstructions Address:0xdf977a00

    Name:.data Address:0xdf978440

    Name:.gnu.linkonce.this_module Address:0xdf978480

    Name:.bss Address:0xdf978a00

    Name:.symtab Address:0xdf977a08

    Name:.strtab Address:0xdf978078

    (gdb) add-symbol-file /home/done/programs/linux-kernel/vlogger/klogger2.ko 0xdf975000 -s .data 0xdf978440 -s .bss 0xdf978a00

    add symbol table from file "/home/done/programs/linux-kernel/vlogger/klogger2.ko" at

    .text_addr = 0xdf975000

    .data_addr = 0xdf978440

    .bss_addr = 0xdf978a00

    (y or n) y

    Reading symbols from /home/done/programs/linux-kernel/vlogger/klogger2.ko...done.

    (gdb) b hook_init

    Breakpoint 2 at 0xdf976d19: file /home/done/programs/linux-kernel/vlogger/hook.c, line 255.

    (gdb)

    你可以调试自己些的LKM模块了

    附gdb的初始化配置文件~/.gdbinit

    define set-remote

    echo set remote baud rate to 115200c/s\n

    set remotebaud 115200

    echo set remote target to local tcp socket\n

    target remote tcp:localhost:8888

    end

    define set-mod-break

    echo set breakpoint in system module init function\n

    break kernel/module.c:2288

    end

    define print-mod-segment

    set $sect_num=mod->sect_attrs->nsections

    set $cur=0

    while $cur < $sect_num

    printf "Name:%-s Address:0x%x\n",mod->sect_attrs->attrs[$cur]->name,mod->sect_attrs->attrs[$cur]->address

    set $cur=$cur+1

    end

    end

    后记:gdb的调试脚本真难写,简单的字符串变量连接和等价判断都显得十分困难,不知道是我水平太差还是gdb的脚本功能太弱,总之比起Windbg来说,内核调试困难程度上了个等级。



    使用kgdb调试linux内核及内核模块



    创建时间:2005-09-09
    文章属性:原创
    文章提交:xcspy (xcspy.com_at_gmail.com)

    作者:xcspy成员 ladybug
    E-mail:xcspy.com@gmail.com
    主页:www.xcspy.com

    1. 几种内核调试工具比较

    kdb:只能在汇编代码级进行调试;
         优点是不需要两台机器进行调试。

    gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。

    kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)

    使用kdb和gdb调试内核的方法相对比较简单,这里只描述如何使用kgdb来调试内核。

    2.软硬件准备

    环境:
    一台开发机developer(192.168.16.5 com1),一台测试机target(192.168.16.30 com2),都预装redhat 9;一根串口线

    下载以下软件包:
    linux内核2.4.23         linux-2.4.23.tar.bz2
    kgdb内核补丁1.9版       linux-2.4.23-kgdb-1.9.patch
    可调试内核模块的gdb     gdbmod-1.9.bz2

    3.ok,开始

    3.1 测试串口线
    物理连接好串口线后,使用一下命令进行测试,stty可以对串口参数进行设置

    在developer上执行:
    stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
    echo hello > /dev/ttyS0
    在target上执行:
    stty ispeed 115200 ospeed 115200 -F /dev/ttyS1
    cat /dev/ttyS1

    串口线没问题的话在target的屏幕上显示hello

    3.2 安装与配置

    3.2.1 安装

    下载linux-2.4.23.tar.bz2,linux-2.4.23-kgdb-1.9.patch,gdbmod-1.9.bz2到developer的/home/liangjian目录

    *在developer机器上

    #cd /home/liangjian
    #bunzip2 linux-2.4.23.tar.bz2
    #tar -xvf linux-2.4.23.tar
    #bunzip2 gdbmod-1.9.bz2
    #cp gdbmod-1.9 /usr/local/bin
    #cd linux-2.4.23
    #patch -p1 < ../linux-2.4.23-kgdb-1.9.patch
    #make menuconfig

    在Kernel hacking配置项中将以下三项编译进内核
    KGDB: Remote (serial) kernel debugging with gdb
    KGDB: Thread analysis
    KGDB: Console messages through gdb

    注意在编译内核的时候需要加上-g选项
    #make dep;make bzImage

    使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具)
    #scp arch/i386/boot/bzImage root@192.168.16.30:/boot/vmlinuz-2.4.23-kgdb
    #scp System.map root@192.168.16.30:/boot/System.map-2.4.23-kgdb
    #scp arch/i386/kernel/gdbstart  root@192.168.16.30:/sbin
    gdbstart为kgdb提供的一个工具,用于激活内核钩子,使内核处于调试状态

    3.2.2 配置

    *在developer机器上

    在内核源码目录下编辑一文件.gdbinit(该文件用以对gdb进行初始化),内容如下:
    #vi .gdbinit
    define rmt
    set remotebaud 115200
    target remote /dev/ttyS0
    end
    #
    以上在.gdbinit中定义了一个宏rmt,该宏主要是设置使用的串口号和速率

    *在target机器上

    编辑/etc/grub.conf文件,加入以下行:
    #vi /etc/grub.conf
    title Red Hat Linux (2.4.23-kgdb)
        root (hd0,0)
        kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1
    #

    在root目录下建立一个脚本文件debugkernel,内容如下:
    #vi debug
    #!/bin/bash
    gdbstart -s 115200 -t /dev/ttyS1 <<EOF

    EOF
    #chmod +x debugkernel
    这个脚本主要是调用gdbstart程序设置target机上使用的串口及其速率,并使内核处于调试状态

    3.3 开始调试

    target上的内核或内核模块处于调试状态时,可以查看其变量、设置断点、查看堆栈等,并且是源码级的调试,和用gdb调试用户程序一样

    3.3.1 内核启动后调试

    *在target机器上

    重启系统,选择以 2.4.23-kgdb内核启动,启动完成后运行debugkenel,
    这时内核将停止运行,在控制台屏幕上显示信息,并等待来自developer的
    串口连接

    #./debug
    About to activate GDB stub in the kernel on /dev/ttyS1
    Waiting for connection from remote gdb...

    *在developer机器上

    #cd /home/liangjian/linux-2.4.23
    # gdb vmlinux
    GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
    Copyright 2003 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux-gnu"...

    执行rmt宏
    (gdb) rmt
    breakpoint () at kgdbstub.c:1005
    1005                    atomic_set(&kgdb_setting_breakpoint, 0);

    这时target上的内核处于调试状态,可以查看其变量、设置断点、查看堆栈等,和用gdb调试用户程序一样

    查看堆栈
    (gdb) bt
    #0  breakpoint () at kgdbstub.c:1005
    #1  0xc0387f48 in init_task_union ()
    #2  0xc01bc867 in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc0387f98) at
    gdbserial.c:158
    #3  0xc010937b in handle_IRQ_event (irq=3, regs=0xc0387f98, action=0xce5a9860)
    at irq.c:452
    #4  0xc0109597 in do_IRQ (regs=
          {ebx = -1072671776, ecx = -1, edx = -1070047232, esi = -1070047232, edi
    = -1070047232, ebp = -1070039092, eax = 0, xds
    = -1070071784, xes = -1070071784, orig_eax = -253, eip = -1072671729, xcs =
    16, eflags = 582, esp = -1070039072, xss = -1072671582}) at irq.c:639
    #5  0xc010c0e8 in call_do_IRQ ()

    查看jiffies变量的值
    (gdb) p jiffies
    $1 = 76153

    如果想让target上的内核继续运行,执行continue命令
    (gdb) continue
    Continuing.

    3.3.2 内核在引导时调试

    kgdb可以在内核引导时就对其进行调试,但并不是所有引导过程都是可调试的,如在kgdb 1.9版中,它在init/main.c的start_kernel()函数中插入以下代码:
    start_kernel()
    {
        ......
            smp_init();
    #ifdef CONFIG_KGDB
            if (gdb_enter) {
                    gdb_hook();             /* right at boot time */
            }
    #endif
        ......
    }

    所以在smp_init()之前的初始化引导过程是不能调试的。

    另外要想让target的内核在引导时就处于调试状态,需要修改其/etc/grub.conf文件为如下形式:
    title Red Hat Linux (2.4.23-kgdb)
        root (hd0,0)
        kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1 gdb gdbttyS=1 gdbbaud=115200

    *在target机器上

    引导2.4.23-kgdb内核,内核将在短暂的运行后暂停并进入调试状态,打印如下信息:
    Waiting for connection from remote gdb...

    *在developer机器上

    #cd /home/liangjian/linux-2.4.23
    # gdb vmlinux
    GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
    Copyright 2003 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux-gnu"...

    执行rmt宏
    (gdb) rmt
    breakpoint () at kgdbstub.c:1005
    1005                    atomic_set(&kgdb_setting_breakpoint, 0);

    查看当前堆栈
    (gdb) bt
    #0  breakpoint () at kgdbstub.c:1005
    #1  0xc0387fe0 in init_task_union ()
    #2  0xc01bc984 in gdb_hook () at gdbserial.c:250
    #3  0xc0388898 in start_kernel () at init/main.c:443

    在do_basic_setup函数处设置断点,并让内核恢复运行
    (gdb) b do_basic_setup
    Breakpoint 1 at 0xc0388913: file current.h, line 9.
    (gdb) continue
    Continuing.
    [New Thread 1]
    [Switching to Thread 1]

    Breakpoint 1, do_basic_setup () at current.h:9
    9               __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));

    内核在do_basic_setup断点处停止运行后查看当前堆栈
    (gdb) bt
    #0  do_basic_setup () at current.h:9
    (gdb)

    3.3.3 内核模块调试调试

    要想调试内核模块,需要相应的gdb支持,kgdb的主页上提供了一个工具gdbmod,它修正了gdb 6.0在解析模块地址时的错误,可以用来正确的调试内核模块

    *在developer机器上

    写了个测试用的内核模块orig,如下:
    void xcspy_func()
    {
        printk("<1>xcspy_func\n");
        printk("<1>aaaaaaaaaaa\n");
    }

    int xcspy_init()
    {
        printk("<1>xcspy_init_module\n");
            
        return 0;
    }

    void xcspy_exit()
    {
        printk("<1>xcspy_cleanup_module\n");
    }

    module_init(xcspy_init);
    module_exit(xcspy_exit);

    编译该模块:
    #cd /home/liangjian/lkm
    #gcc -D__KERNEL__ -DMODULE -I/home/liangjian/linux-2.4.23/include -O -Wall -g -c -o orig.o orig.c
    #scp orig.o root@192.168.16.30:/root

    开始调试:
    # gdbmod vmlinux
    GNU gdb 6.0
    Copyright 2003 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i686-pc-linux-gnu"...

    设置符号文件的搜索路径
    (gdb) set solib-search-path /home/liangjian/lkm

    执行rmt宏
    (gdb) rmt
    breakpoint () at kgdbstub.c:1005
    1005                    atomic_set(&kgdb_setting_breakpoint, 0);

    设置断点使得可以调试内核模块的init函数,查内核源码可知,内核是通过module.c文件的第566行(sys_init_module函数中)mod->init来调用模块的init函数的
    (gdb) b module.c:566
    Breakpoint 1 at 0xc011cd83: file module.c, line 566.
    (gdb) c
    Continuing.
    [New Thread 1352]
    [Switching to Thread 1352]

    这时在target机器上执行insmod orig.o,developer则相应的在断点处被暂停,如下
                                                                                                                              
    Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
    mod_user=0x80904d8) at module.c:566
    566             if (mod->init && (error = mod->init()) != 0) {

    使用step命令进入模块的init函数
    (gdb) step
    xcspy_init () at orig.c:12
    12              printk("<1>xcspy_init_module\n");
    (gdb) n
    15      }
    (gdb)

    说明:
    调试内核模块的非init函数相对比较简单,只要先在target上执行insmod orig.o,这时由于模块的符号被加载,可以直接在developer的gdb中对想调试的模块函数设置断点,如bt xcspy_func,后面当xcspy_func被调用时就进入了调试状态。
    如果想调试内核模块的init函数,由于在执行insmod之前模块的符号还没有被加载,不能直接对模块的init函数设置断点,所以相对来说要困难一些。可以采用两种变通的方法:1,采用上面介绍的在内核调用模块的init函数被调用之前的某处插入断点,如bt sys_init_module()或bt module.c:566;2,在developer上让内核处于运行状态,在target上先执行一遍insmod orig.o,这时orig.o的符号已经被加载到内存中,可以直接在developer的gdb中对模块的init函数设置断点,如bt xcspy_init,然后在target上rmmod orig.o,当下次在target上重新加载orig.o时就进入了调试状态,developer在xcspy_init处被暂停。



    重启服务器

    1. # echo 1 > /proc/sys/kernel/sysrq  
    2. # echo b > /proc/sysrq-trigger  

    1. /proc/sys/kernel/sysrq

           向sysrq文件中写入1是为了开启SysRq功能。根据linux/Documentations/sysrq.txt中所说:SysRq代表的是Magic System Request Key。开启了这个功能以后,只要内核没有挂掉,它就会响应你要求的任何操作。但是这需要内核支持(CONFIG_MAGIC_SYSRQ选项)。向/proc/sys/kernel/sysrq中写入0是关闭sysrq功能,写入1是开启,其他选项请参考sysrq.txt。需要注意的是,/proc/sys/kernel/sysrq中的值只影响键盘的操作。

           那么怎么使用SysRq键呢?

           在x86平台上,组合键”<ALT> + SysRq + <command key>“组成SysRq键以完成各种功能。但是,在一些键盘上可能没有SysRq键。SysRq键实际上就是”Print Screen“键。并且可能有些键盘不支持同时按三个按键,所以你可以”按住ALT键“,”按一下SysRq键“,再”按一下<command key>键“,如果你运气好的话,这个会有效果的。不过放心,现在的键盘一般都支持同时按3个或3个以上的键。

           <command key>有很多,这里只挑几个来说,其他的可以参考sysrq.txt文件。

    • 'b' —— 将会立即重启系统,并且不会管你有没有数据没有写回磁盘,也不卸载磁盘,而是完完全全的立即关机
    • 'o' —— 将会关机
    • 's' —— 将会同步所有以挂在的文件系统
    • 'u' —— 将会重新将所有的文件系统挂在为只读属性

     

    2. /proc/sysrq-trigger

           从文件名字就可以看出来这两个是有关系的。写入/proc/sysrq-trigger中的字符其实就是sysrq.txt中说的<command key>键所对应的字符,其功能也和上述一样。

     

    所以,这两行命令先开启SysRq功能,然后用'b'命令让计算机立刻重启。

     

    /proc/sysrq-trigger该文件能做些什么事情呢? 

    # 立即重新启动计算机 (Reboots the kernel without first unmounting file systems or syncing disks attached to the system
    echo "b" > /proc/sysrq-trigger

    # 立即关闭计算机(shuts off the system)
    echo "o" > /proc/sysrq-trigger

    # 导出内存分配的信息 (可以用/var/log/message 查看)(Outputs memory statistics to the console) 
    echo "m" > /proc/sysrq-trigger

    # 导出当前CPU寄存器信息和标志位的信息(Outputs all flags and registers to the console
    echo "p" > /proc/sysrq-trigger

    # 导出线程状态信息 (Outputs a list of processes to the console)
    echo "t" > /proc/sysrq-trigger

    # 故意让系统崩溃 ( Crashes the system without first unmounting file systems or syncing disks attached to the system
    echo "c" > /proc/sysrq-trigger

    # 立即重新挂载所有的文件系统 (Attempts to sync disks attached to the system)
    echo "s" > /proc/sysrq-trigger

    # 立即重新挂载所有的文件系统为只读 (Attempts to unmount and remount all file systems as read-only)
    echo "u" > /proc/sysrq-trigger

    呵呵,此外还有两个,类似于强制注销的功能
    e — Kills all processes except init using SIGTERM

    i — Kills all processes except init using SIGKILL



    由于很多原因,Linus没有将调试器放在内核中,这也使得Linux内核调试变得比较困难,不过还好有类似KGDB等辅助工具,通过它们,我们也可以完成Linux内核的调试。

     

    一、环境准备

    目标机系统:Ubuntu 8.04  i386 desktop 32bit,目标机使用的是虚拟机系统

    开发机系统:Ubuntu 8.04  i386 desktop 32bit(开发机系统用什么不是很重要,不一定必须相同)

    1)  目标机环境准备

    在目标机上需要做的主要是编译并使用调试版本的内核。

    1.       编译内核,2.6.26以上的内核内置KGDB,推荐使用gconfig,并保证Kernel Hacking下的enable KGDBenable the Magic SysRq key enable Compile the kernel with debug info选中;

    2.       使用调试版本的内核,并设置为启动时等待GDB连接

    以上两点可以参考文章:《VirtualBox下Ubuntu8.10的KGDB内核调试

     

    可以将目标机的启动方式改为字符界面启动,用如下的方法:在/etc/目录下建立文件inittab,并在其中加入下面一行:

    id:3:initdefault:

    然后将/etc/rc3.d/下的S30gdm改成K30gdm

    sudo mv /etc/rc3.d/S30gdm  /etc/rc3.d/K30gdm

     

    由于使用了字符界面启动,因此Magic SysRq key将只能以命令的形式使用,Alt+G+SysRq组合键不再有效,而只能通过echo g>/proc/sysrq-trigger命令。可以在bash中设置一个alias,以免每次都要输入一长串命令。由于上述命令需要root权限,因此推荐修改root用户的bash配置文件.bashrc,在最后加入一行:

    alias g=’echo g > /proc/sysrq-trigger’

    2)  开发机环境准备

    由于目标机使用的是虚拟机,因此开发机需要做一些虚拟机相关的配置尤其是串口设置(可以参考文章:《Ubuntu下VirtualBox虚拟机串口设置》)。其次是gdb调试器的一些设置。

    将目标机中编译好的内核树copy到开发机下面(注意符号连接的指向改变),切换到内核树目录下,建立一下三个gdb脚本文件:

    set-remote文件(用于gdb连接到目标机的脚本):

    echo set remote baud rate to 115200c/s\n

    set remotebaud 115200

    echo set remote target to local tcp socket\n

    target remote tcp:localhost:8888

    set-mod-break文件(用于设置模块加载时的断点):

    echo set breakpoint in system module init function\n

    break kernel/module.c:2288

    print-mod-segment文件(用于显示所加载模块在内存中的各个段的地址):

    set $sect_num=mod->sect_attrs->nsections

    set $cur=0

    while $cur < $sect_num

        printf "Name:%-s Address:0x%x\n",mod->sect_attrs->attrs[$cur]->name,mod->sect_attrs->attrs[$cur]->address

        set $cur=$cur+1

    end

    三、调试流程

    1)  启动虚拟机中的目标机系统,系统在运行到等待gdb连接时停下;

    2)  在开发机中使用socat tcp4-listen:8888 /tmp/vbox将主机中的/tmp/vbox管道(与目标机的串口相连)与主机中端口为8888socket相连;

    3)  进入到开发机中的Linux内核树目录下,使用gdb /.vmlinuz进入调试模式;

    4)  gdb中执行source set-remote连接到目标机,并输入ccontinue)命令继续启动目标机;

    5)  目标机起动后,在目标机中输入g(echo g > /proc/sysr-trigger命令的别名)命令,断下目标机,并将控制权限转交给开发机中的gdb

    6)  进入开发机的gdb,执行命令source set-mod-break,在模块加载处设置断点,并输入c命令继续执行目标机;

    7)  在目标机中使用命令insmod xxx.ko加载LKM模块,会自动产生断点,执行再次返回到开发机中的gdb

    8)  在开发机的gdb中执行source print-mod-segment,显示刚加载的模块xxx.ko的各个段在内存中的地址,然后继续在gdb中执行命令add-symbol-file /dev-machine/path/to/xxx.ko 0xe11b7000 –s .data 0xe11b5800 –s .bss 0xe11b80500xe11b7000.text段的地址,并确定加载符号;

    9)  这样就可以在开发中的gdb中设置断点了,命令b init_module_func将在模块初始化函数入口点设置断点,设置断点后用c命令执行目标机,接着会在init_module_func处触发断点,然后可以用stepnextprintgdb命令进行单步调试和变量查看了。





    创建xen虚拟机:
    virt-install  -n bootmgr -r200 -f /root/xen/bootmgr.img -s 1 -l  http://127.0.0.1/bootmgr/tt/
     
    virt-install  -n centos -r512 -f /root/xen/centos.img -s 6 -l  http://200.1.13.6/bootmgr/tt(注意:安装的时候ip要输入和200同个网段的)
     
    3.5. 常见命令
    xm create /path/to/config_file
    xm shutdown DomainName
    xm reboot DomainName
    xm pause DomainName
    xm resume DomainName
    xm console DomainName
    更多命令请使用 xm –help查看
     
     
     
     
     
    mkdir -p /data0/software/
    cd /data0/software/
    # 32位系统 centos 5.x 
    wget http://pkgs.repoforge.org/qemu/qemu-img-0.14.1-2.el5.rfx.i386.rpm
    wget http://pkgs.repoforge.org/qemu/qemu-0.14.1-2.el5.rfx.i386.rpm
    rpm -ivh qemu-img-0.14.1-2.el5.rfx.i386.rpm
    rpm -ivh qemu-0.14.1-2.el5.rfx.i386.rpm
    # 64位系统 centos 5.x
    wget http://pkgs.repoforge.org/qemu/qemu-img-0.14.1-2.el5.rfx.x86_64.rpm
    wget http://pkgs.repoforge.org/qemu/qemu-0.14.1-2.el5.rfx.x86_64.rpm
    rpm -ivh qemu-img-0.14.1-2.el5.rfx.x86_64.rpm
    rpm -ivh qemu-0.14.1-2.el5.rfx.x86_64.rpm
    # 32位系统  centos 6.x
    wget http://pkgs.repoforge.org/qemu/qemu-0.15.0-1.el6.rfx.i686.rpm
    wget http://pkgs.repoforge.org/qemu/qemu-img-0.15.0-1.el6.rfx.i686.rpm
    rpm -ivh qemu-0.15.0-1.el6.rfx.i686.rpm
    rpm -ivh qemu-img-0.15.0-1.el6.rfx.i686.rpm
    # 64位系统  centos 6.x
    wget http://pkgs.repoforge.org/qemu/qemu-0.15.0-1.el6.rfx.x86_64.rpm
    wget http://pkgs.repoforge.org/qemu/qemu-img-0.15.0-1.el6.rfx.x86_64.rpm
    rpm -ivh qemu-0.15.0-1.el6.rfx.x86_64.rpm
    rpm -ivh qemu-img-0.15.0-1.el6.rfx.x86_64.rpm
     
     
    #安装完毕,qemu的bios的启动信息在 /usr/share/qemu下
    #创建个虚拟机目录
    mkdir /data0/software/win2003
    cd /data0/software/win2003
    #创建个10G硬盘镜像
    qemu-img create disk.10G 10G
     
    #假设win2003镜像位置在当前目录
    #启动虚拟机的命令
    #boot d 是从光驱启动 boot c 是硬盘
    qemu -L /usr/share/qemu -m 512 -hda disk.10G -localtime -boot d -cdrom ./win2003.iso  -localtime -net nic,vlan=0 -net tap,vlan=0,ifname=tap0,script=no -net user
     
     
    -boot d :从光驱引导 a(软盘引导) c(硬盘引导) d(光驱引导)
    -cdrom : ISO文件,也可以直接使用光驱设备(/dev/cdrom)...别忘了插入光盘
    -hda : 就是虚拟机里的硬盘啦,也就是刚才 qemu -img创建出的东东。
    -enable-audio : 声卡支持
    其它参数:
    -full-screen :Start in full screen.
    -usb: Enable the USB driver (will be the default soon)
    -kernel bzImage:Use bzImage as kernel image.
    -append cmdline:Use cmdline as kernel command line
    -initrd file:Use file as initial ram disk.
     
     

    QEMU 提供两种模拟模式。第一种,系统模拟,安装完全的虚拟机。运行在该系统的软件看到的计算机与主机系统完全不同 — 例如,可以在实际的 x86-64 openSUSE 计算机上运行 PowerPC Debian 系统。用户模式模拟没有这么完整。这种方式下,QEMU 模拟库会用于每个二进制文件,他们将主计算机看成自己的,因此 PowerPC 二进制文件能看到主机 x86-64 openSUSE 系统的 /etc 目录和其他配置文件。用户模式模拟能简化对本地资源、网络等的访问。

    每个模拟方式都有其安装要求。对于系统模拟,安装客户操作系统就像安装一台单独的计算机(下载并使用预先配置的磁盘镜像是一个替代方法)。对于用户空间模拟,不需要安装整个系统,但要考虑安装使用软件所需的支持库。也许还需要配置跨编译器以生成想要测试的二进制文件。

     

     

    # 按 ctrl + alt 释放虚拟机中的鼠标
     
    #网络桥虚拟网卡:
    yum install tunctl
    yum install bridge-utils
     
    #rc.local启动脚本
    iptables -t nat -A POSTROUTING -o eth0 -s 10.0.67.0/24 -j MASQUERADE
    tunctl -t tap0
    ifconfig tap0 10.0.67.1 netmask 255.255.255.0
    #虚拟机里网关设置成 10.0.67.1 即可联网
     
    ___________________________________________________________
    #以下为有问题后检查用
    echo 1>/proc/sys/net/ipv4/ip_forward
    查看 tun模块
    lsmod | grep tun
    modprobe tun
     
     
    本地虚拟机启动方法:
    .\qemu-system-i386.exe -L .\Bios -m 512 -hda .\bootmgr-s001.vmdk -localtime -boot d
     
     

     

     

    17.3. 使​用​ qemu-img

    qemu-img 命​令​行​工​具​是​ Xen 和​ KVM 用​来​格​式​化​各​种​文​件​系​统​的​。​可​使​用​ qemu-img 格​式​化​虚​拟​客​户​端​映​像​、​附​加​存​储​设​备​以​及​网​络​存​储​。​qemu-img 选​项​及​用​法​如​下​。​

    格​式​化​并​创​建​新​映​像​或​者​设​备​

    创​建​新​磁​盘​映​像​文​件​名​为​ size,格​式​为​ format。​

     

    # qemu-img create [-6] [-e] [-b base_image] [-f format] filename [size]

     

    If base_image is specified, then the image will record only the differences from base_image. No size needs to be specified in this case. base_image will never be modified unless you use the "commit" monitor command.

    将​现​有​映​像​转​换​成​另​一​种​格​式​

    转​换​选​项​是​将​可​识​别​格​式​转​换​为​另​一​个​映​像​格​式​。​

    命​令​格​式​:

     

    # qemu-img convert [-c] [-e] [-f format] filename [-O output_format] output_filename

     

    convert the disk image filename to disk image output_filename using format output_format. it can be optionally encrypted ("-e" option) or compressed ("-c" option).

    only the format "qcow" supports encryption or compression. the compression is read-only. it means that if a compressed sector is rewritten, then it is rewritten as uncompressed data.

    加​密​法​是​使​用​非​常​安​全​的​ 128 位​密​钥​的​ AES 格​式​。​使​用​长​密​码​(16 个​字​符​以​上​)获​得​最​大​程​度​的​保​护​。​

    当​使​用​可​增​大​的​映​像​格​式​,比​如​ qcow 或​ cow 时​,映​像​转​换​可​帮​助​您​获​得​较​小​的​映​像​。​在​目​的​映​像​中​可​检​测​并​压​缩​空​白​字​段​。​

    获​得​映​像​信​息​

    info 参​数​显​示​磁​盘​映​像​信​息​。​info 选​项​的​格​式​如​下​:

     

    # qemu-img info [-f format] filename

     

     

    给​出​磁​盘​映​像​文​件​名​信​息​。​使​用​它​可​获​得​在​磁​盘​中​保​留​空​间​大​小​,它​可​能​与​显​示​的​大​小​有​所​不​同​。​如​果​在​磁​盘​映​像​中​保​存​有​ vm 快​照​,则​此​时​也​会​显​示​。​

    支​持​格​式​

    映​像​格​式​通​常​是​自​动​获​取​的​。​支​持​以​下​格​式​:

     

    raw

        Raw 磁​盘​映​像​格​式​(默​认​)。​这​个​格​式​的​优​点​是​可​以​简​单​、​容​易​地​导​出​到​其​它​模​拟​器​中​。​如​果​您​的​文​件​系​统​支​持​中​断​(例​如​在​ Linux 中​的​ ext2 或​者​ ext3 以​及​ Windows 中​的​ NTFS),那​么​只​有​写​入​的​字​段​会​占​用​空​间​。​使​用​ qemu-img info 了​解​ Unix/Linux 中​映​像​或​者​ ls -ls 使​用​的​实​际​大​小​。​ 

    qcow2

        QEMU 映​像​格​式​,最​万​能​的​格​式​。​使​用​它​可​获​得​较​小​映​像​(如​果​您​的​系​统​不​支​持​中​断​,例​如​在​ Windows 中​,它​会​很​有​用​)、​额​外​的​ AES 加​密​法​、​zlib 压​缩​以​及​对​多​ VM 快​照​的​支​持​。​ 

    qcow

        旧​的​ QEMU 映​像​格​式​。​只​用​于​与​旧​版​本​兼​容​。​ 

    cow

        写​入​映​像​格​式​的​用​户​模​式​ Linux 副​本​。​包​含​ cow 格​式​的​目​的​只​是​为​了​与​前​面​的​版​本​兼​容​。​它​无​法​在​ Windows 中​使​用​。​ 

    vmdk

        VMware 3 和​ 4 兼​容​映​像​格​式​。​ 

    cloop

        Linux 压​缩​回​送​映​像​,只​有​在​重​复​使​用​直​接​压​缩​的​ CD-ROM 映​像​时​有​用​,比​如​在​ Knoppix CD-ROM 中​。​ 

     

    例如:

    将img文件转化为qcow2_cow文件形式:

     

    qemu-img convert root.img -O qcow2 kvm-centos-5.4-64-weibo_duilie_php5.2.14.qcow2_cow

     

    反之一样。

     
     
     
     
     
     
        按需支付是云计算服务众所周知的优势之一。
        以EC2为例,它所提供的三种服务方式中,On—Demond  instance 提供以Gb/小时为颗粒度的计费单位,无须预付费,也无需承诺试用时长,并可以通过Auto Scaling功能自动增删所租用的虚拟资源,做到了按需支付,我们目前所宣传的也基本上指的是这种模式。
        另外一种Reserved  Instance 收费方式与On-demond  instance 本质一样,只不过Reserved Instance 需要事先承诺合同时长,如一年或三年,并需要交纳一定的一次性费用,但是此后在实际使用中仍然按照小时计费,而且单价要比On-demond  instance 平均降低50%。在服务等级上,Reserved  instance 也要高于On-demond  instance ,亚马逊保证Reserved  instance 用户随时可以获得其所需要的服务资源,而On-demond  instance 则没有这方面的保证,当然,这种情况也很少出现。
        最后,说说其第三种 Spot   Instance ,也是最有意思的一种。在这种服务中,用户可以自己定价,定下用户愿意接受的最高价格,来租用EC2服务的闲散资源。亚马逊根据供需情况会周期性的发布即时价格,当用户最高限价高于其即时价格时,服务进行,且实际支付价格为系统即时价格。当用户最高限价低于即时价格时,系统自动终止服务,待即时价格低于用户最高限价时服务再次启动。这对于用户的预算是一个更灵活的保证方式。这种模式更适合于需要大量计算能力但对计算响应要求不高的用户,如科学计算等。当然,用户需要自行保证使用 Spot   instance 的应用对于随时宕机具有调整能力
     
     
     
     
     
     
     
    使用QEMU+GDB能够实现源代码级的内核调试,但是,存在一个问题——当内核允许中断时,单步命令(n与s)会进入时钟中断。通过浏览QEMU的源代码,大体把原因找了出来。 单步命令(n与s)在gdb远程调试通讯协议中是s(参看info gdb),qemu的gdb stub在受到s命令后将虚拟CPU进入单步状态,但是在接收到s命令前,qemu的虚拟CPU是停止的(在等待gdb的命令),注意,这个时
    候,虚拟时钟计时并没有停止,所以,很可能在qemu的虚拟CPU还没开始的时候就需要触发时钟中断了,但是虚拟CPU还在停止状态,中断无法触发。接收到s命令后,虚拟CPU开始执行指令。这时,如
    果内核允许中断,虚拟时钟就将触发中断,所以s命令执行一条指令后停止在时钟中断处理程序的开始处,而不是希望的函数中下一条指令处。

      现在看一下问题的解决方法。在我看来,需要修改gdb远程调试内核时单步命令的语义。有两个方向。

      1.在gdb上修改。在处理用户的n与s命令时不是发送协议中的s命令,而是分两步。首先确定下一条指令的开始位置(或者下一行源程序对应的指令的开始位置)。对于有些RISC机器机器指令固定为某个长度,那么确定这个位置比较简单,但是对于像x86这样的变长指令的体系结构就需要稍微麻烦一点(需要确定当前指令的长度等)。然后假如第一步确定的地址是naddr。现在像处理用户的tbreak *naddr一样处理就可以了,接着发送继续运行命令c就可以了。

      2.在qemu的gdb stub上修改协议命令s的处理方法。接收到s命令后不是让虚拟CPU进入单步执行状态,而是确定在没有中断的情况下,下一条指令的位置(注意对于当前是跳转指令的情况处理比较复杂),然后在这个位置设置临时断点,在虚拟CPU到达这个断点进入gdb stub后立即将其取消。

      这两种处理方法中,我认为1比较好,实现起来清晰明了,但是需要对gdb的代码比较熟悉。2方法比较复杂,尤其是在当前指令是跳转指令时,不太容易确定临时断点的位置。

      另外作为暂时的权宜之计,我们可以只使用tbreak +offset来代替n与s命令。





    1:前言:
        最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核及内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁 安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只需要重新编译2.6.26(或更高)内核即可.kgdb安装及模块调试过程也遇到不少问题,网上网下不断的搜索与 探索,才算调通.现在记录下来,供朋友们参考.
        首先说一下,开始本打算安装kdb进行内核调试,后来听说kdb只能进行汇编级别的调试,所以放弃,改用kgdb.
    2: 系统环境:
    虚拟环境:   VMWare Workstation 5.5(英文版)
    操作系统:   CentOS-4.6-i386(原内核2.6.9,将会把内核升级至2.6.26)
    注:CentOS 是RedHat的一个社区版本.
         (由于我们采用的linux kernel 2.6.26已经集成kgdb,kgdb再不需要单独下载)
    3:系统的安装:
    在VMWare中新建一台计算机:
    点击 Next 
    选中Custom 点击 Next 
    选中 New-Workstation 5,点击Next 
    选中 Linux ,Version选中Other Linux 2.6.x kernel 点击Next 
    Virtual machine name 输入Client(Development) 点击Next 
    Processors 选中 One, 点击Next 
    Memory 输入256,点击Next 
    Network connection 选中Use network address translation(NAT) (选第一个貌似也可以) 点击Next 
    I/O adapter types 
    SCSI Adapters 选中默认的LSI Logic(这里如果你后面使用了SCSI格式的Disk,编译内核时需要添加相应的驱动,我选择的是IDE的硬盘,kernel默认就支持了) 
    Disk 选中Create a new virtual disk 点击Next 
    Virtual Disk Type 选中IDE,点击Next 
    Disk capacity Disk size 输入80G (下面的Allocate all disk space now不要选中,表示在真正使用才分配磁盘空间, Split disk into 2 GB files项,可不选,如果你的系统分区为fat32格式,最好选中) 点击Next. 
    Disk file ,输入Disk的名称,如:disk1.vmdk ,点击Finish.完成 
    安装CentOS 4.6(过程略过)
    安装完成后,关闭计 算机,然后Clone一台同样的计算机.步骤如下:
    点击VM->Clone 
    选中默认的From current state,点击Next 
    选中Create a full clone, 点击Next 
    Virtual Machine name 输入Server(Targe),将克隆的机器命令为目标机. 
    说 明一下,kgdb 需要两台计算机通过串口进行远程调试,两台计算机分别为: 
    Client(Development):开发机,也称客户机,将 在该计算机上进行程序的开发,GDB将在本计算机上运行.用于输入命令控制Server(target)的运行. 
    Server(Target): 目标机,也称服务器,就是被调试的计算机,在Development机上开发好的内核模块程序将拷贝到Target上运行,其运行受到 Development命令的控制. 
    分别为两个系统增加一个串口,以"Output to named pipe"方式,其中:
    Client端选择"this end is the client", "the other end is a virtual machine" 
    Server端选择"this end is the server", "the other end is a virtual machine" 
    备注: 两个pipe的名称要相同,并且选中下面的Connect at power on,及Advanced里面的Yield CPU on poll 
    以后的部分,Server上的操作与Client上的操作将以不同的背景色 显示,输入的命令将以不同的字体颜色并带下划线显示.请注意: 
    Server(Target) 输入: cat /dev/ttyS0
    系统输出的信息: hello Client(Development) 输入: echo "hello" >/dev/ttuS0 
    串 口添加完成后,使用如果命令测试: 
    在Server上cat /dev/ttyS0 
    然后到Client上 echo "hello" > /dev/ttyS0 
    这时回到Server上,如果能看到输入的hello,说明串口通讯正常. 
    4:升级内核 2.6.26(添加关于KGDB的选项)
          说明一下,这里是要升级Server的内核,因为kgdb是要Server上运行的,但是编译需要在Client完成(或者你也可以在Server上编 译,之后再拷贝到Client上),因为调试时Client上的gdb会用到编译的内核及源代码.Client也需要升级,保证Client同 Server上的内核一致,这样Client上编译的模块拿到Server上加载就不会有什么问题. 
    首先下载kernel 2.6.26
          我习惯在windows上下载,然后共享,再到linux使用smbclient连接,拷贝到Linux上.你也可以直接在linux上下载. 
          smbclient用法 : smbclient //192.168.0.100/share -Uadministrator 回车后,会提示输入admin的密码.之后就可以通过get获取了 192.168.0.100是我在windows主机的IP,share为共享名,-U后面是用户名
    编译Kernel2.6.26 
    进入Client(Development)系统,将linux-2.6.26.tar.bz2拷贝到/usr/src目录下: 
    cd /usr/src 
    tar jxvf linux-2.6.26.tar.bz2 
    ln -s linux-2.6.26 linux 
    cd linux 
    make menuconfig 
    File System --> 下面把ext3,ext2都编译进内核(就是把前面的M变成*) 
    Kernel Hacking --> 
          选中Compile the kernel with frame pointers
          选中KGDB:kernel debugging with remote gdb 
          并确认以下两项也是选中的(他们应该默认是选中的) 
          > kernel debugging 
          > Compile the kernel with debug info 
    对 于其它选项,请按实际情况,或你的要求定制. 
    在其它网友的说明里面,会有Serial port number for KGDB等选项,但是我使用的版本未找到这些选项,所以忽略过. 
    保存退出 
    make -j10 bzImage
    -j10表示使 用10个线程进行编译. 
    make modules
    编译内核模块 
    make modules_install
    安装内核模 块 
    make install 
    安装内核 
    将Client系统中的linux-2.6.26整个目录同步到Server上. 
    在Client系统上运行下列命令: 
    cd /usr/src/linux   
    scp -r linux-2.6.26 root@Server(Target)IP:/usr/src/
         系统会提示输入root的密码,输入完了就会开始复制文件了,(这里要注意,如果原系统已经是2.6.26的内核,可以只拷贝arch/i386 /boot/bzImage,及System.map文件到Server上,然后修改/boot/grub/grub.conf,但由于我是从2.6.9 升级上来,所以需要将整个linux原代码目录拷贝到Server上进行升级) 
    升级Srever系统的内核 
    进入 Server(Target)系统,usr/src目录: 
    ln -s linux-2.6.26 linux   
    cd linux   
    make modules_install
    安装内核模块 
    make install
    安装内核 
    安 装完成后,在/boot/目录下会有即个新添加的文件./boot/grub/grub.conf文件也会添加一个新的启动项,我的如下(行号不是文件的 一部分):
     1 # grub.conf generated by anaconda
     2 #
     3 # Note that you do not have to rerun grub after making changes to this file
     4 # NOTICE:  You have a /boot partition.  This means that
     5 #          all kernel and initrd paths are relative to /boot/, eg.
     6 #          root (hd0,0)
     7 #          kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
     8 #          initrd /initrd-version.img
     9 #boot=/dev/hda
    10 default=0
    11 timeout=5
    12 splashimage=(hd0,0)/grub/splash.xpm.gz
    13 hiddenmenu
    14 title CentOS (2.6.26)
    15     root (hd0,0)
    16     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00
    17     initrd /initrd-2.6.26.img
    18 title CentOS-4 i386 (2.6.9-67.ELsmp)
    19     root (hd0,0)
    20     kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
    21     initrd /initrd-2.6.9-67.ELsmp.img
     
           注意里面的NOTICE说明,我的系统/boot是一个独立的分区,所以下面配置的文件路径都是相对于/boot/目录的,像 /vmlinuz-2.6.26,实际到它的绝对位置应该是/boot/vmlinuz-2.6.26. 在你的系统上请根据实际情况处理. 
    修改一下grub.conf,修改成下面这样:
     1 # grub.conf generated by anaconda
     2 #
     3 # Note that you do not have to rerun grub after making changes to this file
     4 # NOTICE:  You have a /boot partition.  This means that
     5 #          all kernel and initrd paths are relative to /boot/, eg.
     6 #          root (hd0,0)
     7 #          kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
     8 #          initrd /initrd-version.img
     9 #boot=/dev/hda
    10 default=0
    11 timeout=5
    12 splashimage=(hd0,0)/grub/splash.xpm.gz
    13 hiddenmenu
    14 title CentOS (2.6.26)
    15     root (hd0,0)
    16     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200
    17     initrd /initrd-2.6.26.img
    18 title CentOS (2.6.26) Wait...(kernel debug)
    19     root (hd0,0)
    20     kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200 kgdbwait
    21     initrd /initrd-2.6.26.img
    22 title CentOS-4 i386 (2.6.9-67.ELsmp)
    23     root (hd0,0)
    24     kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
    25     initrd /initrd-2.6.9-67.ELsmp.img
     
    说明: 
    第一个启动项在原来的基础上添加了kgdb的参数kgdboc=ttyS0,115200
    kgdboc 的意思是 kgdb over console,这里将kgdb连接的console设置为ttyS0,波特率为115200,如果不在内核启动项中配置该参数,可以在进入系统后执行命 令: 
    echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc    
    第二个启动 项,同第一个使用同一个内核,只是添加了kgdbwait参数
    kgdbwait 使 kernel 在启动过程中等待 gdb 的连接。 
    我的启动菜单如下: 
    CentOS(2.6.26) 
    CentOS(2.6.26)Wait...(kernel debug) 
    CentOS-4 i386(2.6.9-67.ELsmp) 
    调用内核模块,就选择第一个,如果要调试内核启动过程,选择第二个. 
    5.内核调试
    重 启Server,通过启动菜单第二项CentOS(2.6.26)Wait...(kernel debug)进入系统. 只到系统出现:
         kgdb: Registered I/O driver kgdboc
         kgdb: Waiting for connection from remote gdb   
    进入Client系统.
    cd /usr/src/linux   
    gdb vmlinux  
    启动gdb开始准备调试:输出大致如下:
    GNU gdb Red Hat Linux (6.3.0.0-1.153.el4rh)
    Copyright 2004 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db 
    library "/lib/tls/libthread_db.so.1" (gdb) set remotebaud 115200 
    (gdb) target remote /dev/ttyS0
    注意:有的文章讲:因为vmware的named piped不能被gdb直接使用,需要使用 socat -d -d /tmp/com_1 /dev/ttyS0转换,然后使用转换后的设备. socat需要自己下载安装. 但在我的环境中,直接使用并没有出错.
    执行该命令后输出如下: 
    Remote debugging using /dev/ttyS0
    kgdb_breakpoint () at kernel/kgdb.c:1674
    1674  wmb(); /*Sync point after breakpoint */
    warning: shared library handler failed to enable breakpoint看到上面的内容说明已经连接成功,但Server上依然是假死状态,这时你可以像使用本地gdb一样设置断点(break),单步执 行(step),或其它命令. 
    (gdb) cont
    继续执行,Server就继续下面的系统初始化了. 
    系统启动完成后的内核 调试:
        进入Server后,执行命令 
    echo g > /proc/sysrq-trigger 
    系统同样会中断,进入假死状态,等待远程gdb的连接.KGDB可能会 输出如下信息: 
    SysRq: GDB    上面的命令(echo g > /proc/sysrq-trigger)可以有一个快捷键(ALT-SysRq-G)代替,当然前提是你编译内核时需要选中相关选项,并且需要修改配置 文件:/etc/sysctl.conf , 我用了一下,不太好用.因为有的桌面系统中PrintScreen/SysRq键是用于截屏的.所以还是直接用命令来的好! 
    我 在~/.bashrc中添加了一句(添加完保存后,要执行source ~/.bashrc应用该配置):
    alias debug='echo g > /proc/sysrq-trigger'
    之后就可以直接输入debug来使内核进入调试状态. 
    Server进入调试状态 后,转换到Client系统,重复上面的步骤. 
    6. Linux内核模块(设备驱动)的调试
    编写内核模块,及Makefile
          我使用的例子是Linux Device Driver 3中的例子scull. 你可以从这里下载LDD3的所有例子程序.
    进入Client系统,解压example.tar.gz,将其中的example/scull目录拷贝到/root/scull 
    然后执 行: 
    cd /root/scull   
    make  
    编译应该会出错,因为Linux Device Driver 3的例子程序是基于2.6.10内核的,请参靠下面的说明修改这些错误:
    编译scull驱动,完成后,scull目录应该多出了scull.ko 及其它中间文件. 
    scp scull.ko root@targetIp:/root/
    将 scull.ko模块文件拷贝到Server上.系统应该会提示输入密码:
    root@targetIp's password:输入正确密码后,应该能看到如下信息,表示复制成功了: 
    scull.ko    100%  258k 258.0kb/s 00:00进入Server系统输入: 
    cd /root/   
    insmod scull.ko  
    加载scull模块. 
    cat /sys/module/globalmem/sections/.text  
    显示scull模块的.text段地址.运行该命令后,会返回 一个16进制的地址,如: 
    0xd099a000echo g > /proc/sysrq-trigger  
    现在Server 系统变成等待状态. 
    再次进入Client系统: 
    cd /usr/src/linux   
    gdb vmlinux   
    (gdb) set remotebaud 115200   
    (gdb) target remote /dev/ttyS0 
    Remote debugging using /dev/ttyS0
    kgdb_breakpoint () at kernel/kgdb.c:1674
    1674  wmb(); /*Sync point after breakpoint */
    warning: shared library handler failed to enable breakpoint出现上面的信息表示连接成功,但此时还不可以设置断点.我试了N次,现在设置端点后,无论Server上对scull做什么操 作,Client上的gdb都不会停止.也有人说这是gdb的BUG,gdb无法正常解析内核模块的符号信息. 说是下载kgdb网站上的gdbmod可以解决该问题,但我试了之后发现根本没有用. 所以只能通过命令手动加载相关符号信息: 
    (gdb) add-symbol-file /root/scull/scull.ko 0xd099a000 
    注意: 0xd099a000地址是在上面获取的,命令输入完了,系统会有如下提示信息(第二行后面的y是自己输入的: 
    add symbol table from file "/root/scull/scull.ko" at .text_addr = 0xd099a000
    (y or n)y
    Reading symbols from /root/scull/scull.ko...done.        (gdb)break scull_write
    Breakpoint 1 at 0xd099a2d9: file /root/scull/main.c,line 338.(gdb)break scull_read
    Breakpoint 2 at 0xd099a1a2: file /root/scull/main.c,line 294(gdb)cont   
    ContinuingClient上的工作暂停,回到Server系统上: 
    Server现在处于运行状态,在Server上运行下面的命 令: 
    cat /proc/devices | grep "scull"
    查看scull模块分配的major.我的系统中返回如下: 
    253 scull
    253 scullp
    253 scullamknod /dev/scull c 253 0
    253是刚才查询到的 版本号. 
    echo "this is a test " > /dev/scull
    测试输入函数:scull_write. 该命令输入完成后,进程应该会停下来, 请切换到Client系统. 
    cat /dev/scull
    测试输出函 数:scull_read. 该命令输入完成后,进程应该会停下来, 请切换到Client系统. 
    回到Client系统,应该能看到gdb的输 出信息:
    Breakpoint 1, scull_write (filp=0xce5870c0,buf=0xb7f44000 
          "this is a test\nias | /usr/bin/which --tty-only --read-alias 
          --show-dot --show-tilde'\n",count=15,f_pos=0xce5c5f9c)
      at /root/scull/main.c:338
    338   {    
    (gdb)_  现在就可以像调试本地程序一样调试scull.ko模块了. 
    以同样的方法也可以调试其它函数.(初始化函数暂时没想到办法调试,因为使用这种方 法需要先加载后,才可以进行调试) 
    你可以自由使用和转载本文档,转载时请注明出处. (jie123108@163.com)
    如果可以,我想写一个脚本自动完成这个添加符号文件的过程,简化调试过程.
     
    Linux Device Driver 3rd中的scull 例程在2.6.26上编译出错的问题
    1。scripts/Makefile.build:46: *** CFLAGS was changed in "examples/scull/Makefile". Fix it to use EXTRA_CFLAGS。 停止。
        解决方法:将 Makefile 中的 CFLAGS 改为 EXTRA_CFLAGS 
    2. examples/scull/main.c:17:26: error: linux/config.h: 没有该文件或目录
        解决方法: 将 main.c 中的这条 include 语句注释掉。 
    3. examples/scull/access.c: 在函数‘scull_u_open’中:    examples/scull/access.c:107: 错误: 提领指向不完全类型的指针
        解决方法:access.c 中添加:#include <linux/sched.h> 
    4. examples/scull/access.c: 在函数‘scull_access_setup’中:
        examples/scull/access.c:355: 警告: 格式字符串不是一个字面字符串而且没有待格式化的实参
        解决方法:将  kobject_set_name(&dev->cdev.kobj,  devinfo->name); 改为:
                       kobject_set_name(&dev->cdev.kobj, "%s", devinfo->name);
    因为 kobject_set_name 有一个像 printf 一样的参数表。 
    补充 : 老外作的改动http://www.cs.fsu.edu/~baker/devices/lxr/source/2.6.25/ldd-examples/基 本上已经可以编译了 
    摘录自: <<Linux Device Driver 3 中的代码在 2.6.27 中编译不能通过的问题>> 
    参考资料:
            <<Using 2.6.26 Linux Kernel Debugger (KGDB) with VM>> 
            <<VMware环境下用kgdb调试内核>> 
            <<Using 2.6.26 Linux Kernel Debugger (KGDB) with VM>>
     
     
     
     
     
    这是我写的一些关于kgdb在2.6.27里面的源代码分析,欢迎指教。 
    KGDB 
    0. 概述 
    前段时间用kgdb调了一下内核,感觉这个东西还不错,不过更令人感兴趣的是它的工作原理. 内核运行得好好的,那么多线程在好几个cpu上面跑,就是因为踩到一个断点,它就把所有东西都放下来让gdb来调,kgdb是怎样办到的? 
    本文是本人看kgdb的代码的一些记录,如有错误的地方敬请指出. 
    一般软件调试的原理是,调试器根据目标文件的调试信息找到源码和机器码之间的映射关系,并把它关心的指令替换成一条断点指令,x86对应的就是asm("int $3"),它的二进制代码是0xcc.当程序执行到这个地方时断点指令被执行,程序被中断,调试器接管它的控制权,这时可以查看内存信息甚至修改内存.当调试器完成任务后把断点指令替换回原来的指令,并把pc寄存器减一,让cpu从被中断的那条指令开始执行.这样程序可以在没有影响执行的情况下被调试. 
    调试内核也离不开一般原理,只不过内核不同一般程序,不能单靠同一台机的gdb来完成.要依靠kgdb,它是内核里面的一小段程序,它可以和另外一台机器上的gdb进行通信,可以执行gdb过来的一些命令并返回信息,完成调试过程. 
    关于kgdb的配置,网上有很多,而且非常详细,这里就不重复了. 
    本文的内容: 
    1.从内核是怎样把控制权交给kgdb的开始,到 
    2.kgdb的入口函数,看kgdb怎样处理竞态,再了解 
    3.kgdb和gdb之间的通信协议,接着是 
    4.kgdb对具体命令的执行,最后再看看 
    5.kgdb是在什么时候被启动的. 
    内核版本是2.6.27. 
    >>>>>>>>>1. 异步通知>>>>>>>>> 
    当内核踩到一个断点时,当前进程是用什么方式通知kgdb,控制权又是怎样到kgdb手上的呢? 
    先回顾一下kgdb的patch在2.4上是怎样让内核通知kgdb的处理代码的,下面是2.4.23的内核打上kgdb补丁的部分代码: 
    (arch/i386/kernel/traps.c) 
    #ifdef CONFIG_KGDB 
    #define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) \ 
    { \ 
    if (linux_debug_hook != (gdb_debug_hook *) NULL && !user_mode(regs)) \ 
    { \ 
    (*linux_debug_hook)(trapnr, signr, error_code, regs) ; \ 
    after; \ 
    } \ 
    #else 
    #define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) 
    #endif 
    #define DO_VM86_ERROR(trapnr, signr, str, name) \ 
    asmlinkage void do_##name(struct pt_regs * regs, long error_code) \ 
    { \ 
    CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,goto skip_trap)\ 
    do_trap(trapnr, signr, str, 1, regs, error_code, NULL); \ 
    skip_trap: \ 
    return; \ 
    DO_VM86_ERROR这个宏是用来生产异常处理函数的。比如你的name参数是int3那就会出来一个do_int3()的函数,用它来处理3号异常,也就是断点异常.而linux_debug_hook是2.4里面指向kgdb主处理函数的函数指针。 
    从上面代码可以看到,在2.4里面kgdb把它的主处理函数直接插入到异常处理函数的开头,当异常发生时kgdb就直接得到控制权. 
    说实话,这种做法比较难看.而在2.6里面,做法其实差不多,但我们有了notifier chian这种异步通知机制.什么是notifier chain?它就是一条回调函数的链表,每一条链表代表一种事件,每个关心这个事件的模块或子系统就在这条特定链上面注册自己的回调函数.代码中notifier_block结构体代表链表里面的一个节点,包含一个函数指针和一个整形变量(描述优先级).当事件发生时对应的函数链上的函数就会按优先级被执行. 
    在内核中根据不同的事件定义了一些不同的链,比如:die_chain,reboot_notifier_list,netlink_chain...等等,更加具体的描述可以在内核Documentation目录中或网上找到.而它们的实现都是对notifier chain进行封装,notifier chain的具体实现请见kernel/notifier.c. 
    die_chain这条链关心的是中断和异常事件,kgdb在它上面注册了自己的函数. 
    (arch/x86/kernel/kgdb.c) 
    515 static struct notifier_block kgdb_notifier = { 
    516 .notifier_call = kgdb_notify, 
    517 
    518 /* 
    519 * Lowest-prio notifier priority, we want to be notified last: 
    520 */ 
    521 .priority = -INT_MAX, 
    522 }; 
    kgdb很谦虚,把自己的优先级设为最低,先让别人执行,最后才考虑自己. 
    异常发生时die_chain上的函数都会被触发.对于kgdb,最关心的当然是3号异常,它就是程序踩到一个断点时引发的异常.函数notify_die()就是遍历die_chain链,执行所有上面的函数: 
    (arch/x86/kernel/traps_32.c) 
    851 void __kprobes do_int3(struct pt_regs *regs, long error_code) 
    852 { 
    853 trace_hardirqs_fixup(); 
    854 
    855 if (notify_die(DIE_INT3, "int3", regs, error_code, 3, SIGTRAP) 
    856 == NOTIFY_STOP) 
    857 return; 
    858 /* 
    859 * This is an interrupt gate, because kprobes wants interrupts 
    860 * disabled. Normal trap handlers don't. 
    861 */ 
    862 restore_interrupts(regs); 
    863 
    864 do_trap(3, SIGTRAP, "int3", 1, regs, error_code, NULL); 
    865 } 
    kgdb的回调函数kgdb_notify()(定义在arch/x86/kernel/kgdb.c)最后被调用,而它又直接调用__kgdb_notify(),这个函数对某些特殊情况做了处理,我们后面在遇到这些情况时会回到这个地方进行说明,在大多数情况下程序会直接进入kgdb的入口函数kgdb_handle_exception(),它定义在kernel/kgdb.c. 
    int3中断 
    do_int3() 
    '->notify_die() 
    |-> .. //其他关心异常的函数们 
    |-> .. 
    |-> .. 
    '-> kgdb_notify() 
    '->__kgdb_notify() //关中断 
    '->kgdb_handle_exception() //kgdb入口 
    所以kgdb是通过异常触发的,而且整个过程都处在异常处理中.当前进程被异常中断,而异常处理通过notifier chain把控制权交给了kgdb。 
    >>>>>>>>>2. 准备工作>>>>>>>>> 
    下面来看kgdb的主函数kgdb_handle_exception(),这个函数前面一段只是在做准备工作,为后面和远程的gdb通信打下基础。 
    在这个地方又有一个问题:在当今多线程和多cpu的情况下,到处都充满了竞态,那个线程都怕被别人搞破坏,那kgdb又是怎样让自己立于不败之地,控制好cpu们的? 
    (kernel/kgdb.c) 
    1392 int 
    1393 kgdb_handle_exception(int evector, int signo, int ecode, struct pt_regs *regs) 
    1394 { 
    1395 struct kgdb_state kgdb_var; 
    1396 struct kgdb_state *ks = &kgdb_var; 
    1397 unsigned long flags; 
    1398 int error = 0; 
    1399 int i, cpu; 
    1400 
    1401 ks->cpu = raw_smp_processor_id(); 
    1402 ks->ex_vector = evector; 
    1403 ks->signo = signo; 
    1404 ks->ex_vector = evector; 
    1405 ks->err_code = ecode; 
    1406 ks->kgdb_usethreadid = 0; 
    1407 ks->linux_regs = regs; 
    1408 
    1409 if (kgdb_reenter_check(ks)) 
    1410 return 0; /* Ouch, double exception ! */ 
    参数evector是中断向量,当这个异常发生时signo是发送给当前进程的信号编号,ecode是错误码,regs是当前进程的寄存器值。 
    ks的类型为kgdb_state,它记录了这次断点发生的信息。不知道是别有用心还是不小心作者给同一个变量ks->ex_vector赋了两次值. 
    kgdb_reenter_check()函数在检查是否出现了一种递归触发的现象,也就是当kgdb正在一个cpu上处理一个断点的过程中,嵌套地触发了kgdb,再次进入kgdb_handle_exception()中,kgdb都已经拿到控制权了,谁还那么无聊自己抢自己的东西。如果遇到这种情况,kgdb尝试在kgdb_reenter_check()里面纠正这个错误,并直接结束这次调用,这只是一个特殊的错误处理,我们暂时不关心它。 
    (kernel/kgdb.c) 
    1412 acquirelock: 
    1413 /* 
    1414 * Interrupts will be restored by the 'trap return' code, except when 
    1415 * single stepping. 
    1416 */ 
    1417 local_irq_save(flags); 
    1418 
    1419 cpu = raw_smp_processor_id(); 
    1420 
    1421 /* 
    1422 * Acquire the kgdb_active lock: 
    1423 */ 
    1424 while (atomic_cmpxchg(&kgdb_active, -1, cpu) != -1) 
    1425 cpu_relax(); 
    local_irq_save()保存当前中断状态,然后禁中断。raw_smp_processor_id()获得当前CPU号。 
    atomic_cmpxchg()函数实现了一个比较+交换的原子操作(原子就是说cpu要不就不做,要做就一定要做完某些操作才能干别的事情,对应这里就是比较和交换要一次过做完).atomic_cmpxchg()比较kgdb_active->count的值是否等用-1,如果是则把cpu的值赋给kgdb_active->count,否则不修改它的值,atomic_cmpxchg返回kgdb_active->count赋值前的值. 
    kgdb_active是一个全局原子变量,定义在kernel/kgdb.c中,用来记录当前正在执行kgdb代码的cpu号,它起到一个锁的作用,因为同一时间只能有一个cpu执行kgdb的代码,这是可以想象得到的,如果两个cpu在两个不同断点被触发,那究竟是谁和远端gdb通信呢?前一条命令被cpu1拿了,后一条却去了cpu2那里,那还得了。 
    kgdb_active的初始值为-1,-1表示当前kgdb的处理函数并没有被触发,相反如果kgdb已经在运行,那么kgdb_active就有它自己的值,这些处理都是针对多cpu的,如果只有一个cpu,这个世界就简单多了。这里是防止多个kgdb的实例在不同cpu被触发引起互相干扰。考虑这种情况,在cpu1上有一个断点让kgdb起来,这时,kgdb_active还是-1,cpu1很顺利就给kgdb_active赋值然后进入后面的操作.这时cpu2中kgdb也被触发.它也想进入后面的操作,但是这时候kgdb_active已经不再是-1,cpu2只能不断地比较kgdb_active的值和执行cpu_relax(),宏cpu_relax()可以简化为一条pause汇编,通过引入一个很短的延迟,加快了紧跟在锁后面的代码的执行并减少能源的消耗,实际上就是让cpu2等。当cpu1在退出kgdb_handle_exception()前会把kgdb_active赋回-1,这样cpu2就可以进行后面的操作了。kgdb使用大量的原子操作来完成锁的功能,后面还会看到. atomic操作加上cpu_relax()跟一个自旋锁很相似。 
    (kernel/kgdb.c) 
    1427 /* 
    1428 * Do not start the debugger connection on this CPU if the last 
    1429 * instance of the exception handler wanted to come into the 
    1430 * debugger on a different CPU via a single step 
    1431 */ 
    1432 if (atomic_read(&kgdb_cpu_doing_single_step) != -1 && 
    1433 atomic_read(&kgdb_cpu_doing_single_step) != cpu) { 
    1434 
    1435 atomic_set(&kgdb_active, -1); 
    1436 touch_softlockup_watchdog(); 
    1437 clocksource_touch_watchdog(); 
    1438 local_irq_restore(flags); 
    1439 
    1440 goto acquirelock; 
    1441 } 
    注释告诉了我们大部分事情.它在考虑这样一种情况:cpu1先进入kgdb,并和远端gdb通信中确定要进行单步调试(step),设置好cpu1上面标志寄存器的X86_EFLAGS_TF位(也就是trap位,或单步调试位)好让cpu在运行完后面一条指令后引起一个debug异常(1号异常)再次进入kgdb,接下来cpu1让程序继续下一条指令.这时候cpu2闯进来了.当cpu1退出kgdb时会把kgdb_active设置为-1,这样上面提到的那个锁就解开了。cpu2就可以进来了,但是kgdb在一个cpu处在单步调试时不想让别的cpu进来扰乱,就让cpu2放弃kgdb_active锁,然后让它再拿一次锁,期望cpu1能够先处理,cpu2的事以后再说。kgdb_cpu_doing_single_step用来记录当前那个cpu处于单步调试的状态,-1代表没有cpu在单步调试状态。后面我们将看到当kgdb收到远程gdb单步执行命令时会设置这个变量. 
    (kernel/kgdb.c) 
    1443 if (!kgdb_io_ready(1)) { 
    1444 error = 1; 
    1445 goto kgdb_restore; /* No I/O connection, so resume the system */ 
    1446 } 
    检查kgdb要用的I/O驱动模块是否已经加载好了,如果没有准备好就没有必要往下走直接返回,让系统起来。关于I/O的初始化这里暂时就不讨论了。 
    (kernel/kgdb.c) 
    1448 /* 
    1449 * Don't enter if we have hit a removed breakpoint. 
    1450 */ 
    1451 if (kgdb_skipexception(ks->ex_vector, ks->linux_regs)) 
    1452 goto kgdb_restore; 
    kgdb_skipexception(),一个挺有意思的函数,注释写着"如果我们命中了一个已经被删除的断点,下面的代码就不要走下去了".这是什么意思?断点被删除了还能够被命中吗?迫不及待看看它的代码,这是一个与体系结构相关的函数,暂时只有x86对它有实质性的实现: 
    (arch/x86/kernel/kgdb.c) 
    530 /** 
    531 * 
    532 * kgdb_skipexception - Bail out of KGDB when we've been triggered. 
    533 * @exception: Exception vector number 
    534 * @regs: Current &struct pt_regs. 
    535 * 
    536 * On some architectures we need to skip a breakpoint exception when 
    537 * it occurs after a breakpoint has been removed. 
    538 * 
    539 * Skip an int3 exception when it occurs after a breakpoint has been 
    540 * removed. Backtrack eip by 1 since the int3 would have caused it to 
    541 * increment by 1. 
    542 */ 
    543 int kgdb_skipexception(int exception, struct pt_regs *regs) 
    544 { 
    545 if (exception == 3 && kgdb_isremovedbreak(regs->ip - 1)) { 
    546 regs->ip -= 1; 
    547 return 1; 
    548 } 
    549 return 0; 
    550 } 
    这段注释基本上没有说明什么问题,实现更是看不懂,kgdb_isremovedbreak()是干那行的?为什么被删除了的断点还会起作用呢?google搜了一些,基本上只能找到一堆patch的信息,还有什么线索呢?不要忘记邮件列表这个好东西,牛B门们都在上面.在kgdb的邮件列表(http://www.sourceforge.net/mailarchive/forum.php?forum_name=kgdb-bugreport)搜了一下,找到了一个位大侠06年发的这样一封信: 
    We keep track of removed entries to prevent breakpoints from occuring after 
    they were removed. Removed breakpoints might have occured on other processors 
    before there removal from code, which will pop up on continuing and cause 
    segfaults on i386. kgdb_skipexception function identifies them. 
    看到"other processors"让人恍然大悟,又是多cpu惹的祸.考虑这样一种情况:有两个cpu,cpu1,cpu2,gdb在之前设置了两个两个断点B1,B2,cpu1命中了B1,并且拿到了前面的kgdb_active锁,在还没有停止其它cpu的活动前(后面我们马上会讲到),cpu2命中了B2,但是由于kgdb_active锁已经被cpu1拿了, cpu2将忙等kgdb_active. cpu1进入了kgdb的执行代码,在和远程gdb通信过程中,gdb要求删除B2,cpu1没法知道正在门外等着的cpu2正是因为B2而来的,cpu1直接把B2删除,然后gdb让程序继续.在cpu1放弃kgdb_active锁之后,cpu2进来了,但是cpu2进来已经没有意义了,如果我们在这里不加以判断,后面就会出大bug了,所以我们需要判断一下触发我们的这个断点是不是已经被删除了,kgdb_isremovedbreak()正是用来作这个判断,如果真的是这种情况,pc寄存器(regs->ip)就退一. 
    (kernel/kgdb.c) 
    1458 kgdb_info[ks->cpu].debuggerinfo = ks->linux_regs; 
    1459 kgdb_info[ks->cpu].task = current; 
    1460 
    1461 kgdb_disable_hw_debug(ks->linux_regs); 
    pre_exception()让驱动在进行连接前有机会去做一些自己特有的初始化,。我们可以先看看关于串口的这个函数. 
    (drivers/serial/kgdboc.c) 
    143 static void kgdboc_pre_exp_handler(void) 
    144 { 
    145 /* Increment the module count when the debugger is active */ 
    146 if (!kgdb_connected) 
    147 try_module_get(THIS_MODULE); 
    148 } 
    看到了,对于串口来说根本就没有做什么事情,只是增加模块的使用计数。 
    回到我们的kgdb_handle_exception(),kgdb用kgdb_info数组记录当前进程和它的寄存器,方便日后和远程gdb通信时使用。kgdb_disable_hw_debug()对应到系统结构上是调用arch/x86/kernel/kgdb.c中的kgdb_disable_hw_debug: 
    (arch/x86/kernel/kgdb.c) 
    300 /** 
    301 * kgdb_disable_hw_debug - Disable hardware debugging while we in kgdb. 
    302 * @regs: Current &struct pt_regs. 
    303 * 
    304 * This function will be called if the particular architecture must 
    305 * disable hardware debugging while it is processing gdb packets or 
    306 * handling exception. 
    307 */ 
    308 void kgdb_disable_hw_debug(struct pt_regs *regs) 
    309 { 
    310 /* Disable hardware debugging while we are in kgdb: */ 
    311 set_debugreg(0UL, 7); 
    312 } 
    就正如注释所说的那样,对于某些体系结构kgdb在运行时需要设置禁止硬件调试,DR7寄存器就是断点控制寄存器,他的低半个字用来允许断点和允许所选择的调试条件,清0就是禁止硬件调试。 
    再次回到主题kgdb_handle_exception() 
    (kernel/kgdb.c) 
    1463 /* 
    1464 * Get the passive CPU lock which will hold all the non-primary 
    1465 * CPU in a spin state while the debugger is active 
    1466 */ 
    1467 if (!kgdb_single_step) { 
    1468 for (i = 0; i < NR_CPUS; i++) 
    1469 atomic_set(&passive_cpu_wait[i], 1); 
    1470 } 
    在这里出来了一个十分重要的变量kgdb_single_step,名字告诉我们这个变量和单步调试有关,或者可以直接说当kgdb要进行单步调试时就会把这个变量设成1,这个条件有些让人费解,大家可以先想想为什么要有这样一个条件,我们在这里先忽略掉它,就当这里根本没有这个条件,这是可以的.在这节最后我们会对这个条件作更多的说明. 
    这里又一个用原子变量作为锁的例子,我们先把下面一段代码也贴出来: 
    (kernel/kgdb.c) 
    1472 /* 
    1473 * spin_lock code is good enough as a barrier so we don't 
    1474 * need one here: 
    1475 */ 
    1476 atomic_set(&cpu_in_kgdb[ks->cpu], 1); 
    1477 
    1478 #ifdef CONFIG_SMP 
    1479 /* Signal the other CPUs to enter kgdb_wait() */ 
    1480 if ((!kgdb_single_step) && kgdb_do_roundup) 
    1481 kgdb_roundup_cpus(flags); 
    1482 #endif 
    1483 
    1484 /* 
    1485 * Wait for the other CPUs to be notified and be waiting for us: 
    1486 */ 
    1487 for_each_online_cpu(i) { 
    1488 while (!atomic_read(&cpu_in_kgdb[i])) 
    1489 cpu_relax(); 
    1490 } 
    kgdb把正在执行kgdb代码的cpu叫做主cpu(primary cpu),其他的cpu叫从cpu(passive cpu).passive_cpu_wait[]和cpu_in_kgdb[]两个数组存的都是原子变量,都是每个cpu占一个元素。它们组合在一起又形成了一道锁,而这个锁锁的是从cpu,让他们进入忙等待的状态。具体看看它的实现过程。1469行把所有cpu的passive_cpu_wait[]都设成了1,从名字上就可以看出它的意思(passive:被动的),就是kgdb想让那个cpu进入等待状态,这里把自己也选上没关系吗?是的,没有关系,后面自然会明白。我们再跳到1481,kgdb_roundup_cpus()被调用,现在来看看这个函数: 
    (arch/x86/kernel/kgdb.c) 
    348 void kgdb_roundup_cpus(unsigned long flags) 
    349 { 
    350 send_IPI_allbutself(APIC_DM_NMI); 
    351 } 
    再找send_IPI_allbutself,发现一大堆的实现,什么bigsmp,es7000,numaq,summit。不过不用担心,其实它们所做的事情都是一样,只不过是硬件不同有不同的软件实现,有兴趣的同学可以在arch/x86/Kconfig中可以看到他们的说明。从名字我们可以猜出这个函数的作用:send发送,IPI(interprocessor interrupt)处理器间的中断,allbutself除了自己,那串起来就是向除了自己之外的cpu发中断,了解中断的同学应该不会对APIC(Advanced Programmable Interrupt Controller)陌生,APIC能够让一个cpu向另外一个发送一个中断。kgdb向所有从cpu(passive cpu)发送了一个NMI(不可屏蔽)中断,用来通知它们,kgdb要正式干活了,其它同志们暂时歇会,不要进来捣乱.那么从cpu在被NMI中断后有什么反应呢?让我们暂时离开主函数,在次回到kgdb在被触发时的情景.和int3一样,对于NMI,x86对NMI也设立了相关的处理函数do_nmi()(定义在arch/x86/kernel/traps_32.c).do_nmi()调用default_do_nmi(),它也会引起die_chain的注意。我们又回到前面的kgdb在die_chain中注册的处理函数__kgdb_notify() 
    (arch/x86/kernel/kgdb.c) 
    442 static int __kgdb_notify(struct die_args *args, unsigned long cmd) 
    443 { 
    444 struct pt_regs *regs = args->regs; 
    445 
    446 switch (cmd) { 
    447 case DIE_NMI: 
    448 if (atomic_read(&kgdb_active) != -1) { 
    449 /* KGDB CPU roundup */ 
    450 kgdb_nmicallback(raw_smp_processor_id(), regs); 
    451 was_in_debug_nmi[raw_smp_processor_id()] = 1; 
    452 touch_nmi_watchdog(); 
    453 return NOTIFY_STOP; 
    454 } 
    455 return NOTIFY_DONE; 
    456 
    457 case DIE_NMI_IPI: 
    458 /* Just ignore, we will handle the roundup on DIE_NMI. */ 
    459 return NOTIFY_DONE; 
    注释也说了,主cpu发过来的中断由DIE_NMI来处理。在2.6.26上面DIE_NMI_IPI和DIE_NMI的处理是一样的.kgdb_nmicallback()会被调用.究竟kgdb用什么方法让从cpu停下来的呢? 
    (kernel/kgdb.c) 
    1536 int kgdb_nmicallback(int cpu, void *regs) 
    1537 { 
    1538 #ifdef CONFIG_SMP 
    1539 if (!atomic_read(&cpu_in_kgdb[cpu]) && 
    1540 atomic_read(&kgdb_active) != cpu && 
    1541 atomic_read(&cpu_in_kgdb[atomic_read(&kgdb_active)])) { 
    1542 kgdb_wait((struct pt_regs *)regs); 
    1543 return 0; 
    1544 } 
    1545 #endif 
    1546 return 1; 
    1547 } 
    看到名字我迫不及待地想看看kgdb_wait 
    (kernel/kgdb.c) 
    564 static void kgdb_wait(struct pt_regs *regs) 
    565 { 
    566 unsigned long flags; 
    567 int cpu; 
    568 
    569 local_irq_save(flags); 
    570 cpu = raw_smp_processor_id(); 
    571 kgdb_info[cpu].debuggerinfo = regs; 
    572 kgdb_info[cpu].task = current; 
    573 /* 
    574 * Make sure the above info reaches the primary CPU before 
    575 * our cpu_in_kgdb[] flag setting does: 
    576 */ 
    577 smp_wmb(); 
    578 atomic_set(&cpu_in_kgdb[cpu], 1); 
    579 
    580 /* Wait till primary CPU is done with debugging */ 
    581 while (atomic_read(&passive_cpu_wait[cpu])) 
    582 cpu_relax(); 
    583 
    584 kgdb_info[cpu].debuggerinfo = NULL; 
    585 kgdb_info[cpu].task = NULL; 
    586 
    587 /* fix up hardware debug registers on local cpu */ 
    588 if (arch_kgdb_ops.correct_hw_break) 
    589 arch_kgdb_ops.correct_hw_break(); 
    590 
    591 /* Signal the primary CPU that we are done: */ 
    592 atomic_set(&cpu_in_kgdb[cpu], 0); 
    593 touch_softlockup_watchdog(); 
    594 clocksource_touch_watchdog(); 
    595 local_irq_restore(flags); 
    596 } 
    代码都列出来了,估计大家也都看到这个锁是怎样让从cpu停止活动的,cpu_relax()前面已经提到过了,它可以转换成一条pause汇编.在这里小弟再总结一下:主cpu在kgdb的主函数中(kgdb_handle_exception)设置数组passive_cpu_wait[]中所有的元素(包括描述自己的那个元素),然后向除了自己以外的所有cpu发NMI IPI中断,因此所有的从cpu都会进入kgdb_wait(),进来后从cpu先设置cpu_in_kgdb[]中自己的那个元素,以通知主cpu,自己已经准备好了,然后反复对自己的那个passive_cpu_wait元素进行判断然后relax,而另外一方面,主cpu也正反复检查cpu_in_kgdb[]的元素,等待所有从cpu都进入等待状态.这样passive_cpu_wait让从cpu等待,cpu_in_kgdb让主cpu保证所有从cpu都进入了等待状态. 
    当然,有上锁就有解锁,在主函数中,当处理完远端gdb请求后退出时就会解锁: 
    (kernel/kgdb.c) 
    1513 if (!kgdb_single_step) { 
    1514 for (i = NR_CPUS-1; i >= 0; i--) 
    1515 atomic_set(&passive_cpu_wait[i], 0); 
    1516 /* 
    1517 * Wait till all the CPUs have quit 
    1518 * from the debugger. 
    1519 */ 
    1520 for_each_online_cpu(i) { 
    1521 while (atomic_read(&cpu_in_kgdb[i])) 
    1522 cpu_relax(); 
    1523 } 
    1524 } 
    一样的做法,只不过这次是解锁和等待所有从cpu离开kgdb_wait()回到日常生活. 
    好了,大家应该不会忘记前面我们提的一个问题,就是为什么要有kgdb_single_step这么一个判断,它在我们前面的代码中连续出现了3次.kgdb_single_step,就正如大家想的那样,当gdb向kgdb发送一个's'单步执行命令时这个变量的值就会变成1.那为什么在kgdb处于单步调试的情况下反而不去停止其它cpu的活动呢? 
    在邮件列表里面,一位仁兄04年写的一封信里给出了答案,道理很简单.如果想看原文,可以在邮件列表里面搜debugger_step这个关键字(在进内核树之前,kgdb_single_step就叫这个名字),在最老的几封信里面. 在2.6.27内核x86平台上,当gdb要求kgdb进行单步调试时,kgdb是会让所有从cpu都处于忙等待,只让被单步调试的那个cpu继续运行,一直到gdb过来一个'c'(continue)为止,才让从cpu们继续.这样我们再回头看看前面的代码,然后考虑这样一个情况:主cpu由于一个断点进入kgdb的代码,一开始,它并不是处于单步调试的状态,所以kgdb_single_step为0,这样kgdb把其它所有cpu都锁住,让它们等待,然后和远程的gdb通信,假如这时候gdb要求单步调试,kgdb就把kgdb_single_step设成1,然后让程序继续执行,在1513行,也是判断kgdb_single_step(用于解锁),这时候,kgdb_single_step已经不是0了,所以主cpu并没有帮其它从cpu解锁,所以主cpu在下一条指令完成后再次进入kgdb主函数,这时候,所有从cpu还在忙等待,所有没有必要在设置passive_cpu_wait[]和向它们发IPI NMI中断,不然从cpu为了响应这个NMI会在kgdb_wait里面再调用一次kgdb_wait,这正是之前kgdb解决过的一个bug. 
    主函数还剩下一点点了,我们把他看完: 
    (kernel/kgdb.c) 
    1492 /* 
    1493 * At this point the primary processor is completely 
    1494 * in the debugger and all secondary CPUs are quiescent 
    1495 */ 
    1496 kgdb_post_primary_code(ks->linux_regs, ks->ex_vector, ks->err_code); 
    1497 kgdb_deactivate_sw_breakpoints(); 
    1498 kgdb_single_step = 0; 
    1499 kgdb_contthread = current; 
    1500 exception_level = 0; 
    这些是在进入和远程gdb通信之前kgdb所做的最后的初始化,kgdb_post_primary_code的意思就是在主cpu在控制整个机器后,它还有什么事情需要做的,这是体系结构相关的,x86对kgdb_post_primary_code实现非常简单,就是保存了一下中断向量号和错误代码。在开始前我们先禁止所有断点,把内存还原成原来的样子。主函数在最后就是还原环境,中间夹着的是最重要的一行代码了,和gdb通信,执行他发来的命令。 
    (kernel/kgdb.c) 
    1502 /* Talk to debugger with gdbserial protocol */ 
    1503 error = gdb_serial_stub(ks); 
    主函数这样就差不多了,有些同志可能注意到了kgdb_cpu_doing_single_step和前面passive_cpu_wait[]/cpu_in_kgdb[]形成的锁在功能上有些重叠了,前者是当有一个cpu在单步调试时,其它cpu不能进kgdb的代码.而后者则是当kgdb在处在单步调试时让其它cpu处在忙等待中.忙等待有怎么会进kgdb的代码呢?对于这个问题,我没有很确切的回答,如果大家知道就请指点指点小弟.这两段代码是一位大侠在一次提交中放到库里的。但是它们的目标都是一样,就是保护单步调试的cpu. 
    在离开之前,主函数在前面究竟做了什么事情?其实没干多少活,大部分时间都在控制竞态,包括单步调试,停止从cpu的活动等。 
    >>>>>>>>>3.gdb远程串行协议(GDB remote serial protocol)>>>>>>>>> 
    在研究kgdb命令具体执行过程之前,我们先来看看gdb和远程的kgdb是通过一种什么的方式通信的。在这里我们只做简略的介绍,详细说明可以在这里找到:http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gdb/remote-protocol.html 
    这个现实世界中充满了各种各样的协议,很多事情都是遵循协议来做事,网络有TCP/IP,SPX/IPX, 电信有ISUP,PRI,SIP,就业,租房,离婚都有协议.gdb也不例外,gdb和远程目标之间用gdb远程串行协议(下面简称为RSP)来通信。 
    RSP是一种基于ASCII码的协议,以gdb发送命令,目标端(在这里是kgdb)返回执行结果或信息的方式来进行的,而命令和回复在传输过程中又是封装在一个包(packet)里面的.每个包以一个'$'开始,接着是实际数据,数据以一个'#'结束,后面跟着两位十六进制数字用作校验和($packet-data#checksum).校验和需要对256取模,因为只有两位。 
    当收到一个数据包,(无论是命令还是回复,无论是gdb端还是目标端)收到数据包的一方应该根据收到的数据和校验和检查这个包的合法性,如果是一个正常的包,应该返回一个'+',如果是一个损坏的包,则应该返回一个'-'。在包里面的数据,可以用',',';',':'三个符号来分隔成不同的段,他们的用法根据不同的命令不同而不同. 
    gdb提供了一个很方便的方式让我们把通信过程记录下来,在gdb里面使用"set remotelogfile [filepath]"指定你要记录的文件,但是这个文件一定要在开始记录前手动创建,下面一段是在sys_mount设置一个断点的log: 
    c info source 
    c b sys_mount 
    w $mc018ae77,1#fa 
    r +$55#6a 
    w +$mc018ae77,1#fa 
    r +$55#6a 
    w +$mc018ae77,1#fa 
    r +$55#6a 
    w +$mc018ae77,a#2a 
    r +$5589e5565383ec148d55#e6 
    w +$mc018ae77,1#fa 
    r +$55#6a 
    w +$mc018ae78,1#fb 
    r +$89#71 
    w +$mc018ae79,5#00 
    r +$e5565383ec#a0 
    w +$mc018ae79,5#00 
    r +$e5565383ec#a0 
    w +$mc018ae78,1#fb 
    r +$89#71 
    w +$mc018ae79,1#fc 
    r +$e5#9a 
    w +$mc018ae7a,1#24 
    r +$56#6b 
    w +$mc018ae7a,1#24 
    r +$56#6b 
    w +$mc018ae7b,1#25 
    r +$53#68 
    w +$mc018ae7c,1#26 
    r +$83#6b 
    w +$mc018ae7c,1#26 
    r +$83#6b 
    w +$mc018ae77,1#fa 
    r +$55#6a 
    w + 
    c info breakpoints 
    c c 
    w $Z0,c018ae7f,1#72 
    gdb规定目标端起码应该实现:'g' 'G' 'm' 'M' 'c' 's',kgdb当然也有实现了,而且还不止这些,有一些是和体系结构没有太紧密的关系,比如断点的设置和删除,kgdb只是记录起这个地址,和设置这个断点的状态;内存的读取和修改,也许这个和体系结构有关,但是具体实现内核已经搞定,kgdb不用关心。另外一方面一些比如's'单步执行这种命令就和体系结构关系十分密切,x86上是通过设置标志寄存器的Trap标记,让cpu运行完下一条指令后触发一个debug中断,其他的体系结构的做法都有所不同。 
    下面具体看看一些具体的命令: 
    'g' 
    格式:$g#67 
    描述:gdb向目标端发送这个命令来获取目标机当前寄存器的值 
    回复:+ $123456789abcdef0...#xx 
    '+'用来应答'g'这个命令,表明目标端正确地收到这个命令,然后就是目标端的回复包,gdb规定用8十六进制个数字来表示一个寄存器的值,所以第一个寄存器的值为12345678,第二个为9abcdef0,依此类推,而具体每个寄存器的含义和寄存器个数又体系结构决定,定义在gdb的代码中. 当然这里8个数字是对32位系统来说的,为什么是8位?限于我们这个协议是基于ASCII的,一个十六进制数只能标记4位,那32位自然是8个十六进制数了。 
    'G' 
    格式:$GXXXXXXXXXXX...#xx 
    描述:和g相反,这个命令用来设置目标机当前寄存器的值 
    回复: + $OK#9a 
    OK表示设置成功,后面我们会讲到不成功的情况. 
    'm' 
    格式:$m6a1bbb,2#b9 
    描述:读取一段内存的值,这里是读取以6a1bbb位起始地址的两个字节 
    回复: + $f488#0a 目标端把值返回. 
    'M' 
    格式:$Mccc5cc,2:a340#01 
    描述:设置一段内存的值,这里是把以ccc5cc位开始地址的两个字节设成a340 
    回复: + $OK#9a 
    's' 
    格式:$sADDR#xx 
    描述:用户进行单步调试时用到,ADDR指明了程序将从那个地址恢复运行,如果忽略ADDR,程序就从断点处继续运行. 
    回复:+ 目标端会马上返回数据正确或错误接收,但不会马上返回信息,具体信息要到下一次断点被触发时才会返回.下面会提到. 
    'c' 
    格式:$cADDR#xx 
    描述:让程序恢复正常运行 
    回复:和's'一样. 
    'Z' 
    格式: $ZTADDR,LENGTH#xx 
    'Z'命令用来设置断点或watch点,用过gdb的同志应该不会陌生了 
    'T'字段定义了这个命令的对象,0:软件断点,1:硬件断点,2:写watch点,3:读watch点,4:访问watch点. 
    'ADDR'就是我们所关心的内存地址,'LENGTH',对于软件中断它指明被断点指令覆盖的内存长度,kgdb对于软件断点忽略掉它,因为触发kgdb的指令与体系结构相关,已经定义在kgdb这边,就如x86的int3在内存里面的二进制指令为"0xcc";对于硬件断点和watch点,'LENGTH'指明gdb关注的内存长度. 
    'z' 
    格式: $zTADDR,LENGTH#xx 
    各项与'Z'相同.用来取消断点。 
    回复: 
    错误回复: 
    格式:+ $E01#a6 
    描述:如果目标端在执行gdb的命令时出错时返回错误回复,比如访问内存时出错.E后面根两位的错误码,错误码在gdb里面没有定义,没有定义其实更加方便,可以让开发端和目标端对错误码的使用带来灵活. 
    空回复: 
    格式:+ $#00 
    描述:当目标端不认识gdb发来的命令时,返回空回复表示自己不支持这个命令. 
    对'c','s'的回复: 
    有好几种对'c','s'的回复,其中比较常见的是'S'和'T' 
    'S' 
    格式: $SAA#b8 
    作用: AA表明触发这次通信的那个异常相关的信号,这个信号就是posix标准中的信号. 
    'T' 
    格式: $TAAN..:R..;N..:R..#xx 
    作用: AA同样是信号号, N..:R.. 这表明一个寄存器和它的值,N标记寄存器号,R是它对应的值,其中,如果N不是一个16进制数而是"thread",那么后面R的值就指明当前的进程号.如果是其它的字符串gdb会省略. 
    Kgdb在回复's','c'时选用了'T'的方式,不过在'T'消息里面只有thread一个字段,没有给gdb传更多的寄存器信息. 
    协议就在这里打住,有兴趣的同学可以继续深入研究各种高级用法. 
    顺便把gdb_serial_stub()开头的代码列出来: 
    (kernel/kgdb.c) 
    1215 /* 
    1216 * This function performs all gdbserial command procesing 
    1217 */ 
    1218 static int gdb_serial_stub(struct kgdb_state *ks) 
    1219 { 
    1220 int error = 0; 
    1221 int tmp; 
    1222 
    1223 /* Clear the out buffer. */ 
    1224 memset(remcom_out_buffer, 0, sizeof(remcom_out_buffer)); 
    1225 
    1226 if (kgdb_connected) { 
    1227 unsigned char thref[8]; 
    1228 char *ptr; 
    1229 
    1230 /* Reply to host that an exception has occurred */ 
    1231 ptr = remcom_out_buffer; 
    1232 *ptr++ = 'T'; 
    1233 ptr = pack_hex_byte(ptr, ks->signo); 
    1234 ptr += strlen(strcpy(ptr, "thread:")); 
    1235 int_to_threadref(thref, shadow_pid(current->pid)); 
    1236 ptr = pack_threadid(ptr, thref); 
    1237 *ptr++ = ';'; 
    1238 put_packet(remcom_out_buffer); 
    1239 } 
    1240 
    1241 kgdb_usethread = kgdb_info[ks->cpu].task; 
    1242 ks->kgdb_usethreadid = shadow_pid(kgdb_info[ks->cpu].task->pid); 
    1243 ks->pass_exception = 0; 
    实现传输的两个函数为put_packet()和get_packet()它们的实现十分简单直接,而传输时使用的是底层驱动的读写操作,这部分的实现和kgdb没有直接的关系,kgdb只是使用了现成的东西而已,这里就不罗嗦了.前面这段就是'T'回复. 
    >>>>>>>>>>4. 命令实现:>>>>>>>>>> 
    kgdb实现了众多的命令,这里不一一进行说明,有些实现十分简单,有些就需要仔细看看,不过其实很多工作gdb端已经完成了,到kgdb这边的都是很直接的,比如用户试图在gdb中打印一个局部变量的值,gdb通过和kgdb得到当前进程寄存器的信息,最终计算出要读的内存地址和长度,kgdb只要应gdb的要求提供相应的信息和设置相应的地址就够了,所以gdb是调试里面的主角.不过我们这里关心的还是kgdb,gdb的工作就忽略了. 
    我们不妨用一个gdb用户的角度来看看,大多数情况下我们只要:设置断点,让程序继续,单步调试,打印内存这些功能.对应gdb发给kgdb的命令并不就是'Z','c','s','m'命令,一般一个gdb的用户的命令都对上十几二十条kgdb命令的发送和接收. 
    4.1 断点 
    先从数据结构入手,kgdb用一个数组kgdb_break[]来描述软件断点(我们这里暂且只讨论软断点): 
    (kernel/kgdb.c) 
    107 /* 
    108 * Holds information about breakpoints in a kernel. These breakpoints are 
    109 * added and removed by gdb. 
    110 */ 
    111 static struct kgdb_bkpt kgdb_break[KGDB_MAX_BREAKPOINTS] = { 
    112 [0 ... KGDB_MAX_BREAKPOINTS-1] = { .state = BP_UNDEFINED } 
    113 }; 
    每个元素为kgdb_bkpt定义在 
    (include/linux/kgdb.h) 
    98 struct kgdb_bkpt { 
    99 unsigned long bpt_addr; 
    100 unsigned char saved_instr[BREAK_INSTR_SIZE]; 
    101 enum kgdb_bptype type; 
    102 enum kgdb_bpstate state; 
    103 }; 
    结构体里面的每个成员都很好理解,bpt_addr就是断点设置的地址,saved_instr[]放的是被断点指令所替换的真实指令.大家也知道一般调试器的实质就是在断点对于的内存地址中把原来的指令或部分指令替换成一条断点指令,对于x86就是"int 3",这条指令要能让系统进入被gdb控制的状态.type对应上面所说的'Z'命令的type字段,state标志每个断点的状态4中状态: 
    (include/linux/kgdb.h) 
    83 enum kgdb_bptype { 
    84 BP_BREAKPOINT = 0, 
    85 BP_HARDWARE_BREAKPOINT, 
    86 BP_WRITE_WATCHPOINT, 
    87 BP_READ_WATCHPOINT, 
    88 BP_ACCESS_WATCHPOINT 
    89 }; 
    90 
    91 enum kgdb_bpstate { 
    92 BP_UNDEFINED = 0, 
    93 BP_REMOVED, 
    94 BP_SET, 
    95 BP_ACTIVE 
    96 }; 
    在kgdb_break[]被初始化时state成员都被设置成BP_UNDEFINED,表明这个数组元素可用,当gdb设置一个断点时,一个数组元素会被设成BP_SET,相反取消断点时则把相应的数组项设成BP_REMOVED.当断点被激活时元素被设置为BP_ACTIVE. 
    几个断点状态的重要切换点: 
    1. BP_UNDEFINED/BP_REMOTED -> DB_SET 和 BP_SET -> BP_REMOVED分别发生在收到gdb的'Z'和'z'指令时.由函数kgdb_set_sw_break()和kgdb_remove_sw_break()实现 
    2. BP_SET -> DB_ACTIVE 一个断点处于BP_SET状态并没有生效,只是说明这个断点是有效的.当gdb发来's'或'c'命令恢复原来程序执行时才会让断点变成BP_ACTIVE.函数kgdb_activate_sw_breakpoints()实现了这个状态转换. 
    3. BP_ACTIVE -> BP_SET 发生在进入和gdb_serial_stub()之前,先把断点都失效在与远端的gdb通信,在讲主函数的时候几经提过. 
    4. 其它状态 -> DB_UNDEFINED 函数remove_all_break()完成这个功能,它主要是在远程gdb与kgdb断开链接时,即进行'D','k','C15'等命令时kgdb对所有断点进行重置. 
    下面具体看看kgdb是怎样设置断点的: 
    (kernel/kgdb.c) 
    616 /* 
    617 * SW breakpoint management: 
    618 */ 
    619 static int kgdb_activate_sw_breakpoints(void) 
    620 { 
    621 unsigned long addr; 
    622 int error = 0; 
    623 int i; 
    624 
    625 for (i = 0; i < KGDB_MAX_BREAKPOINTS; i++) { 
    626 if (kgdb_break[i].state != BP_SET) 
    627 continue; 
    628 
    629 addr = kgdb_break[i].bpt_addr; 
    630 error = kgdb_arch_set_breakpoint(addr, 
    631 kgdb_break[i].saved_instr); 
    632 if (error) 
    633 return error; 
    634 
    635 kgdb_flush_swbreak_addr(addr); 
    636 kgdb_break[i].state = BP_ACTIVE; 
    637 } 
    638 return 0; 
    639 } 
    kgdb_arch_set_breakpoint()是一个和体系结构相关的函数,但是暂时在内核代码里面只有一个地方实现了,这里所做的就是读取断点地址对应的指令并保存起来,然后用一条可以触发断点的指令替换这段内存: 
    (kernel/kgdb.c) 
    167 /* 
    168 * Weak aliases for breakpoint management, 
    169 * can be overriden by architectures when needed: 
    170 */ 
    171 int __weak kgdb_arch_set_breakpoint(unsigned long addr, char *saved_instr) 
    172 { 
    173 int err; 
    174 
    175 err = probe_kernel_read(saved_instr, (char *)addr, BREAK_INSTR_SIZE); 
    176 if (err) 
    177 return err; 
    178 
    179 return probe_kernel_write((char *)addr, arch_kgdb_ops.gdb_bpt_instr, 
    180 BREAK_INSTR_SIZE); 
    181 } 
    这里出来一个有趣的单词__weak,它是一个宏,和__packed是同一种东西都是gcc的扩展属性: 
    #define __packed __attribute__((packed)) 
    #define __weak __attribute__((weak)) 
    如果这个关键字用在函数定义上面,一般情况下和一般函数没有两样。但是当有一个同名函数但是不带__weak被定义时,所有对这个函数的调用都是指向后者(不带__weak那个),如果有两个一样的函数都用了__weak,那么真正调用那个,就要看连接器了。 
    设置断点的过程其实就是像大家想像的那样,把相应地址的指令读上来并保存起来,把一条断点指令写到这个地址上去. 
    关于断点的函数像kgdb_set_sw_break(),kgdb_remove_sw_break()这些函数的实现都是直接了当的,这里就不一一说明了. 
    4.2 continue 和 step 
    'c'(continue)和's'(step)命令就稍微复杂一点。 
    (kernel/kgdb.c) gdb_serial_stub() 
    1303 case 'c': /* Continue packet */ 
    1304 case 's': /* Single step packet */ 
    1305 if (kgdb_contthread && kgdb_contthread != current) { 
    1306 /* Can't switch threads in kgdb */ 
    1307 error_packet(remcom_out_buffer, -EINVAL); 
    1308 break; 
    1309 } 
    1310 kgdb_activate_sw_breakpoints(); 
    1311 /* Fall through to default processing */ 
    1312 default: 
    1313 default_handle: 
    1314 error = kgdb_arch_handle_exception(ks->ex_vector, 
    1315 ks->signo, 
    1316 ks->err_code, 
    1317 remcom_in_buffer, 
    1318 remcom_out_buffer, 
    1319 ks->linux_regs); 
    1305行对变量kgdb_contthread进行判断,kgdb_contthread在正常情况下就是等于current,它是为gdb的'Hc'命令服务的,为了更好地了解这个变量,我们再看看这个命令在gdb在远程协议中的意义: 
    $HCT...#xx (set thread) 
    Set thread for subsequent operations (`m', `M', `g', `G', et.al.). C = `c' for thread used in step and continue; T... can be -1 for all threads. C = `g' for thread used in other operations. If zero, pick a thread, any thread. 
    我们这里只关心'Hc'命令,因为kgdb_contthread和'Hc'相关.上面的文档也说了,'H'命令是位下面的操作选择线程,而'Hc'这是针对step和continue这两个动作的.再看看邮件列表上面的同志是怎样说的: 
    > Before s command gdb sets thread to be stepped using Hc. An Hc0 packet 
    > indicates that all threads are to be resumed and current thread is to be 
    > single stepped. An Hc<thread id> indicates that only current thread is to be 
    > single stepped while holding other threads where they are. 
    在gdb发起单步调试命令之前都会先使用'Hc'命令对线程进行一些指定,'Hc0'代表让所有的线程都恢复工作,只有当前的这个线程继续被单步调试,如果是'Hc<thread id>'则代表只有当前线程可以跑,其它的都得等等.对于单步调试'Hc<thread id>'是最常见的.那问一个问题,gdb是怎样知道这个pid的?大家应该不会忘了上面在讲远程协议时关于's','c'的返回值吧,当后面一个断点被触发时它们的返回值才发回给gdb,而返回的内容就包含了这个pid('T'回复),告诉gdb现在断点是从那个线程里面触发的.kgdb在处理'Hc'命令时只是对这个pid进行合法检查,然后就把这个pid对应的task_struct找出来赋给了kgdb_contthread. 第1305行的这个检查是在看gdb要求这个pid是不是当前进程(也是kgdb报告上去的),如果不是就代表进程发生过切换,而在kgdb中这是不应该发生的,向gdb报告这个错误,然后等待下一个指令. 
    1310行激活所有断点,这是因为,'c','s'指令其实就意味着程序要往下走了.断点要归位,准备好下次的触发. 
    接着进入体系结构相关的'c','s'处理: 
    (arch/x86/kernel/kgdb.c) 
    354 /** 
    355 * kgdb_arch_handle_exception - Handle architecture specific GDB packets. 
    356 * @vector: The error vector of the exception that happened. 
    357 * @signo: The signal number of the exception that happened. 
    358 * @err_code: The error code of the exception that happened. 
    359 * @remcom_in_buffer: The buffer of the packet we have read. 
    360 * @remcom_out_buffer: The buffer of %BUFMAX bytes to write a packet into. 
    361 * @regs: The &struct pt_regs of the current process. 
    362 * 
    363 * This function MUST handle the 'c' and 's' command packets, 
    364 * as well packets to set / remove a hardware breakpoint, if used. 
    365 * If there are additional packets which the hardware needs to handle, 
    366 * they are handled here. The code should return -1 if it wants to 
    367 * process more packets, and a %0 or %1 if it wants to exit from the 
    368 * kgdb callback. 
    369 */ 
    370 int kgdb_arch_handle_exception(int e_vector, int signo, int err_code, 
    371 char *remcomInBuffer, char *remcomOutBuffer, 
    372 struct pt_regs *linux_regs) 
    373 { 
    374 unsigned long addr; 
    375 unsigned long dr6; 
    376 char *ptr; 
    377 int newPC; 
    378 
    379 switch (remcomInBuffer[0]) { 
    380 case 'c': 
    381 case 's': 
    382 /* try to read optional parameter, pc unchanged if no parm */ 
    383 ptr = &remcomInBuffer[1]; 
    384 if (kgdb_hex2long(&ptr, &addr)) 
    385 linux_regs->ip = addr; 
    386 case 'D': 
    387 case 'k': 
    388 newPC = linux_regs->ip; 
    389 
    390 /* clear the trace bit */ 
    391 linux_regs->flags &= ~X86_EFLAGS_TF; 
    392 atomic_set(&kgdb_cpu_doing_single_step, -1); 
    393 
    394 /* set the trace bit if we're stepping */ 
    395 if (remcomInBuffer[0] == 's') { 
    396 linux_regs->flags |= X86_EFLAGS_TF; 
    397 kgdb_single_step = 1; 
    398 atomic_set(&kgdb_cpu_doing_single_step, 
    399 raw_smp_processor_id()); 
    400 } 
    401 
    402 get_debugreg(dr6, 6); 
    403 if (!(dr6 & 0x4000)) { 
    404 int breakno; 
    405 
    406 for (breakno = 0; breakno < 4; breakno++) { 
    407 if (dr6 & (1 << breakno) && 
    408 breakinfo[breakno].type == 0) { 
    409 /* Set restore flag: */ 
    410 linux_regs->flags |= X86_EFLAGS_RF; 
    411 break; 
    412 } 
    413 } 
    414 } 
    415 set_debugreg(0UL, 6); 
    416 kgdb_correct_hw_break(); 
    417 
    418 return 0; 
    419 } 
    420 
    421 /* this means that we do not want to exit from the handler: */ 
    422 return -1; 
    423 } 
    对应'c','s'命令的处理其实十分相近,都是先看看命令里面带没带地址参数,如果有就改变pc寄存器的值.x86需要清除标志寄存器里面的X86_EFLAGS_TF位和kgdb_cpu_doing_single_step,这两个东东前面都提到过了,X86_EFLAGS_TF是用来触发debug异常的,而kgdb_cpu_doing_single_step则是防止竞态(kgdb_handle_exception中).对应'c'命令这些清理是必要的.如果是's'命令396~399行则重新对这些变量和寄存器赋值,我们也终于看到老朋友kgdb_single_step和kgdb_cpu_doing_single_step被赋值了,而这个地方也是它们唯一一处被设置的地方(不包括清0操作). 
    下面又是一个调试寄存器,dr6.前面我们已经看到过dr7.dr6是什么意思呢?它是一个调试状态寄存器,当调试事件被触发时,它会告诉我们触发的原因. dr6里面只有7位被用到了(第0,1,2,3和13,14,15),其余的都保留.403行判断条件关心的是第14位,第14位用来表示调试事件是否因为单步调试即控制寄存器里面的TF位(X86_EFLAGS_TF)被值位而造成的.如果是就正常,把寄存器归0,以便下一次检测.如果不是单步调试造成的,再检查低4位,低4位分别代表4个硬件调试断点(x86的4个硬件断点),谁触发谁的那一位就被置成1,而breakinfo[]也是关于硬件断点的数据结构,这里我们只关心软件断点,所以就不多说了.对硬件断点有兴趣的同学可以在网上找找相关资料. 
    当一切正常,0被返回,然后kgdb对这个异常处理也结束了,所有东西又回到想以前一样. 
    到这里大家可能已经发现了一个问题,对于这几个关于's','c'命令的处理代码里面,并没有看到对pc寄存器(指令寄存器)的修改。如果这是真的话,被断点指令替换了的那部分指令不就没有执行而直接到下一条指令了?这绝对是影响运行结果的,大家都不会相信这是事实,那真相是怎样的呢?光在kgdb这边已经没有什么线索了,让我们换个角度看看gdb在处理用户的s和c(注意,前面也看到平均每条用户的指令就对应十多条gdb发给kgdb的指令).用上面提到的gdb提供的log来看这个问题不太好,因为gdb似乎对发给kgdb和从kgdb收到的命令和回复在log里面进行了修改,把它们的顺序重新排了一下,隐藏了真相。也没太好的办法,只好在gdb_serial_stub()里面加了三条printk语句,分别是kgdb收到的消息和kgdb发出去的消息,还有当kgdb处理完一次和gdb通信后打出一条消息表示表示。 
    下面一段是这样一种情况:在系统起来后用gdb在sys_mount(),sys_mknod()上面设置两个断点,然后让目标己继续。在目标机上mount一个设备,随便比如sda1,不一定要有效的命令只要能触发sys_mount()就够了,当然目标机这时也就停下来了。在开发机上的gdb执行's'(用户的)我们可以看到下面的输出: 
    Nov 21 16:45:41 localhost kernel: kgdb exit 
    Nov 21 16:46:09 localhost kernel: kgdb in: g 
    Nov 21 16:46:09 localhost kernel: kgdb out: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000 
    Nov 21 16:46:09 localhost kernel: kgdb in: P8=7fae18c0 
    Nov 21 16:46:09 localhost kernel: kgdb out: 
    Nov 21 16:46:09 localhost kernel: kgdb in: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: z0,c018ae7f,1 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: z0,c017dbaf,1 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfbc,4 
    Nov 21 16:46:09 localhost kernel: kgdb out: e8870508 
    Nov 21 16:46:09 localhost kernel: kgdb in: m80587e8,8 
    Nov 21 16:46:09 localhost kernel: kgdb out: 2f6465762f736461 
    Nov 21 16:46:09 localhost kernel: kgdb in: m80587f0,8 
    Nov 21 16:46:09 localhost kernel: kgdb out: 3100000011000000 
    Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc0,4 
    Nov 21 16:46:09 localhost kernel: kgdb out: f8870508 
    Nov 21 16:46:09 localhost kernel: kgdb in: m80587f8,8 
    Nov 21 16:46:09 localhost kernel: kgdb out: 2f6d6e742f736461 
    Nov 21 16:46:09 localhost kernel: kgdb in: m8058800,8 
    Nov 21 16:46:09 localhost kernel: kgdb out: 312f000041000000 
    Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc4,4 
    Nov 21 16:46:09 localhost kernel: kgdb out: 008a0508 
    Nov 21 16:46:09 localhost kernel: kgdb in: m8058a00,8 
    Nov 21 16:46:09 localhost kernel: kgdb out: 686673706c757300 
    Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc8,4 
    Nov 21 16:46:09 localhost kernel: kgdb out: 0000edc0 
    Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfcc,4 
    Nov 21 16:46:09 localhost kernel: kgdb out: 00000000 
    Nov 21 16:46:09 localhost kernel: kgdb in: Hc982 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: s 
    Nov 21 16:46:09 localhost kernel: kgdb exit 
    Nov 21 16:46:09 localhost kernel: kgdb in: g 
    Nov 21 16:46:09 localhost kernel: kgdb out: 1500000058297c23a4cff7c1e887050898cff7c1b4cff7c10000edc00000000082ae18c08203200060000000680000007b0000007b000000ffff0000ffff0000 
    Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c018ae7f,1 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c017dbaf,1 
    Nov 21 16:46:09 localhost kernel: kgdb out: OK 
    Nov 21 16:46:09 localhost kernel: kgdb in: s 
    Nov 21 16:46:09 localhost kernel: kgdb exit 
    这是's'(用户)命令的一部分,不过这部分已经足够说明问题了。"kgdb in"就是gdb发过来的命令,"kgdb out"自然就是回复,"kgdb exit"代表离开一次kgdb的代码,也就是完成一次和gdb的通信。(一次通信可以完成很多条命令) 
    我们把关注的指令按顺序摘出来: 
    kgdb in: g 
    kgdb out: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000 
    kgdb in: G 
    15000000 58297c23 00000000 e8870508 98cff7c1 b4cff7c1 0000edc0 00000000 7fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000 
    kgdb out: OK 
    kgdb in: z0,c018ae7f,1 
    kgdb out: OK 
    kgdb in: z0,c017dbaf,1 
    kgdb out: OK 
    kgdb in: s 
    kgdb exit 
    kgdb in: Z0,c018ae7f,1 
    kgdb out: OK 
    kgdb in: Z0,c017dbaf,1 
    kgdb out: OK 
    kgdb in: s 
    kgdb exit 
    先轻轻回顾一下协议命令,'g',gdb向kgdb要寄存器的值;'G',gdb设置目标机的寄存器;'z'取消一个断点;'Z'设置一个断点;'s'单步执行.这里先指明地址c018ae7f是sys_mount的地址,c017dbaf是sys_mknod的地址。 
    故事一开始是sys_mount断点被触发,gdb先读取目标机的寄存器'g',然后gdb对这些值进行修改,和上面'g'的值对比,只修改了第65,66个16进制数,我们在讲协议的时候讲过gdb把每8个16进制数变成一个寄存器值,那我们按8拆分,发现只有第9个寄存器被修改了它的值从80ae18c0改为7fae18c0,7fae18c0这个数看上去很眼熟,是的它正是我们的sys_mount,只不过数字有点错位了.那我们一定会想这个寄存器是不是pc寄存器呢?没错就是它.在include/asm-x86/kgdb.h定义了每个寄存器的含义. 
    24 enum regnames { 
    25 GDB_AX, /* 0 */ 
    26 GDB_CX, /* 1 */ 
    27 GDB_DX, /* 2 */ 
    28 GDB_BX, /* 3 */ 
    29 GDB_SP, /* 4 */ 
    30 GDB_BP, /* 5 */ 
    31 GDB_SI, /* 6 */ 
    32 GDB_DI, /* 7 */ 
    33 GDB_PC, /* 8 also known as eip */ 
    34 GDB_PS, /* 9 also known as eflags */ 
    35 GDB_CS, /* 10 */ 
    36 GDB_SS, /* 11 */ 
    37 GDB_DS, /* 12 */ 
    38 GDB_ES, /* 13 */ 
    39 GDB_FS, /* 14 */ 
    40 GDB_GS, /* 15 */ 
    41 }; 
    pc寄存器正是第9个.寄存器赋值函数在gdb_cmd_setregs()中实现,结构十分简单,这里就不罗嗦了.这里gdb把pc指针减了一,让cpu再次运行断点所在位置的指令.这里又会有另外一个问题,刚刚在gdb_serial_stub()中我们也看到了,如果是's'或'c'命令,kgdb会在恢复运行之前激活所有断点.这样断点不就又被触发吗?gdb肯定没那么傻B,在发's'命令之前,先发两个'z'命令把所有断点都清了,再单步调试,这样,kgdb上面都没有断点了,随便让它激活多少遍断点都没所谓了.而且's'单步调试马上又会把控制权交给kgdb.在执行完断点原来位置的那条指令后,马上又用两条'Z'把原来两个断点加上.然后继续单步调试. 
    总结一下这个过程: 
    1.gdb获得当前pc寄存器值,并把它减一. 
    2.取消所有断点. 
    3.单步调试原来断点位置的指令. 
    4.单步调试引起的debug中断让kgdb在次拿到控制权,kgdb再把所有断点加回去. 
    5.后面继续单步调试. 
    'c'命令的过程和's'的过程差不多,只是在第5步不会继续单步而是'c'继续执行. 
    一句话,gdb控制了整个过程. 
    查看变量的gdb操作其实也没太多好说的了,无非就变成一个或多个gdb的'm'命令,还是那句,gdb才是调试的主角,它做了大部分的事情. 
    >>>>>>>>>>5 初始化时机.>>>>>>>>>> 
    看完上面这些内容,难免有人会问,那kgdb在什么时候起来的呀?假如我们用"kgdboc=ttyS0,115200 kgdbwait"作为启动参数.就在 drivers/serial/kgdboc.c 中的module_init(init_kgdboc)里面.就这样完了吗?那内核究竟在启动的那个阶段对kgdb有反应?这里就扯远一点,暂时离开一下kgdb.我们从module_init开始,它的定义在include/linux/init.h 
    259 #define module_init(x) __initcall(x); 
    204 #define __initcall(fn) device_initcall(fn) 
    199 #define device_initcall(fn) __define_initcall("6",fn,6) 
    159 /* initcalls are now grouped by functionality into separate 
    160 * subsections. Ordering inside the subsections is determined 
    161 * by link order. 
    162 * For backwards compatibility, initcall() puts the call in 
    163 * the device init subsection. 
    164 * 
    165 * The `id' arg to __define_initcall() is needed so that multiple initcalls 
    166 * can point at the same handler without causing duplicate-symbol build errors. 
    167 */ 
    168 
    169 #define __define_initcall(level,fn,id) \ 
    170 static initcall_t __initcall_##fn##id __used \ 
    171 __attribute__((__section__(".initcall" level ".init"))) = fn 
    (include/linux/init.h) 
    135 typedef int (*initcall_t)(void); 
    一个程序编译好之后有代码段,只读数据段,数据段等一些段,而__attribute__((__section__(...)))这个gcc的编译扩展属性,它能够把函数或数据放入指定名字的段中。 
    那__define_initcall这一团是什么意思呢?首先这是一个定义和初始化的语句,initcall_t是一个函数指针类型,所以这里定义一个静态的函数指针,它的名字是__initcall_##fn##id,"##"在宏里面是连接两个字符串,所以这个变量名是根据进来的参数名字不一样而不同.并且把传进来fn(我们这里就是init_kgdboc())赋给这个变量。最重要的是把这个变量放在".initcall" level ".init"段中,我们的level是6,所以定义的这个函数指针放在.initcall6.init这个段中。 
    连接器允许我们通过连接脚本修改默认段的起始位置,段的内容等很多东西,先来看看x86的脚本arch/x86/kernel/vmlinux_32.lds.S。从名字我们大概可以猜出我们上面的那些段对应于这个文件中的: 
    133 .initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) { 
    134 __initcall_start = .; 
    135 INITCALLS 
    136 __initcall_end = .; 
    137 } 
    连接脚本里面,'.'代表当前位置,所以__initcall_start和__initcall_end分别代表.initcall.init段的开始位置和结束位置,这种东西可以理解为一个常量,后者我们下面马上会用到。 
    而INITCALLS则在include/asm-generic/vmlinux.lds.h里面定义: 
    363 #define INITCALLS \ 
    364 *(.initcallearly.init) \ 
    365 VMLINUX_SYMBOL(__early_initcall_end) = .; \ 
    366 *(.initcall0.init) \ 
    367 *(.initcall0s.init) \ 
    368 *(.initcall1.init) \ 
    369 *(.initcall1s.init) \ 
    370 *(.initcall2.init) \ 
    371 *(.initcall2s.init) \ 
    372 *(.initcall3.init) \ 
    373 *(.initcall3s.init) \ 
    374 *(.initcall4.init) \ 
    375 *(.initcall4s.init) \ 
    376 *(.initcall5.init) \ 
    377 *(.initcall5s.init) \ 
    378 *(.initcallrootfs.init) \ 
    379 *(.initcall6.init) \ 
    380 *(.initcall6s.init) \ 
    381 *(.initcall7.init) \ 
    382 *(.initcall7s.init) 
    像__initcall_start和__initcall_end一样,__early_initcall_end的值也是段里面的一个位置。 
    上面两段话合起来就是说在连接内核时,把所有放在中间文件(输入文件)中的.initcallxx.init段里面的东西都连接到输出文件的.initcall.init段中。 
    所以我们刚才的那个函数指针最后会在目标文件的.initcall.init段中.那现在问题是谁会去管这个段? 它就是do_initcalls 
    (init/main.c) 
    749 static void __init do_initcalls(void) 
    750 { 
    751 initcall_t *call; 
    752 
    753 for (call = __early_initcall_end; call < __initcall_end; call++) 
    754 do_one_initcall(*call); 
    755 
    756 /* Make sure there is no pending stuff from the initcall sequence 
    */ 
    757 flush_scheduled_work(); 
    758 } 
    do_initcalls()遍历一次放在__early_initcall_end和__initcall_end之间的函数指针,并依次调用它们指向的函数,当然包括我们的init_kgdboc()了。那do_initcalls()又是在那里被调用了呢? 
    start_kernel() 
    '->rest_init() 
    '->kernel_init() 
    '->do_basic_setup() 
    '->do_initcalls() 
    这样清楚了吧,do_initcalls是在init进程里面才被调用的。init_kgdboc()所做的事情由于本人知识有限,暂时说不出来,不过它所干的最重要的两件事情是:根据内核启动参数寻找对应的驱动和在初始化完成后直接执行一条asm("int $3")汇编手动触发一个中断,把控制权交给kgdb。
     
     
     
    kgdb调试linux内核(针对2.6内核)
    http://kgdb.linsyssoft.com/downloads.htm
    下载相应的KGDB内核补丁。
    从linux内核官方网站上下载对应的版本内核。对内核打补丁,打补丁时要根据KGDB的README和series文件说明,按顺序打。
    软硬件准备
    环境:
    一台开发机developer(192.168.0.1com1),一台测试机target(192.168.0.2 com1),都预装redhat 9;一根串口线
    测试串口线
    物理连接好串口线后,使用一下命令进行测试,stty可以对串口参数进行设置
    在developer上执行:
    stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
    echo hello > /dev/ttyS0
    在target上执行:
    stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
    cat /dev/ttyS1
    串口线没问题的话在target的屏幕上显示hello
    =========== 通过串口 =============
    在Kernel hacking配置项中将以下三项编译进内核
    KGDB: Remote (serial) kernel debugging with gdb
    KGDB: Thread analysis
    KGDB: Console messages through gdb
    *注意(1) Serial port number for KGDB 这个选项表示选择哪个串口,1表示ttyS1,0表示ttyS0。
    使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具)
    #scp arch/i386/boot/bzImage root@192.168.0.2:/boot/vmlinuz-2.4.23-kgdb
    *在target机器上
    编辑/etc/grub.conf文件,加入以下行:
    #vi /etc/grub.conf
    title Red Hat Linux (2.4.23-kgdb)
    root (hd0,0)
    kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1
    #
    重起机器
    在developer上的刚编译的内核源代码目录下,使用gdb命令调试新的内核。
    # gdb vmlinux
    GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
    Copyright 2003 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB. Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux-gnu"...
    (gdb) set remotebaud 115200
    (gdb) target remote /dev/ttyS0
    如果成功的话,下面会出现进入断点的信息。
    ============ 通过网络 ============
    变异内核的时候选择通过网络调试。
    Method for KGDB communication (KGDB: On ethernet - in kernel)
    grub的设置
    kernel vmlinuz ro root=/dev/hda1 kgdboe=@192.168.0.2/,◎192.168.0.1/
    可以参考README文件。
    在developer上的刚编译的内核源代码目录下,使用gdb命令调试新的内核。
    # gdb vmlinux
    (gdb) target remote udp:192.168.0.2:6443
    如果成功的话,下面会出现进入断点的信息。
    后记:
    如果只vmlinux文件拷贝到其他机器上,也可以调试,但看不到断点处的代码,因为vmlinux所在的目录下面没有源代码。而且它使用的是绝对路径,vmlinux所在的路径改变了,也会看不到源代码
     
     
    4.6. 调试器和相关工具
    调试模块的最后手段是使用调试器来单步调试代码, 查看变量值和机器寄存器. 这个方法费时, 应当尽量避免. 但是, 通过调试器获得的代码的细粒度视角有时是很有价值的.
    在内核上使用一个交互式调试器是一个挑战. 内核代表系统中的所有进程运行在自己的地址空间. 结果, 用户空间调试器所提供的一些普通功能, 例如断点和单步, 在内核中更难得到. 本节中, 我们看一下几个调试内核的方法; 每个都有缺点和优点.
    gdb 对于看系统内部是非常有用. 在这个级别精通调试器的使用要求对 gdb 命令有信心, 需要理解目标平台的汇编代码, 以及对应源码和优化的汇编码的能力.
    调试器必须把内核作为一个应用程序来调用. 除了指定内核映象的文件名之外, 你需要在命令行提供一个核心文件的名子. 对于一个运行的内核, 核心文件是内核核心映象, /proc/kcore. 一个典型的 gdb 调用看来如下:
    gdb /usr/src/linux/vmlinux /proc/kcore
    第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage 或者给启动环境特别编译的任何东东.
    gdb 命令行的第二个参数是核心文件的名子. 如同任何 /proc 中的文件, /proc/kcore 是在被读的时候产生的. 当 read 系统调用在 /proc 文件系统中执行时, 它映射到一个数据产生函数,而不是一个数据获取函数; 我们已经在本章"使用 /proc 文件系统"一节中利用了这个特点. kcore 用来代表内核"可执行文件", 以一个核心文件的形式; 它是一个巨大的文件, 因为他代表整个的内核地址空间, 对应于所有的物理内存. 从 gdb 中, 你可查看内核变量,通过发出标准 gdb 命令. 例如, p jiffies 打印时钟的从启动到当前时间的嘀哒数.
    当你从gdb打印数据, 内核仍然在运行, 各种数据项在不同时间有不同的值; 然而, gdb 通过缓存已经读取的数据来优化对核心文件的存取. 如果你试图再次查看 jiffies 变量, 你会得到和以前相同的答案. 缓存值来避免额外的磁盘存取对传统核心文件是正确的做法, 但是在使用一个"动态"核心映象时就不方便. 解决方法是任何时候你需要刷新 gdb 缓存时发出命令 core-file /proc/kcore; 调试器准备好使用新的核心文件并且丢弃任何旧信息. 然而, 你不会一直需要发出 core-file 在读取一个新数据时; gdb 读取核心以多个几KB的块的方式, 并且只缓存它已经引用的块.
    gdb 通常提供的不少功能在你使用内核时不可用. 例如, gdb 不能修改内核数据; 它希望在操作内存前在它自己的控制下运行一个被调试的程序. 也不可能设置断点或观察点, 或者单步过内核函数.
    注意, 为了给 gdb 符号信息, 你必须设置 CONFIG_DEBUG_INFO 来编译你的内核. 结果是一个很大的内核映象在磁盘上, 但是, 没有这个信息, 深入内核变量几乎不可能.
    有了调试信息, 你可以知道很多内核内部的事情. gdb 愉快地打印出结构, 跟随指针, 等等. 而有一个事情比较难, 然而, 是检查 modules. 因为模块不是传递给gdb 的 vmlinux 映象, 调试器对它们一无所知. 幸运的是, 作为 2.6.7 内核, 有可能教给 gdb 需要如何检查可加载模块.
    Linux 可加载模块是 ELF 格式的可执行映象; 这样, 它们被分成几个节. 一个典型的模块可能包含一打或更多节, 但是有 3 个典型的与一次调试会话相关:
    .text
    这个节包含有模块的可执行代码. 调试器必须知道在哪里以便能够给出回溯或者设置断点.( 这些操作都不相关, 当运行一个调试器在 /proc/kcore 上, 但是它们在使用 kgdb 时可能有用, 下面描述).
    .bss
    .data 
    这 2 个节持有模块的变量. 在编译时不初始化的任何变量在 .bss 中, 而那些要初始化的在 .data 里.
    使 gdb 能够处理可加载模块需要通知调试器一个给定模块的节加载在哪里. 这个信息在 sysfs 中, 在 /sys/module 下. 例如, 在加载 scull 模块后, 目录 /sys/module/scull/sections 包含名子为 .text 的文件; 每个文件的内容是那个节的基地址.
    我们现在该发出一个 gdb 命令来告诉它关于我们的模块. 我们需要的命令是 add-symble-flile; 这个命令使用模块目标文件名, .text 基地址作为参数, 以及一系列描述任何其他感兴趣的节安放在哪里的参数. 在深入位于 sysfs 的模块节数据后, 我们可以构建这样一个命令:
    (gdb) add-symbol-file .../scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0
    我们已经包含了一个小脚本在例子代码里( gdbline ), 它为给定的模块可以创建这个命令.
    我们现在使用 gdb 检查我们的可加载模块中的变量. 这是一个取自 scull 调试会话的快速例子:
    (gdb) add-symbol-file scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0
    add symbol table from file "scull.ko" at
    .text_addr = 0xd0832000
    .bss_addr = 0xd0837100
    .data_addr = 0xd0836be0
    (y or n) y
    Reading symbols from scull.ko...done.
    (gdb) p scull_devices[0]
    $1 = {data = 0xcfd66c50,
    quantum = 4000,
    qset = 1000,
    size = 20881,
    access_key = 0,
    ...}
    这里我们看到第一个 scull 设备当前持有 20881 字节. 如果我们想, 我们可以跟随数据链, 或者查看其他任何感兴趣的模块中的东东.
    这是另一个值得知道的有用技巧:
    (gdb) print *(address)
    这里, 填充 address 指向的一个 16 进制地址; 输出是对应那个地址的代码的文件和行号. 这个技术可能有用, 例如, 来找出一个函数指针真正指向哪里.
    我们仍然不能进行典型的调试任务, 如设置断点或者修改数据; 为进行这些操作, 我们需要使用象 kdb( 下面描述 ) 或者 kgdb ( 我们马上就到 )这样的工具.
    许多读者可能奇怪为什么内核没有建立更多高级的调试特性在里面.答案, 非常简单, 是 Linus 不相信交互式的调试器. 他担心它们会导致不好的修改, 这些修改给问题打了补丁而不是找到问题的真正原因. 因此, 没有内嵌的调试器.
    其他内核开发者, 但是, 见到了交互式调试工具的一个临时使用. 一个这样的工具是 kdb 内嵌式内核调试器, 作为来自 oss.sgi.com 的一个非官方补丁. 要使用 kdb, 你必须获得这个补丁(确认获得一个匹配你的内核版本的版本), 应用它, 重建并重安装内核. 注意, 直到本书编写时, kdb 只在IA-32(x86)系统中运行(尽管一个给 IA-64 的版本在主线内核版本存在了一阵子, 在被去除之前.)
    一旦你运行一个使能了kdb的内核, 有几个方法进入调试器. 在控制台上按下 Pause(或者 Break) 键启动调试器. kdb 在一个内核 oops 发生时或者命中一个断点时也启动, 在任何一种情况下, 你看到象这样的一个消息:
    Entering kdb (0xc0347b80) on processor 0 due to Keyboard Entry
    [0]kdb>
    注意, 在kdb运行时内核停止任何东西. 在你调用 kdb 的系统中不应当运行其他东西; 特别, 你不应当打开网络 -- 除非, 当然, 你在调试一个网络驱动. 一般地以单用户模式启动系统是一个好主意, 如果你将使用 kdb.
    作为一个例子, 考虑一个快速 scull 调试会话. 假设驱动已经加载, 我们可以这样告诉 kdb 在 sucll_read 中设置一个断点:
    [0]kdb> bp scull_read
    Instruction(i) BP #0 at 0xcd087c5dc (scull_read)
    is enabled globally adjust 1
    [0]kdb> go
    bp 命令告诉 kdb 在下一次内核进入 scull_read 时停止. 你接着键入 go 来继续执行. 在将一些东西放入一个 scull 设备后, 我们可以试着通过在另一个终端的外壳下运行 cat 命令来读取它, 产生下面:
    Instruction(i) breakpoint #0 at 0xd087c5dc (adjusted)
    0xd087c5dc scull_read: int3
    Entering kdb (current=0xcf09f890, pid 1575) on processor 0 due to
    Breakpoint @ 0xd087c5dc
    [0]kdb>
    我们现在位于 scull_read 的开始. 为看到我们任何到那里的, 我们可以获得一个堆栈回溯:
    [0]kdb> bt
    ESP EIP Function (args)
    0xcdbddf74 0xd087c5dc [scull]scull_read
    0xcdbddf78 0xc0150718 vfs_read+0xb8
    0xcdbddfa4 0xc01509c2 sys_read+0x42
    0xcdbddfc4 0xc0103fcf syscall_call+0x7
    [0]kdb>
    kdb 试图打印出调用回溯中每个函数的参数. 然而, 它被编译器的优化技巧搞糊涂了. 因此, 它无法打印 scull_read 的参数.
    到时候查看一些数据了. mds 命令操作数据; 我们可以查询 schull_devices 指针的值, 使用这样一个命令:
    [0]kdb> mds scull_devices 1
    0xd0880de8 cf36ac00 ....
    这里我们要求一个(4字节)字, 起始于 scull_devices 的位置; 答案告诉我们的设备数组在地址 0xd0880de8; 第一个设备结构自己在 0xcf36ac00. 为查看那个设备结构, 我们需要使用这个地址:
    [0]kdb> mds cf36ac00
    0xcf36ac00 ce137dbc ....
    0xcf36ac04 00000fa0 ....
    0xcf36ac08 000003e8 ....
    0xcf36ac0c 0000009b ....
    0xcf36ac10 00000000 ....
    0xcf36ac14 00000001 ....
    0xcf36ac18 00000000 ....
    0xcf36ac1c 00000001 ....
    这里的 8 行对应于 scull_dev 结构的开始部分. 因此, 我们看到第一个设备的内存位于 0xce137dbc, quantum 是 4000 (16进制 fa0), 量子集大小是 1000 (16进制 3e8 ), 当前有 155( 16进制 9b) 字节存于设备中.
    kdb 也可以改变数据. 假想我们要截短一些数据从设备中:
    [0]kdb> mm cf26ac0c 0x50
    0xcf26ac0c = 0x50
    在设备上一个后续的 cat 会返回比之前少的数据.
    kdb 有不少其他功能, 包括单步(指令, 不是 C 源码的一行), 在数据存取上设置断点, 反汇编代码, 步入链表, 存取寄存器数据, 还有更多. 在你应用了 kdb 补丁后, 一个完整的手册页集能够在你的源码树的 documentation/kdb 下发现.
    4.6.3. kgdb 补丁
    目前为止我们看到的 2 个交互式调试方法( 使用 gdb 于 /proc/kcore 和 kdb) 都缺乏应用程序开发者已经熟悉的那种环境. 如果有一个真正的内核调试器支持改变变量, 断点等特色, 不是很好?
    确实, 有这样一个解决方案. 在本书编写时, 2 个分开的补丁在流通中, 它允许 gdb, 具备完全功能, 针对内核运行. 这 2 个补丁都称为 kgdb. 它们通过分开运行测试内核的系统和运行调试器的系统来工作; 这 2 个系统典型地是通过一个串口线连接起来. 因此, 开发者可以在稳定地桌面系统上运行 gdb, 而操作一个运行在专门测试的盒子中的内核. 这种方式建立 gdb 开始需要一些时间, 但是很快会得到回报,当一个难问题出现时.
    这些补丁目前处于健壮的状态, 在某些点上可能被合并, 因此我们避免说太多, 除了它们在哪里以及它们的基本特色. 鼓励感兴趣的读者去看这些的当前状态.
    第一个 kgdb 补丁当前在 -mm 内核树里 -- 补丁进入 2.6 主线的集结场. 补丁的这个版本支持 x86, SuperH, ia64, x86_64, 和 32位 PPC 体系. 除了通过串口操作的常用模式, 这个版本的 kgdb 可以通过一个局域网通讯. 使能以太网模式并且使用 kgdboe参数指定发出调试命令的 IP 地址来启动内核. 在 Documentation/i386/kgdb 下的文档描述了如何建立.[16]
    作为一个选择, 你可使用位于 http://kgdb.sf.net 的kgdb补丁. 这个调试器的版本不支持网络通讯模式(尽管据说在开发中), 但是它确实有内嵌的使用可加载模块的支持. 它支持 x86, x86_64, PowerPC, 和 S/390 体系.
     


    展开全文
  • 问题描述 在学习内核中 notifier_chain 的过程中,需要编写内核模块测试注册的 die notifier_handler 是否生效,就编写了一个非常简单的内核模块,在内核模块初始化函数中向 0 地址写值来触发内核 oops.主要的代码...

    问题描述

    在学习内核中 notifier_chain 的过程中,需要编写内核模块测试注册的 die notifier_handler 是否生效,就编写了一个非常简单的内核模块,在内核模块初始化函数中向 0 地址写值来触发内核 oops.主要的代码内容如下:

    static int __init my_init(void)
    {
      *((char *)NULL) = 1;
    
      return 0;
    }
    

    编译成功后第一次加载内核模块的时候,触发了 oops 用来测试,这之后再次加载此内核模块会卡住。

    使用 strace 跟踪 insmod 过程,发现 insmod 程序卡在 finit_module 系统调用处.具体的信息如下:

    getcwd("/home/longyu/test_insmod_module_oops", 4096) = 37
    stat("/home/longyu/test_insmod_module_oops/./mymodule.ko", {st_mode=S_IFREG|0644, st_size=278928, ...}) = 0
    openat(AT_FDCWD, "/home/longyu/test_insmod_module_oops/./mymodule.ko", O_RDONLY|O_CLOEXEC) = 3
    read(3, "\177ELF\2\1", 6)               = 6
    lseek(3, 0, SEEK_SET)                   = 0
    fstat(3, {st_mode=S_IFREG|0644, st_size=278928, ...}) = 0
    mmap(NULL, 278928, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5d42cbc000
    finit_module(3, "", 0
    

    这时在另外一个终端中执行 lsmod 后发现此模块已经加载且引用计数为 1。尝试 rmmod 模块,结果却失败了,添加 -f 选项也不能成功 remove.

    问题分析

    根据上面的情况,我觉得内核在加载模块的过程中调用 module_init 中注册的初始化函数前就会将此内核模块的引用计数设置为 1,init 函数执行成功后则减掉 1.

    在这种情况下,如果我们在加载模块过程中触发了 oops,正常的初始化流程会被打断,oops 处理后内核不再继续后续的模块初始化过程,这就导致内核认为现在已经有一个组件在使用加载的模块提供的功能,这时我们卸载模块,即便指定 -f 参数强制卸载也会失败,重新加载则会卡在 finit_module 函数上.

    这里的情况与一个模块重复加载是完全不同的,对于一个已经成功加载的模块,再次加载内核会报 File exist 的错误.

    执行模块 init 函数时模块的引用计数值是否为 1

    为了验证我的想法,我使用如下代码进行测试.在下面的代码中,我在模块的 init 函数中加了循环延时,模拟 init 函数执行异常的情况.

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/delay.h>
    
    static int __init my_init(void)
    {
      static long count = 0;
    
      while (count < 10000000) {
    	count++;
    	msleep(100);
      }
      return 0;
    }
    
    static void __exit my_exit(void)
    {
      return;
    }
    
    module_init(my_init);
    module_exit(my_exit);
    
    MODULE_LICENSE("GPL");
    

    加载后执行 lsmod 查看

    longyu@virt-debian10:~/test_insmod_module_oops$ lsmod | grep mymodule
    mymodule               16384  1
    longyu@virt-debian10:~/test_insmod_module_oops$ sudo rmmod mymodule
    rmmod: ERROR: Module mymodule is in use
    longyu@virt-debian10:~/test_insmod_module_oops$ sudo rmmod -f mymodule
    rmmod: ERROR: ../libkmod/libkmod-module.c:793 kmod_module_remove_module() could not remove 'mymodule': Device or resource busy
    rmmod: ERROR: could not remove module mymodule: Device or resource busy
    

    可以看到此时模块的引用计数为 1,卸载模块会失败,报错信息表明模块正在被使用.确认了这点之后,我想找到相应的内核代码,就研究了下内核模块的加载流程,找到了设置内核模块引用计数与解除引用计数的关键代码.

    初始化内核模块引用计数的函数

    linux-4.19 的内核源码中,module_unload_init 初始化内核模块的引用计数,相关代码如下:

    /* MODULE_REF_BASE is the base reference count by kmodule loader. */
    #define MODULE_REF_BASE	1
    
    /* Init the unload section of the module. */
    static int module_unload_init(struct module *mod)
    {
    	/*
    	 * Initialize reference counter to MODULE_REF_BASE.
    	 * refcnt == 0 means module is going.
    	 */
    	atomic_set(&mod->refcnt, MODULE_REF_BASE);
    
    	INIT_LIST_HEAD(&mod->source_list);
    	INIT_LIST_HEAD(&mod->target_list);
    
    	/* Hold reference count during initialization. */
    	atomic_inc(&mod->refcnt);
    
    	return 0;
    }
    

    这里将内核模块的 refcnt 设置为 MODULE_REF_BASE + 1 即值为 2.

    解除引用的地方

    模块的初始化函数在 do_init_module 函数中调用,当初始化函数执行完成后,内核调用 module_put 函数来解除引用,调用 module_put 会使 refcnt 的值减 1.相关的代码如下:

    	/* Drop initial reference. */
    	module_put(mod);
    	trim_init_extable(mod);
    

    module_put 函数代码如下:

    void module_put(struct module *module)
    {
    	int ret;
    
    	if (module) {
    		preempt_disable();
    		ret = atomic_dec_if_positive(&module->refcnt);
    		WARN_ON(ret < 0);	/* Failed to put refcount */
    		trace_module_put(module, _RET_IP_);
    		preempt_enable();
    	}
    }
    

    可以看到 module_put 函数会在 refcnt 为正整数时将其值减 1.在 module_unload_init 中 refcnt 被设置为 2,这里再减去 1,那么 refcnt 应该为 1,可是模块正常加载后查看 /sys/modules/module_name/refcnt 却发现值为 0,那么问题来了这个 refcnt 的值是在哪里减为 0 的呢?

    模块加载完成后 refcnt 的值是怎样减为 0 的呢?

    我重新阅读 finit_module 相关代码,没有发现其它解除引用的点.直接修改代码,在 do_init_module 函数调动后添加打印,确认是否内核中的 refcnt 在 do_init_module 函数的其它子函数中被修改了.

    相关代码如下:

        err = do_init_module(mod);
    
        printk(KERN_INFO "mod->refcnt is %d\n", mod->refcnt);
        return err;  
    

    重新编译内核,测试发现加载模块后,dmesg 打印了如下信息:

    / # modprobe tun
    tun: Universal TUN/TAP device driver, 1.6
    mod->refcnt is 1
    

    可以看到这里 refcnt 的值确实为 1,在这个点后函数逐级返回,然后系统调用完成返回用户态.我猜测这个可能是用户态的 insmod 命令干的事情,

    真的是 insmod 命令修改的 refcnt 的值吗?

    我这里有个疑惑,如果是 insmod 命令修改的内核中模块的 refcnt 值,可能有两种方式:

    1. 通过其它系统调用完成
    2. 通过操作 sys 下的文件完成

    第一种方式可以通过 strace insmod 命令查看,没有发现可疑的其它系统调用,排除了这种可能.

    第二种方式我没有立刻排除,而是准备下载 insmod 命令的源码来寻找蛛丝马迹.

    insmod 命令是指向 kmod 命令的链接,我执行 apt-get source kmod 下载 kmod 命令的源代码.

    insmod 命令的源码在 kmod 源码目录下的 tools/insmod.c 中,与我这里的问题相关的代码内容如下:

        err = kmod_module_insert_module(mod, flags, opts);
        if (err < 0) {
            ERR("could not insert module %s: %s\n", filename,
                mod_strerror(-err));
        }   
        kmod_module_unref(mod);
    
    end:
        kmod_unref(ctx);
        free(opts);
        return err >= 0 ? EXIT_SUCCESS : EXIT_FAILURE;
    

    这里 kmod_module_insert_module 函数通过系统调用 init_module 或 finit_module 来加载内核.init_module 与 finit_module 系统调用的关键区别在于 ko 文件内容传递到内核的方式不同.

    init_module 通过传统的 copy_from_user 来拷贝 ko 代码,finit_module 则通过文件描述符来调用 vfs 底层的读函数来读取 ko 代码到内核中.

    在上面的代码中 kmod_module_unref 与 kmod_unref 都比较可疑,但是经过阅读代码,没有看到任何操作 /sys 目录的代码,这说明 insmod 命令并不参与 refcnt 减为 0 的过程.

    refcnt 真的减为 0 了吗?

    内核模块在加载的过程中会在 /sys/module 目录中创建一个单独的目录,并且按照既定的模板创建文件.

    一个典型的示例如下:

    /sys/module/tun # ls 
    coresize   holders    initsize   initstate  notes      refcnt     sections   taint      uevent
    

    这是通过调用 sysfs 相关的函数创建的,这些文件一些与普通文件一样可以通过标准的 read 与 write 函数来读写,vfs 屏蔽底层文件系统的差异.

    执行 ls -l 命令得到的输出信息如下:

    /sys/module/tun # ls -lh *
    -r--r--r--    1 0        0           4.0K Aug  8 09:09 coresize
    -r--r--r--    1 0        0           4.0K Aug  8 09:09 initsize
    -r--r--r--    1 0        0           4.0K Aug  8 09:09 initstate
    -r--r--r--    1 0        0           4.0K Aug  8 08:32 refcnt
    -r--r--r--    1 0        0           4.0K Aug  8 09:09 taint
    --w-------    1 0        0           4.0K Aug  8 08:32 uevent
    

    可以看到 refcnt 文件所有用户只有读权限,这也就意味着用户态的程序是无法修改 refcnt 文件内容的,之前怀疑 insmod 命令搞鬼是方向上的错误.

    refcnt sysfs 文件创建相关代码

    module.c 中 refcnt sysfs 文件创建的相关代码如下:

    int module_refcount(struct module *mod)
    {
    	return atomic_read(&mod->refcnt) - MODULE_REF_BASE;
    }
    
    .............
    
    static struct module_attribute modinfo_refcnt =
    	__ATTR(refcnt, 0444, show_refcnt, NULL);
    
    static ssize_t show_refcnt(struct module_attribute *mattr,
    			   struct module_kobject *mk, char *buffer)
    {
    	return sprintf(buffer, "%i\n", module_refcount(mk->mod));
    }
    

    这里 show_refcnt 函数就是我们在读取 /sys/module/xx/refcnt 文件时最底层执行的函数,这里 buffer 的内容就是我们将读到的值,可以看到这里调用了 module_refcount 来计算 refcnt 的值,而不是直接使用 refcnt 的值.

    module_refcount 函数中将模块的 refcnt 减掉 MODULE_REF_BASE 的值返回,这就是我们在用户态看到的 refcnt 为 0 的原因(MODULE_REF_BASE 宏的值为 1).

    这说明在这个问题里面其实根本不存在什么 refcnt 减掉 1 的操作这只是一个偏见

    内核模块加载过程中创建 sysfs 文件的相关过程

    最底层的函数与相关的结构体

    上文中我提到的 module_attribue 结构体的完整定义如下:

    struct module_attribute {
    	struct attribute attr;
    	ssize_t (*show)(struct module_attribute *, struct module_kobject *,
    			char *);
    	ssize_t (*store)(struct module_attribute *, struct module_kobject *,
    			 const char *, size_t count);
    	void (*setup)(struct module *, const char *);
    	int (*test)(struct module *);
    	void (*free)(struct module *);
    };
    

    module_attribute 可以看做是一个类,它继承了 attribute 基类的内容,并扩展了自己的方法.

    struct attribute 结构体定义如下:

    struct attribute {
    	const char		*name;
    	umode_t			mode;
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
    	bool			ignore_lockdep:1;
    	struct lock_class_key	*key;
    	struct lock_class_key	skey;
    #endif
    };
    

    attribute 是所有 attribute 的抽象,它描述了每一个 attribue 都具有的 name 与 mode 属性.__ATTR 宏用来初始化 struct module_attribute 结构体的字段,其定义如下:

    #define __ATTR(_name, _mode, _show, _store) {				\
    	.attr = {.name = __stringify(_name),				\
    		 .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },		\
    	.show	= _show,						\
    	.store	= _store,						\
    }
    

    可以看到这里设置了 show 与 store 函数指针的值,实际上这两个函数就是我们在读写 sysfs 文件内容时会调用到的底层函数.

    module_attribute 中的 show 与 store 接口

    show 是我们在读取 sysfs 下的普通文件内容时调用到的底层函数,store 是我们在写入 sysfs 下的普通文件时调用到的底层函数.

    show 与 store 函数中 buffer 中将存放我们最终读取或写入的内容.可以看到 refcnt attribute 中只初始化了 show 函数,没有初始化 store 函数,这与我们看到的 refcnt sys 文件只能读取是一致的.

    cat /sys/module/xx/refcnt 的执行流程

    熟悉了底层的 show 与 store 函数接口后,我修改 module.c,在 refcount 中添加 dump_stack 函数来打印内核堆栈.修改后的 show_refcnt 函数内容如下:

    static ssize_t show_refcnt(struct module_attribute *mattr,
                   struct module_kobject *mk, char *buffer)
    {
        dump_stack();                                                                                                                                                            
        return sprintf(buffer, "%i\n", module_refcount(mk->mod));
    }
    

    重新编译内核,读取 refcnt 文件的值,这时内核打印出的堆栈信息如下:

    / # cat /sys/module/tun/refcnt 
    CPU: 0 PID: 249 Comm: cat Not tainted 4.19.98 #3
    Stack:
     62efb810 602d9ed6 6242f000 638d53d0
     602efb90 6242f000 62efb820 602d9f1b
     62efb840 60083999 624d6000 624df708
    Call Trace:
     [<6006658d>] ? printk+0x0/0x94
     [<6001bfdf>] show_stack+0x13b/0x155
     [<602d9ed6>] ? dump_stack_print_info+0xe5/0xee
     [<602d9f1b>] dump_stack+0x2a/0x2c
     [<60083999>] show_refcnt+0x19/0x48
     [<6004ec17>] module_attr_show+0x18/0x24
     [<601398ac>] sysfs_kf_seq_show+0xa7/0x15a
     [<60138022>] kernfs_seq_show+0x1c/0x1e
     [<600f26ab>] seq_read+0x1c8/0x3ca
     [<6002de83>] ? set_signals+0x30/0x36
     [<601389b7>] kernfs_fop_read+0x38/0x1c4
     [<6009c833>] ? __alloc_pages_nodemask+0x120/0xa9c
     [<600d0915>] do_iter_read+0xe7/0x191
     [<600d1da7>] vfs_readv+0x6b/0x9a
    

    这个内核堆栈的调用层次比表面上看上去要复杂的多.它涉及到了内核中多个抽象层次.下图是不同层次的一个关系图:
    在这里插入图片描述
    我自上而下来描述.

    sysfs 文件系统调用的不同层次

    第一层 kernfs_fop_read 是 file_operations 结构体中 read 虚函数的一个实例,这一层由 VFS 层调用,屏蔽不同的文件系统的区别.对于 kernfs 这个文件系统来说,它有三类不同的文件类型:

    1. 目录
    2. 链接
    3. 普通文件

    这三种不同类型的文件每一种都实现一个不同的 file_operations 虚函数表.这一层屏蔽了不同文件类型的差异,不同的文件类型变化反应为不同的 file_operations 虚函数表.

    第二层中根据不同实现分为两种:

    1. 使用 seq file 接口
    2. 直接访问内存

    第三层中是对 seq file 接口的抽象层,屏蔽不同的 seq_file 接口的区别.在这一层上根据需求有不同的实现,这种变化封装到每一个 seq_operations 虚函数表中.搜索内核源码可以找到很多相关的实例,截取部分信息如下:

    ......
    ./trace/trace.c:4815:static const struct seq_operations tracing_saved_tgids_seq_ops = {
    ./trace/trace.c:4892:static const struct seq_operations tracing_saved_cmdlines_seq_ops = {
    ./trace/trace.c:5057:static const struct seq_operations tracing_eval_map_seq_ops = {
    ./kallsyms.c:622:static const struct seq_operations kallsyms_op = {
    ./module.c:4247:static const struct seq_operations modules_op = {
    

    第四层是对文件不同权限的适配层.根据读写权限的不同,主要分为以下四种情况:

    1. 只读
    2. 只写
    3. 读写

    这四种不同的权限控制的变化封装在 kernfs_ops 中,不同的权限对应不同的 kernfs_ops 函数表.

    实例代码如下:

    static const struct kernfs_ops sysfs_file_kfops_empty = {
    };
    
    static const struct kernfs_ops sysfs_file_kfops_ro = {
    	.seq_show	= sysfs_kf_seq_show,
    };
    
    static const struct kernfs_ops sysfs_file_kfops_wo = {
    	.write		= sysfs_kf_write,
    };
    
    static const struct kernfs_ops sysfs_file_kfops_rw = {
    	.seq_show	= sysfs_kf_seq_show,
    	.write		= sysfs_kf_write,
    ...........
    

    第五层中是 sys 目录中不同类功能的抽象,它抽象出了不同类别,内核模块的 sys 子目录就是不同类别中的一个实例,它有自己单独的 sysfs_ops 函数表的实现.

    相关的代码如下:

    static const struct sysfs_ops module_sysfs_ops = {
    	.show = module_attr_show,
    	.store = module_attr_store,
    };
    

    最后一层是 module_attribute 的抽象层,可以说它是单个类别中不同 sys 文件实现的抽象层.对于内核模块这个类别来说,它屏蔽了 module 的 sys 目录下不同功能文件的具体实现.

    重新 insmod 的时候进程卡在哪里?

    最后来分析下当一个模块初始化时 oops 之后重新 insmod 的时候 insmod 命令卡住的问题.

    首先我们需要了解的是内核模块的生命周期存在三个主要阶段:

    1. MODULE_STATE_GOING
    2. MODULE_STATE_COMING
    3. MODULE_STATE_LIVE

    GOING 是将要 unloading 的状态,COMING 是将要 loading 的状态,LIVE 是 loading 完成到 unloading 中间的工作状态.

    在 load_module 函数中,内核模块的状态首先会被设定为 COMING,然后在 add_unformed_module 函数中又被修改为 UNFORMED 状态,当初始化函数成功执行后状态会切换为 LIVE.

    add_unformed_module 函数负责将模块添加到内核中的模块链表中,不同的内核模块按照名字区别.

    当 add_unformed_module 函数执行时发现内核中模块链表中已经有了一个相应的内核模块节点,它会判断这个节点的状态,如果状态不为 LIVE,它会将 insmod 进程加入到 module_wq 队列中等待上一次加载完成,如果状态为 LIVE 则直接返回 -EEXIST,insmod 命令会打印 file exist 的报错信息.

    当内核中的模块链表中不存在新添加的模块时,模块可以正常添加到内核的模块链表中.注意修改内核模块链表是在获取 module_mutex 互斥信号量的前提下进行的,这样就避免了产生不一致的问题.

    add_unformed_module 函数代码如下:

    static int add_unformed_module(struct module *mod)
    {
    	int err;
    	struct module *old;
    
    	mod->state = MODULE_STATE_UNFORMED;
    
    again:
    	mutex_lock(&module_mutex);
    	old = find_module_all(mod->name, strlen(mod->name), true);
    	if (old != NULL) {
    		if (old->state != MODULE_STATE_LIVE) {
    			/* Wait in case it fails to load. */
    			mutex_unlock(&module_mutex);
    			err = wait_event_interruptible(module_wq,
    					       finished_loading(mod->name));
    			if (err)
    				goto out_unlocked;
    			goto again;
    		}
    		err = -EEXIST;
    		goto out;
    	}
    	mod_update_bounds(mod);
    	list_add_rcu(&mod->list, &modules);
    	mod_tree_insert(mod);
    	err = 0;
    
    out:
    	mutex_unlock(&module_mutex);
    out_unlocked:
    	return err;
    }
    

    说到这里问题的答案已经很明显了.当我在第一次加载那个测试模块时,它在初始化函数执行时产生了 oops 导致后续流程不能正常执行,模块的状态不能切换为 LIVE,这样当重新加载相同的模块时,insmod 命令会被内核挂起来等待旧模块加载完成的事件,然而这个事件永远不会发生了,insmod 就会一直阻塞.

    不过这时候你可以发送个信号给 insmod 命令,将其唤醒,然后它检测返回值判断唤醒原因并不是需要的事件已经发生,就进行异常处理,这样 insmod 命令就能够继续运行,打印报错信息了.

    总结

    从问题出发去研究内核代码是个很好的方法,但是对于问题本身的认识却可能将我们导向一个完全不同的方向.

    我们应该从一开始就质疑问题本身的存在性问题,然后再出发去寻找答案.可是在实践过程中,我们几乎不会这样做.我们总是过早的陷入到寻找答案的过程中,绕了一大圈可能才能意识到问题本身这个点上来,甚至于在寻找无果的情况下直接放弃,这也非常常见.

    这可以说是我们行动中的一个盲区,意识到这个盲区并不断的努力改进是我们能力提升的一个很大的突破点.力求确保自己从一开始就站在正确的方向上,这样才能逐渐逼近真相,没有这点保障,问题的解决就显得非常困难了.

    展开全文
  • 内核模块中对文件的读写

    千次阅读 2012-05-26 18:48:12
    在文件的所有实例都关闭后,内核释放这个 数据 结构。  在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。如下所示:  struct file {  union {  struct list_head fu_list; 文 件...

    一般可以用两种方法:

    第一种是用系统调用。

    第二种方法是filp->open()等函数。下面分别来说下这两种方法。


    1
    利用系统调用:
    sys_open,sys_write,sys_read等

    其实分析过sys_open可以知道,最后调用的也是filp->open。
    sys_open ==> do_sys_open ==> filp->open


    其实sys_open最后也是调用了filp->open。
    其实好像Linux2.6.20后面就不推荐使用sys_open,那我们这里就就后者进行详细的介绍

    2 filp->open等函数。
    在模块中,
    用户空间的open,read,write,llseek等函数都是不可以使用的。应该使用其在内核中对应的函数。可以使用filp->open配合struct file里的read/write来进行对文件的读写操作。

    struct file
      struct file结构体定义在include/linux/fs.h中定义。

           文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。

          它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。

         在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。如下所示:
      struct file {
      union {
      struct list_head fu_list; 文件对象链表指针linux/include/linux/list.h
      struct rcu_head fu_rcuhead; RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
      } f_u;
      struct path f_path;     包含dentry和mnt两个成员,用于确定文件路径
      #define f_dentry f_path.dentry   f_path的成员之一,当前文件的dentry结构
      #define f_vfsmnt f_path.mnt      表示当前文件所在文件系统的挂载根目录
      const struct file_operations *f_op; 与该文件相关联的操作函数
      atomic_t f_count;        文件的引用计数(有多少进程打开该文件)
      unsigned int f_flags;     对应于open时指定的flag
      mode_t f_mode;         读写模式:open的mod_t mode参数
      off_t f_pos;               该文件在当前进程中的文件偏移量
      struct fown_struct f_owner;         该结构的作用是通过信号进行I/O时间通知的数据。
      unsigned int f_uid, f_gid;          文件所有者id,所有者组id
      struct file_ra_state f_ra;            在linux/include/linux/fs.h中定义,文件预读相关
      unsigned long f_version;
      #ifdef CONFIG_SECURITY
      void *f_security;
      #endif
                                /* needed for tty driver, and maybe others */
      void *private_data;
      #ifdef CONFIG_EPOLL
                            /* Used by fs/eventpoll.c to link all the hooks to this file */
      struct list_head f_ep_links;
      spinlock_t f_ep_lock;
      #endif                          /* #ifdef CONFIG_EPOLL */
      struct address_space *f_mapping;
      };

     

    例子1:

    1. #include <linux/kernel.h>
    2. #include <linux/module.h>
    3. #include <linux/fs.h>
    4. #include <asm/uaccess.h>
    5. #include <linux/mm.h>

    6. MODULE_AUTHOR("Kenthy@163.com.");
    7. MODULE_DESCRIPTION("Kernel study and test.");


    8. void fileread(const char * filename)
    9. {
    10.   struct file        *filp; //  文件结构体代表一个打开的文件
    11.   struct inode     *inode; //内核中用inode结构表示具体的文件,而用file结构表示打开的文件描述符
    12.   mm_segment_t       fs;
    13. /*其中:
      typedef struct {
      unsigned long seg;
      } mm_segment_t;
      */

    14.   off_t   fsize;
    15.   char    *buf;
    16.   unsigned long  magic;
    17.   printk("<1>start....\n");
    18.   filp=filp_open(filename,O_RDONLY,0);          //文件结构
    19.   inode=filp->f_dentry->d_inode;                          //和具体文件联系一起
    20.   
    21.   magic=inode->i_sb->s_magic;                        //

    22.   printk("<1>file system magic:%li \n",magic);
    23.   printk("<1>super blocksize:%li \n",inode->i_sb->s_blocksize);
    24.   printk("<1>inode %li \n",inode->i_ino);
    25.   fsize=inode->i_size;
    26.   printk("<1>file size:%i \n",(int)fsize);
    27.   buf=(char *) kmalloc(fsize+1,GFP_ATOMIC);    //

    28.   fs=get_fs();
    29.   set_fs(KERNEL_DS);
    30.   filp->f_op->read(filp,buf,fsize,&(filp->f_pos));
    31.   set_fs(fs);

    32.   buf[fsize]='\0';
    33.   printk("<1>The File Content is:\n");
    34.   printk("<1>%s",buf);


    35.   filp_close(filp,NULL);
    36. }

    37. void filewrite(char* filename, char* data)
    38. {
    39.   struct file *filp;
    40. mm_segment_t fs;
    41. filp = filp_open(filename, O_RDWR|O_APPEND, 0644);
    42. if(IS_ERR(filp))
    43.     {
    44.       printk("open error...\n");
    45.       return;
    46.         }   

    47.   fs=get_fs();
    48.   set_fs(KERNEL_DS);
    49.   filp->f_op->write(filp, data, strlen(data),&filp->f_pos);
    50.   set_fs(fs);
    51.   filp_close(filp,NULL);
    52. }

    53. int init_module()
    54. {
    55.   char *filename="/root/test1.c";

    56.   printk("<1>Read File from Kernel.\n");
    57.   fileread(filename);
    58.   filewrite(filename, "kernel write test\n");
    59.   return 0;
    60. }

    61. void cleanup_module()
    62. {
    63.   printk("<1>Good,Bye!\n");
    64. }
    复制代码

     

    基本思想:
    一个是要记得编译的时候加上-D__KERNEL_SYSCALLS__  
    另外源文件里面要#include   <linux/unistd.h>  
    如果报错,很可能是因为使用的缓冲区超过了用户空间的地址范围。

    一般系统调用会要求你使用的缓冲区不能在内核区。这个可以用set_fs()、get_fs()来解决。

    在读写文件前先得到当前fs:  
    mm_segment_t   old_fs=get_fs();  
    并设置当前fs为内核fs:

    set_fs(KERNEL_DS);  
    在读写文件后再恢复原先fs:  

    set_fs(old_fs);  


    set_fs()、get_fs()等相关宏在文件include/asm/uaccess.h中定义。  
    个人感觉这个办法比较简单。  

     在linux内核编程时,进行系统调用(如文件操作)时如果要访问用户空间的参数,可以用set_fs,get_ds等函数实现访问

    get_ds获得kernel的内存访问地址范围(IA32是4GB),

    set_fs是设置当前的地址访问限制值,

    get_fs是取得当前的地址访问限制值。

    进程由用户态进入核态,linux进程的task_struct结构中的成员addr_limit也应该由0xBFFFFFFF变为0xFFFFFFFF(addr_limit规定了进程有用户态核内核态情况下的虚拟地址空间访问范围,在用户态,addr_limit成员值是0xBFFFFFFF也就是有3GB的虚拟内存空间,在核心态,是0xFFFFFFFF,范围扩展了1GB)。

    使用这三个函数是为了安全性。为了保证用户态的地址所指向空间有效,函数会做一些检查工作。
    如果set_fs(KERNEL_DS),函数将跳过这些检查


    另外就是用flip_open函数打开文件,得到struct file *的指针fp。使用指针fp进行相应操作,如读文件可以用fp->f_ops->read。最后用filp_close()函数关闭文件。 filp_open()、filp_close()函数在fs/open.c定义,在include/linux/fs.h中声明。 

    解释一点:
    系统调用本来是提供给用户空间的程序访问的,所以,对传递给它的参数(比如上面的buf),它默认会认为来自用户空间,在->write()函数 中,为了保护内核空间,一般会用get_fs()得到的值来和USER_DS进行比较,从而防止用户空间程序“蓄意”破坏内核空间;

    --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



    eg2:

    1. #include<linux/module.h>
    2. #include<linux/kernel.h>
    3. #include<linux/init.h>

    4. #include<linux/types.h>

    5. #include<linux/fs.h>
    6. #include<linux/string.h>
    7. #include<asm/uaccess.h>                  /* get_fs(),set_fs(),get_ds() */

    8. #define FILE_DIR       "/root/test.txt"

    9. MODULE_LICENSE("GPL");
    10. MODULE_AUTHOR("kenthy@163.com");

    11. char *buff = "module read/write test";
    12. char tmp[100];

    13. static struct file *filp = NULL;

    14. static int __init    wr_test_init(void)
    15. {
    16.     mm_segment_t old_fs;
    17.     ssize_t ret;
    18.    
    19.     filp = filp_open(FILE_DIR, O_RDWR | O_CREAT, 0644);
    20.    
    21.     //    if(!filp)

    22.     if(IS_ERR(filp))
    23.         printk("open error...\n");
    24.    
    25.     old_fs = get_fs();
    26.     set_fs(get_ds());
    27.    
    28.     filp->f_op->write(filp, buff, strlen(buff), &filp->f_pos);
    29.    
    30.     filp->f_op->llseek(filp,0,0);
    31.     ret = filp->f_op->read(filp, tmp, strlen(buff), &filp->f_pos);
    32.    
    33.     set_fs(old_fs);
    34.    
    35.     if(ret > 0)
    36.         printk("%s\n",tmp);
    37.     else if(ret == 0)
    38.         printk("read nothing.............\n");
    39.     else
    40.         {
    41.             printk("read error\n");
    42.             return -1;
    43.         }

    44.     return 0;
    45. }

    46. static void __exit   wr_test_exit(void)
    47. {
    48.     if(filp)
    49.         filp_close(filp,NULL);
    50. }

    51. module_init(wr_test_init);
    52. module_exit(wr_test_exit);
    复制代码




    3.Makefile

    1. obj-m := os_attack.o

    2. KDIR := /lib/modules/$(uname -r)/build/
    3. PWD := $(shell pwd)

    4. all:module

    5. module:
    6.         $(MAKE) -C $(KDIR) M=$(PWD) modules


    7. clean:
    8.         rm -rf *.ko *.mod.c *.o Module.* modules.* .*.cmd .tmp_versions
    复制代码




    注意:
    在调用filp->f_op->read和filp->f_op->write等对文件的操作之前,应该先设置FS。
    默认情况下,filp->f_op->read或者filp->f_op->write会对传进来的参数buff进行指针检查。如果不是在用户空间会拒绝访问。因为是在内核模块中,所以buff肯定不在用户空间,所以要增大其寻址范围。

    拿filp->f_op->write为例来说明:
    filp->f_op->write最终会调用access_ok ==> range_ok.
    而range_ok会判断访问的地址是否在0 ~ addr_limit之间。如果在,则ok,继续。如果不在,则禁止访问。而内核空间传过来的buff肯定大于addr_limit。所以要set_fs(get_ds())。
    这些函数在asm/uaccess.h中定义。以下是这个头文件中的部分内容:

    #define MAKE_MM_SEG(s)    ((mm_segment_t) { (s) })

    #define KERNEL_DS    MAKE_MM_SEG(-1UL)
    #define USER_DS        MAKE_MM_SEG(PAGE_OFFSET)

    #define get_ds()    (KERNEL_DS)
    #define get_fs()    (current_thread_info()->addr_limit)
    #define set_fs(x)    (current_thread_info()->addr_limit = (x))

    #define segment_eq(a, b)    ((a).seg == (b).seg)


    可以看到set_fs(get_ds())改变了addr_limit的值。这样就使得从模块中传递进去的参数也可以正常使用了。

    在写测试模块的时候,要实现的功能是写进去什么,然后读出来放在tmp数组中。但写完了以后filp->f_ops已经在末尾了,这个时候读是什么也读不到的,如果想要读到数据,则应该改变filp->f-ops的值,这就要用到filp->f_op->llseek函数了。上网查了下,其中的参数需要记下笔记:


    系统调用:
    off_t sys_lseek(unsigned int fd, off_t offset, unsigned int origin)
    offset是偏移量。
    若origin是SEEK_SET(0),则将该文件的位移量设置为距文件开始处offset 个字节。
    若origin是SEEK_CUR(1),则将该文件的位移量设置为其当前值加offset, offset可为正或负。
    若origin是SEEK_END(2),则将该文件的位移量设置为文件长度加offset, offset可为正或负。
    展开全文
  • 一、内核模块开发 1.module开发 (0)引言  如果你正在开发的设备驱动程序中需要与用户层的接口,一般可选的方法有:  1.注册虚拟的字符设备文件(包含misc字符设备节点),以这个虚拟设备上的 read/write/ioctl ...
  • 曾经多少次想要在内核游荡?曾经多少次茫然不知方向?你不要再对着它迷惘,让我们指引你走向前方…… 内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同...
  • 一个简单多任务内核实例的分析

    千次阅读 2013-05-16 08:23:51
    一个简单多任务内核实例的分析 转载请注明出处:http://blog.csdn.net/rosetta   简介 Linux-0.00是由Linus Torvalds写的Linux最初版本(未发布),只是打印AAA和BBB而没有更多的功能,比如内存管理、文件...
  • Linux内核与用户空间有很大的不同:抛开漫不经心的态度,你要格外小心,因为在你代码中的一个小小的bug都会影响整个系统。这里没有简单的方法来做浮点运算、堆栈既固定又小,你写的代码总是异步所以你需要考虑并发性。...
  • 多进程+共享内存+信号量综合实例

    千次阅读 2016-12-25 22:25:13
    具体说明看注释,另外关于信号量的陷进可参考 http://os.51cto.com/art/201311/418977_all.htmhttp://blog.csdn.net/killmice/article/details/41516399测试框架#include #include #include <string.h>#include ...
  • 这点2.4的内核跟2.6的内核有所不同,郁闷了我一整天,2.4内核模块是以*.o形式的,而2.6内核是以*.ko形式的, 这有个改变的方法,参考: http://blog.chinaunix.net/articl ... 742&blogId=2662 [root@jiecho]# make ...
  • 实例解析linux内核I2C体系结构

    千次阅读 2011-10-19 22:56:51
    实例解析linux内核I2C体系结构 作者:刘洪涛,华清远见嵌入式学院讲师。 一、概述 谈到在linux系统下编写I2C驱动,目前主要有两种方式,一种是把I2C设备当作一个普通的字符设备来处理,另一种是利用linux I2C驱动...
  • 内核LCD驱动结构分析及实例分析

    千次阅读 2017-10-20 18:08:24
    内核中,LCD是作为帧缓冲设备来使用的,帧缓冲设备是标准的字符设备,主设备号为29。内核LCD驱动的结构包含了字符设备的结构和paltform设备的结构,以smdk2410为例,其主要涉及4个文件:  linux-2.6.22.6\arch\arm...
  • 前言: 是德科技(NYSE:KEYS)-原安捷伦电子测量事业部,是全球电子测量技术和市场的领导者,致力于推动无线通信、模块化和软件解决方案的持续创新,专注于为...STM32数字示波器源码+数字信号处理教程、配套实例截图:
  • ubuntu下2.6.21内核的驱动开发实例

    千次阅读 2007-08-07 18:35:00
    * 申请中断应该在设备打开的时候进行,而不要在模块初始化的时候进行,因为中断信号线非常有限,如果在模块初始化的时候申请了中断,那么驱动程序可能只是占用而从未使用,也会阻止起其他驱动使用该中断,而在设备...
  • 内核 内核主要是用来管理任务的,除了管理还提供了一些队列算法接口,方便产品使用。 2.模块化编程思维 对于单片机产品开发来说,我觉得可以分为3层:硬件层、中间层、应用层。 我们先来说说中间层和应用层。 中间层...
  • Linux内核0.11——内核体系结构

    千次阅读 2017-08-31 11:54:04
    Linux内核体系结构linux内核主要由五部分组成:进程调度模块、内存管理模块、文件系统模块、进程间通信模块、网络接口模块。如上图包括了各个部分的依赖关系,也大概表明了其在内核目录中的分布,其中进程调度模块是...
  • 在2.6内核中, netlink相关的接口函数随着版本的变化很大,现在网上流传的多数代码是以较老的版本(2.6.24以下)作为依托。这里,我将一段基于2.6.18的简单的代码移植到了2.6.27中,希望可以帮助大家理解其中的变化...
  • 10.5.2 内核模块的Makefiles文件 10.5.3 内核模块的多个文件 第十一章 设备驱动程序 11.1 概述 11.1.1 I/O软件 11.1.2 设备驱动程序 11.2 设备驱动基础 11.2.1 I/O端口 11.2.2 I/O接口及设备控制器 ...
  • linux内核开发

    千次阅读 2021-01-28 14:42:51
    内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同的:抛开...学习内核编程的最简单的方式也许就是写个内核模块:一段可以动态加载进内核的代码。模块所能做
  • Linux用户空间与内核空间、内核的功能与作用 在讲述内核的功能及其作用以前,先简单介绍一下内核空间和用户空间 用户空间和内核空间 概念简介 内核空间(User Space):用户程序的运行空间。为了安全,它们是隔离的...
  • python实例手册

    千次阅读 2017-05-27 12:22:17
    pip模块安装 pip官方安装脚本 加载环境变量 变量 打印 字符串 s 整数 d 浮点 f 原样打印 r 列表 列表元素的个数最多 536870912 元组 不可变 字典enumerate 使用函数得到索引值和对应值 tab补全 history file 函数 ...
  • Linux 死锁检测模块 Lockdep 简介 原文地址 http://kernel.meizu.com/linux-dead-lock-detect-lockdep.html 25 July 2016 死锁概念 死锁是指多个进程(线程)因为长久等待已被其他...
  • linux内核线程

    万次阅读 2018-10-16 18:22:49
    内核经常需要在后台执行一些操作,这种任务就可以通过内核线程(kernle thread)完成,内核线程是独立运行在内核空间的标准进程。...实际上,内核线程只能由其他内核线程创建,linux驱动模块中可以用kernel_threa...
  • Linux内核分析及内核编程

    千次阅读 2005-09-10 08:26:00
    121-01518-5900页88.00元(估价)倪 倪继利著2005年8月出版ISBN 7-121-01518-5900页88.00元(估价)倪 内 容 简 介 本书作者在整理自己多年研发笔记的基础上,以精心挑选的典型开发实例向读者详细地讲述了内核源...
  • 内核编译选项详解

    千次阅读 2014-11-26 22:07:58
    在内核映像启动以及内核模块加载时根据内核本身在内存中的地址,修正物理地址/虚拟地址相互转换函数。只针对非XIP方式运行的内核。 2、general setup ->cross-compiler tool prefix 在内核配置阶
  • 内核

    千次阅读 2011-03-02 22:59:00
    它曾经使用自旋锁实现,到了2.6.11版修改为信号量,可是在2.6.26-rc2又退回到使用自旋锁的老路上;它甚至引发了linux的创始人Linus Torvalds和著名的完全公平调度(CFS)算法的贡献者Ingo Molnar之间的一场争议。这...
  • Linux 内核引导选项简介

    千次阅读 2017-02-23 20:20:41
    概述内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 ...
  • Linux内核剖析 之 内核同步

    千次阅读 2015-05-28 08:48:52
    主要内容  1、内核请求何时以交错(interleave)的方式执行以及交错程度如何。... 为了更好地理解内核是如何执行的,我们把内核看做必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 21,610
精华内容 8,644
关键字:

信号内核模块实例