Linux驱动开发——高级I/O操作(五)

news2024/9/22 15:49:08

目录

mmap设备文件操作

定位操作

习题 


mmap设备文件操作


        显卡一类的设备有一片很大的显存,驱动程序将这片显存映射到内核的地址空间,方便进行操作。如果用户想要在屏幕上进行绘制操作,将要在用户空间开辟出一片至少一样大的内存,将要绘制的图像数据填充在这片内存空间中,然后调用 write 系统调用,将数据复制到内核空间的显存中,从而进行图像绘制。不难发现,在这个过程中有大量的数据要复制,对于显卡这类对性能要求非常高的设备,这种复制带来的性能损耗显然是不可接受的。
        要消除这个复制操作就需要应用程序能够直接访问显存,但是显存被映射在内核空间,应用程序没有这个访问权限。字符设备驱动提供了一个 mmap 接口,可以把内核空间中的那片内存所对应的物理地址空间再次映射到用户空间,这样一个物理内存就有了两份映射,或者说有两个虚拟地址,一个在内核空间,一个在用户空间。这样就可以通过直接操作用户空间的这片映射之后的内存来直接访问物理内存,从而提高了效率。下面是一个虚拟的帧缓存设备的驱动程序,其实现了 mmap 接口
 

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define VFB_MAJOR	256
#define VFB_MINOR	1
#define VFB_DEV_CNT	1
#define VFB_DEV_NAME	"vfbdev"

struct vfb_dev {
	unsigned char *buf;
	struct cdev cdev;
};

static struct vfb_dev vfbdev;

static int vfb_open(struct inode * inode, struct file * filp)
{
	return 0;
}

static int vfb_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static int vfb_mmap(struct file *filp, struct vm_area_struct *vma)
{
	if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(vfbdev.buf) >> PAGE_SHIFT, \
		vma->vm_end - vma->vm_start, vma->vm_page_prot))
		return -EAGAIN;
	return 0;
}

ssize_t vfb_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	int ret;
	size_t len = (count > PAGE_SIZE) ? PAGE_SIZE : count;

	ret = copy_to_user(buf, vfbdev.buf, len);

	return len - ret;
}

static struct file_operations vfb_fops = {
	.owner = THIS_MODULE,
	.open = vfb_open,
	.release = vfb_release,
	.mmap = vfb_mmap,
	.read = vfb_read,
};

static int __init vfb_init(void)
{
	int ret;
	dev_t dev;
	unsigned long addr;

	dev = MKDEV(VFB_MAJOR, VFB_MINOR);
	ret = register_chrdev_region(dev, VFB_DEV_CNT, VFB_DEV_NAME);
	if (ret)
		goto reg_err;

	cdev_init(&vfbdev.cdev, &vfb_fops);
	vfbdev.cdev.owner = THIS_MODULE;
	ret = cdev_add(&vfbdev.cdev, dev, VFB_DEV_CNT);
	if (ret)
		goto add_err;

	addr = __get_free_page(GFP_KERNEL);
	if (!addr)
		goto get_err;

	vfbdev.buf = (unsigned char *)addr;
	memset(vfbdev.buf, 0, PAGE_SIZE);

	return 0;

get_err:
	cdev_del(&vfbdev.cdev);
add_err:
	unregister_chrdev_region(dev, VFB_DEV_CNT);
reg_err:
	return ret;
}

static void __exit vfb_exit(void)
{
	dev_t dev;

	dev = MKDEV(VFB_MAJOR, VFB_MINOR);

	free_page((unsigned long)vfbdev.buf);
	cdev_del(&vfbdev.cdev);
	unregister_chrdev_region(dev, VFB_DEV_CNT);
}

module_init(vfb_init);
module_exit(vfb_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <email>");
MODULE_DESCRIPTION("This is an example for mmap");


        代码第75 行使用__get_free_page 动态分配了一页内存(关于内存分配的知识我们在后面会较为细致的学习),内核空间按页来管理内存,在进行映射时,地址要按照页大小对齐。代码第 98 行在模块卸载的时候使用 free_page 来释之前分配的内存。

        代码第 50 行至第 56 行是操作方法集合的结构,为了说明问题,我们只实现了open、release、 mmap 和 read,其中 open 和 release 接口只是简单地返回 0表示操作成功。而read接口函数,即 vfb_read 在第13 行首先判断了读取的字节数是否超过了分配内存的大小(PAGE_SIZE 是页大小的宏,通常是 4096 字节),如果超过了则限定多只能读一页数据。代码第 45 行使用 copy_to_user 内的据复制到用户间。第 47 行返回实际读取的字节数。注意,copy_to_user 返回未复制成功的字节数,全部复制成功则返回0。

        代码第 32行至第 38 行是 mmap 接口的实现,在这里主要是调用了 remap_pfn_range,该函数的原型如下。

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);


        第一个参数 vma 是用来描述一片映射区域的结构指针,一个进程有很多片映射的域,每一个区域都有这样对应的一个结构,这些结构通过链表和红黑树组织在一起。该结构描述了这片映射区域虚拟的起始地址、结束地址和访问的权限等信息。第二个参数addr 是用户指定的映射之后的虚拟起始地址,如果用户没有指定,那么由内核来指定该地址。第三个参数是物理内存所对应的页框号,就是将物理地址除以页大小得到的值.第四个参数是想要映射的空间的大小。最后一个参数 prot 是该内存区域的访问权限。过该函数后,一片物理内存区域将会被映射到用户空间,而这片物理内存本身在之前》被映射到了内核空间,所以这片物理内存区域被映射了两次,在用户空间和内核空间都可以被访问。图4.3 是对应的示意图。
 


        代码第 34 行中的 virt_to_phys(vfbdev.buf)> >PAGE_SHIFT就是首先把在内核空间的虚拟地址 vfbdev.buf 通过 virt_to_phys 转换成对应的物理地址,然后将该物理地址右移PAGE_SHIFT 比特位(其实就是除以页的大小)得到了物理页框号。其他的实参都是vma 中的成员来指定的,而其中的起始地址、大小和权限都可以由用户在系统调用函数中指定,struct vm_area_struct 结构由内核来构造。

下面是对该驱动的测试程序。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

int main(int argc, char * argv[])
{
	int fd;
	char *start;
	int i;
	char buf[32];

	fd = open("/dev/vfb0", O_RDWR);
	if (fd == -1)
		goto fail;

	start = mmap(NULL, 32, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (start == MAP_FAILED)
		goto fail;
	
	for (i = 0; i < 26; i++)
		*(start + i) = 'a' + i;
	*(start + i) = '\0';

	if (read(fd, buf, 27) == -1)
		goto fail;

	puts(buf);

	munmap(start, 32);
	return 0;

fail:
	perror("mmap test");
	exit(EXIT_FAILURE);
}


        代码第 19 行调用了 mmap 系统调用,第一个参数是想要映射的起始地址,通常设置为 NULL,表示由内核来决定该起始地址。第二个参数 32 是要映射的内存空间的大小。第三个参数 PROT_READ | PROT_WRITE 表示映射后的空间是可读、可写的。第四个参数 MAP_SHARED 是指映射是多进程共享。最后一个参数是位置偏移,为0表示从头开始。
        代码第23 行至第 25行是直接对映射之后的内存进行操作。代码第27 行则读出之前操作的内容,可对比判断操作是否成功。下面是编译、测试用的命令。
 

 

定位操作


        支持随机访问的设备文件,访问的文件位置可以由用户来指定,并且对于读写这类操作,下一次访问的文件位置将会紧接在上一次访问结束的位置之后,上面模拟的虚拟显卡设备并不支持这一类操作。首先每次读取的位置都是从文件最开头的位置开始的,也就是说形参 pos 没有使用上;其次是没有 llseek 系统调用所对应的接口函数。

        要让驱动支持定位操作,首先来看看 pos 形参的作用。文件对用户的抽象是一段线性存储的数据,那么可以把文件看成一个数组,每个数组元素占一个字节,那么 pos参数就是访问这个数组的下标的地址。例如,虚拟显卡分配了一页的内存,即文件的内容,如果一页是 4096字节,那么*pos 的值就可以为 0~4095,*pos 就指定了要访问的数据的地址相对于起始地址偏移的字节数。不同于普通文件的是,这个设备文件的大小是固定的。而且,虚拟显卡设备在每次读取后,驱动应该负责更新*pos 的值。

        和lIseek 对应的驱动接口是 file_operations 结构中 llseek 函数指针指向的函数,其类型如下。
 

loff_t (*llseek) (struct file *, loff_t, int);


        指针指向的函数有三个参数,第一个参数指向代表打开文件的 file 结构;第二个参数是偏移量;第三个参数是位置,分别是 SEEK_SET、SEEK_CUR 和 SEEK_END。llseek接口要做的事情就是根据传入的参数来调整保存在 file 结构中的文件位置值。
        下面分别是加入文件定位操作后的驱动代码和应用程序代码。

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define VFB_MAJOR	256
#define VFB_MINOR	1
#define VFB_DEV_CNT	1
#define VFB_DEV_NAME	"vfbdev"

struct vfb_dev {
	unsigned char *buf;
	struct cdev cdev;
};

static struct vfb_dev vfbdev;

static int vfb_open(struct inode * inode, struct file * filp)
{
	return 0;
}

static int vfb_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static int vfb_mmap(struct file *filp, struct vm_area_struct *vma)
{
	if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(vfbdev.buf) >> PAGE_SHIFT, \
		vma->vm_end - vma->vm_start, vma->vm_page_prot))
		return -EAGAIN;
	return 0;
}

ssize_t vfb_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	int ret;
	size_t len = (count > PAGE_SIZE) ? PAGE_SIZE : count;

	if (*pos + len > PAGE_SIZE)
		len = PAGE_SIZE - *pos;

	ret = copy_to_user(buf, vfbdev.buf + *pos, len);
	*pos += len - ret;

	return len - ret;
}

static loff_t vfb_llseek(struct file * filp, loff_t off, int whence)
{
	loff_t newpos;

	switch (whence) {
	case SEEK_SET:
		newpos = off;
		break;
	case SEEK_CUR:
		newpos = filp->f_pos + off;
		break;
	case SEEK_END:
		newpos = PAGE_SIZE + off;
		break;
	default:                /* can't happen */
		return -EINVAL;
	}

	if (newpos < 0 || newpos > PAGE_SIZE)
		return -EINVAL;

	filp->f_pos = newpos;

	return newpos;
}

static struct file_operations vfb_fops = {
	.owner = THIS_MODULE,
	.open = vfb_open,
	.release = vfb_release,
	.mmap = vfb_mmap,
	.read = vfb_read,
	.llseek = vfb_llseek,
};

static int __init vfb_init(void)
{
	int ret;
	dev_t dev;
	unsigned long addr;

	dev = MKDEV(VFB_MAJOR, VFB_MINOR);
	ret = register_chrdev_region(dev, VFB_DEV_CNT, VFB_DEV_NAME);
	if (ret)
		goto reg_err;

	cdev_init(&vfbdev.cdev, &vfb_fops);
	vfbdev.cdev.owner = THIS_MODULE;
	ret = cdev_add(&vfbdev.cdev, dev, VFB_DEV_CNT);
	if (ret)
		goto add_err;

	addr = __get_free_page(GFP_KERNEL);
	if (!addr)
		goto get_err;

	vfbdev.buf = (unsigned char *)addr;
	memset(vfbdev.buf, 0, PAGE_SIZE);

	return 0;

get_err:
	cdev_del(&vfbdev.cdev);
add_err:
	unregister_chrdev_region(dev, VFB_DEV_CNT);
reg_err:
	return ret;
}

static void __exit vfb_exit(void)
{
	dev_t dev;

	dev = MKDEV(VFB_MAJOR, VFB_MINOR);

	free_page((unsigned long)vfbdev.buf);
	cdev_del(&vfbdev.cdev);
	unregister_chrdev_region(dev, VFB_DEV_CNT);
}

module_init(vfb_init);
module_exit(vfb_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <E-mail>");
MODULE_DESCRIPTION("This is an example for mmap");
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

int main(int argc, char * argv[])
{
	int fd;
	char *start;
	int i;
	char buf[32];

	fd = open("/dev/vfb0", O_RDWR);
	if (fd == -1)
		goto fail;

	start = mmap(NULL, 32, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (start == MAP_FAILED)
		goto fail;
	
	for (i = 0; i < 26; i++)
		*(start + i) = 'a' + i;
	*(start + i) = '\0';

	if(lseek(fd, 3, SEEK_SET) == -1)
		goto fail;

	if (read(fd, buf, 10) == -1)
		goto fail;

	buf[10] = '\0';
	puts(buf);

	munmap(start, 32);
	return 0;

fail:
	perror("mmap test");
	exit(EXIT_FAILURE);
}

        驱动代码第 45 行和第46 行判断文件访问是否超过了边界,如果是则调整访问的长度。代码第48 行在复制时考虑到了偏所带来的影响,代码第 49 行则是更新位置值。代码第54行至第78行是文件定位操作的实现,根据 whence 的不同,设置了新的文件位置值,代码第72行和第 73 行则是判断新的位置值是否合法,代码第 75 行将新的文件位
置值更新到fe结构的f_pos成员中。
        测试程序相对于之前的变化是在读操作之前首先使用 lseek 将文件位置定位为 3,那么之后的操作都将从文件的第3 个字节开始读取。下面是编译、测试用的命令

习题 

1、 ioctl接口函数的命令不包含哪个部分(B  )。
[A] 幻数  [C] 参数传输方向  [D] 参数大小   [B] 权限


2、关于 proc 文件系统说法不正确的是(D )。
[B] 通常挂载在/proc 目录下  [A] 是一种伪文件系统  [C] 包含了进程相关的信息   [D] 硬件信息主要输出到该文件系统

(更新为sysfs系统)

3.关于阻塞型 IO 说法不正确的是( D)。
[A] 当资源不可用时,进程主动睡眠
[B] 当资源可用时,由其他内核执行路径唤醒
[C] 可以设置超时后被自动唤醒
[D] 只会唤醒一个进程


4.IO多路复用在 ( D)发生阻塞。
[B] 驱动的read 接口函数中[A]驱动的poll 接口函数中
[D]select、poll 或epoll 系统调用中[C]驱动的write 接口函数中


5.关于异步IO 说法正确的是 (C)
[A] 在IO完成后系统调用才会返回
[B] 在 IO完成后不会通知调用者
[C] 在一次异步操作中,可以将多个 IO 请求合并
[D] 在 IO完成后,设备驱动直接调用调用者注册的回调函数


6.关于异步通知说法错误的是 (D )。
[A] 类似于中断
[B] 可以获取资源的具体状态是可读还是可写
[C] 是由驱动来启动信号的发送的
[D] 当打开一个字符设备文件后,异步通知是默认使能的


7.mmap 的最大优点是 ( B)。
[A] 将用户空间的一片内存映射到内核空间,从而提高效率
[B] 将内核空间的一片内存映射到用户空间,从而提高效率
[C] 使字符设备可以实现随机访问
[D] 额外分配了物理内存,从而提高了效率


8,关于文件定位操作说法正确的是( D)。
[A] 当一个进程定位到文件的一个位置后,另一个打开同样文件的进程所访问的文件位置也随之变化
[B] 串口这类字符设备是必须要实现文件定位操作的
[C] 在字符设备驱动中实现了 lseek 后,可以让所有的字符设备都随机访问
[D] 大多数字符设备都不支持文件定位操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/453132.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python学习之DateTime、TimeDelta、Strftime(Format)及其示例

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤1.引入库2.使用date.today()打印日期3.Python当前日期和时间&#xff1a;now() today()总结 4.如何使用 Strftime()格式化日期和时间输出1. 首先&#xff0c;我们将看到一个简单的如何格式化年份的步骤。通过一个例子来理…

使用buildroot编译完整系统【IMX6ULLPRO】

目录 构建 IMX6ULL Pro 版的根文件系统 编译系统 ​编辑 镜像文件 构建 IMX6ULL Pro 版的根文件系统 配置文件说明 编译系统 下面以 100ask_imx6ull_pro_ddr512m_systemV_qt5_defconfig 配置文 件为例&#xff0c;在 ubuntu 终端上说明 Buildroot 的配置过程&#x…

玩转ChatGPT:辅助编程

一、写在前面 首先让小Chat介绍自己在编程方面的天赋&#xff1a; 总结起来&#xff1a;TA掌握了海量的编程知识&#xff0c;能做到自动代码生成、代码审查优化、编程教学辅导以及实时问题解答。我问TA学习了多少案例&#xff0c;TA说&#xff1a;忘了&#xff0c;但保证够用。…

OpenCV-手势语言识别

OpenCV-手势语言识别 OpenCV-手势语言识别Python环境、TensorFlow环境设置直方图模型保存set_hand_hist.py相关代码如下&#xff1a;载入手势图片 OpenCV-手势语言识别 本部分包括Python环境、TensorFlow环境和OpenCV-Python环境。 Python环境、TensorFlow环境 需要Python 3…

Spring Boot-入门、热部署、配置文件、静态资源

Spring Boot Spring Boot概述 一、微服务概述 1、微服务 微服务&#xff08;Microservices&#xff09;是一种软件架构风格。微服务是以专注单一责任与功能的小型功能区块 &#xff08;Small Building Blocks&#xff09;为基础&#xff1b;利用模块化的方式组合出复杂的大…

DockerImage镜像版本说明

参考文章 1、https://medium.com/swlh/alpine-slim-stretch-buster-jessie-bullseye-bookworm-what-are-the-differences-in-docker-62171ed4531d 2、https://stackoverflow.com/questions/52083380/in-docker-image-names-what-is-the-difference-between-alpine-jessie-stret…

顺序表 和 链表 的区别

顺序表 基于数组 就是对数组进行相关的操作 进行存储数据 数组有个很大的缺点就是 可能会产生内存浪费 针对数组这一缺点 就产生了链表 链表顾名思义 就是像链条一样将数据串起来 链表是将内存中的小空间利用起来 让内存的利用率提高 但是也产生了很大的缺点 就是不能随…

数据通信基础 - 差错控制(奇偶校验、海明码、CRC循环冗余校验码)

文章目录 1 概述1.1 检错和纠错1.2 差错控制原理 2 差错控制的方法2.1 奇偶校验2.2 海明码2.3 CRC循环冗余校验码 3 扩展3.1 网工软考真题 1 概述 1.1 检错和纠错 无论通信系统如何可靠&#xff0c;都不能做到完美无缺。因此&#xff0c;必须考虑怎样发现和纠正信号传输中的差…

【实用教程】教你制作好看的论文区位图

区位图是反映目标区所在位置和与周边地区自然、经济相互作用关系的体现区位的地图。好的区位图能为论文打好专业的基调&#xff0c;给读者留下更好观感&#xff0c;是科研论文不可或缺的一部分。 层次&#xff1a;面向国际期刊的区位图一定要从国际角度出发&#xff0c;清楚表示…

第一性原理差分电荷密度计算能得到什么数据?

第一性原理差分电荷密度&#xff08;DFT&#xff09;计算是一种用于计算分子和材料结构的计算化学方法。它基于物理学原理和量子力学理论&#xff0c;通过解决薛定谔方程来计算电子在分子和材料结构中的行为。DFT是一种非常重要的计算方法&#xff0c;因为它可以提供关于分子和…

自媒体必备素材库,免费、商用,赶紧马住~

自媒体经常需要用到各类素材&#xff0c;本期就给大家安利6个自媒体必备的素材网站&#xff0c;免费、付费、商用都有&#xff0c;建议收藏起来~ 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYwNDUx 菜鸟图库可以找到设计、办公、图片、视频、音频等各种素材。视频素…

托福高频真词List03 // 附阅读真题

目录 4月23日单词 4月23日真题 4月23日单词 adjacentneighboringnearbyadj 毗邻的dependablereliableadj 可靠的 chronology a list that pairs past events with dates n 年表remarkably closeextremely close极为接近competenceabilityn 才能supplementadd tov 补充supplem…

Linux_红帽8学习笔记分享_6

Linux_红帽8学习笔记分享_6 文章目录 Linux_红帽8学习笔记分享_61. RPM软件包的使用技巧1.1如何查询指定软件包是否安装1.2如何删除指定软件包1.3如何安装指定软件包1.5依赖关系 2. YUM软件仓库的配置及使用2.1修改YUM软件仓库的配置文件 3.YUM常见命令使用技巧3.1查询指定软件…

Django框架之创建项目、应用并配置数据库

django3.0框架创建项目、应用并配置数据库 创建项目 进入命令行 新建一个全英文的目录 进入目录 输入命令 django-admin startproject project 项目目录层级 查看当前目录层级 tree /f 目录文件说明 创建数据库 做一个学生管理系统做演示&#xff0c;使用navicat创建数据…

【LPC55s69】使用FAL分区管理与easyflash变量管理

文章目录 1.FAL组件1.1什么是FAL1.2 使用ENV配置FAL1.3 FAL SFUD 移植1.4 FAL SFUD 测试用例1.5 测试结果 2.DFS文件系统2.1 什么是DFS2.2 DFS架构2.3 使用ENV配置DFS2.4 DFS挂载到FAL分区测试2.5 测试结果 3.Easyflash移植到FAL分区3.1 简述EasyFlash3.2EasyFlash软件包使用3.…

Docker的数据管理(dockerfile等)

文章目录 一、管理docker容器中数据1、 数据卷2、数据卷容器 二、容器互联&#xff08;使用centos镜像&#xff09;三、Docker 镜像创建1、基于现有镜像创建2、基于本地模板创建3、基于Dockerfile创建 四、Dockerfile操作常用的指令:五、Dockeerfile案例 一、管理docker容器中数…

HNU数据结构与算法分析-小班7

HNU数据结构与算法分析-小班7

【Python】可视化KITTI激光雷达点云绘制立体框

前言 最近在复现PointRCNN论文过程中发现没有可视化代码&#xff0c;于是查找资料找到了&#xff1a; pointRCNN 结果可视化——tiatiatiatia 按照参考链接下载代码【轮子库】并可视化成功了 mayavi库的学习 主要是利用了mlab模块&#xff1a; Mayavi2 旨在提供一个简单…

隐私计算商业化,原语科技的开源路线是否最优解?

2023年2月&#xff0c;《数字中国建设整体布局规划》出台&#xff0c;首次对我国数字中国建设提出了宏观规划。其中&#xff0c;尤为值得关注的是隐私计算领域。 隐私计算能够实现数据的所有权和使用权分离&#xff0c;可以在保护个人数据隐私安全的基础上&#xff0c;推动数字…

《Linux0.11源码解读》理解(一) 从开机到加载bootsec

计算机启动时, 内存(RAM)没有任何东西, 自然也无法跑操作系统. 但是可以执行固化在ROM里面的BIOS程序. 在按下电源键的一刻. CPU的cs和ip寄存器硬件被设置为0xf000和0xfff0, 于是cs:ip也就指向0xffff0这个地址, 而这个地址正是指向了ROM的BIOS范围(这里是0xfe000~0xfffff, 20根…