地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离。由于指令和变量所占的大小不同,故它们相对于文件开头的偏移量参差不齐。
编译器的工作就是给各个符号编址。编译器根据所在硬件平台的特性,将源代码中的每一个符号(指令和数据)都安装本硬件平台的特性分配空间,在不考虑对齐的情况下,这些符号在空间上都是彼此相邻,连续分布的,它们在程序中距第一个符号的距离便是它们在程序中的地址。
编译器给程序中各符号(变量名或函数名等)分配的地址,就是各符号相对于文件开头的偏移量。
在有的编译器中,同时支持segment和section这两个关键字,它们的功能都是在程序中宣称一个区域。它们属于伪指令,CPU不知道有这些东西,只是为了给程序员在逻辑上将程序划分成几个段。
关键字section并没对程序中的地址产生任何影响,即在默认情况下有没有section都一个样,section中数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用。
section用vstart=修饰后,可以被赋予一个虚拟起始地址virtual start address(这与x86CPU开启分页后的虚拟地址是两码事),它被用来计算在该section内的所有内存引用地址。vstart=xxxx修饰后它并不是告诉编译器程序加载到地址xxxx,加载是加载器的工作,编译器只会规划代码。编译器以相对于文件开头偏移来编址的好处是利于重定位。
mbr用vstart=0x7c00来修饰的原因,是因为mbr要被加载器(BIOS)加载到物理地址0x7c00,mbr中后续的物理地址都是0x7c00+(cs段寄存器此时是0)。所以vstart使用的时机是:我预先知道了我的程序将来被加载到某地址处。
由于程序指定自己将来会被加载器放到某个地址,所以告诉编译器把它编成这个地址,将来CPU用此地址才能找到。vstart只是告诉编译器以新的数字作为后面数据的起始值,它本身没改变数据在文件中的地址。
实模式是指8086CPU的寻址方式、寄存器大小、指令用法等,是用来反应CPU在该环境下如何工作的概念。
CPU大体上可以划分为3个部分,它们是控制单元、运算单元、存储单元。
控制单元是CPU的控制中心,CPU需要它才知道下一步要做什么。而控制单元大致由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)、控制寄存器OC(Operation Controller)组成。程序被加载到内存后,也就是指令这时都在内存中了,指令指针寄存器IP指向内存中下一条待执行指令的地址,控制单元根据IP寄存器的指向,将位于内存中的指令逐个装载到指令寄存器,然后指令译码器将位于指令寄存器的指令按照指令格式来解码。
存储单元是指CPU内部的L1、L2缓存及寄存器,待处理的数据就存在这些存储单元中,这里的数据是说指令中的操作数。 这些缓存都是采用的SRAM(Static RAM)存储器,具有静态存取的功能,SRAM不需要刷新电路即能保存它的的内部数据,因此SRAM性能较强劲。缺点是它的集成度较低,相同容量之下,SRAM的体积比DRAM要大很多。二级缓存都不大,目前来说顶多4MB左右,所以现代CPU用二级缓存的数量取胜,如L1、L2、L3共三级。
寄存器可分为两大类,程序员可以使用的寄存器称为程序可见寄存器,如通用寄存器、段寄存器。程序不可见寄存器是指程序员不可使用,也无法访问到它们,系统运行期间可能要用到的寄存器。
运算单元负责算术运算(加减乘除)和逻辑运算(比较、位移),它从控制单元那里接受命令(信号),它没有自主意识,只是个执行部件。
总结:控制单元要取下一条待运行的指令,该指令的地址在程序计数器PC中,在x86CPU上,程序计数器就是cs:ip。于是读取ip寄存器后,将此地址送上总线,CPU根据此地址便得到了指令,并将其存入到指令寄存器IR中。这时轮到指令译码器上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应的操作数从内存中取回自己的存储单元,若操作数是在寄存器中就直接用了。操作码和操作数齐了,操作控制器给运算单元下命,于是运算单元便真正开始执行指令了。ip寄存器的值被加上当前指令的大小,于是ip又指向了下一条指令。
寄存器是一种物理存储元件,只不过它比一般的存储介质要快,能够跟上CPU上的步伐,所以在CPU内部有很多这样的寄存器来给CPU存取数据。
缓存是一项伟大的发明,成功解决了速度不匹配设备直接的数据传输,在一般情况下,IO是整个系统的瓶颈,缓存的出现,有效减少了低速IO设备的访问频率,从而大幅度提升了速度。
SRAM是用寄存器来存储数据的,这就是SRAM快的原因。而寄存器是使用触发器实现的,这也是一种存储电路,工作速度极快,是纳秒级别的,这是和cpu一个级别了。
在实模式下,默认用到的寄存器都是16位的宽的。在32位CPU在实模式下,虽然操作数是16位,但依然可以使用32位寄存器。
CPU以“当前IP寄存器中的值 + 当前执行指令的机器码长度”的和作为新的代码段内偏移地址,将其存入IP寄存器,再到该新地址处读取指令并执行。如果下一条指令需要跨段访问,还要加载新的段基址到CS寄存器。
实模式的“实”体现在:程序中用到的地址都是真实的物理地址,“段基址:段内偏移”产生的逻辑地址就是物理地址,也就是程序员看到的完全是真实的地址。
在8086之前的CPU,没有段的概念,首先程序无法重定位,必须加载固定内存位置,程序对地址的依赖性强。Intel早期的工程师为此发明了“段”,即CPU访问内存用“段 + 偏移”的形式,这种策略首次在8086CPU上。为了支持段机制,CPU新增了段寄存器,如cs,ds,es等。
为了让16位的寄存器寻址能够访问20位的地址空间,CPU通过“段基址左移4位 + 段内偏移”形成20位地址,从而突破了16位寄存器作为偏移无法访问1MB的限制。但是,现在可以能访问的最大地址是0xFFFF:0xFFFF,即0x10FFEF。相比0xFFFFF的范围,超出了0xFFF0的空间,也就是64K-16字节,这部分就是传说中的高端内存区(Hige Memory Area, HMA)。由于8086一共就20条地址线,即A0~A19,内存地址0xFFFFF+是要用到A20地址线(实模式到保护模式要打开A20),可是8086没有,只能接收20位长的地址。所以超过了20位而产生进位,就给丢弃了。其作用相当于对1MB取模,形成回卷的效果。
可分为三大类:
ret(return)指令的功能是在栈顶(寄存器ss:sp所指向的地址)弹出2两个字节的内容来替换IP寄存器。不用换基地址,属于近返回。
retf(return far) 是从栈顶取得4字节,分别替换ip寄存器和cs寄存器。
在8086CPU中,也就是实模式下,call指令调用函数有四种方式。
“近”就是指同一段内,不用切换段,不用换基地址,只需给出段内偏移地址。
可以使用near关键字来修饰,near表示在内存或寄存器中取2个字节,这是一种数据类型转换。near可以省略。机器码是e8+llhh(操作数)
“相对”既然是相对量,就有正负之分。操作数是个有符号数。在同一段内的函数(近调用),必须要用相对地址的形式,这是硬件设计的问题。
call相对近调用中的操作数并不是被CPU直接用了,CPU又将其恢复成绝对地址:当前的IP指针 + 操作数 + 机器码大小 = 目标函数绝对地址。
“间接”是指目标函数的地址没有直接给出,是通过寄存器或内存,总之不以立即数的形式出现。
“绝对”是指目标函数的地址是绝对地址。
机器码是ff16。
在寄存器前面添加数据类型伪指令(far、near、short)对寄存器宽度做了强制转换,会发生警告。
“直接”意指不需要经过寄存器或内存,操作数以立即数的形式给出。
“远”就意指需要跨段。
对于直接绝对远调用,far可以不加。操作码是0x9a+2字节偏移+2字节段基地址
和上一种的区别就是“直接”变成“间接”,也就是说,段基地址和段内偏移都不是立即数,要么在内存,要么在寄存器。
但是段基址和偏移地址都是16位的,既然要用两个,干脆一个都不用。所以这种间接绝对远调用,不支持寄存器寻址,只支持内存寻址。
指令格式是:call far 内存地址,一定要加far关键字,否则和第二种间接绝对近调用一样了。
相对短转移的机器码大小是2字节,操作码是0xeb,可知其操作数是1字节。“短”体现在,即跳转范围只能是1字节有符号数所表示的范围,-128~127。
关键字short可以省略,但省略后并不能保证nasm依然把它编译成相对短转移。前面说,操作数的范围是-128~127,如果操作数不在此范围,将会在编译阶段报错(前提是加了short关键字)。
相对近转移机器码是3字节,操作码是0xe9,相对于短转移范围增大了,操作数依然是地址相对量,范围是-32768~32767。
同相对近转移相比,间接绝对近转移其目标地址是绝对地址,并且未在指令中直接给出,而是存在寄存器或内存中。其操作码是0xff。
其操作数是立即数,并且是绝对地址, 有跨段需求。操作码是0xea
指令格式:jmp far 内存地址。
计算机能发展到今天这样兴盛,是和诞生各种各样的硬件是分不开的。外部设备种类繁多,原理各异,输出信号也多种多样,数据格式不相同,有的外设用串行数据,有的是并行数据,并且它们都在自己的时序下工作,无论它们的速度如何,在CPU看来都太慢了。
CPU为了减少自己的等待时间,还得为低速设备准备数据缓冲区。CPU用的信号都是TTL电平,外设大多数都是机电设备,机电设备不能用TTL电平驱动,CPU系统总线上传送的都是并行数据(比如8位、16位CPU···),外设并行、串行都有,还得转换格式。所以不可能让CPU一一适应它们,否则CPU的工作就太多了。为了管理这些外部设备,CPU工程师给CPU和外设之间加了一个代理,举个例子,如果是串行设备,CPU就同串行接口通信把数据发给它后,数据再经由串行接口发送给串行设备,串行设备有了反馈后,把数据发送给串行接口,再经串行惊恐返回给CPU,并行设备也是如此。
任何不兼容的问题,都可以通过增加“一层”来解决。在CPU和外设和外设直接的这一层就是IO接口。IO接口形式不限,它可以是个电路板,也可以是块芯片,甚至可以个插槽,它的作用就是在CPU和外设之间互做协调转换,如CPU和外设速度不匹配,他就是变速箱,CPU和外设信号不通用,他就是翻译机。例如,声卡就是驱动音响设备的IO接口。显卡是驱动显示器的IO接口等。
IO接口是连接CPU与外部设备逻辑控制部件,既然称为逻辑,就说明可分为硬件和软件两部分。硬件部分所做的都是一些实质具体的工作,其功能是协调CPU和外设之间的种种不匹配,如双方速度不匹配,那IO接口内部就实现数据缓冲以减少等待时间,数据格式不匹配,IO接口就在这两种格式间互相转换。IO接口内部实际上也是由软件来控制运作的,这就是所谓的“逻辑”部分,所以软件是指用来控制接口电路工作的驱动程序以及完成内部数据传输所需的程序。
IO接口可分按照是否可编程来分类,可分为可编程接口芯片和不可编程接口芯片。
计算机与IO接口的通信是通过计算机指令实现的,当我们需要定制某些功能时,我们也必须用计算机指令告诉IO接口:哪些设备连接在此IO接口上,此IO接口的工作模式等。这种通过软件指令选择IO接口上的功能、工作模式的做法,称为“IO接口控制编程”。这通常是用端口读写指令in/out来实现的。
为了简化CPU访问外部设备的工作,能够轻松地同任何硬件通信,于是约定好IO接口的功能
同一时刻,CPU只能和一个IO接口通信,当很多的IO接口同时想和CPU通信时,这个工作不需要CPU来做,在此之间添加一层来解决,这一层的责任除了仲裁IO接口的竞争,还要连接各种内部总线。由于它的使命,它的名字就叫做输入输出控制中心(I/O control hub, ICH),也就是南桥芯片。
由于南桥和北桥是成对出现的,至少在支持IntelCPU的主板是这样的。南桥用于连接pci、pci-express、AGP等低速设备,北桥用于连接高速设备,如内存。
CPU通过内部总线连接到南桥芯片中的内部,这个内部总线是专用的,它只通向位于南桥的CPU接口。从名字可以看出,南桥二字的桥(hub),意为“公共、集合”,所以在南桥内部集成了一些IO接口,如并口硬盘PATA(IDE硬盘),串口硬盘SATA,USB、PCI设备、电源管理等接口。这些接口是必不可少的。
为了支持非必要的设备(主要为了方便扩展),南桥提供了专门用于扩展的接口,这就是PCI接口。在主板上有很多插槽,它们就是预留的pci接口,pci设备可以即插即用。由于它们延伸到了南桥外面,很多pci设备都可以连接上来,所以这条延长的PCI接口便成了PCI总线。
IO接口在诞生之初,就被设计出通过寄存器的方式同CPU通信,其内部有专用于数据交互的寄存器,只不过这里所说的这些寄存器位于IO接口中,为了区别于CPU内部的寄存器,IO接口的寄存就称为端口。
IO接口是连接CPU和硬件的桥梁,一端是CPU,另一端是硬件。端口是IO接口开发给CPU 的接口。一般端口IO接口都有一组端口,用途不一。端口也是寄存器,寄存器就有数据宽度,有8位、16位、32位,各设备不一,根据厂商规定。
IA32体系系统中,因为用于存储端口的寄存器是16位的,所以最大有65535个端口。由于端口用的是独立编址,因此CPU提供了专门的指令干这事,in和out。
in 指令用于从端口读取数据,其一般形式是:
in al, dx;
in ax, dx ;其中 al 和 ax 用来存储从端口获取的数据,dx 是指端口号
这是固定写法,只要用in指令,源操作数(端口号)必须是dx,而目的操作数是用al,还是ax,取决于dx端口指代的寄存器是8位宽度,还是16位宽度。
out 指令用往端口中写数据,其一般形式是:
out dx, al;
out dx, ax;
out 立即数, al;
out 立即数, ax ;这和 in 指令相反,out 指令中的目的操作数是端口号。
总结in和out指令的共性:
某些接口也叫适配器,适配器是驱动某一外部设备的功能模块。显卡也称为显示适配器,不过归根结底它就是IO接口,专门用来连接CPU和显示器。我们想操作显示器,没有直接的办法,只能通过它的IO接口——显卡。
显卡是pci设备,所以是安装在主板pci插槽上的,pci总线是共享并行架构,并行数据就要保证数据发送后必须同时到达目的地,因为关系到数据的顺序。例如8位并行总线就需要同时发送这8位,接收方也要同时接收这8位才行。虽然貌似并行传输是高效的,但对于要保证同时接收n位数据,这是有困难的,并随着并行数据的位宽而增大。于是串行传输很好的解决了这一问题,一次只发一位,到达目的地在组合到一起。于是就有了PIC Express总线,这是串行设备,简称pcie。现在的显卡都是串口了,串口显卡一次虽然只传输1位,但是它以传输的频率取胜,不光显卡,现在的硬盘都是串口的,可见串行传输速率可是极高的。
显存由显卡提供,它是位于显卡内部的一块内存,所以它称为显存。 显卡的工作就是不断地读取这块内存,随后将其内容发送到显示器。
在黑白图形模式中,显存位于屏幕像素是一对一的,因为只有两种颜色,所以只要显存中的对应位置位1,屏幕上的相应像素就被点亮,呈现处白色。若该位为0,则该像素不会被点亮,呈现出黑色。而在图形模式中,是用24位对应一个像素,所以才呈现出彩色。
各外部设备都是通过软件指令的形式与上层接口通信的,显卡也不例外,所以他也有自己的BIOS。位置在0xC0000~0xC7FFF。显卡支持三种模式,文本模式、黑白图像模式、彩色图形模式。我们只关注文本模式。显卡的文本模式也是分为多种模式的,用“列数*行数”。显卡再加电后,默认就置为模式80*25,也就是屏可以打印2000个字符。
显卡的文本模式显存是从0xB8000~0xBFFFF,范围是32KB,一屏显示2000字符,每个字符占两个字节,故没屏实际占用40000字节。即32KB/4000B约等于8屏的数据。所以这就是Linux可以用alt + Fn建实现tty的切换,这只是原理,具体的实现设计要涉及显卡的寄存器设置。屏幕上每个字符的低字节是字符的ASCII码,高字节是字符属性元信息。在高字节中,低4位是前景色,高4位是背景色。颜色用RBG红绿蓝三种基色调和,第4位用来控制亮度,第7位用来控制字符是否闪烁。
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
jmp $ ; 通过死循环使程序悬停在此
times 510-($-$$) db 0
db 0x55,0xaa
编译nasm -o mbr.bin mbr.S
回车
将生成的mbr.bin写入我们的虚拟硬盘,使用dd命令。
dd if=/mbr.bin \
of=/your_path/bochs/bd60M.img \
bs=512 count=1 conv=notrunc
回车之后,启动bochs,指向c命令,将会在屏幕的左上角出现绿色背景,红色跳动的字符。
针对硬盘的IO接口是硬盘控制器,硬盘控制器和硬盘是连接在一起的。在很久以前它们是分开的,后来开发出一种新的接口,这样才将硬盘和硬盘控制器整合在一起,这种接口便称为集成设备电路(Integrated Drive Electronics, IDE)。随着IDE接口标准的影响力越来越广泛,全齐标准化协会将此姐使用的技术规范归纳成全球硬盘标准,这样就产生了ATA(Advanced Technology Attachment)。由于前几年出现了硬盘串行接口(Serial ATA,SATA),由于其是串行,所以之前的ATA接口只好改称并行ATA,即(Parallel ATA,PATA)。
这两种线缆完全不同,左边PATA接口的线缆也称位IDE线,一个IDE线上可以挂两块硬盘,一个是主盘(Master),一个是从盘(Slave)。在之前,主盘和从盘分工明显,到后来兼容性越来越好,以至于区别不明显。以前的足迹只支持4块IDE(PATA)硬盘,所以主板上提供两个IDE插槽。这两个接口也是以0起始编写,IDE0、IDE1。不过按照ATA的说法,这两个插槽称为通道,IDE0叫做Primary通道,IDE1叫做Secondary通道。即使主板上安装的是SATA硬盘,它也兼容PATA的编程接口。
硬盘控制器属于IO接口,让硬盘工作,需要通过读写硬盘控制器的端口。下面列出部分端口,具体需要查看AT手册。
端口可以被分为两组,Command Block registers 和 Control Block registers。Command Block registers用于向硬盘驱动器写入命令或者从硬盘控制器获得硬盘状态。Control Block registers用于控制硬盘工作状态。
data寄存器是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器的宽度为16位。在读硬盘时,硬盘准备好数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现了这个缓冲区中有数据了,便将此处的数据写入响应的扇区中。
读硬盘时,端口0x171或0x1F1的寄存器名字叫Error寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,error和feature这两个名字指的是同一个寄存器,只是因为不同环境下有不同的用途,为了区别这两种用途,所以在相应环境下有不同的名字。这两个寄存器都是8位宽度。
Sector count寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是8位寄存器,最大值为255,若指定为0,则表示要操作256个扇区。
硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称为CHS,但每次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,但这对于磁头来说很直观,它就是根据这些信息来定位扇区的。我们希望磁盘中扇区从0开始一次递增编号,不用考虑扇区所在的物理结构,这就是LBA,这是一种逻辑上为扇区编址的方法,全称为逻辑块地址(Logical Block Address)。
LBA有两种,一种是LBA28,用28为来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,最大支持128GB。由于硬盘越来越大,另一种是LBA48,最大可寻址范围是2的48次方,最大支持1310721TB,即128PB。
LBA寄存器,LBA low、LBA min、LBA hige三个寄存器都是8位宽度的。LBA low存储28位地址中的0 ~ 7位,LBA min存储28位地址中的8 ~ 15位,LBA hige存储28位地址中的16 ~ 23位。最后四位用device寄存器的低4位。
device寄存器是个杂项,宽度是8位。此寄存器的低4位用来存储LBA地址的低24~27位。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。第6位用来设置是否启用LBA方式,1代表启用LBA模式,0代表启用CHS模式。另外两位固定为1,称为MBS位。
在读硬盘时,端口0x1F7或0x177的寄存器是Status,它是8位宽度的寄存器,用来给出硬盘的状态信息。第0位是ERR位,如果此位1,表示命令出错了,具体原因可见error寄存器。第3位是data request位,如果此位为1,表示硬盘已经把数据准备好。第6位是DRDY,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行命令。第7位是BSY位,表示硬盘是否繁忙,如果为1表示硬盘正在忙。
写硬盘时,端口0x1F7或者0x177的寄存器名称是command,和上面说过的error和feature寄存器情况一样,它和status寄存器是同一个。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。‘’
现在需要使用到的命令:
硬盘中的指令很多,各指令的用法也不同。有的指令直接往command寄存器中写就行了,有的还要再feature寄存器中写入参数,最权威的是去参考ATA手册。
约定一个操作步骤:
硬盘工作完成后,获取它已经准备好的数据,常用的数据传送如下:
第1种方式,应用此方式的数据源设备一定是随时准备好了数据,CPU随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU取数据时不用提前打招呼。
第2种方式,也称为程序I/O、PIO(Programming Input/Output Model),是指传输之前,由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比CPU慢很多。CPU需要数据时,先检查该设备的状态,所以对硬盘可以用此方式来获取数据。
第3种方式,也称中断驱动I/O。上面提到的“查询传送方式”有这样的缺陷,由于CPU需要不断查询设备状态,意味着只有最后一刻的查询才有意义,之前的查询都是发生在数据尚未准备好的时间段里,所以说效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备将数据准备好后再通知CPU来取,这样效率就高了。通知CPU可以采取中断的方式,当数据源准备好数据后,它通过发中断通知CPU来拿数据,避免查询,效率更高。
第4种方式,在中断方式中,虽然提高了CPU的利用率,但通过中断方式来通知CPU,CPU就要通过压栈来保护现场,还要执行传输指令,最后恢复现场。所以“直接存储器存取方式”,可以避免CPU参与传输,完全由数据源设备和内存直接传输。CPU直接到内存拿数据就好了。不过DMA是由硬件实现的,不是软件的概念,所以需要DMA控制器才行。
第5种方式,引入I/O处理机,它其实是一种处理器,只不过用的是另一套擅长IO的系统,随时可以处理数据。有了I/O处理机,CPU甚至可以不知道有传输这回事,不过也是需要单独的硬件来支持。
;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h
; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,1 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
mov eax,esi ;恢复ax
;第2步:将LBA地址存入0x1f3 ~ 0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。
;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
程序最开始的%include "boot.inc"
,这个%include是nasm编译器中的预处理指令,让编译器在编译之前把boot.inc文件包含进来。
; 配置文件 boot.inc 关于加载器的信息
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
nasm要用-I指定库目录,我把boot.inc放在子目录include中,
nasm -I include/ -o mbr.bin mbr.S 回车
接下来使用dd
命令写入虚拟硬盘。
测试内核加载器,下面是loader.S测试代码:
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4
jmp $ ; 通过死循环使程序悬停在此
还是一样使用nasm -I include/ -o loader.bin loader.S
回车
使用dd
命令将loader.bin文件写入第2个扇区。第0个扇区是MBR,第1个扇区是空的未使用。
dd if=./loader.bin of=/此处替换成你的安装目录/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc