0%

bootsect

开篇的废话

当处理器的RESET 引脚的电平由低变高时,处理器会执行一个硬件初始化,以及一个可选的内部自测试(BIST),然后将内部寄存器初始化到一个预置的状态。

在刚刚启动时,CPU从一个特定的地址开始执行指令。

  • 以8086为例,这个地址是 0xFFFF0(由初始化的CS:IP 决定,此时的 CS 和 IP 分别是 0xF000和0xFFF0)。

16 位 CPU ,8086 寻址空间为 1M ,0xF FFF0是实际上是内存空间中的最后一条指令。

  • 以80386为例,这个地址是0xFFFFFFF0(此时的CS和IP 分别是0xF000和0xFFF0)。

注意,32位CPU 下,这里采用的地址计算方式不是CS * 0x10+IP ,而是使用 段描述符高速缓存器中的段基地址。

如果使用bochs调试就可以看到此时的 CS 描述符高速缓存器中的段基地址为 0xffff0000,此时的 IP 为0xFFF0,所以最终指向的第一条指令为0xFFFF FFF0,同样,这个地址是内存空间中的最后一条指令。

在32位CPU 下,该指令是jmpf 0xf000:e05b,跳转执行的内存地址为 0xfe05b(在1M 内存内执行),实际上该地址就是ROM-BIOS 部分。

ROM-BIOS 会被映射到 1M 内存和4G的最后部分(具体地址请自行考究)

在执行了 BIOS 之后,BIOS 会将存储设备(软盘或硬盘)的头512字节数据加载到0x7c00,并且会检查512字节的内容是否以 0x550xaa结尾,否则引导程序无效。

关于这个地址为什么是 0x7c00,你可以参考 为什么主引导记录的内存地址是0x7C00?

Image 是 Linux 0.12 的镜像文件

在 Linux 0.12 中,这个 头 512 字节实际上就是 bootsect.S

bootsect.S 源码

1
2
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOTSEG  = 0x07c0
INITSEG = 0x9000

entry start
start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs

0x7c00开始的512字节(其实就是引导扇区bootsect部分)复制到0x90000开始的内存位置,最后跳转到0x9000:go位置执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
go:	mov	ax,cs		
mov dx,#0xfef4 ! arbitrary value >>512 - disk parm size

mov ds,ax
mov es,ax
push ax

mov ss,ax ! put stack at 0x9ff00 - 12.
mov sp,dx

#设置 ds 、es ss 段寄存器的的值为0x9000
#为什么设置栈指针为0xfef4,实际栈指针大于等于512即可
#还需考虑到后续还要加载 setup 的大小
#在linux 0.11 中,这个值是0x9ff00,这里为了放软盘参数表,准备了12字节的空间留待后续使用
#0x9000:0xfef4 = 0x9 FEF4 + 12 = 0x9 ff00

push #0
pop fs
mov bx,#0x78 ! fs:bx is parameter table address
seg fs
lgs si,(bx) ! gs:si is source

mov di,dx ! es:di is destination
mov cx,#6 ! copy 12 bytes
cld

rep
seg gs
movw

mov di,dx
movb 4(di),*18 ! patch sector count

#从fs:bx处读出软盘参数表的地址,低字软盘参数表的偏移地址读到SI 中,高字软盘参数表的段地址读到GS中
#将软盘参数表复制到0x9000:0xfef4 ,即上面的空出来的地址
#修改软盘扇区中的偏移为4的位置,设置为18
#该参数是软盘参数中每磁道的最大扇区数
#注意:0x0 0078 处并不是一个中断向量

seg fs
mov (bx),di
seg fs
mov 2(bx),es

#修改软盘参数表中的偏移地址和段地址,使其指向修改后的软盘参数表位置 0x9000:0xfef4
pop ax
mov fs,ax
mov gs,ax

#fs gs = 0x9000

xor ah,ah ! reset FDC
xor dl,dl
int 0x13

#调用0x13 中断,重置 第一块软盘 的磁盘系统

读取setup模块

接着准备读取setup模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
SETUPLEN = 4	
......
load_setup:
xor dx, dx ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue ;jnc - jump not cf

push ax ! dump error code
call print_nl
mov bp, sp
call print_hex
pop ax

xor dl, dl ! reset FDC
xor ah, ah
int 0x13
j load_setup ; j - jmp
#调用 0x13 号中断,从 0x9000:0x200 开始读取4个扇区
#磁头为0,驱动器为 0 的软盘
#柱面为0,扇区号为2
#如果返回码(CF = 0 )表示读取成功
#否则打印错误的状态码,重置 第一个软盘的磁盘系统,重新读取扇区

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

xor dl,dl
mov ah,#0x08 ! AH=8 is get drive parameters
int 0x13
xor ch,ch
seg cs
mov sectors,cx 。
mov ax,#INITSEG
mov es,ax
#使用 0x13 中断的 08功能读取驱动器的参数,ES:DI = 0x900:0xfef4
#重新读取软盘参数表
#CX 中是柱面数和扇区数
#保存到标号 sectors

! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10

mov cx,#9
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
#使用 0x10 中断的 3 号功能,读取光标的位置
#使用 0x10 中断的 0x13 号功能,往显示区域写入字符
! ok, we've written the message, now
.....

sectors:
.word 0 #初始化内容是2个字节的0x00

msg1:
.byte 13,10 #13 10 是 CR LR 的ASCII 码
.ascii "Loading"

读取system模块

system模块读取到内存中:

1
2
3
4
5
6
7
8
mov	ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it ; 读磁盘上system模块
call kill_motor ; 关闭驱动器马达
call print_nl
#SYSSEG = 0x1000
#调用了3个例程,其中最后复杂的是 read_it
#读取 system 到 0x1000:0x00 处

依次来看,首先看read_it 例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
read_it:
mov ax,es
test ax,#0x0fff
die:
jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet? ; 是否已经加载了全部数据?
jb ok1_read
ret

#使用 teset 指令测试 SYSSEG 的值的低12位是否为0
#低12位为0 ,意味着所对应的地址在64 KB 的边界处(test指令可能导致其他问题,列入0x00 不会导致死循环)
#如果 SYSSEG 低于 ENDSEG 的值就 jmp 至 ok1_read
#否则 说明已经高于 ENDSEG,返回调用者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9

#从标号sectors 读取每磁道的扇区数
#减去当前磁道已读扇区数,得到当前磁道未读扇区数
#并且计算当前磁道扇区数占用的字节数(*512)+偏移(初始值为0)是否超过了64KB
#没有超过则jmp 至 ok2_read
#如果超过,则计算最多能读取的字节数
#0 - 偏移 = 最多能读取的字节数
#该值右移9位(/512)即为最多能读取的扇区数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ok2_read:
call read_track ; 读当前磁道上指定扇区和需读扇区数的数据
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ah,#0x10
mov es,ax
xor bx,bx
jmp rp_read

#ok2_read 例程
#read_track 的操作成功后的返回参数为 AH=00 AL=传输的扇区数
#标号 sread 处存放的是当前磁道已读扇区数
#传输的扇区数 + 当前磁道已读扇区数 =?当前磁道扇区数(标号sectors)
#如果小于则说明当前磁道未读,jmp 至 ok3_read。
#如果不小于,说明当前磁道所有扇区已经读取,应该读取下一个磁道的的 1号磁头
#如果是0号磁头,则 jmp 至 ok4_read
#否则读取下一个磁道

#ok4_read 例程
#将当前磁道号回填 标号head 处

#ok3_read 例程
#将 标号 head 当前已读扇区数置0(为什么?因为换了磁头)
#检查上次读的扇区数量加偏移是否超出了 64 KB
#如果没有超出则 jmp 至 rp_read 继续读取
#如果超出则调整 段地址为下一个64KB 开始处,偏移置0 ,jmp 至 rp_read

这里反复提到rp_read 例程,该例程其实很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
#检测当前的段地址是否小于 ENDSEG
#如果小于,说明system还没读完,jmp 至 ok1_read 继续读取
#如果不小于,说明system已经读取完成,返回。

#题外话
#这里 system 段地址起始地址为 0x1000 结束地址为 0x4000
#也就是说,system 的大小为 起始内存为 0x10000 结束内存为 0x40000
#即 192 KB 的空间,后面编译的时候验证

最后查看read_track 例程,这也是真正读取扇区到内存中的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
read_track:
pusha ; push all
pusha
mov ax, #0xe2e ! loading... message 2e = .
mov bx, #7
int 0x10
popa

mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
and dx,#0x0100
mov ah,#2

push dx ! save for error dump
push cx
push bx
push ax

int 0x13
jc bad_rt
add sp, #8
popa
ret

#首先调用 0x10 中断打印一串提示信息
#从标号 track 获取当前磁道号
#从标号 sread 获取已读扇区号 + 1 作为起始扇区
#从标号 head 获取当前磁头号
#调用 0x13 中断读取扇区
#注意 AL 扇区数来自 ok1_read 例程的计算


bad_rt:
push ax ! save error code
call print_all ! ah = error, al = read

xor ah,ah
xor dl,dl
int 0x13

add sp, #10
popa
jmp read_track

#bad_rt 例程作为处理读取错误的情况存在
#调用 print_all 打印之前保存在栈中的相关寄存器
#并且 重置 软盘的磁盘操作系统,从栈中恢复在读取扇区之前的寄存器参数,调用 read_track 重新读取扇区

这段代码真的读的我难受。

在磁头号、磁道号、扇区号的处理中参数太多,很容易理不清。

以上代码请配合 13h10h中断的功能表进行参考,相关参数并未做说明。

读取system模块到内存之后,调用kill_motor例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kill_motor:
push dx
mov dx,#0x3f2
xor al, al
outb
pop dx
ret

#0x3f2 是软盘控制器的只写端口号
#AL =0x00
#软盘驱动器A
#复位FDC
#禁止DMA和中断请求
#关闭软驱A的马达

最后print_nl例程进行回车换行。

参考:

软盘控制器的编程方法

检查根文件系统设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ROOT_DEV = 0
SWAP_DEV = 0

seg cs
mov ax,root_dev
or ax,ax
jne root_defined
seg cs ; 取出sectors的值(每磁道扇区数)
mov bx,sectors
mov ax,#0x0208 ! /dev/PS0 - 1.2Mb
cmp bx,#15 ; sectors=15则说明是1.2MB的软驱
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18 ; sectors=18则说明是1.44MB的软驱
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
jmpi 0,SETUPSEG

#如果 root_dev 为不为0 则 jmp 至 root_defined
#如果 root_dev 为0 则取出 标号 sectors (每磁道的扇区数量)处的值
#如果该值为 15 则设置逻辑设备号为 0x0208
#如果该值为 18 则设置逻辑设备号为 0x021c
#最后将AX(即逻辑设备号)的值回填到标号 root_dev 处
#最后jmp 至 SETUPSEG:0x0000
#SETUPSEG 值为0x9020 ,即 jmp 0x90200
#也就是setup 加载的位置

#如果没有更改的话,这个的值为 默认 0x301 代表第一个硬盘的第一个分区

这里的 ROOT_DEVtools下的build 程序 写入,来看看具体是如何写入的。

看了 Makefile 执行的命令如下:

看了下源码,C 还不太熟,调试了了下:

argc=4 也就是说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define DEFAULT_MAJOR_ROOT 3
#define DEFAULT_MINOR_ROOT 1//6
if (argc > 4) {
if (strcmp(argv[4], "FLOPPY")) {
if (stat(argv[4], &sb)) {
perror(argv[4]);
die("Couldn't stat root device.");
}
major_root = MAJOR(sb.st_rdev); /* 取设备名状态结构中设备号 */
minor_root = MINOR(sb.st_rdev);
} else {
major_root = 0;
minor_root = 0;
}
/* 若argc=4,则取系统默认的根设备号 */
} else {
major_root = DEFAULT_MAJOR_ROOT;
minor_root = DEFAULT_MINOR_ROOT;
}
//如果argc=4,则 major_root =3 minor_root = 1
....
buf[508] = (char) minor_root;
buf[509] = (char) major_root;

1
2
objdump -m i8086 -b binary -D Image --stop-address 0x200
xxd -g 1 -seek 508 -l 2 Image

而偏移 508 和 509 处在 bootsect中是预留好的:

1
2
3
4
5
6
7
8
.org 506
swap_dev:
.word SWAP_DEV
root_dev:
.word ROOT_DEV

boot_flag:
.word 0xAA55a

小结

512 字节bootsect被安排的非常清楚,没有一个字节是多余的。

这段程序我一开始我以为我会读的非常容易。

直到读到了 “读取 system 模块”部分,这部分参数太多,看着脑子乱,发现看不进去,看的恶心。

进而这几天都在摸鱼想这个(说明好的代码可读性的重要性)。

今天为什么能读下来了?我觉得原因有几个:

  • 不要过分关注一些硬件细节。比如 “读取 system 模块”也就在这里用下,大致知道他的用途即可,能看懂就看懂,看不懂也没关系。

    我在看《一个64位操作系统的实现》时,也是被作者开篇的文件系统给恶心到了,起始认真看下去即可,指示概念太多,一下子缓不过来。

  • 参考了更多的书籍。《LInux 内核 完全注释》中已经给出了很详细的说明,《Linux内核设计的艺术》中这部分也没有做更多的介绍。

即使已经读完这部分代码,我仍然对扇区部分有点迷糊,我找到一篇比较好的博客,有兴趣的可以参考该部分

参考

x86汇编语言:从实模式到保护模式——p33