大体流程(以读为例)
CPU和外围设备的通信
I/O总线和I/O接口
CPU和外围设备打交道靠的是I/O总线和I/O接口,I/O总线负责传输数据和CPU发送给外围设备的命令。I/O接口则相当于一个处理器接口,用于对CPU发送的命令进行判断,将CPU的命令翻译成外围设备可以“看懂”的命令,I/O接口可以是芯片或别的什么,只要能处理命令都可以。
I/O接口通过ICH(I/O Controller Hub)与总线连接。每一个外围设备都需要有一个单独的I/O接口,用以和总线收发数据。
I/O端口和端口访问
I/O接口其实是一个笼统的概念,其功能其实比看起来要复杂的。它的主要功能是向总线收发命令和翻译命令。但具体来说,CPU其实是通过端口(Port)和外围设备交流的。
硬件上的端口类似于寄存器,不同的是这些寄存器是处于I/O接口电路中的,而且没有固定的符号来表示他们,也就是说,我们使用这些端口时,使用特定的端口号,也就是数字来访问的。
一个I/O接口可以有多个端口,用于不同的目的,比如用于和硬盘通信的PATA/SATA接口,就具有命令端口、状态端口、参数端口、数据端口。
命令端口:发送0x20表示读数据,0x30表示写数据
状态端口:判断硬盘的工作是否正常,操作是否成功,发生了那种错误等
参数端口:告诉硬盘操作的扇区数量、起始的逻辑扇区号
数据端口:向硬盘收发数据
端口和普通寄存器一样,拥有自己的数据宽度,主要是可以是8位、16位也可以是32位,取决于设计者(本文采用16位)。
端口编址
端口编制普遍有两种
一种是将端口映射到内存地址空间中,称为统一编址,当访问这块内存地址时,实际访问的是I/O接口。
另一种是,独立编址,不依赖于内存。而是处理器不仅直接连接内存,还连接I/O接口。为了放置处理器发送的数据被错误的对象接收,这类处理器还设置了一个特别的引脚M/IO#,#表示低电平。当引脚为低电平时,连接内存的电路关闭,连接I/O的电路打开。反之亦然。
文采用独立编址
所有端口有是统一的编号,如I/O接口Ayou3个端口分别为0x0021~0x0023,I/O接口B有5个端口为,0x0303~0x0307等。intel的十六位端口最多可以支持65536个端口的存在。
个人计算机中和硬盘连接的PATA/SATA,每个则分配了8个端口。ICH中包含了两个PATA/SATA接口,分别用于主硬盘和副硬盘,分别使用端口0x1f0~0x1f7和0x170~0x177。
端口访问指令
从端口读指令是in,一般形式为
in al, dx ;机器码 EC
in ax, dx ;机器码 ED
in指令的目的操作数必须是al、或ax,源操作数也应当是dx。这样生成的操作码是一字节的。操作数可以存放在al或ax中,操作数则可以存放在dx中。
其中存放端口和操作数可以使用mov指令,如
mov ax, 0x1f2 ;将16位端口0x1f2移动到ax中
mov dx, 0x20 ;将操作数0x20移动到dx中
不过或许为了方便,in指令也可以被编译为二字节的如:
in al, 0xf0 ;E4 F0
in ax, 0x03 ;E5 03
但是,这种指令的操作数,最多只能是一字节,只能访问0x00~0xff号端口。
与指令in相反,out是向端口写指令,一般形式可以有:
out 0x37, al ;向0x37端口写入(这是一个8位端口)
out 0xf5, ax ;同上(这是一个16位端口)
out dx, al ;端口号在dx寄存器中
out dx, al ;16位端口
目的操作数可以是1字节端口号,或者dx。目的操作数必须是ax或al
具体过程
初始化并决定加载的位置
从硬盘读取数据之前,我们首先需要确定的就是要把读取到的数据存在内存的什么位置。这并不难理解,如果不提前找好数据存储的位置,那我们读出来的数据放哪呀
SECTION mbr align=16 vstart=0x7c00 ;通过vstart直接设置偏移地址0x7c00,可以省去手动加0x7c00的过程
;初始化
;设置堆栈段和栈指针
mov ax,0
mov ss,ax
mov sp,ax
;定义用户加载的地址
mov ax,[cs:phy_base] ;计算用于加载数据的逻辑段地址
mov dx,[cs:phy_base+0x02] ;由于0x10000的值过大,因此分成两半来存放,ax存放0000,dx存放:0001
mov bx,16 ;将phy_base向右移动4位,如此一来才可以作为段地址。
div bx
mov ds,ax ;设置DS和ES的为上面计算的段地址
mov es,ax
;0x10000并不是固定的位置,任何只要是空余的地址都可以作为,加载程序
phy_base dd 0x10000 ;数据被加载的物理起始地址
访问硬盘
- 设置要读取的扇区数量。0x1f2号端口接收端口的数量,这是一个8位端口
mov dx, 0x1f2
mov al, 0x01
out dx, al
注意,如果写入0,就表示要读取256个扇区,每读一个这个端口的数值就会减一。如果读的过程中发生错误,这个端口会包含未读取的扇区数。
- 设置起始LBA扇区号
扇区的读写是连续的,因此只需要写入第一个扇区号即可。因为我们使用28位的扇区号,对寄存器来说太长了,因为我们将它分成四段,分别存放在0x1f3~0x1f6号端口。如下
mov dx, 0x1f3
mov al, 0x02 ;假设从2号扇区开始,地址0~7
out dx, al
inc dx ;0x1f4
mov al, 0x00 :地址8~15
out dx,al
inc dx ;0x1f5
out dx, al ;地址23~16
inc dx ;0x1f6
mov al , 0xe0 ;LBA模式 主硬盘 地址24~27
out dx, al
倒数第二行源操作数表示的内容和前面的操作数有所不同,其表示的内容如下
3. 请求硬盘读。0x1f7是硬盘的命令端口,向命令端口写入0x20就是请求硬盘读。
mov dx, 0x1f7
mov al, 0x20
out dx, al
- 等待读写操作完成。这一步还需要使用端口0x1f7,它不仅是命令端口同时也是状态端口,硬盘的各种状态都可以从0x1f7获得。如下
mov dx, 0x1f7
.waits:
in al, dx
and al, 0x88 ;结合下一行判断,硬盘是否忙碌。
cmp al, 0x08 ;判断硬盘是否可以进行数据交互,如果结果为零表示硬盘空闲,且可以镜像数据交互
jnz .waite
- 连续读出数据。通过0x1f0端口。
mov cx, 256 ;这里单位是字,等于512字节,也就是一个硬盘块的大小
mov dx, 0x1f0
.readw:
in ax, dx
mov [bx], ax ;将读取的数据存放在ds:bx位置
add bx, 2
loop .readw
当这些步骤完成之后,从硬盘读的过程就算完成了。当然这里可能会有一个问题就是,256个字真的就是一个文件的大小吗?如果不是,那么我们怎样才能知道它的大小,并完整的读出所有内容呢?
其实解决这个问题的方法就是,在创建文件的过程中就把文件的如大小等的基本信息计算好,放在文件开头的256个字节中,这样只要读取了最开头的256个字节就可以更具这里面的信息将之后的内容都都出来了。
具体的细节可以查看《用户程序》