Linux内核模块编程与内核模块LICENSE -《详解(第3版)》预读

Linux内核模块简介

Linux内核的整体结构已经非常庞大,而其包含的组件也非常多。我们怎样把需要的部分都包含在内核中呢?
一种方法是把所有需要的功能都编译到Linux内核。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。
有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中呢?
Linux提供了这样的一种机制,这种机制被称为模块(Module)。模块具有这样的特点。

  1. 模块本身不被编译入内核映像,从而控制了内核的大小。
  2. 模块一旦被加载,它就和内核中的其他部分完全一样。

为了使读者建立对模块的初步感性认识,我们先来看一个最简单的内核模块“Hello World”,如代码清单4.1所示。
代码清单4.1  一个最简单的Linux内核模块

01 /*
02  * a simple kernel module: hello
03  *
04  * Copyright (C) 2014 Barry Song  ([email protected])
05  *
06  * Licensed under GPLv2 or later.
07  */
08
09 #include <linux/init.h>
10 #include <linux/module.h>
11
12 static int __init hello_init(void)
13 {
14     printk(KERN_INFO "Hello World enter\n");
15     return 0;
16 }
17 module_init(hello_init);
18
19 static void __exit hello_exit(void)
20 {
21     printk(KERN_INFO "Hello World exit\n ");
22 }
23 module_exit(hello_exit);
24
25 MODULE_AUTHOR("Barry Song <[email protected]>");
26 MODULE_LICENSE("GPL v2");
27 MODULE_DESCRIPTION("A simple Hello World Module");
28 MODULE_ALIAS("a simplest module");

这个最简单的内核模块只包含内核模块加载函数、卸载函
数和对GPL
v2许可权限的声明以及一些描述信息,位于本书配套源代码的/kernel/drivers/hello目录。编译它会产生hello.ko目标文件,通
过“insmod ./hello.ko”命令可以加载它,通过“rmmod hello”命令可以卸载它,加载时输出“Hello World
enter”,卸载时输出“Hello World exit”。
内核模块中用于输出的函数是内核空间的printk()而非用户空间的printf(),printk()的用法和printf()基本相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段,在Linux驱动的调试章节中将详细讲解这个函数。
在Linux中,使用lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系,例如:

Module                  Size  Used by
hello                   9 472  0
nls_iso8859_1          12 032  1
nls_cp437              13 696  1
vfat                   18 816  1
fat                    57 376  1 vfat
...

lsmod命令实际上读取并分析“/proc/modules”文件,与上述lsmod命令结果对应的“/proc/modules”文件如下:

$ cat /proc/modules
hello 12393 0 - Live 0xe67a2000 (OF)
nls_utf8 12493 1 - Live 0xe678e000
isofs 39596 1 - Live 0xe677f000
vboxsf 42561 2 - Live 0xe6767000 (OF)
...

内核中已加载模块的信息也存在于
/sys/module目录下,加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又包含一个refcnt文件和一
个sections目录,在/sys/module/hello目录下运行“tree –a”得到如下目录树:

[email protected]:/sys/module/hello# tree -a
.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│   └── .note.gnu.build-id
├── refcnt
├── sections
│   ├── .exit.text
│   ├── .gnu.linkonce.this_module
│   ├── .init.text
│   ├── .note.gnu.build-id
│   ├── .rodata.str1.1
│   ├── .strtab
│   └── .symtab
├── srcversion
├── taint
└── uevent

3 directories, 15 files

modprobe命令比insmod命令要强大,它在加载某模块时,会同时加载该模
块所依赖的其他模块。使用modprobe命令加载的模块若以“modprobe -r
filename”的方式卸载将同时卸载其依赖的模块。模块之间的依赖关系上存放在根文件系统的/lib/modules/<kernel-
version>/modules.dep文件中,实际上是在整体编译内核的时候由depmod工具生成的,它的格式非常简单:

kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

使用modinfo <模块名>命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持的参数以及vermagic:

# modinfo hello.ko
filename:       hello.ko
alias:          a simplest module
description:    A simple Hello World Module
license:        GPL v2
author:         Barry Song <[email protected]>
srcversion:     081230411494509792BD4A3
depends:        
vermagic:       3.8.0-39-generic SMP mod_unload modversions 686

Linux内核模块程序结构

一个Linux内核模块主要由如下几个部分组成。
(1)模块加载函数
当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
(2)模块卸载函数
当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
(3)模块许可证声明
许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染 (kernel tainted)的警告。

Linux内核模块领域,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional
rights”、“Dual BSD/GPL”、“Dual
MPL/GPL”和“Proprietary”(关于模块是否可以采用非GPL许可权如“Proprietary”,这个在学术界和法律界都有争议)。
大多数情况下,内核模块应遵循GPL兼容许可权。Linux内核模块最常见的是以MODULE_LICENSE( "GPL v2" )语句声明模块采用GPL v2。
(4)模块参数(可选)。
模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。
(5)模块导出符号(可选)。
内核模块可以导出符号(symbol,对应于函数或变量),这样其他模块可以使用本模块中的变量或函数。
(6)模块作者等信息声明(可选)。

模块加载函数

Linux内核模块加载函数一般以_ _init标识声明,典型的模块加载函数的形式如代码清单4.2所示。
代码清单4.2  内核模块加载函数

1     static int _ _init initialization_function(void)
2     {    
3         /* 初始化代码 */
4     }
5     module_init(initialization_function);

模块加载函数则以
“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码。在Linux内核里,
错误编码是一个接近于0的负值,在<linux/errno.h>中定义,包含-ENODEV、-ENOMEM之类的符号值。总是返回相应的
错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。
在Linux内核中,可以使用request_module(const char *fmt, …)函数加载内核模块,驱动开发人员可以通过调用
request_module(module_name);
这种灵活的方式加载其他内核模块。
在Linux中,所有标识为_ _init的函数如果直接编译进入内核,成为内核镜像的一部分,在连接的时候都放在.init.text这个区段内。
#define _ _init        _ _attribute_ _ ((_ _section_ _ (".init.text")))
所有的_ _init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些_ _init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等)的内存。
除了函数以外,数据也可以被定义为_ _initdata,对于只是初始化阶段需要的数据,内核在初始化完后,也可以释放它们占用的内存。例如,下面的代码中将hello_data定义为__initdata。

static int hello_data __initdata = 1;

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, world %d\n", hello_data);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);

模块卸载函数

Linux内核模块加载函数一般以_ _exit标识声明,典型的模块卸载函数的形式如代码清单4.3所示。
代码清单4.3  内核模块卸载函数

1    static void _ _exit cleanup_function(void)
2    {
3          /* 释放代码 */
4    }
5    module_exit(cleanup_function);

模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完成与模块加载函数相反的功能。

们用__exit来修饰模块卸载函数,可以告诉内核如果相关的模块被直接编译进内核(即built-in),则cleanup_function()
函数会被省略直接不连接进最后的镜像。既然模块被built-in了,就不可能卸载它了,卸载函数也就没有存在的必要了。除了函数以外,只是退出阶段采用
的数据也可以用__exitdata来形容。

模块参数

我们可以用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数,例如下列代码定义了1个整型参数和1个字符指针参数:

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

在装载内核模块时,用户可以向模块传递参数,形式为
“insmode(或modprobe)模块名
参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。如果模块被built-in,就无法insmod了,但是bootloader可以通过在
bootargs里设置“模块名.参数名=值”的形式给该built-in的模块传递参数。
参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。
除此之外,模块也可以拥有参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)”。

块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当“参数读/写权限”为0时,表示此参数不存在sysfs文件系统下对应的文
件节点,如果此模块存在“参数读/写权限”不为0的命令行参数,在此模块的目录下还将出现parameters目录,包含一系列以参数名命名的文件节点,
这些文件的权限值就是传入module_param()的“参数读/写权限”,而文件的内容为参数的值。
运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。
现在我们定义一个包含两个参数的模块(如代码清单4.4,位于本书源代码/kernel/drivers/param目录),并观察模块加载时被传递参数和不传递参数时的输出。
代码清单4.4  带参数的内核模块

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 static char *book_name = "dissecting Linux Device Driver";
05 module_param(book_name, charp, S_IRUGO);
06
07 static int book_num = 4000;
08 module_param(book_num, int, S_IRUGO);
09
10 static int __init book_init(void)
11 {
12     printk(KERN_INFO "book name:%s\n", book_name);
13     printk(KERN_INFO "book num:%d\n", book_num);
14     return 0;
15 }
16 module_init(book_init);
17
18 static void __exit book_exit(void)
19 {
20     printk(KERN_INFO "book module exit\n ");
21 }
22 module_exit(book_exit);
23
24 MODULE_AUTHOR("Barry Song <[email protected]>");
25 MODULE_LICENSE("GPL v2");
26 MODULE_DESCRIPTION("A simple Module for testing module params");
27 MODULE_VERSION("V1.0");

对上述模块运行“insmod book.ko”命令加载,相应输出都为模块内的默认值,通过查看“/var/log/messages”日志文件可以看到内核的输出:

# tail -n 2 /var/log/messages
Jul  2 01:03:10 localhost kernel:  <6> book name:dissecting Linux Device Driver
Jul  2 01:03:10 localhost kernel:  book num:4000

当用户运行“insmod book.ko book_name=‘GoodBook‘ book_num=5000”命令时,输出的是用户传递的参数:

# tail -n 2 /var/log/messages
Jul  2 01:06:21 localhost kernel:  <6> book name:GoodBook
Jul  2 01:06:21 localhost kernel:  book num:5000
Jul  2 01:06:21 localhost kernel:  book num:5000

另外,在/sys目录下,也可以看到book模块的参数:

[email protected]:/sys/module/book/parameters$ tree
.
├── book_name
└── book_num

导出符号

Linux的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
    导出的符号将可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。代码清单4.5给出了一个导出整数加、减运算函数符号的内核模块的例子。
代码清单4.5  内核模块中的符号导出

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 int add_integar(int a, int b)
05 {
06     return a + b;
07 }
08 EXPORT_SYMBOL_GPL(add_integar);
09
10 int sub_integar(int a, int b)
11 {
12     return a - b;
13 }
14 EXPORT_SYMBOL_GPL(sub_integar);
15
16 MODULE_LICENSE("GPL v2");

从“/proc/kallsyms”文件中找出add_integar、sub_integar相关信息:

# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar    [export_symb]
e679403c r __kstrtab_sub_integar    [export_symb]
e6794038 r __kcrctab_sub_integar    [export_symb]
e6794024 r __ksymtab_add_integar    [export_symb]
e6794048 r __kstrtab_add_integar    [export_symb]
e6794034 r __kcrctab_add_integar    [export_symb]
e6793000 t add_integar    [export_symb]
e6793010 t sub_integar    [export_symb]

模块声明与描述

在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名,例如:

MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,表明该驱动模块所支持的设备,如代码清单4.6所示。
代码清单4.6  驱动所支持的设备列表

1 /* 对应此驱动的设备表 */
2 static struct usb_device_id skel_table [] = {
3 { USB_DEVICE(USB_SKEL_VENDOR_ID,
4       USB_SKEL_PRODUCT_ID) },
5     { } /* 表结束 */
6 };
7
8 MODULE_DEVICE_TABLE (usb, skel_table);

此时,并不需要读者理解MODULE_DEVICE_TABLE的作用,后续相关章节会有详细介绍。

模块的使用计数

Linux 2.4内核中,模块自身通过MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏来管理自己被使用的计数。
Linux
2.6以后的内核提供了模块计数管理接口try_module_get(&module)和module_put
(&module),从而取代Linux
2.4内核中的模块使用计数管理宏。模块的使用计数一般不必由模块自身管理,而且模块计数管理还考虑了SMP与PREEMPT机制的影响。

int try_module_get(struct module *module);

该函数用于增加模块使用计数;若返回为0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。

void module_put(struct module *module);

该函数用于减少模块使用计数。
try_module_get
()与module_put()的引入与使用与Linux 2.6以后的内核下的设备模型密切相关。Linux
2.6以后的内核为不同类型的设备定义了struct module
*owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)去增加管理此设
备的owner模块的使用计数;当不再使用此设备时,内核使用module_put(dev->owner)减少对管理此设备的owner模块的使
用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载。只有当设备不再被使用时,模块才允许被卸载。
在Linux
2.6以后的内核下,对于设备驱动而言,很少需要亲自调用try_module_get()与module_put(),因为此时开发人员所写的驱动通常
为支持某具体设备的owner模块,对此设备owner模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现,从而简化了设备
驱动开发。

模块的编译

我们可以为代码清单4.1的模板编写一个简单的Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules

kernel_modules:
      make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
      make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

该Makefile文件应该与源代码hello.c位于同一目录,开启其中的EXTRA_CFLAGS=-g -O0可以得到包含调试信息的hello.ko模块。运行make命令得到的模块可直接在PC上运行。
如果一个模块包括多个.c文件(如file1.c、file2.c),则应该以如下方式编写Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o

模块与GPL

Linux内核有2种方法导出符号给模块使用,一种方法是EXPORT_SYMBOL(),另外一种EXPORT_SYMBOL_GPL()。这一点和模块A导出符号给模块B用是一致的。

核的Documentation/DocBook/kernel-hacking.tmpl明确表明“the symbols exported by
EXPORT_SYMBOL_GPL()can only be seen by modules with a MODULE_LICENSE()
that specifies a GPL compatible
license.”由此可见内核用EXPORT_SYMBOL_GPL()导出的符号是不可以被非GPL模块引用的。
由于相当多的内核符号都是以
EXPORT_SYMBOL_GPL()导出的,所以历史上曾经有一些公司的做法是把内核的EXPORT_SYMBOL_GPL()直接改为
EXPORT_SYMBOL(),然后将修改后的内核以GPL形式发布。这样修改内核之后,模块不再使用内核的EXPORT_SYMBOL_GPL()符
号,因此模块不再需要GPL。对此Linus的回复是:“I think both them said that anybody who were
to change a xyz_GPL to the non-GPL one in order to use it with a non-GPL
module would almost immediately fall under the "willful infringement"
thing, and that it would make it MUCH easier to get triple damages
and/or injunctions, since they clearly knew about
it”。因此,这种做法可能构成“蓄意侵权(willful infringement)”。
  
 另外一种做法是写一个wrapper内核模块(这个模块遵循GPL),把EXPORT_SYMBOL_GPL()导出的符号封装一次再次以
EXPORT_SYMBOL()形式导出,而其他的模块不直接调用内核而是调用wrapper函数,如图4.1所示。这种做法也具有争议。
 
图4.1将EXPORT_SYMBOL_GPL重新以EXPORT_SYMBOL导出
一般认为,保守的做法是Linux内核不能使用非GPL许可权。

Linux内核模块编程与内核模块LICENSE -《详解(第3版)》预读,布布扣,bubuko.com

时间: 2024-10-09 18:42:09

Linux内核模块编程与内核模块LICENSE -《详解(第3版)》预读的相关文章

Linux 内核编程 or 内核模块编程的文件读写与信号传输问题

Linux内核编程时,内核代码执行只能直接访问内存上的数据,硬盘上的文件系统必须通过间接的方式才能被内核读写.一般内核操作文件读写的方式有三种:1.通过/proc/文件作为桥梁完成硬盘文件系统与内核的交互:2.通过ioctl方式实现交互:3.直接利用虚拟文件系统的函数vfs_read().vfs_write()读写文件.三种方式的具体实现方法网上有很多详细教程,可以参考.这里对三种方法做出比较. proc机制是一种很老的文件读写方式,通用性好,实现也算成熟,使用时需要自己实现内核上层的读写函数,

Linux内核模块编程与内核模块LICENSE -《具体解释(第3版)》预读

Linux内核模块简单介绍 Linux内核的总体结构已经很庞大,而其包括的组件或许多.我们如何把须要的部分都包括在内核中呢?一种方法是把全部须要的功能都编译到Linux内核.这会导致两个问题.一是生成的内核会很大,二是假设我们要在现有的内核中新增或删除功能,将不得不又一次编译内核. 有没有一种机制使得编译出的内核本身并不须要包括全部功能,而在这些功能须要被使用的时候,其相应的代码被动态地载入到内核中呢?Linux提供了这样的一种机制,这样的机制被称为模块(Module).模块具有这样的特点. 模

Linux网络编程——原始套接字实例:简单版网络数据分析器

通过<Linux网络编程--原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: unsigned char msg[1024] = { //--------------组MAC--------14------ 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6, // dst

《linux内核设计与分析》内核模块编程

内核模块编程 一.准备工作 虚拟机:VMware Workstation 12操作系统:ubuntu当前内核版本:linux-headers-4.4.0-22-generic 二.有关于内核模块的知识 模块是具有独立功能的程序,它可以被单独编译,但不能独立运行.它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的.模块通常由一组函数和数据结构组成,用来实现一种文件系统.一个驱动程序或其他内核上层的功能. 内核模块的相关指令: 查看内核版本 uname –a 模块

inux内核模块编程入门

linux内核模块编程入门 2013-07-06 23:59:54 分类: LINUX 原文地址:linux内核模块编程入门 作者:s270768095 模块编程属于内核编程,因此,除了对内核相关知识有所了解外,还需要了解与模块相关的知识. 1.应用程序与内核模块的比较 为了加深对内核模块的了解,表一给出应用程序与内核模块程序的比较. 表一 应用程序与内核模块程序的比较   C语言应用程序 内核模块程序 使用函数 Libc库 内核函数 运行空间 用户空间 内核空间 运行权限 普通用户 超级用户

小记Linux下对mac80211内核模块修改

mac80211内核模块实现了对802.11协议的处理过程.其中mlme.c文件中的内容实现了对Deauth管理帧的处理.考虑到Deauth攻击至今仍没有好的防御方法(据说有802.11w,可是我一直没见到应用他的设备,为什么?求解),aireplay-ng工具仍然可以很轻松的Deauth客户下线,所以想修改客户端的内核源码来防止对客户端的Deauth.(针对AP则需要对AP进行修改) 方法简记如下: 1. 要编译内核模块,需要下载操作系统源码并编译make: 2. 找到/net/mac8021

Linux内核分析(二)----内核模块简介|简单内核模块实现

Linux内核分析(二) 昨天我们开始了内核的分析,网上有很多人是用用源码直接分析,这样造成的问题是,大家觉得很枯燥很难理解,从某种意义上来说linux系统本身就是由一个个模块构成的,所以我会结合内核模块的设计,去分析内核,从而达到对linux内核的理解. 今天我们会分析到以下内容: 1.      Linux内核模块简介 2.      简单内核模块实现 l  Linux内核模块简介 1.       何为内核模块 在上一篇博文中我们先通过内核配置,在配置的过程中我们对内核的组件进行了选择(当

Linux模块编程框架

Linux模块编程框架 Linux是单内核系统,可通用计算平台的外围设备是频繁变化的,不可能将所有的(包括将来即将出现的)设备的驱动程序都一次性编译进内核,为了解决这个问题,Linux提出了可加载内核模块(Loadable Kernel Module,LKM)的概念,允许一个设备驱动通过模块加载的方式,在内核运行起来之后"融入"内核,加载进内核的模块和本身就编译进内核的模块一模一样.一个程序在编译的地址的相对关系就已经确定了,运行的时候只是进行简单的偏移,为了使模块加载进内核后能够被放

《Linux设备驱动开发具体解释(第3版)》进展同步更新

本博实时更新<Linux设备驱动开发具体解释(第3版)>的最新进展. 2015.2.26 差点儿完毕初稿. 本书已经rebase到开发中的Linux 4.0内核,案例多数基于多核CORTEX-A9平台. [F]是修正或升级:[N]是新增知识点:[D]是删除的内容 第1章 <Linux设备驱动概述及开发环境构建>[D]删除关于LDD6410开发板的介绍[F]更新新的Ubuntu虚拟机[N]加入关于QEMU模拟vexpress板的描写叙述 第2章 <驱动设计的硬件基础> [