WHBBS文章发信人: scz (小四), 信区: Security WWW-POST 标 题: FreeBSD 4.0 动态内核链接机制(KLD)编程指南 发信站: 武汉白云黄鹤站 (Wed Nov 15 10:11:46 2000) , 站内信件 FreeBSD 4.0 动态内核链接机制(KLD)编程指南 原著:<> :Andrew Reiter < mailto: arr@watson.org > :http://www.watson.org/~arr/ 翻译:小四 < mailto: scz@nsfocus.com > :http://www.nsfocus.com 日期:2000-10-26 10:54 目录: ★ 简介 ★ 所有KLD的共性 ★ KLD系统调用实现框架 ★ KLD字符型设备驱动程序实现框架 ★ 参考资料 ★ 简介 本文的目的在于介绍FreeBSD操作系统下基础的KLD开发设计技术。 FreeBSD 3.1下提供过可加载内核模块技术(LKM),FreeBSD 4.0下提供的动态内 核链接机制(KLD),可以简单地理解成LKM的升级。采用KLD,可以增加系统调用、调 试设备驱动程序、提供访问内核数据空间的方便接口。下面我们对比一下LKM和KLD: -------------------------------------------------------------------------- 1. LKM采用用户态的链接器重定位二进制数据后再压入内核空间。 KLD机制由内核亲自进行重定位操作 2. LKM采用特殊的数据结构,LKM Driver了解这种数据结构,并通过它与内核交互, 比如VFS LKM采用一个结构,该结构里包含指向VFS TABLES的指针。 LKM的目的单纯明确,很难将LKM代码移植成真正的内核代码。 KLD采用常规代码,一个KLD文件可以不包含任何模块,也可以包含多个模块。每 个模块均包含自初始化代码,并完成自注册。 KLD的代码和内核代码保持一致。很容易从内核中提取部分代码移植成KLD代码。 3. 现在KLD的依赖关系和版本信息从内核里剥离出来,完全位于模块层。 -------------------------------------------------------------------------- 这份指南直奔两个KLD开发者感兴趣的主题,希望你具有基本的FreeBSD内核知识 以及K & R C编程技能。必须提醒的是,例子代码在FreeBSD 4.0下调试通过。下面我 们将要介绍的主题有三: -------------------------------------------------------------------------- 1. 所有KLD的共性 2. KLD系统调用实现框架 3. KLD字符型设备驱动程序实现框架 -------------------------------------------------------------------------- 本文的目的是帮助那些正在学习KLD编程的朋友快速掌握KLD编程接口,进入更高 层次。 ★ 所有KLD的共性 所有的KLD代码都有一个主入口函数和一个宏,并且简单地采用Makefile文件编译。 -------------------------------------------------------------------------- 1. 主入口函数,或者说加载/卸载句柄 2. DECLARE_MODULE()宏 3. 利用Makefile文件进行编译 -------------------------------------------------------------------------- 下面是一个典型的主入口函数: -------------------------------------------------------------------------- static int helloworld_load ( module_t mod, int what, void * arg ) { int err = 0; switch ( what ) { case MOD_LOAD: /* * uprintf() 是内核空间函数,类似于printf()。当在内核空间使用 * printf()时,输出内容需要用dmesg查看。uprintf()将直接输出到 * 当前正在使用的tty上 */ printf( "MOD_LOAD: dmesg -c test\n" ); uprintf( "System call loaded at slot: %d\n", syscall_num ); break; case MOD_UNLOAD: printf( "MOD_UNLOAD: dmesg -c test\n" ); uprintf( "System call unloaded from slot: %d\n", syscall_num ); break; case MOD_SHUTDOWN: uprintf( "System shutdown\n" ); break; default: err = EINVAL; break; } /* end of switch */ return( err ); } /* end of helloworld_load */ -------------------------------------------------------------------------- 该函数类似Linux下的init_module和cleanup_module,注意无论加载/卸载KLD,都要 经过该函数。函数名字自己定义,将来作为函数指针传递给DECLARE_MODULE()宏。当 使用kldload/kldunload加载/卸载KLD的时候,helloworld_load()被调用。 在/usr/include/sys/module.h里定义了一个函数指针类型: typedef int ( * modeventhand_t ) ( module_t mod, int /*modeventtype_t*/ what, void * arg ); helloworld_load()正是modeventhand_t型常量,从名字看,模块--事件--句柄,有 意思。 typedef struct module * module_t; module_t mod是指向module结构的指针。module结构按照链表方式组织,可以从结构 中获取指向其它module结构的指针。结构成员还包含诸如KLD ID号之类的有用信息。 int what实际是枚举类型变量,modeventtype_t( enum modeventtype ),目前只有 三个有效值: MOD_LOAD 执行kldload时被调用 MOD_UNLOAD 执行kldunload时被调用 MOD_SHUTDOWN shutdown时被调用 DECLARE_MODULE()对于KLD很重要,然而通常所见并不是DECLARE_MODULE(),有两个 宏封装了它,使得编程更加方便。/usr/include/sys/module.h里定义了 DECLARE_MODULE 宏: -------------------------------------------------------------------------- #define DECLARE_MODULE(name, data, sub, order) \ SYSINIT(name##module, sub, order, module_register_init, &data) \ struct __hack -------------------------------------------------------------------------- 下面我们来看看各个参数的意义: name 模块名,注意这个不是KLD名,KLD名就是将来Makefile编译产生的静态文件名 模块名将在SYSINIT调用中被使用。下面这个例子清楚表明了KLD名和模块名的 区别。 [root@ /usr/home/scz/src]> kldstat -v -i 4 Id Refs Address Size Name 4 1 0xc0ae2000 2000 flkm_2 <-- 这是KLD名 Contains modules: Id Name 84 donothing <-- 这是模块名 85 helloworld <-- 这也是模块名 [root@ /usr/home/scz/src]> data 指向 struct moduledata 的指针。/usr/include/sys/module.h里定义了该结 构: -------------------------------------------------------------------------- /* * Struct for registering modules statically via SYSINIT. */ typedef struct moduledata { char *name; /* module name */ modeventhand_t evhand; /* event handler */ void *priv; /* extra data */ } moduledata_t; -------------------------------------------------------------------------- name 模块名 evhand 对应上面介绍过的helloworld_load() sub 该参数的有效取值参看/usr/include/sys/kernel.h文件里定义的 enum sysinit_sub_id {} 枚举列表。我们将要介绍的两种类型的KLD固定采用 SI_SUB_DRIVERS order 该参数的有效取值参看/usr/include/sys/kernel.h文件里定义的 enum sysinit_elem_order {} 枚举列表。我们将要介绍的两种类型的KLD固定采用 SI_ORDER_MIDDLE 一般并不直接使用DECLARE_MODULE()宏,常见的是SYSCALL_MODULE和DEV_MODULE,它 们分别对DECLARE_MODULE进行了封装,这种封装便于开发KLD代码,也便于理解KLD代 码。 /usr/include/sys/sysent.h里定义了 SYSCALL_MODULE 宏 -------------------------------------------------------------------------- #define SYSCALL_MODULE(name, offset, new_sysent, evh, arg) \ static struct syscall_module_data name##_syscall_mod = { \ evh, arg, offset, new_sysent \ }; \ \ static moduledata_t name##_mod = { \ #name, \ syscall_module_handler, \ &name##_syscall_mod \ }; \ DECLARE_MODULE(name, name##_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE) -------------------------------------------------------------------------- name 模块名 offset 对应系统调用号。通常利用KLD机制增加系统调用的时候,并没有保留系统调 用号供它使用。正确的做法是指定NO_SYSCALL,此时系统将动态选取一个可 用系统调用号对应我们增加的系统调用 new_sysent 指向struct sysent结构的指针,每个系统调用都对应一个这样的结构,结构 里定义了形参个数和系统调用实现体指针。 evh 对应上面介绍过的helloworld_load() arg 用于struct syscall_module_data结构,通常该参数设置成NULL /usr/include/sys/conf.h里定义了 DECLARE_MODULE 宏 -------------------------------------------------------------------------- #define DEV_MODULE(name, evh, arg) \ static moduledata_t name##_mod = { \ #name, \ evh, \ arg \ }; \ DECLARE_MODULE(name, name##_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE) -------------------------------------------------------------------------- name 模块名 evh 类似上面介绍过的helloworld_load() arg 用于struct module_data结构,通常该值设置成NULL 无论开发什么样的KLD,至少有一个加载/卸载句柄(主入口函数),至少有一个上 面介绍的宏。在这份编程指南里不讨论更复杂的情形, http://thc.pimmel.com/files/thc/bsdkern.html讨论了更多的复杂的编程技巧,如 果你对KLD编程想进一步的话,请参看上述链接。 我们不必担心Makefile的复杂性,/usr/share/mk目录下提供了许多普适性很强 的预设置的Makefile,可以简单采用.include <...>命令引用它们。此次感兴趣的是 /usr/share/mk/bsd.kmod.mk文件,建议你先阅读一下该文件。可能需要的设置是 -------------------------------------------------------------------------- SRCS = flkm.c KMOD = flkm KO = ${KMOD}.ko .include -------------------------------------------------------------------------- SRCS 源文件名 KMOD KLD名,注意不是模块名 ★ KLD系统调用实现框架 下面是一个非常简单的例子,演示如何利用动态内核链接机制增加系统调用。除 了必须有一个加载/卸载句柄和一个DECLARE_MODULE宏(或者针对它的封装),还有四 点需要注意: -------------------------------------------------------------------------- 1. 如果增加的系统调用需要形参,必须采用自定义结构组织这些形参 2. 系统调用实现体必须是static int型的函数 3. 根据系统调用具体实现组织struct sysent结构 4. 设置offset变量为NO_SYSCALL -------------------------------------------------------------------------- 所有的系统调用,在内核里的函数实现体只有两个形参: -------------------------------------------------------------------------- 1. struct proc * 2. void * -------------------------------------------------------------------------- 来自用户空间的形参需要定义到一个自定义结构中,比如: -------------------------------------------------------------------------- /* * 来自用户空间的syscall()将把函数形参组织到这个结构里,如果对应系统调用并 * 不需要形参,则无须定义这样一个结构,该结构完全为了传递形参 */ struct helloworld_args { char * str; int val; }; -------------------------------------------------------------------------- 一般libc会将用户空间的形参组织到类似这样的结构中。而我们通过KLD增加的系统 调用没有经过libc的封装处理,所以只能使用syscall(2)直接调用这个新增加的系统 调用,后面会有例子演示。 下面是一个系统调用内核函数实现体: -------------------------------------------------------------------------- /* 这是我们将要增加的系统调用 */ static int helloworld ( struct proc * p, struct helloworld_args * arg ) { int err = 0; /* Generic return(err) */ int size = 0; char kernel_str[ 1024 + 1 ]; /* Holds kernel land copy of arg->str */ /* * _IMPORTANT_: * * When one has a contiguous set of data and wish to copy this from * user land to kernel land (or vice versa) the copy(9) functions * are recommended for doing this. */ /* * 不知道这里是否和Linux一样,可以直接访问用户空间?看后面代码意思是 * 可以的,只不过不建议直接访问用户空间而已 * * 刚才自己增加了一点代码验证这个问题,答案是肯定的 * 参看flkm_call.c的演示代码 * * 注意拷贝方向,源/目的与常见函数不一样 */ err = copyinstr( arg->str, &kernel_str, 1024, &size ); if ( err == EFAULT ) { return( err ); } uprintf( "hello world\n" ); uprintf( "The user string passed was: %s\n", arg->str ); uprintf( "The value passed was: %d\n", arg->val ); uprintf( "The kernel string passed was: %s\n", kernel_str ); return( 0 ); } /* end of helloworld */ -------------------------------------------------------------------------- 该系统调用取出来自用户空间的形参,一个字符串和一个整型变量,并在当前使用的 tty(发生该系统调用时进程所使用的终端)上显示它们。 接下来需要根据系统调用具体实现组织一个struct sysent结构,该结构在 /usr/include/sys/sysent.h文件里定义: -------------------------------------------------------------------------- struct sysent /* system call table */ { int sy_narg; /* number of arguments */ sy_call_t * sy_call; /* implementing function */ }; -------------------------------------------------------------------------- 每个系统调用对应有一个struct sysent结构,sy_narg定义来自用户空间的形参个数, 显然只有函数指针对于C调用风格是不够的,想想*printf()这种可变参数的函数。 sy_call对应系统调用内核函数实现体。/usr/include/sys/sysent.h文件里定义了: typedef int sy_call_t __P( ( struct proc *, void * ) ); 下面是该结构的例子: -------------------------------------------------------------------------- /* * on FreeBSD every system call is described by a sysent structure, which * holds the corresponding system call function (here helloworld) and the * appropriate count of arguments (here 2) */ static struct sysent helloworld_sysent = { 2, /* sy_narg */ helloworld /* sy_call */ }; -------------------------------------------------------------------------- 现在,如果你还记得前面提到过的,最后应该提供一个offset参数到 SYSCALL_MODULE宏。这个参数对应系统调用号,作为通过KLD动态增加的新系统调用, 应该设置该值成NO_SYSCALL,意味着由系统找出下一个可用系统调用号,当然你可以 明确指定一个系统调用号,不推荐这样做。可以直接传递NO_SYSCALL给宏,然而最好 给一个静态整型变量赋值NO_SYSCALL,传递一个指针给宏,KLD加载成功后系统会将 最终选取的系统调用号回填到这个静态整型变量。顺便提一句, /usr/include/sys/syscall.h里定义了已经实现的系统调用号列表。于是,我们只需 要这样一行代码: -------------------------------------------------------------------------- /* * every system call has a certain number (called slot or syscall_num on BSD). * This number represents the index in the global sysent list holding every * syscall. BSD is able to search a free slot for a syscall (by setting it * to NO_SYSCALL) which is used here. */ static int syscall_num = NO_SYSCALL; -------------------------------------------------------------------------- NO_SYSCALL在/usr/include/sys/sysent.h里定义,值为-1。 我们已经介绍完通过KLD动态增加一个系统调用的必须操作,剩下的就是编写加 载/卸载句柄,并调用SYSCALL_MODULE()宏: -------------------------------------------------------------------------- /* * 该函数类似Linux下的init_module和cleanup_module * 函数名字自己定义,将来作为函数指针传递给SYSCALL_MODULE()宏 */ static int helloworld_load ( module_t mod, int what, void * arg ) { int err = 0; switch ( what ) { case MOD_LOAD: /* * uprintf() 是内核空间函数,类似于printf()。当在内核空间使用 * printf()时,输出内容需要用dmesg查看。uprintf()将直接输出到 * 当前正在使用的tty上 */ printf( "MOD_LOAD: dmesg -c test\n" ); uprintf( "System call loaded at slot: %d\n", syscall_num ); break; case MOD_UNLOAD: printf( "MOD_UNLOAD: dmesg -c test\n" ); uprintf( "System call unloaded from slot: %d\n", syscall_num ); break; case MOD_SHUTDOWN: uprintf( "System shutdown\n" ); break; default: err = EINVAL; break; } /* end of switch */ return( err ); } /* end of helloworld_load */ SYSCALL_MODULE( helloworld, &syscall_num, &helloworld_sysent, helloworld_load, NULL ); -------------------------------------------------------------------------- Makefile文件很简单,如下: -------------------------------------------------------------------------- SRCS = flkm.c KMOD = flkm KO = ${KMOD}.ko .include -------------------------------------------------------------------------- make -f flkm.mk后产生flkm文件,可以用file flkm查看文件类型。以root身份执行 kldload -v ./flkm加载该KLD文件。 下面是从用户空间通过syscall(2)调用helloworld系统调用的例子: -------------------------------------------------------------------------- #include #include #include #include int main ( int argc, char * argv[] ) { int syscall_num; struct module_stat stat; char hello[] = "I'll be back."; stat.version = sizeof( stat ); /* modstat will retrieve the module_stat structure for our module named helloworld (see the SYSCALL_MODULE macro which sets the name to syscall) */ modstat( modfind( "helloworld" ), &stat ); /* extract the slot (syscall) number */ syscall_num = stat.data.intval; /* 必须在调用前加载内核模块,否则core dump,程序没有做边界检查 */ return( syscall( syscall_num, hello, 1977 ) ); } /* end of main */ -------------------------------------------------------------------------- ★ KLD字符型设备驱动程序实现框架 绝大多数Unix系统支持字符型设备驱动程序,它们通常不对应真实物理设备,仅 仅提供一种对伪设备的读/写/IO控制接口。类似前面介绍增加系统调用,下面将逐步 介绍如何编写KLD模式的字符设备驱动程序,幸运的是,你会发现创建一个非常有用 的字符型设备驱动程序并不困难。 下面4点对于所有字符型设备驱动程序实现都是必要的: -------------------------------------------------------------------------- 1. 定义一个struct cdevsw结构 2. 设备回调函数 3. 加载/卸载句柄 4. DEV_MODULE()宏 -------------------------------------------------------------------------- /usr/include/sys/conf.h里定义了 struct cdevsw 结构 -------------------------------------------------------------------------- /* * Character device switch table */ struct cdevsw { d_open_t *d_open; /* Func. pointer to dev open function */ d_close_t *d_close; /* Func. pointer to dev close function */ d_read_t *d_read; /* Func. pointer to dev read function */ d_write_t *d_write; /* Func. pointer to dev write function */ d_ioctl_t *d_ioctl; /* Func. pointer to dev ioctl function */ d_poll_t *d_poll; /* Func. pointer to dev poll function */ d_mmap_t *d_mmap; /* Func. pointer to dev mmap function */ d_strategy_t *d_strategy; /* Func. pointer to dev strategy func. */ const char *d_name; /* base device name, e.g. 'vn' */ int d_maj; /* Device major value */ d_dump_t *d_dump; /* Func. pointer to dev dump function */ d_psize_t *d_psize; /* Func. pointer to dev psize function */ u_int d_flags; /* D_TAPE, D_DISK, D_TTY, D_MEM */ int d_bmaj; /* Block Device major value (used by D_DISK) */ }; -------------------------------------------------------------------------- 显然该结构类似Linux下的struct file_operations结构,定义了设备相关的回调函 数。并不需要提供所有的回调函数,如果你想提供一个只写设备,不但/dev/目录下 的设备文件权限设置成只写,更重要的是struct cdevsw结构中d_read成员赋值 noread。为了简化讨论,在我们的例子中,只提供了d_open、d_close、d_read和 d_write四个回调函数,我们的struct cdevsw结构如下: -------------------------------------------------------------------------- static struct cdevsw chardev_cdevsw = { chardev_open, chardev_close, chardev_read, chardev_write, noioctl, nopoll, nommap, nostrategy, "chardev", /* 这里和/dev/下的名字不必一致 */ 38, /* /usr/src/sys/conf/majors 主设备号是重要标识 */ nodump, nopsize, D_TTY, /* D_TAPE, D_DISK, D_TTY, D_MEM */ -1 /* Block Device major value (used by D_DISK) */ }; -------------------------------------------------------------------------- /usr/share/examples/kld/cdev/目录下提供了其他一些字符型设备驱动程序例子。 注意我们的例子采用38作为主设备号,/usr/src/sys/conf/majors文件里对此定义如 下: 38 lkm ssigned to Loadable Kernel Modules 假设将来来自应用层的调用步骤如下: open(2) -> write(2) -> read(2) -> close(2) 首先打开/dev/目录下的设备文件,然后写一个字符串到该设备,携入的字符串被保 存在驱动程序静态缓冲区中,稍后应用程序会读取这个字符串,最后关闭前面所打开 的设备文件。 -------------------------------------------------------------------------- /******************************************************************* * * * Function Prototype * * * *******************************************************************/ static int chardev_close ( dev_t dev, int cflag, int devtype, struct proc * p ); static int chardev_open ( dev_t dev, int oflags, int devtype, struct proc * p ); static int chardev_read ( dev_t dev, struct uio * uio, int ioflag ); static int chardev_write ( dev_t dev, struct uio * uio, int ioflag ); /******************************************************************* * * * Static Global Var * * * *******************************************************************/ /* * Used as the variable that is the reference to our device * in devfs... we must keep this variable sane until we * call kldunload. */ static dev_t chardev; static char chardev_buf[ 512 + 1 ]; /* 设备驱动程序维护的内部缓冲区 */ static int chardev_buflen; /*----------------------------------------------------------------------*/ /* * Simply "closes" our device that was opened with chardev_open. */ static int chardev_close ( dev_t dev, int cflag, int devtype, struct proc * p ) { memset( chardev_buf, 0, 513 ); chardev_buflen = 0; uprintf( "Closing device \"chardev\"\n" ); return( 0 ); } /* end of chardev_close */ /* * This open function soley checks for open(2) flags. We are only * allowing for the flags to be O_RDWR for the purpose of showing * how one could only allow a read-only device, for example. */ static int chardev_open ( dev_t dev, int oflags, int devtype, struct proc * p ) { memset( chardev_buf, 0, 513 ); chardev_buflen = 0; uprintf( "Opened device \"chardev\" successfully\n" ); return( 0 ); } /* end of chardev_open */ /* * The read function just takes the buf that was saved * via chardev_write() and returns it to userland for * accessing. */ static int chardev_read ( dev_t dev, struct uio * uio, int ioflag ) { int err = 0; if ( chardev_buflen <= 0 ) { err = -1; } else { /* 对象是以NULL结尾的串,长度包括结尾的NULL */ /* copy buf to userland */ err = copystr( chardev_buf, uio->uio_iov->iov_base, 513, &chardev_buflen ); } return( err ); } /* end of chardev_read */ /* * chardev_write takes in a character string and saves it * to buf for later accessing. */ static int chardev_write ( dev_t dev, struct uio * uio, int ioflag ) { int err = 0; /* 对象是以NULL结尾的串,长度包括结尾的NULL */ err = copyinstr( uio->uio_iov->iov_base, chardev_buf, 513, &chardev_buflen ); if ( err != 0 ) { uprintf( "Write to \"chardev\" failed\n" ); } return( err ); } /* end of chardev_write */ -------------------------------------------------------------------------- 现在你该相信我了吧,实现一个简单的字符型设备驱动程序相当容易。通过这种 技术向内核空间传递数据,对比sysctl能够实现的功能。man 3 sysctl, man 8 sysctl看看。 下面是这个字符型设备驱动程序的加载/卸载句柄。对于设备驱动程序,在 MOD_LOAD流程那里,必须调用make_dev()向设备文件系统(devfs)中注册我们的设备。 devfs是设备文件系统,提供访问FreeBSD内核中设备名字空间的能力。在 MOD_UNLOAD流程那里,必须调用destroy_dev(),形参来自make_dev()的返回值 (dev_t型)。 -------------------------------------------------------------------------- /* * 该函数类似Linux下的init_module和cleanup_module * 函数名字自己定义,将来作为函数指针传递给DEV_MODULE()宏 */ static int chardev_load ( module_t mod, int what, void * arg ) { int err = 0; switch ( what ) { case MOD_LOAD: chardev = make_dev( &chardev_cdevsw, 0, UID_ROOT, GID_WHEEL, 0600, "chardev" ); uprintf( "chardev loaded\n" ); break; case MOD_UNLOAD: destroy_dev( chardev ); uprintf( "chardev unloaded\n" ); break; case MOD_SHUTDOWN: uprintf( "System shutdown\n" ); break; default: err = EINVAL; break; } /* end of switch */ return( err ); } /* end of chardev_load */ DEV_MODULE( chardev, chardev_load, NULL ); -------------------------------------------------------------------------- 无论什么类型的KLD,必须有一个*_MODULE宏,至少指明本模块加载/卸载句柄以及何 种类型。如上最后一行代码所示。 至此一个非常简单的字符型设备驱动程序框架完成了。编写类似前面的Makefile, 编译产生KLD静态文件,并在/dev/目录下创建设备文件: [root@ /usr/home/scz/src]> mknod /dev/chardev c 38 0 [root@ /usr/home/scz/src]> ls -l /dev/chardev crw-r--r-- 1 root wheel 38, 0 Oct 28 04:56 /dev/chardev [root@ /usr/home/scz/src]> 这个KLD被加载后,open()、close()、read()和write()系统调用都可以用于 /dev/chardev设备文件。记得在KLD被卸载出内核前调用close()关闭该设备,否则, 嘿嘿,你死定了。 正如简介里所言,本文讲述的是KLD编写基础知识,相当简短。再深入的技巧请 翻阅THC的技术资料。 ★ 参考资料 1) man 4 kld 关于KLD的man手册 2) http://thc.pimmel.com/files/thc/bsdkern.html THC编写的利用LKM/KLD攻击FreeBSD的经典文献 3) /usr/share/mk/* 缺省Makefile 4) http://subterrain.net/~awr/KLD-Tutorial/code/kld-examples.tar.gz 文中所附例子代码 5) /usr/share/examples/kld/cdev/ 系统自带的其他字符型设备驱动程序例子 <完>