Skip to content

Latest commit

 

History

History
308 lines (243 loc) · 31.8 KB

3th.md

File metadata and controls

308 lines (243 loc) · 31.8 KB

完善MBR

地址、section、vstart 浅尝辄止

什么是地址:

本质上,程序中各种数据结构的访问,就是通过“该数据结构的起始地址+该数据结构所占内存的大小”来实现的。这就解释了为什么定义变量要给出变量类型,因为变量类型规定了变量所占内存大小,每种类型都有其对应的内存容量。

编译器给程序中各符号(变量名或函数名等)分配的地址,就是各符号相对于文件开头的偏移量

什么是section

就是段,帮助程序员划分为逻辑段的,但是但是对于nasm来说不关心。方便进行代码的编写。

什么是vstart

vstart 只是按照开发人员的意愿安排新的起始地址,不再以文件开头 0 为起始,其地址若超过文件大小则不会落在文件内,所以是虚拟的。

不管程序用的是不是虚拟地址,只要交给地址总线一个地址,地址总线就会去寻找该地址处的内容。 根据这个原则,只要保证该地址处的内容正是你所需要的即可。如果程序员用 vstart 指定了新的地址,干涉了编译器编址的方式,程序员要清楚地知道自己需要的东西是否会出现在物理内存中这个新的地址处。

cpu实模式

cpu工作原理

CPU 的工作原理。控制单元要取下一条待运行的指令,该指令的地址在程序计数器 PC 中,在 x86CPU 上,程序计数器就是 cs:ip。于是读取 ip 寄存器后,将此地址送上地址总线,CPU 根据此地址便得到了指令,并将其存入到指令寄存器 IR 中。这时候轮到指令译码器上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用了,免了取操作数这一过程。操作码有了,操作数也齐了,操作控制器给运算单元下令,开工,于是运算单元便真正开始执行指令了。ip 寄存器的值被加上当前指令的大小,于是 ip 又指向了下一条指令的地址。接着控制单元又要取下一条指令了,流程回到了本段开头,CPU 便开始了日复一日的循环,由于 CPU 特别不容易坏,所以唯一它停下来的条件就是停电。

实模式下的寄存器

CPU 中的寄存器大致上分为两大类。

  1. 一类是其内部使用的,对程序员不可见。“是否可见”不是说寄存器是否能看得见,是指程序员是否能使用。CPU 内部有其自己的运行机制,是按照某个预定框架进行的,为了 CPU 能够运行下去,必然会有一些寄存器来做数据的支撑,给 CPU 内部的数据提供存储空间。这一部分对外是不可见的,我们无法使用它们,比如全局描述符表寄存器 GDTR、中断描述符表寄存器 IDTR、局部描述符表寄存器 LDTR、任务寄存器 TR、控制寄存器 CR0~3、指令指针寄存器 IP、标志寄存器 flags、调试寄存器 DR0~7。
  2. 另一类是对程序员可见的寄存器。我们进行汇编语言程序设计时,能够直接操作的就是这些寄存器,如段寄存器、通用寄存器。

实模式下的内存寻址方式

在实模式在,cpu访问数据按照段基址+段内偏移地址的方式进行访问。

寻址方式,从大方向来看可以分为三大类: (1)寄存器寻址; (2)立即数寻址; (3)内存寻址。 在第三种内存寻址中又分为: (1)直接寻址; (2)基址寻址; bx (3)变址寻址; di si (4)基址变址寻址。bx+di/si

ret call retf call far jmp near jmp far 还有一堆近转移,远转移之类的,这些就不多说了,属于是汇编语言基础。

标志寄存器

有无条件转移,当然也有有条件转移
这些条件就放在了标志寄存器 flags 中。在名字上看,flag 加了 s,说明是 flag 的复数形式,是 flag 的集合,在此寄存器中有很多标志位。
实模式下标志寄存器是 16 位的 flags,在 32 位保护模式下,扩展(extend)了标志寄存器,成为 32位的 eflags。
值得一提的是,对于C语言这种高级语言而言,判断条件是存储在内存内的。

标志位仅做了解即可。

以下标志位仅在 8088 以上 CPU 中有效。
第 0 位的是 CF 位,即 Carry Flag,意为进位。运算中,数值的最高位有可能是进位,也有可能是借位,所以 carry 表示这两种状态。不管最高位是进位,还是借位,CF 位都会置 1,否则为 0。它可用于检测无符号数加减法是否有溢出,因为 CF 为 1 时,也就是最高位有进位或借位,肯定是溢出。再说点没用的,第 1、3、5、15 位没有专门的标志位,空着占位用。
第 2 位为 PF 位,即 Parity Flag,意为奇偶位。用于标记结果低 8 位中 1 的个数,如果为偶数,PF 位为 1,否则为 0。注意啦,是最低的那 8 位,不管操作数是 16 位,还是 32 位。奇偶校验经常用于数据传输开始时和结束后的对比,判断传输过程中是否出现错误。
第 4 位为 AF 位,即 Auxiliary carry Flag,意为辅助进位标志,用来记录运算结果低 4 位的进、借位情况,即若低半字节有进、借位,AF 为 1,否则为 0。
第 6 位为 ZF 位,即 Zero Flag,意为零标志位。若计算结果为 0,此标志为 1,否则为 0。
第 7 位为 SF 位,即 Sign Flag,意为符号标志位。若运算结果为负,则 SF 位为 1,否则为 0。
第 8 位为 TF 位,即 Trap Flag,意为陷阱标志位。此位若为 1,用于让 CPU 进入单步运行方式,若为0,则为连续工作的方式。平时我们用的 debug 程序,在单步调试时,原理上就是让 TF 位为 1。可见,软件上的很多功能,必须有硬件的原生支持才能得以实现。
第 9 位为 IF 位,即 Interrupt Flag,意为中断标志位。若 IF 位为 1,表示中断开启,CPU 可以响应外部可屏蔽中断。若为 0,表示中断关闭,CPU 不再响应来自 CPU 外部的可屏蔽中断,但 CPU 内部的异常还是要响应的,因为它关不住。
第 10 位为 DF 位,即 Direction Flag,意为方向标志位。此标志位用于字符串操作指令中,当 DF 为1 时,指令中的操作数地址会自动减少一个单位,当 DF 为 0 时,指令中的操作数地址会自动增加一个单位,意即给地址的变化提供个方向。其中提到的这个单位的大小,取决于用什么指令。
第 11 位为 OF 位,即 Overflow Flag,意为溢出标志位。用来标识计算的结果是否超过了数据类型可表示的范围,若超出了范围,就像水从锅里溢出去了一样。若 OF 为 1,表示有溢出,为 0 则未发生溢出。专门用于检测有符号数运算结果是否有溢出现象。以下标志位仅在 80286 以上 CPU 中有效。相对于 8088,它支持特权级和多任务。
第 12~13 位为 IOPL,即 Input Output Privilege Level,这用在有特权级概念的 CPU 中。有 4 个任务特权级,即特权级 0、特权级 1、特权级 2 和特权级 3。故 IOPL 要占用 2 位来表示这 4 种特权级。如果您对此感到迷茫,不用担心,这些将来咱们在保护模式下也得实践。
第 14 位为 NT,即 Nest Task,意为任务嵌套标志位。8088 支持多任务,一个任务就是一个进程。当一个任务中又嵌套调用了另一个任务(进程)时,此 NT 位为 1,否则为 0。

以下标志位仅用于 80386 以上的 CPU。
第 16 位为 RF 位,即 Resume Flag,意即恢复标志位。该标志位用于程序调试,指示是否接受调试故障,它需要与调试寄存器一起使用。当 RF 为 1 时忽略调试故障,为 0 时接受。
第 17 位为 VM 位,即 Virtual 8086 Model,意为虚拟 8086 模式。这是实模式向保护模式过渡时的产物,现在已经没有了。CPU 有了保护模式后,功能更加强大了,但为了兼容实模式下的用户程序,允许将此位置为 1,这样便可以在保护模式下运行实模式下的程序了。实模式下的程序不支持多任务,而且程序中的地址就是真实的物理地址。所以在保护模式下每运行一个实模式下的程序,就要为其虚拟一个实模式环境,故称为虚拟模式。以下标志位仅用于 80486 以上的 CPU。
第 18 位为 AC 位,即 Alignment Check,意为对齐检查。什么是对齐呢?是指程序中的数据或指令其内存地址是否是偶数,是否是 16、32 的整数倍,没有余数,这样硬件每次对地址以自增地方式(每次自加 2、16、32 等)访问内存时,自增后的地址正好对齐数据所在的起始地址上,这就是对齐的原理。对齐并不是软件逻辑中的要求,而是硬件上的偏好,如果待访问的内存地址是 16 或 32 的整数倍,硬件上好处理,所以运行较快。若 AC 位为 1 时,则进行地址对齐检查,为 0 时不检查。以下标志位只对 80586(奔腾)以上 CPU 有效。
第 19 位为 VIF 位,即 Virtual Interrupt Flag,意为虚拟中断标志位,虚拟模式下的中断标志。
第 20 位为 VIP 位,即 Virtual Interrupt Pending,意为虚拟中断挂起标志位。在多任务情况下,为操作系统提供的虚拟中断挂起信息,需要与 VIF 位配合。
第 21 位为 ID 位,即 Identification,意思为识别标志位。系统经常要判断 CPU 型号,若 ID 为 1,表示当前 CPU 支持 CPU id 指令,这样便能获取 CPU 的型号、厂商等信息。若 ID 为 0,则表示当前 CPU 不

实模式小结

实模式(Real Mode)是x86架构的CPU在启动时的一种工作模式,它允许操作系统直接访问所有的物理内存和硬件资源。在这种模式下,CPU的操作受到一些限制,例如:

  • 地址空间限制在1MB,即从0x00000到0xFFFFF的地址范围内。
  • 内存访问是分段的,使用16位的段基址和16位的偏移量来计算物理地址。
  • 指令集有限,不支持某些高级指令。 实模式通常用于操作系统的引导过程,因为在这个阶段操作系统还没有准备好切换到保护模式(Protected Mode),后者提供了更大的地址空间和更高级的内存管理功能。一旦操作系统加载并初始化了必要的组件,它就会切换到保护模式,以利用其提供的高级功能。

实模式被保护模式淘汰的原因,最主要是安全隐患
在实模式下,用户程序和操作系统可以说是同一特权的程序,因为实模式下没有特权级,它处处和操作系统平起平坐,所以可以执行一些具有破坏性的指令。
程序可以随意修改自己的段基址,这样便在 1MB 的内存空间内不受阻拦,可以随意访问任意物理内存,包括访问操作系统所在的内存数据。这就给程序员开放了无限的自由,程序员访问内存可以说是指哪打哪。
由于完全没有保护性可言,用户程序甚至可以覆盖操作系统在内存中的映像,整个计算机世界的和平全靠程序员的心情

操作显存

cpu如何与外设通信 -- io接口

对于外设而言存在多种不同的形式,如何协调这些设备是一个值得考虑的问题。
任何不兼容的问题,都可以通过增加一“层”来解决。在 CPU 和外设之间的这一层就是 IO 接口。IO接口形式不限,它可以是个电路板,也可以是块芯片,甚至可以是个插槽,它的作用就是在 CPU 和外设之间相互做协调转换,如 CPU 和外设速度不匹配,它就是变速箱,CPU 和外设信号不通用,它就是翻译机。

整点具体的,机箱里的声卡就是驱动音响设备的 IO 接口,本章介绍的显卡也同样是一种 IO 接口,它是用来驱动显示器的。也许您打开机箱后也未发现我说的声卡和显卡,那是不是就没有它们呢?当然不会,要是听不到声音看不到图像,人们买电脑干吗?用来学习的?其实它们被集成在主板芯片组中了,您用的就是集成声卡和集成显卡。这下清楚多了吧,下面咱们还是继续说点抽象的。

IO 接口是连接 CPU 与外部设备的逻辑控制部件,既然称为逻辑,就说明可分为硬件和软件两部分。硬件部分所做的都是一些实质具体的工作,其功能是协调 CPU 和外设之间的种种不匹配,如双方由于速度不匹配,那 IO 接口就实现数据缓冲以减少等待时间,数据格式不匹配,IO 接口就在这两种格式间互相转换。IO 接口内部实际上也是由软件来控制运作的,这就是所谓的“逻辑”部分,所以软件是指用来控制接口电路工作的驱动程序以及完成内部数据传输所需要的程序。

其实说简单点,io接口就是作为解耦设备和cpu的中间层。

CPU 太忙了,它的时间特别宝贵,为了简化 CPU 访问外部设备的工作,能够轻松地同任何硬件通信,大家就约定好 IO 接口的功能。

  1. 设置数据缓冲,解决 CPU 与外设的速度不匹配CPU 和外设速度上的差异可以通过设置缓冲区来解决,也就是说,数据先存储在缓冲区里,等需要的时候(无论缓冲区是否满了)就传送出去。
  2. 设置信号电平转换电路CPU 和外设的信号电平不同,如 CPU 所用的信号是 TTL 电平,而外设大多数是机电设备,故不能使用 TTL 电平驱动,可以在接口电路中设置电平转换电路来解决。
  3. 设置数据格式转换外设是多种多样的,输出的信息可能是数字信号、模拟信号等,而 CPU 只能处理数字信号。数字信号需要经过数/模转换(D/A)成模拟量才能被送到外设以驱动硬件,模拟量也同样需要经过模/数(A/D)转换成数字量才能被 CPU 处理。所以接口电路中需要包括 A/D 转换器和 D/A 转换器。另外,即使双方使用的都是数字信号,这也牵涉到格式和字长的问题,如 CPU 使用的是 8 位、16 位或 32 位并行数据,而外设使用并行或串行数据都有可能,所以 IO 接口中必须能够识别格式并且转换成对方需要的形式才行。
  4. 设置时序控制电路来同步 CPU 和外部设备硬件的工作也按照某种时序,它们都有自己的时序系统,就像 CPU 工作在自己的晶振时序上一样。双方时序不同,接口电路就要协调这两种不同的时间计法。例如,CPU 发控制信号、定时信号给 IO 接口电路,IO 接口用它们来控制和管理硬件。随后硬件有了反馈后,其应答信号也需要通过接口返回给 CPU,这样 CPU 先“问”,硬件后“回答”,就实现了一次握手,之后便可以实现 IO 的同步操作。
  5. 提供地址译码CPU 同多个硬件打交道,每个硬件要反馈的信息很多,所以一个 IO 接口必须包含多个端口,即 IO接口上的寄存器,来存储这些信息内容。但同一时刻,只能有一个端口和 CPU 数据交换,这就需要 IO 接口提供地址译码电路,使 CPU 可以选中某个端口,使其可以访问数据总线。

同一时刻,CPU 只能和一个 IO 接口通信,为了解决这个问题,就有加了个输入输出控制中心(I/O control hub ICH),也就是南桥芯片,这一层的责任蛀牙是为了仲裁io接口的竞争以及连接各种内部总线。南桥用于连接 pci、pci-express、AGP 等低速设备,北桥用于连接高速设备,如内存。

IO 接口在诞生之初,就被设计成要通过寄存器的方式同 CPU 通信,其内部有专用于数据交互的寄存器,只不过这里所说的这些寄存器位于 IO 接口中,为了区别于 CPU 内部的寄存器,IO 接口中的寄存器就称为端口(这可不是网络应用程序所开的那种端口,如网络服务器会启动 80 端口,这是两码事)。

如何访问到端口呢?外设中的 rom 既然可以通过内存映射来访问,端口也可以,确实有些微机系统中是这样做的,把一些内存地址作为端口的映射,访问这些内存地址就相当于访问了这些端口。还有一些微机系统把端口独立编址,把所有端口从 0 开始编号,位于一个 IO 接口上的所有端口号都是连续的。以后讲解硬盘的时候大家就会看到了。


;main boot record program
;
;load base addr equ 0xA00
;load start sectir 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

    
    mov ax,0600h
    mov bx,0700h
    mov cx,0
    mov dx,184fh

    int 10h 

    mov byte [gs:0x00],'1'
    mov byte [gs:0x01],0xA4

    mov byte [gs:0x00],' '
    mov byte [gs:0x01],0xA4

    mov byte [gs:0x00],'M'
    mov byte [gs:0x01],0xA4

    mov byte [gs:0x00],'B'
    mov byte [gs:0x01],0xA4

    mov byte [gs:0x00],'R'
    mov byte [gs:0x01],0xA4

    jmp $

    times 510-($-$$) db 0
    db 0x55,0xaa    

new mbr的代码,对于写入的字符以及相应的背景颜色信息,与显存相映射。 对于一个字符来讲是占据两个字节的,第一个字节为具体的字符,第二个字节为该字符的属性,例如颜色以及背景色等信息。

最后就可以在bochs仿真机上看到闪烁的1 mbr字符。

bochs调试

xp/nuf <addr>
用于查看内存的,另外还有一个命令,区别是x命令后接的是线性地址,但是xp接的是物理地址p就是physical n指定要显示的显示单元数 u指定显示单元大小 f指定要用哪种进制显示

u和disasm是一样的,反汇编指令 u/1 <addr>,将指定地址的内容反汇编为指令

Debugger control 类

q | quit | exit 用于退出调试状态
set是指令族,咱们通常用set设置寄存器的值。
(1)例如 set reg = val。可以设置的寄存器包括通用寄存器和段寄存器。
(2)也可以设置每次停止执行时,是否反汇编指令:set u on|off。 show 是指令族,有很多子功能,咱们常用的就下面这 3 个。

  1. show mode
    每次 CPU 变换模式时就提示,模式是指保护模式、实模式,比如从实模式进入到保护模式时会有提示。
  2. show int
    每次有中断时就提示,同时显示三种中断类型,这三种中断类型包括“softint”“extint”和“iret”。可以单独显示某类中断,如执行 show softint 只显示软件主动触发的中断,show extint 则只显示来自外部设备的中断,show iret 只显示 iretd 指令有关的信息。
  3. show call
    每次有函数调用发生时就会提示。traceon|off 如果此项设为 on,每次执行一条指令,bochs 都会将反汇编的代码打印到控制台,这样在单步调试时免得看源码了 u | disasm [/num] [start] [end] 将物理地址 start 到 end 之间的代码反汇编,如果不指定地址,则反汇编 EIP 指向的内存。num 指定反汇编的指令数。 setsize = 16|32|64 在使用反汇编命令时,用来告诉调试器段的大小。 set 指令也会设置在停止时是否反汇编命令。前面 set 命令中有说过。 ctrl+c 中断执行,回到 bochs 控制台。

Execution control 类

  1. c|cont|continue,左边列出的三个命令都意为向下持续执行,若没断点则一直运行下去。最常用的是 c。
  2. s|step [count] 执行 count 条指令,count 是指定单步执行的指令数,若不指定,count 默认为 1。此指令若遇到函数调用,则会进入函数中去执行。最常用的是 s。
  3. p|n|next 执行 1 条指令,若待执行的指令是函数调用,不管函数内有多少指令,把整个函数当作一个整体来执行。最常用的是 n。

Breakpoint management 类

  1. 以地址打断点:
  • vb|vbreak [seg:off] 以虚拟地址添加断点,程序执行到此虚拟地址时停下来,注意虚拟地址是“段:段内偏移”的形式。最常用的是 vb。
  • lb|lbreak [addr]以线性地址添加断点,程序执行到此线性地址时停下来。最常用的是 lb。
  • pb|pbreak|b|break [addr]以物理地址添加断点。程序执行到此物理地址时停下来。b 比较常用。
  1. 以指令数打断点:
  • sb [delta] delta 表示增量,意味再执行 delta 条指令程序就中断。
  • sba [time] CPU 从运行开始,执行第 time 条指令时中断,从 0 开始的指令数。
  1. 以读写 IO 打断点:
  • watch 也有子命令,常用的是这两个。
    • watch r|read [phy_addr] 设置读断点,如果物理地址 phy_addr 有读操作则停止运行。
    • watch w|write [phy_addr] 设置写断点,如果物理地址 phy_addr 有写操作则停止运行。此命令非常有用,如果某块内存不知何时被改写了,可以设置此中断。
  • watch 显示所有读写断点。
    • unwatch 清除所有断点。
    • unwatch [phy_addr] 清除在此地址上的读写断点。
  • blist 显示所有断点信息,功能等同于 info b。
  • bpd|bpe [n] 禁用断点(break point disable)/启用断点(break point enable),n 是断点号,可以用 blist命令先检查出来。
  • d|del|delete [n] 删除某断点,n 是断点号,可以用 blist 命令先检查出来。D 最常用。

CPU and memory contents 类

x /nuf [line_addr] 显示线性地址的内容。n、u、f 是三个参数,都是可选的,如果没有指定,则 n 为 1, u 是 4 字节,f 是十六进制。解释如下。 n 显示的单元数 u 每个显示单元的大小,u 可以是下列之一: (1)b 1 字节;
(2)h 2 字节;
(3)w 4 字节;
(4)g 8 字节。
f 显示格式,f 可以是下列之一: (1)x 按照十六进制显示;
(2)d 十进制显示;
(3)u 按照无符号十进制显示;
(4)o 按照八进制显示;
(5)t 按照二进制显示;
(6)c 按照字符显示;
(7)s 按照 ASCIIz 显示;
(8)i 按照 instr 显示。
xp /nuf [phy_addr] 显示物理地址 phy_addr 处的内容,注意和 x 的区别,x 是线性地址。
setpmem [phy_addr] [size] [val] 设置以物理内存 phy_addr 为起始,连续 size 个字节的内容为 val。此命令非常有用,在某些情况下不易调试时,可以在程序中通过某个地址的值来判断分支,需要用 setpmem 来配合。注意啦,size 最多只能设置 4 个字节宽度的数据,如果 size 大于 4 便会报错:Error:setpmem: bad length value = 8。size 小于等于 4 是正确的,setpmem 0x7c00 4 0x9。
r|reg|regs|registers 任意四个命令之一便可以显示 8 个通用寄存器的值+eflags 寄存器+eip 寄存器。r 是我常用的查看寄存器的命令。
ptime 显示 Bochs 自启动之后,总执行指令数。其实这个命令不常用,感兴趣的同学可以用 ptime 和“sb 指令数”来验证结果是否正确。
print-stack [num] 显示堆栈,num 默认为 16,表示打印的栈条目数。输出的栈内容是栈顶在上,低地址在上,高地址在下。这和栈的实际扩展方向相反,这一点请注意。
?|calc 内置的计算器。
info 是个指令族,执行 help info 时可查看其所有支持的子命令,如下:
info pb|pbreak|b|break 查看断点信息,等同于 blist。
info CPU 显示 CPU 所有寄存器的值,包括不可见寄存器。
info fpu 显示 FPU 状态。
info idt 显示中断向量表 IDT。
info gdt [num]显示全局描述符表 GDT,如果加了 num,只显示 gdt 中第 num 项描述符。
info ldt 显示局部描述符表 LDT。
info tss 显示任务状态段 TSS。
info ivt [num]显示中断向量表 IVT。和 gdt 一样,如果指定了 num,则只会显示第 num 项的中断向量。
info ivt的部分执行结果,显示中断向量表的具体情况 alt text info flags|eflags 显示状态寄存器,其实在用 r 命令显示寄存器值时也会输出 eflags 的状态,还会输出通用寄存器的值,我通常会用 r 来看。
sreg 显示所有段寄存器的值。
dreg 显示所有调试寄存器的值。
creg 显示所有控制寄存器的值。
info tab 显示页表中线性地址到物理地址的映射。
page line_addr 显示线性地址到物理地址间的映射。

显卡概述

在之前我们编写了一个简单MBR程序,可以再bochs启动时在屏幕中显示1 MBR的绿色字符。但是后面这种方法就用不了了。

mbr运行在实模式下,所以可以使用bios的0x10中断打印字符串。这是因为中断向量表只在实模式下存在,而BIOS中断是要依赖于中断向量表的,但是我们都知道,实际上我们后面是在保护模式下进行的,保护模式下是不存在中断向量表的。

某些 IO 接口也叫适配器,适配器是驱动某一外部设备的功能模块。显卡也称为显示适配器,不过归根结底它就是 IO 接口,专门用来连接 CPU 和显示器。我们想操作显示器,没有直接的办法,只能通过它的 IO 接口—显卡。所以其实我们操作屏幕,实际上是操作的显卡,然后通过显卡映射到屏幕上。

显卡是 pci 设备,所以是安装在主板上 pci 插槽上的,pci 总线是共享并行架构,并行数据就要保证数据发送后必须同时到达目的地,因为这关系到数据的顺序,不能发过去后成一团乱麻。例如 8 位并行总线就需要同时发送这 8 位,接收方也要同时接收这 8 位才行。虽然貌似并行传输是高效的,但对于要保证同时接收 n 位数据,这是有困难的,随着并行数据的位宽越来越大,这种困难也越来越明显。于是串行传输很好地解决了这一问题,一次只发一位,这样顺序问题解决了,数据到目的地看再组合到一起就成了。于是就有了 PCI Express 总线,这就是串行设备,简称 pcie。现在的显卡都是串口的了,不光是显卡,现在的硬盘都是串口的,可见串行传输速率可是极高的。

硬盘概述

对于硬盘的工作原理这里不多说,主要着眼于,操作系统最终到底是如何与硬盘进行交互的。

CPU 是怎样和硬盘打交道的呢?针对硬盘的 IO 接口是什么?答案就是硬盘控制器。
硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU 只同它们说话,由它们将 CPU 的话转译给外部设备。这是它们的共同点,但不同的是显卡和显示器是分开的,硬盘控制器和硬盘是连接在一起的。

硬盘控制器端口

硬盘控制器属于 IO 接口,让硬盘工作,我们需要通过读写硬盘控制器的端口,端口的概念在此重复一下,端口就是位于 IO 控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。

端口可以被分为两组,Command Block registersControl Block registers
Command Block registers用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers 用于控制硬盘工作状态。在 Control Block registers 组中的寄存器已经精减了,而且咱们基本上用不到。

硬盘准备数据的步骤: (1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
(2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
(3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
(4)往该通道上的 command 寄存器写入操作命令。
(5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。
(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
(7)将硬盘数据读出

硬盘工作完成后,它已经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下。 (1)无条件传送方式。
(2)查询传送方式。
(3)中断传送方式。
(4)直接存储器存取方式(DMA)。
(5)I/O 处理机传送方式。

第 1 种“无条件传送方式”,应用此方式的数据源设备一定是随时准备好了数据,CPU 随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU 取数据时不用提前打招呼。

第 2 种“查询传送方式”,也称为程序 I/O、PIO(Programming Input/Output Model),是指传输之前,由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比CPU 慢很多。CPU 需要数据时,先检查该设备的状态,如果状态为“准备好了可以发送”,CPU 再去获取数据。硬盘有 status 寄存器,里面保存了工作状态,所以对硬盘可以用此方式来获取数据。

第 3 种“中断传送方式”,也称为中断驱动 I/O。上面提到的“查询传送方式”有这样的缺陷,由于 CPU需要不断查询设备状态,所以意味着只有最后一刻的查询才是有意义的,之前的查询都是发生在数据尚未准备好的时间段里,所以说效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备将数据准备好后再通知 CPU 来取,这样效率就高了。通知 CPU 可以采用中断的方式,当数据源设备准备好数据后,它通过发中断来通知 CPU 来拿数据,这样避免了 CPU 花在查询上的时间,效率较高。

第 4 种“直接存储器存取方式(DMA)”。在中断传送方式中,虽然极大地提高了 CPU 的利用率,但通过中断方式来通知 CPU,CPU 就要通过压栈来保护现场,还要执行传输指令,最后还要恢复现场。似乎有同学说此方式已经很爽了,你还想怎样?哈哈,其实更爽的是一点都不要浪费 CPU 资源,不让 CPU参与传输,完全由数据源设备和内存直接传输。CPU 直接到内存中拿数据就好了。这就是此方式中“直接”的意思。不过 DMA 是由硬件实现的,不是软件概念,所以需要 DMA 控制器才行。

第 5 种“I/O 处理机传送方式”。不知大家发现了没有,在说上面每一种的时候都把它们各自说得特别好,似乎完美不可替代了,就像电视上的广告一样,每次都把自己的产品描述得无与伦比,甚至全宇宙第一,但该公司一出新产品,就开始自曝曾经无与伦比的老一代产品的问题以突显现在产品更胜一筹。DMA已经借助其他硬件了,CPU 已经很轻松了,难道还有更爽的方式?是啊,DMA 方式中 CPU 还嫌爽的不够,毕竟数据输入之后或输出之前还是有一部分工作要由 CPU 来完成的,如数据交换、组合、校验等。如果 DMA 控制器再强大一点,把这些工作帮 CPU 做了就好了。也是哦,既然为了解放 CPU,都已经引用一个硬件(DMA)了,干脆一不做二不休,再引入一个硬件吧。于是,I/O 处理机诞生啦,听名字就知道它专门用于处理 IO,并且它其实是一种处理器,只不过用的是另一套擅长 IO 的指令系统,随时可以处理数据。有了 I/O 处理机的帮忙,CPU 甚至可以不知道有传输这回事,这下 CPU 才真正爽到家啦。同样,这也是需要单独的硬件来支持。

再次强调一遍,对于操作系统而言,所谓读写的io端口实际上就是控制器上的各种寄存器。通过寄存器就可以直接与硬件本身打交道,即使是对于存储数据也是一样的,比如data寄存器,很多时候其实就是作为硬盘和内存之间的缓冲区,写入到硬盘的时候是先写入到data寄存器,然后通过data寄存器再写入到硬盘内。