汇编语言一发入魂 0x06 - 硬盘操作
今天我们来学习如何从硬盘读取数据。主要从编程的角度来学习关于硬盘的知识,即学习如何通过端口控制硬盘。
硬盘
从存储数据的介质上区分,硬盘可以分为机械硬盘和固态硬盘,机械硬盘采用磁性碟片来存储数据,固态硬盘通过闪存颗粒存储数据。从编程的角度看,固态硬盘是兼容机械硬盘的,所以我们以机械硬盘为例,简要介绍一下硬盘。
基础知识
机械硬盘主要由磁盘盘片、磁头、主轴与传动轴等组成,数据存放在磁盘盘片中。每个盘片分为上下两面,每面由一个磁头(Head)进行读写。磁头统一固定在同一个支架上,由步进电动机控制,同时在盘片的中心和边缘之间来回移动。当盘片高速旋转时,磁头每步进一次,都会从它所在的位置开始,绕着圆心“画”出一个看不见的圆圈,这就是磁道(Track)。磁道是数据记录的轨迹。因为所有磁头都是联动的,故每个盘面上的同一条磁道又可以形成一个虚拟的圆柱,称为柱面(Cylinder)。
每条磁道划分为若干扇区(Sector),扇区是硬盘读写数据的最小单位。每个扇区以扇区头开始,然后是512字节的数据区。扇区头包含了每个扇区自己的信息,主要有本扇区的磁道号、磁头号和扇区号。
访问模式
硬盘读取数据的模式有两种:
CHS模式,即向硬盘控制器分别发送磁头号、柱面号和扇区号来访问。LBA模式,通过对所有扇区统一编址,形成逻辑扇区,访问时提供逻辑扇区号即可。使用LBA模式不需要考虑扇区的物理位置,对于编程来说比较友好,下面我们来介绍LBA模式。
LBA模式又分为LBA28和LBA48两种。LBA28使用28个bit表示逻辑扇区号,每个扇区512B,总共可管理128GB的硬盘;LBA48使用48个bit表示逻辑扇区号,可管理131072TB的硬盘容量。。
在上一篇文章中,我们给出了部分I/O设备的端口地址分配,其中主硬盘接口分配的端口号是0x1f0~0x1f7,副硬盘接口分配的端口号是0x170~0x177。下面给出当使用LBA28方式访问硬盘时每个端口的用途。
0x1f0,16位数据端口,用于读取或写入数据,每次读写1个字,循环直到读完所有数据。0x1f1,读取时的错误信息或写入时的额外参数。0x1f2,指定读取或写入的扇区数。0x1f3,LBA地址低8位。0x1f4,LBA地址中8位。0x1f5,LBA地址高8位。0x1f6,低4位保存LBA地址的前4位,高4位指定访问模式和访问的设备。其中第4位指示硬盘号,0表示主盘,1表示从盘;第6位指定访问模式,0表示CHS 模式,1表示LBA 模式。第5、7位为1。0x1f7,既是命令端口,又是状态端口。作为命令端口时,写入0x20表示请求读硬盘;写入0x30表示请求写硬盘。作为状态端口时,第0位为1表示前一个命令执行错误,具体原因可访问端口0x1f1;第3位为1表示硬盘已经准备好和主机进行数据交互;第7位为1表示硬盘忙。
实战
有了上面的知识我们就可以开始编码从硬盘读取数据了。我们打算创建一个1KB大小的虚拟硬盘,即1024字节大小。虚拟硬盘的大小其实并不影响我们的学习,能说明问题就可以了,小一点,理解起来可能会更容易。1024字节刚好可以分成两个逻辑扇区,逻辑扇区0和逻辑扇区1。逻辑扇区0作为主引导记录会被BIOS自动加载到内存,我们的目标就是将逻辑扇区1读入内存0x10000处。
代码
.code16
movw $0x1000, %ax
movw %ax, %es
xorw %di, %di
movw $0x1f2, %dx
movb $1, %al
outb %al, %dx
movw $0x1f3, %dx
movb $1, %al
outb %al, %dx
movw $0x1f4, %dx
movb $0, %al
outb %al, %dx
movw $0x1f5, %dx
movb $0, %al
outb %al, %dx
movw $0x1f6, %dx
movb $0, %al
orb $0xe0, %al # b'1110xxxx' LBA 主硬盘
outb %al, %dx
movw $0x1f7, %dx
movb $0x20, %al # 读硬盘
outb %al, %dx
.wait: # 等待硬盘不忙且准备好数据
inb %dx, %al
andb $0x88, %al
cmpb $0x08, %al
jnz .wait
movw $256, %cx
movw $0x1f0, %dx
rep insw
movw $0xb800, %ax
movw %ax, %ds
movw %es:0, %ax
movw %ax, 0
jmp .
.org 510
.word 0xAA55
解释
第3、4、6行用于设置es:di,将es设置为0x1000,di设置为0。指向物理地址的0x10000处,我们的数据将加载到这里。配合第41行的insw指令。
第8~10行向端口0x1f2写入1,表示要读取1个扇区。
第12~14行向端口0x1f3写入1,这是28位逻辑扇区号的0~7位。
第16~18、20~22行分别向端口0x1f4和0x1f5写入逻辑扇区号的中间8~15位和16~23位。
第24~27行向端口0x1f6写入0xe0,即二进制1110 0000。其中低4位是LBA编号的前4位,高4位1110表示以LBA 模式访问主硬盘。
第29~31行向端口0x1f7写入命令0x20,表示读硬盘。
告诉硬盘我们要读取数据之后便采用忙等的形式一直测试硬盘的状态,直到硬盘准备好数据。
第33~37行不断的读取硬盘的状态并测试。andb $0x88, %al用于提取第7位和第3位,即硬盘是否忙,数据是否准备好。cmpb $0x08, %al用于测试,如果相等则表明硬盘不忙且已经准备好数据,不相等则继续测试。
第39行将计数寄存器cx设置为256。因为insw一次从端口读取2字节数据,一个扇区512字节,需要读取256次。
第40行将dx设置为数据端口0x1f0。
第41行使用insw指令从端口读取数据,数据将会被读取到es:di指向的内存单元。配合rep前缀便可以实现批量操作。
第43~47行用于将物理地址0x10000处的数据打印在屏幕上。我们会在虚拟硬盘文件的逻辑扇区1开始处放置一个字符'A',所以屏幕此时会打印出字符'A'。与以往不同,这个字符是从硬盘中读取出来的。
编译并制作虚拟硬盘
- 编译主引导扇区文件
$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
- 创建数据文件
$ echo 'A' >> message.data
使用echo命令创建一个文件,向其中写入字符'A'。因为echo的输出是以行为单位的,所以字符'A'后面还会跟一个换行符'\n'。而'\n'的ASCII编码正好是0a,即在数值上等于显示属性黑底绿字,高亮显示。
- 将主引导扇区文件和数据文件合并成虚拟硬盘文件
$ dd if=/dev/zero of=boot.img bs=512 count=2
$ dd if=boot.bin of=boot.img conv=notrunc
$ dd if=message.data of=boot.img seek=1 conv=notrunc
此时我们便得到了虚拟硬盘文件boot.img,查看一下内容:
$ xxd -a boot.img
00000000: b800 108e c031 ffba f201 b001 eeba f301 .....1..........
00000010: b001 eeba f401 b000 eeba f501 b000 eeba ................
00000020: f601 b000 0ce0 eeba f701 b020 eeec 2488 ........... ..$.
00000030: 3c08 75f9 b900 01ba f001 f36d b800 b88e <.u........m....
00000040: d826 a100 00a3 0000 ebfe 0000 0000 0000 .&..............
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U.
00000200: 410a 0000 0000 0000 0000 0000 0000 0000 A...............
00000210: 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
000003f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
其中前512字节是主引导扇区,以0x55、0xaa结束。第二个扇区开始处的两个字节为0x41、0x0a。
运行
$ qemu-system-i386 -drive file=boot.img,format=raw
启动参数中我们指明的虚拟硬盘的格式为raw,如果不指定并且qemu识别不了的话总是有警告。运行结果如下:

有兴趣的同学可以尝试将message.data中的'A'修改为别的字符,在不修改主引导扇区代码的情况下重新构建虚拟硬盘文件,运行并观察结果。
