Cpu启动时,如果在bios中设置了从软盘启动,则bios会自动把软盘的第一个扇区(512字节)搬移到0x7c00,然后会从0x7c00开始运行,我们需要在这512字节的程序中实现把boot从软盘中搬移到内存中的功能。
此时x86系列的cpu还处在实模式中,可以利用bios提供的中断完成软盘读写,也就是著名的int 13。
1 软盘操作 1.1 软盘结构
以1.44M软盘为例,软盘有上、下两个盘面,每个盘面被划分为80个track,每个track被划分为18个Sector,每个Sector大小为512 BYTES。
2×80×18×512=1440×1024=1.44MB
如果是单纯的进行软盘读写操作,知道软盘的这些知识就够了。
1.2 软盘读写 中断号 | 寄存器 | 作用 |
13h | ah=00h al=驱动器号(0表示A盘) | 复位软驱 |
ah=02h al=要读扇区数 ch=磁道号 cl=起始扇区号 dh=磁头号 dl=驱动器号 es:bx=数据缓冲区 | 从cl/ch/dh/dl指向的扇区开始读取al个扇区的数据到es:bx指向的缓冲区 | |
ah=03h al=要写扇区数 ch=磁道号 cl=起始扇区号 dh=磁头号 dl=驱动器号 es:bx=数据缓冲区 | 从es:bx指向的缓冲区读取al个扇区的数据写入cl/ch/dh/dl指向的al个扇区 |
对于软盘来说, DL=0(0表示A盘)
CL的取值范围是:1—18
CH的取值范围是:0—79
DH 的取值范围是:0—1
如果软盘的CL,CH,DH取值超过了范围,中断调用出错。
使用int13时对应扇区数的计算方法如下:
1.3 软盘编程用软盘存放操作系统时,bios只会把前512个字节(即第一个扇区,引导区)读入内存的0x7c00处执行。程序的长度也被限制在了512字节大小,一个出入保护模式的程序就会超出。
为了突破512字节的限制,需要在这个扇区内放置一段引导程序,用于把后面的程序加载到内存中。这也是第一个扇区被称为引导区的原因。
下面的就是一段简单的加载程序,reset软驱后从LABEL_SECTOR2开始读入17个扇区。加上0号扇区(引导区),一共是18个扇区,也就是第一个磁道。这是因为13号中断不能跨磁道读取,所以这里只读入第一个磁道剩下的17个扇区。
; reset floppy xor ah, ah xor dl, dl int 13h
; read sector 2 to memory mov ah, 03h mov al, 11h mov ch, 00h mov cl, 03h mov dh, 00h mov dl, 00h mov bx, 0100h int 13h |
下面的程序用于保证除引导程序外的所有程序都在引导区之外,完成把磁盘内容拷贝入memory的操作后,就会跳入LABEL_SECTOR2。
jmp LABEL_SECTOR2
; fill boot sector times 510 - ($ - $$) db 1 dw 0xaa55
; Sector 2 LABEL_SECTOR2: |
下面是完整的程序代码
org 07c00h jmp LABEL_BEGIN
LABEL_BEGIN: mov ax, cs mov ds, ax mov ss, ax mov es, ax
; reset floppy xor ah, ah xor dl, dl int 13h
; read sector 2 mov ah, 02h mov al, 01h mov ch, 00h mov cl, 02h mov dh, 00h mov dl, 00h mov bx, LABEL_SECTOR2 int 13h
jmp LABEL_SECTOR2
; fill boot sector uutimes 510 - ($ - $$) db 0 dw 0xaa55
; Sector 2 jmp into loader ; org 0100h LABEL_SECTOR2: mov ax, cs mov ds, ax mov es, ax call DispStr ; 调用显示字符串例程 jmp $ ; 无限循环 DispStr: mov ax, BootMessage mov bp, ax ; ES:BP = 串地址 mov cx, 16 ; CX = 串长度 mov ax, 01301h ; AH = 13, AL = 01h mov bx, 000ch ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮) mov dl, 0 int 10h ; 10h 号中断 ret BootMessage: db "Hello, OS world!"
; fill floppy times (2 * 18 * 80 * 512 - ($ - $$)) db 0 |
软盘,INT 13H和IMG文件
1:软盘
软盘是以扇区为基本单位来进行操作的,每扇区512字节,共2880个扇区,空间大小为1.44M.
这2880个扇区又可以用(磁头、磁道、扇区)这三个参数来描述。我们简单记(磁头、磁道、扇区)为(x,y,z),那么
X的取值范围是:0—1
Y的取值范围是:0--79
Z的取值范围是:1—18
也就是说软盘有2磁头、每磁头有80磁道、每磁道有18扇区,共2880个扇区(2880=2*80*18)。
如果把2880个扇区从0开始编号,一直到2879结束,那么
编号为i的扇区和(X,Y,Z)的换算公式为:i=80*18*x+18*y+z-1
2:INT 13H
INT13H是磁盘的BOIS中断,对于读写扇区操作,中断的完整调用参数如下:
AL=扇区数
AH=中断子功能号 ;2=读扇区,3=写扇区
CL的6,7位,CH = 磁道号 ;每磁头最多可以有2^10=1024个磁道
CL的低6位 = 扇区号 ;每磁道最多可以有2^6=64个扇区
DH = 磁头号 ;最多可以有256个磁头
DL = 驱动器号 ;0=软盘,80H=硬盘
ES:BX=数据缓冲区的地址
其他:
1).
对于软盘来说,实际的INT 13H中DL=0,而CL,CH,DH的取值范围也不可能取到上面的数值,根据1:中的数据,有
CL的取值范围是:1--18
CH的取值范围是:0-79
DH 的取值范围是:0—1
如果软盘的CL,CH,DH取值超过了范围,中断调用出错。
AL的取值也不是任意的,一次调用INT13H进行读写扇区的只可以在一个磁道内的扇区进行。如果超出了一个磁道,必须要更新INT13H的中寄存器,重复调用INT13H。
对于软盘来说 AL必须要小于19-CL。
下面是一个从软盘的(x,y,z)扇区中连续读出n个扇区的内容到缓冲区BUFFER中的代码片段( 代码使用了一些80386的指令和MASM6.0才支持的伪指令):
MOV AX,DS
MOV ES,AX
MOV BX,OFFSET
MOV BP, n
MOV CL,z
MOV CH,y
MOV DL,0
MOV DH,x
.WHILE BP ;bp记录的是还没有进行读操作的扇区数量
;WHILE伪指令,和高级语言的WHILE一样理解就可以了,下面的。IF也一样
;对AL的取值进行计算
MOV AL,19
SUB AL,CL ;最多可以读19-CL个扇区
XOR AH,AH
.IF AX>BP
MOV AX,BP
XOR BP,BP
.ELSE
SUB BP,AX ;更新BP
.ENDIF
MOV AH,2
INT 13H ;读扇区
;更新CL,CH,DH
MOV CL,1
.IF CH==79
INC DH
XOR CH,CH
.ELSE
INC CH
.ENDIF
XOR AH,AH
SHL AL,9 ;AX=AL*512,等于已经处理的字节数
;SHL AL,9和SHL AX,4都是80386+才支持的指令
SHR AX,4 ;AX=AX/16
ADD ES,AX ;更新ES:BX,这里是更新了ES,也可以更新BX。
.endw
2).
INT13H只理论上最多处理 2^24个扇区*512字节/扇区=8G的磁盘空间,这对现在的硬盘来说,是远远不够的,于是后来对INT13H进行了扩展,用AH=42H、AH=43H分别对大硬盘进行操作,这里就不详细讨论了。
3:IMG文件
IMG文件是软盘的镜像文件,文件大小也是1.44M,它和软盘是一种线性的对应关系。
软盘上一个编号为(x,y,z)的512字节的扇区,对应IMG文件中的以2400H*x+4800H*y+(z-1)*512为基址的512个字节(注意,2400H,4800H是16进制数)。
IMG文件的字节和软盘的扇区对应关系也可以如下图所示(注意那些是十进制,那些是16进制)。
对于扩展INT 13中断,参数如下:
中断号 功能 调用寄存器 返回寄存器 备注
INT 13
AH=41H 检测扩展中断功能是否安装 AH = 41h
BX=55AAh
DL = 驱动器号(80h到FFH)
失败:AH=1
CF置位
成功:AH=版本号
CF=0 BX=AA55H
INT 13
AH=42H 磁盘扩展读操作 AH = 42H
DL = 驱动器号
DS:SI=指向LBA地址包的指针 失败:AH=错误号
CF置位
成功:AH=0
CF=0
地址包定义:
偏移 大小 描述
00H 字节 地址包大小
01H 字节 保留(为0)
02H 字 传输包个数
04H 双字 指向数据指针
08H 4字 起始地址
其中LBA=((柱面*磁头/柱面+磁头)*扇区/柱面)+扇区-1
INT 13
AH=43H 磁盘扩展写操作 AH=43H
AL=写标志
DL = 驱动器号
DS:SI=指向LBA地址包的指针 失败:AH=错误号
CF置位
成功:AH=0
CF =0 同上
INT 13
AH=48H 获取磁盘参数 AH=48H
DL=驱动器号
DS:SI=指向保存参数缓冲区的指针
失败:AH=错误号
CF置位
成功:AH=0
CF=0
参数缓冲区定义:
偏移 大小 描述
00H 字 缓冲区大小
02H 字 信息标志位
04H 双字 物理柱面数
08H 双字 物理磁头数
0CH 双字 物理每柱扇区数
10H 4字 扇区总数
18H 字 每扇区字节数
1.1 保护模式下实现软盘编程
要发挥x86芯片的功能,必须要进入保护模式。
系统启动时所加载的512字节的MBR区为bootloader区,用于加载真正的boot程序。在bootloader区中,cpu还运行于实模式,因此bootloader通过bios中断加载boot。进入boot区后,一般来说已经完成了切换入保护模式的动作。
在保护模式下,不能使用bios中断,需要通过读写软盘控制器芯片8237来完成。
1.1.1 软盘控制器 I/O address | Read or Write | Register |
0x3f2 | Write | DOR: Digital Output Register |
0x3f4 | Read | FDC Status: Floppy Disk Status Register |
0x3f5 | Read/Write | FDC Data: Floppy Disk Data Register |
0x3f7 | Read | DIR: Digital Input Register |
Write | DCR: Disk Control Register |
注:FDC为软盘控制器
1.1.1.1 DOR数字输出寄存器DOR是一个8为寄存器,他控制驱动器马达的开启、驱动器选择、启动/复位FDC以及允许/禁止DMA请求
位 | Name | Description |
7 | MOT_EN3 | Driver D motor:1-start;0-stop |
6 | MOT_EN2 | Driver C motor:1-start;0-stop |
5 | MOT_EN1 | Driver B motor:1-start;0-stop |
4 | MOT_EN0 | Driver A motor:1-start;0-stop |
3 | DMA_INT | DMA interrupt; 1 enable; 0-disable |
2 | RESET | FDC Reset |
1 | DRV_SEL1 | Select driver |
0 | DRV_SEL0 |
1.1.1.2 FDC Status:FDC状态寄存器
FDC status用于反映软盘驱动器FDC的基本状态。通常,在CPU想FDC发送命令或从FDC获取结果前,都要读取FDC的状态为,以判断当前的FDC data寄存器是否就需,以及确定数据传输方向。
位 | Name | Description |
7 | RQM | Data ready: FDD ready |
6 | DIO | Direction: 1 - FDD to CPU; 0 – CPU to FDD |
5 | NDM | DMA set: 1-not DMA; 0-DMA |
4 | CB | Controller busy |
3 | DDB | Driver D busy |
2 | DCB | Driver C busy |
1 | DBB | Driver B busy |
0 | DAB | Driver A busy |
1.1.1.3 FDC Data:FDC数据寄存器
FDC Data寄存器用于向FDC发送控制命令或从FDC读取状态,实现数据读写等。FDC的使用比较复杂,可支持多种命令。每个命令都通过一个命令序列实现:命令阶段、执行阶段和结果阶段。
1) 重新校正命令(FD_RECALIBRATE)
软盘启动时调用
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x07 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | US1 | US2 | Drive no. | |
执行 |
|
| 磁头移动到track0 | |||||||
结果 |
| 无 | 无 |
2) 磁头寻道命令(FD_SEEK)
把磁头定位到制定位置,在读写前执行
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x0F |
1 | 0 | 0 | 0 | 0 | 0 | HD | US1 | US2 | 磁头号、驱动器号 | |
2 | C | 磁道号 | ||||||||
执行 |
|
| 磁头移动到制定磁道 | |||||||
结果 |
| 无 | 无 |
3) 读扇区数据命令(FD_READ)
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | MT | MF | SK | 0 | 0 | 1 | 1 | 0 | 0xE6(MT=MF=SK=1) |
1 | 0 | 0 | 0 | 0 | 0 | 0 | US1 | US2 | 驱动器号 | |
2 | C | 磁道号track | ||||||||
3 | H | 磁头号head | ||||||||
4 | R | 起始扇区号start sector | ||||||||
5 | N | 扇区字节数 | ||||||||
6 | EOT | 磁道最大扇区号 | ||||||||
7 | GPL | 扇区建间隔长度(3) | ||||||||
8 | DTL | N=0时,制定扇区字节书 | ||||||||
执行 |
|
| 从软盘读取扇区 | |||||||
结果 | 1 | ST0 | 状态字节0 | |||||||
2 | ST1 | 状态字节1 | ||||||||
3 | ST2 | 状态字节2 | ||||||||
4 | C | 磁道号track | ||||||||
5 | H | 磁头号head | ||||||||
6 | R | 起始扇区号 | ||||||||
7 | N | 扇区字节数 |
注:
MT:多磁道操作。MT=1表示允许多磁道操作
MF:记录方式。MF=1表示选用MFM记录方式,否则是FM记录方式d
SK:是否跳过有删除标志的扇区。SK=1表示跳过。
返回的返回的状态ST0、ST1和ST2的含义如下:
ST0:
位 | 名称 | 说明 |
7 | ST0_INTR | 中断原因。00-正常结束;01-异常结束;10-命令无效;11-软盘驱动器状态改变 |
6 | ||
5 | ST0_SE | 寻道操作或重新校正操作结束(seek end) |
4 | ST0_ECE | 设备检查错误(0磁道校正错误)(Equip. Check Error) |
3 | ST0_NR | 软盘未就绪(Not Ready) |
2 | ST0_HA | 磁头地址。中断时磁头地址(Head Address) |
1 | ST0_DS | 驱动器号(Driver Select) |
0 |
ST1:
位 | 名称 | 说明 |
7 | ST1_EOC | 范文超过磁道最大扇区号(End of Cylinder) |
6 |
| Reserve |
5 | ST1_CRC | CRC校验出错 |
4 | ST1_OR | 数据传输超时(Over Run) |
3 |
| Reserve |
2 | ST1_ND | 未找到制定扇区(No Data) |
1 | ST1_WP | 写保护(Write Protect) |
0 | ST1_MAM | 未找到扇区地址标志ID(Miss Address Mask) |
ST2:
位 | 名称 | 说明 |
7 |
| Reserve |
6 | ST2_CM | SK=0时,读数据遇到删除标志(Control Mark) |
5 | ST2_CRC | CRC校验出错 |
4 | ST2_WC | 扇区ID信息的磁道号C不不符(Wrong Cylinder) |
3 | ST2_SEH | 检索条件满足(Scan Equal Hit) |
2 | ST2_SNS | 检索条件不满足(Scan Not Satisfied) |
1 | ST2_BC | 磁道坏(Bad Cylinder) |
0 | ST2_MAM | 未找到扇区地址标志ID(Miss Address Mask) |
4) 写扇区数据命令(FD_WRITE)
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | MT | MF | 0 | 0 | 0 | 1 | 0 | 1 | 0xC5(MT=MF=1) |
1 | 0 | 0 | 0 | 0 | 0 | 0 | US1 | US2 | 磁头号、驱动器号 | |
2 | C | 磁道号track | ||||||||
3 | H | 磁头号head | ||||||||
4 | R | 起始扇区号start sector | ||||||||
5 | N | 扇区字节数 | ||||||||
6 | EOT | 磁道最大扇区号 | ||||||||
7 | GPL | 扇区建间隔长度(3) | ||||||||
8 | DTL | N=0时,制定扇区字节书 | ||||||||
执行 |
|
| 向软盘写入扇区 | |||||||
结果 | 1 | ST0 | 状态字节0 | |||||||
2 | ST1 | 状态字节1 | ||||||||
3 | ST2 | 状态字节2 |
5) 检测中断状态命令(FD_SENSEI)
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x08 |
执行 |
|
|
| |||||||
结果 | 1 | ST0 | 状态字节0 | |||||||
2 |
| 磁头所在磁道号 |
6) 设定驱动器参数命令(FD_SPECIFY)
阶段 | 序 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 说明 |
cmd | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x03 |
1 | SRT(单位2ms) | HUT(单位32ms) | 马达速度、磁头卸载时间 | |||||||
2 | HLT(单位4ms) | ND | 磁头加载时间,非DMA模式 | |||||||
执行 |
|
| 设置控制器 | |||||||
结果 |
| 无 | 无 |
1.1.1.4 DIR:数字输入寄存器
DIR寄存器只有D7位有效,用于表示软盘更换状态,其余用于硬盘控制器。
1.1.1.5 DCR:磁盘控制寄存器DCR仅是用户D0与D1位,用于表示数据传输率。
00-500kpbs, 01-300kpbs, 10-250kpbs。
1.1.2 保护模式下代码实现 1.1.2.1 初始化1) Reset
- outb(FLOPPY_REG_DOR, 0x08); // 重启
- for (i=0 ; i<100 ; i++)
- __asm__("nop"); // 延时,保证重启完成
- outb(FLOPPY_REG_DOR, 0xc); // 选择DMA模式,选择软驱A
1) 设置磁盘数据传输速度
outb(FD_DCR, 0); // 500kpbs
2) Output_byte函数
用于FDC命令的输出,FDC的每条命令需要确保上条命令已经完成
- static void output_byte(char byte)
- {
- int counter;
- unsigned char status;
- for(counter = 0 ; counter < 10000 ; counter++) {
- status = inb(FD_STATUS) & (STATUS_READY | STATUS_DIR);
- if (status == STATUS_READY) {
- outb(FD_DATA, byte);
- return;
- }
- }
- printf("Unable to send byte to FDC\r");
- }
3) 设置驱动器参数
- output_byte(FD_SPECIFY);
- output_byte(0xCF); /* 马达步进速度、磁头卸载时间=32ms */
- output_byte(6); /* Head load time =6ms, DMA */
1.1.2.2 读扇区
- typedef struct {
- unsigned int size, sect, head, track, stretch;
- unsigned char gap,rate,spec1;
- }floppy_struct;
- static floppy_struct floppy_type =
- {2880,18,2,80,0,0x1B,0x00,0xCF }; /* 1.44MB diskette */
- static u32 current_dev = 0;
- /* (2 * 18 * 80 * 512) */
- void FloppyReadSector(u32 sectNo, u8 *buf)
- {
- u32 head, track, block, sector, seek_track;
- if (NULL == buf)
- {
- printf("FloppyReadSector para error.\r");
- return;
- }
- if (sectNo >= (floppy_type.head * floppy_type.track * floppy_type.sect))
- {
- printf("FloppyReadSector sectNo error: %x.\r", sectNo);
- return;
- }
- /* 计算参数 */
- sector = sectNo % floppy_type.sect + 1;
- block = sectNo / floppy_type.sect;
- track = block / floppy_type.head;
- head = block % floppy_type.head;
- seek_track = track << floppy_type.stretch;
-
- /* 软盘重新校正 */
- output_byte(FD_RECALIBRATE);
- output_byte(current_dev);
-
- /* 寻找磁道 */
- output_byte(FD_SEEK);
- output_byte(current_dev);
- output_byte(seek_track);
-
- /* 设置DMA,准备传送数据 */
- SetDMA(buf, FD_READ);
- /* 发送读扇区命令 */
- output_byte(FD_READ); /* command */
- output_byte(current_dev); /* driver no. */
- output_byte(track); /* track no. */
- output_byte(head); /* head */
- output_byte(sector); /* start sector */
- output_byte(2); /* sector size = 512 */
- output_byte(floppy_type.sect); /* Max sector */
- output_byte(floppy_type.gap); /* sector gap */
- output_byte(0xFF); /* sector size (0xff when n!=0 ?) */
- }
程序很清楚,不再多说,写命令于此类似。
唯一不清楚的是SetDMA函数。
我们在设置DOR时设置的DMA工作方式为enable,也就是说数据会通过DMA方式传送,因此必须设置DMA控制器。
1.1.2.3 DMA传输
- /* DMA commands */
- #define DMA_READ 0x46
- #define DMA_WRITE 0x4A
- #define immoutb_p(val,port) \
- __asm__("outb %0,%1\n\tjmp 1f\n1:\tjmp 1f\n1:"::"a" ((char) (val)),"i" (port))
- void SetDMA(u8 *buf, u8 cmd)
- {
- long addr = (long)buf;
- Cli();
- /* mask DMA 2 */
- immoutb_p(4|2,10);
- /* output command byte. I don't know why, but everyone (minix, */
- /* sanches & canton) output this twice, first to 12 then to 11 */
- __asm__("outb %%al,$12\n\tjmp 1f\n1:\tjmp 1f\n1:\t"
- "outb %%al,$11\n\tjmp 1f\n1:\tjmp 1f\n1:"::
- "a" ((char) ((cmd == FD_READ)?DMA_READ:DMA_WRITE)));
- /* 8 low bits of addr */
- immoutb_p(addr,4);
- addr >>= 8;
- /* bits 8-15 of addr */
- immoutb_p(addr,4);
- addr >>= 8;
- /* bits 16-19 of addr */
- immoutb_p(addr,0x81);
- /* low 8 bits of count-1 (1024-1=0x3ff) */
- immoutb_p(0xff,5);
- /* high 8 bits of count-1 */
- immoutb_p(3,5);
- /* activate DMA 2 */
- immoutb_p(0|2,10);
- Sti();
- }
该函数由linux0.11移植而来,可参照DMA控制器手册进行设置。不看也可以,注释写得很清楚,拿过来用就是了