0%

setup.S 模块

如果直接看上一篇比较懵,其实很正常。

本身使用的汇编语法是 AS 86汇编 ,虽然和 Intel 语法很相似,但是指令上阅读还是很难受的。

我读的时候都是一遍对照着 Bochs 的反汇编 一遍看源码,偶尔还查查指令,大致理解了这段代码的意思。

如果想对这段代码有一个真实的认识,建议 Bochs 调试一遍即可。

挖个坑

由于 Linux 0.12 的内核镜像 和根文件系统 相互独立,如果想在硬盘上引导系统就会出现这样一个问题,根文件系统和内核镜像文件不能共存。

Bochs 的配置中 启动盘是软盘 指向 Linux 0.12 镜像,硬盘是 根文件系统。

作者 赵炯 给出了两种方法:

  • 多个分区。内核镜像放在主分区,根文件系统放在另一个分区中。
  • 内核镜像和根文件系统放在同一个分区,内核镜像放在开始的一些扇区中,根文件系统从指定的扇区开始存放。

两种希望最后能够摸索出来。

setup.S 程序

setup.S 的主要作用是利用 BIOS 中断从设备中提取内核运行所运行的机器系统数据,机器系统数据被加载到内存 0x9000:0x0000 ~ 0x9000:0x01FC,即原来原来的 bootsect 只有2字节没有被覆盖。只能感叹,内存的使用规划明确,使用率极高。bootsect执行结束,执行setup就立刻将器数据覆盖。

1
2
#断点在 setup.s 中即可
b 0x9000:0x200
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
	mov	ax,#INITSEG		! this is done in bootsect already, but...

mov ds,ax
#DS = 0x9000
! Get memory size (extended mem, kB)
mov ah,#0x88
int 0x15
mov [2],ax
#使用 0x15 中断的 0x88 功能 获取连续内存大小
#实测Bochs 中 AX 7c00 ,Bochs分配的32MB内存,7c00正好是剩余的31MB内存
! check for EGA/VGA and some config parameters
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx
; 检测屏幕当前行列值。若显示卡是VGA卡,则请求用户选择显示行列值
mov ax,#0x5019 ; 预设行列值(ah = 80列,al = 25行)
cmp bl,#0x10
je novga
call chsvga

#中断0x10 的0x12 功能,是显示器的配置中断
#子功能号读取配置信息
#将配置信息写入 0x9000:0x8 ~ 0x9000:0xC 部分
novga:
mov [14],ax

mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
#中断0x10 的 0x3 号功能
#获取光标的位置(DH=行 DL=列)

! Get video-card data:
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width
#获取当前显示模式
#AL=显示模式 AL=窗口?
#BH=页码

! Get hd0 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb

#中断向量 0x41 处的值其实是硬盘参数表的偏移地址和段地址
# ds:si=(0x0000:0x41*4):(0x:0000:0x41*4+2) #希望这样看能够看懂
# 复制硬盘参数表到 0x9000:0x80 硬盘参数表一共10字节

! Get hd1 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb

#和上面类似(查文档说为DOS 中断向量)
#复制第二块硬盘的硬盘参数表到 0x9000:0x80
! Check that there IS a hd1 :-)

mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3
je is_disk1 ; 是硬盘吗?(类型 = 3?).
#使用 0x13 中断的0x15 功能,读取磁盘类型
#如果操作失败说明(CF = 1) 说明没有第二块硬盘,jmp 至no_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
#AL=0x00
#es:di = 0x9000:0x90
#换成 intel 语法是这样 rep stosb byte ptr es:[di], al
#也就是从0x00 覆盖 es:di 所指的内存
#这里的作用为,如果没有第二块硬盘,就用零填充,即清空第二块硬盘的参数表。
is_disk1:

从这里开始,就开始为进入保护模式做准备。

如果不熟悉保护模式,看这部分可能会吃力。

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
! now we want to move to protected mode ...

cli ! no interrupts allowed !
#后面的部分会覆盖实模式下的中断向量,所以不允许可屏蔽中断的发生
! first we move the system to it's rightful place

mov ax,#0x0000
cld ! 'direction' = 0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax
sub di,di
sub si,si
mov cx,#0x8000 ; 移动0x8000个字
rep
movsw
jmp do_move
#es:di 一开始为 0x00:0x00
#ds:si 一开始为 0x1000:0x00
#0x1000:0x00 ~ 0x9000:0x0000 这段内容是什么,其实就是之前的 system 模块
#为什么是0x1000:0x00 ~ 0x9000:0x0000, 这段空间共 512KB
#因为当时规划 system 部分 512KB 足够使用了
#实际上在前面的 system 加载过程中,仅使用到了 0x1000:0x00 ~ 0x4000:0x00 部分。

#回到这段代码
#这段代码实际上将 system 模块从 0x1000:0x000 整体移动至 0x0000:0x00 开始的地方
#注意:实际上这里覆盖了实模式下的中断向量表部分,所以在此之前进行了可屏蔽中断的设置
#为什么每次移动了0x8000 个字
#0x1000:0x0000 ~ 0x2000:0x0000 之间有0x10000 字节
#1个字为2字节 0x10000 字节 = 0x8000 字

这段代码执行后,内存中bootsectsetupsystem的内存分布如下,这里引用 作者赵炯的图,以便对此刻的内存有一个直观的认识:

1
2
3
4
5
6
7
8
9
10
11
12
! then we load the segment descriptors

end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate

#SETUPSEG 实际上是 0x9020
#ds= 0x9020 实际上指向的是 setup 开始的地址
#使用 lidt 加载 IDT (中断描述符表)到IDT 寄存器
#使用 lgdt 加载 GDT(全局描述符表)到GDT 寄存器
1
2
3
4
5
6
7
8
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
#共6字节
#前两个字节指定 GDT 的 段界限(limit)
#后4个字节指定 GDT 的 段基地址(Base)
#这里都是0,0
#是因为 进入保护模式之前必须设置 IDT ,虽然 IDT 里啥页没有
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
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries

.word 512+gdt,0x9 ! gdt base = 0X9xxxx
; (线性地址空间)基地址:0x90200 + gdt
#limit 为0x800 每个描述符占8个字节,即可以设置 256个全局描述符
#这里的 gdt 也是个标号
#base的高字部分为 0x09
#低字部分为 512 +gdt
#为什么这样设置?因为 标号 gdt 表示从 gdt 到 setup文件开头的偏移
#此时setup 被加载到内存位置为 0x90200
#也就是说标号 gdt 在内存中的地址应为 0x90200 + gdt
#即高字为 0x09 ,低字为 0x200 +gdt = 512 + gdt
gdt:
.word 0,0,0,0 ! dummy
#处理器要求索引为0 的全局描述符为 空


.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec ; 代码段为只读,可执行
.word 0x00C0 ! granularity=4096, 386 ; 颗粒度4K,32位
#段基地址(base)为 0x0000 0000
#段界限(limit)为 0x0 07ff
#粒度(Granularity) 为 1 ,即实际使用的段界限为 0x7ff* 0x1000 + 0xfff = 0x7F FFFE
#这段内存有多长?大概是差 2字节就是8MB ,这里说8MB 也没错
#TYPE 为 1010 可执行、可读的代码段

.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
#段基地址(base)为 0x0000 0000
#段界限(limit)为 0x0 07ff
#粒度(Granularity) 为 1 ,即实际使用的段界限为 0x7ff* 0x1000 + 0xfff = 0x7F FFFE
#这段内存有多长?大概是差 2字节就是8MB ,这里说8MB 也没错
#TYPE 为 0010 可读、可写的数据段

接着开启A20 地址线:

1
2
3
4
5
6
7
8
9
10
11
! that was painless, now we enable A20

call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
#这里使用键盘控制器(8042芯片)来处理 Gate
#从而开启 A20 Gate

关于A20 地址线的问题,实模式下只能寻址1MB 内存,内存地址为 0x0 0000 ~ 0xF FFFF,进行A20屏蔽之后,对第21根地址线的处理特殊化,使得运算结果恒为0,使得地址回绕无效,从而能访问1MB以上的内存。

后面一部分是对8259A 进行编程:

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
mov	al,#0x11		! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2 ; $ 表示当前指令的地址,
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al

#.word 0x00eb ,0x00eb 是 jmp $+2的机器码;)
#暂时不知道这么做的原因?可能是向增加占用的空间?

#略过这部分,大致理解为 对 8259A 芯片(中断控制器)进行设置
#修改中断控制器,使得 IRQ0x00 ~IRQ0X0f 对应的中断号为 0x20 ~0x2F
#为什么要修改中断控制器中对应的中断号?
#因为后面要自定义 0x00 ~0x1F 的中断向量
#最终效果是屏蔽掉8259A 主芯片和 从芯片的所有中断请求。

进入保护模式:

1
2
3
4
5
6
7
8
9
10
11
mov	ax,#0x0001	! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)

#lmsw指令 将 AX 加载到CR0寄存器,AX 的值仅位0 为 1
#即加载之后CR0 的位0 为1,处理器切换到保护模式
#注意 jmpi 0,8 执行时,处理器已经处于保护模式了
#索引号为8 ,偏移地址为0
#索引号为8 即为GDT 中的第3个描述符,代码段描述符
#段基地址为0x0000 0000 ,偏移为 0x0000
#最后jmp 只 0x0000 0000 处

0x0000 0000处存放的实际上是 system模块,而system模块一开始的部分是head模块,我想这也是它为什么叫 head的原因。

为什么system模块的一开始是head

最后这里我产生了几个疑问?为什么模块的一开始是head部分,前面我说system模块是 0 磁头 0 磁面 6 扇区的位置,对应的镜像文件,也就是第6个512字节的部分,但是为什么能将system模块控制在这个部分?目前 猜测和build部分有关。

使用make -n查看 make过程中执行的指令,发现head相关的指令有两条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#makefile 中的执行指令
as --32 -o boot/head.o boot/head.s
#head.s 中使用的 AT&T 汇编 ,使用的编译器为 AS
#指定生成了的32位的代码
#
ld -m elf_i386 -M -x -Ttext 0 -e startup_32 boot/head.o init/main.o \
kernel/kernel.o mm/mm.o fs/fs.o \
kernel/blk_drv/blk_drv.a kernel/chr_drv/chr_drv.a \
kernel/math/math.a \
lib/lib.a \
-o tools/system > System.map
# -m
# -M
# -Ttext 0
# -e startup_32

我觉得需要理解链接这个过程做了什么才能理解linus本人是如何精确的安排代码到镜像文件中。

小结

总结下 setup做了什么:

  • 初始化系统的相关参数,放在0x9000:0x0000 ~ 0x9000:0x01FC,覆盖前面已经使用过了的bootsect
  • 移动system模块到0x0000:0x0000开始的位置;
  • 加载 IDT 寄存器、加载GDT 寄存器;
  • 开启 A20 gate (使得能够访问1MB以上的内存);
  • 重编程 8259A 芯片(为自定义中断向量做准备);
  • 进入保护模式;

注意:此时的GDT 在 0x9000:0x2000 处加载的 setup模块部分。