C语言程序

工具

首先,需要的工具软件列表:

  • gcc编译器:

编译C语言程序

1. 为什么没有main函数

main函数链接时需要一些系统库文件。而我们的系统目前并没有任何的系统库可以用,会导致报错。

所以此处不能使用main函数。

那么我们使用默认的入口_start符号(不设置,则默认为0)

准备工作

输出当前现存显存显示位置是否为素数的c语言代码

boot/loaderELF.c

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
int is_prime(unsigned short n)
{
//返回1表示素数,返回0表示非素数
int i = 0;
for (i = 2; i < n; i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}

int _start(){
unsigned short* pvga = (unsigned short*)0xb8000; //填充到显示内存的初始地址
for(int i = 0;i <= 0x7fff;i++){
//char: 0x3 ,color: 0x104
if(is_prime(i) == 1) {
*(pvga + i) = (unsigned short)0x1704; //显存填充,蓝色背景白色棱形
} else {
*(pvga + i) = (unsigned short)0x1700; //显存填充背景色
}
}
fin:
goto fin;
}


说明:
1)计算素数数字,左上为0,位置从左到右并逐行增加,和显存的偏移量相同
2)使用简单的循环遍历计算当前位置的数字是否为素数
3)如果位置不为素数,输出空白,为素数输出棱形
3)0x1704: 0x17代表蓝色背景白色文字,0x04在ascii码里面是棱形```

编译成目标文件

$ gcc boot/loaderELF.c -m32 -c build/loaderELF.o

gcc编译参数

-O0: 无优化。编译器不会进行任何优化,生成的代码与源代码几乎完全相同。
-nostdinc: 不搜索默认路径头文件
-nostdlib: 不使用标准库
-fno-builtin: 不使用内建函数
-Wall
-Wstrict-prototypes
-Wmissing-prototypes

链接C语言程序

1. 为什么要指定程序入口
由于在保护模式下,我们默认加载到 0x10000处执行代码。所以,下载需要做的是
1)指定当前c语言程序的入口地址为0x10000
2)复制程序段的执行程序段到 0x10000

链接并指定程序入口

ld -m elf_i386 -s -Ttext 0x00010000 build/loaderELF.o -o build/loaderELF.bin

最后的bin文件大小大概 是14Kb.

ELF信息查看

查看文件信息

file loaderELF.bin

images/3_1_1.png

查看反编译内容

objdump -S loaderELF.bin

images/3_1_2.png

查看文件信息

readelf -e loaderELF.bin

images/3_1_3.png

查看纯二进制内容

xxd loaderELF.bin

images/3_1_4.png

GDT全局描述符表

什么是GDT全局描述符表

GDT全称为Global Descriptor Table,全局描述符表。

保护模式的寻址方式不在使用寄存器分段的方式直接寻址方式了。而采用的是使用GDT(全局分段描述表)来寻址。从而使用更多的内存地址。

创建GDT全局描述符表使用到一个48位的寄存器:GDTR寄存器。

1)首先,在内存中划分一些内存段,并且每个内存段赋予一个索引。

2)然后,使用lgdt指令,设置GDT的索引和表信息的内存地址到GDTR寄存器。

3)进入保护模式,指令跳转,从实模式分段方式寻址切换到使用GDT分段方式寻址。

  1. GDT可以被放在内存的任何地方,只要提供内存地址给GDTR寄存器就可以了。

GDT格式

GDT全局描述符表

  • 表基地址,表基地址位GDT段表在内存的地址,GDT段表是一个列表,存储了多个 GDT段描述符。
  • 表界限:GDT段表的空间信息,以字节为单位。

images/2_7_1.png

GDT全局描述符表 = GDT段表基地址 | 16位表界限

GDT段表 =  GDT段描述符 |  GDT段描述符 | GDT段描述符 …

表界限 = GDT字节数 - 1 (表示 0 - 0x…)

GDT段描述符

images/2_7_2.png

GDT段描述符,用来描述在GDT方式在内存中分配的一个段信息,总共8字节64位。

GDT段描述符结构

为了兼容以前的CPU,GDT段描述符的信息被分割成几个部分,格式如下:

GDT段描述符 = 

高32位:段基址 (高8位)| 段描述符(高4位) | 段界限(高4位) | 段描述符(低8位)| 段基址 (中8位)

低32位:段基址 (低16位) | 段界限(低16位)

  • 32位段基址 = 段基址 (高8位) + 段基址 (中8位) + 段基址 (低16位)

  • 20段界限 = 段界限(高4位) + 段界限(低16位)

  • 12位段描述符 = 段描述符(高4位 )+ 段描述符(低8位)

段描述符定义

  • 段基址:规定段的起始地址,长度32位.
  • 段界限:规定段的大小,长度20位。段界限可以是以4KB或者1B为单元大小
  • 段属性:确定段的各种性质.长度(12位)

段属性:

  • G 粒度位: 段界限的单位大小,G=1表示段界限以4KB为单元单位,G=0表示段界限以1B为单元单位
  • D/B 表示操作数为多少位, 0表示16位操作数,1表示32位操作数
  • L : 0 表示非64位代码段,1表示64位代码段
  • AVL :可用字段,暂时没什么用
  • P 段存在位:通常为1,表示段存在于内存中,0则此段为非法的,不能被用来实现地址转换
  • DPL 特权级(2位): 用来实现保护机制
  • S 为0表示系统段,为1表示非系统段
  • type 类型(4位): 用于区别不同类型的描述符。内存段或者门的子类型

type值

Type位 说明 取值
代码段时
X:3位 代码段值为1 0:为数据段
1:为代码段
C:2位 访问位 0:为普通段
1:为一致码段
R:1位 是否可读 0:只执行
1:可读
A:0位 访问位. 该段是否被访问过 0 :未访问
1:已访问
数据段时
X:3位 数据段值为1 0:为数据段
1:为代码段
E:2位 扩展方向 0:向高位扩展
1:向低位扩展
W:1位 是否可写 0:只读
1:可写
A:0位 访问位 0: 未访问
1:已访问

段界限:

段界限边界值 = (描述符的段界限值 + 1) × (段界限颗粒读:4Kb 或者 1b) -1

反之:
描述符的段界限值 = (段界限边界值 + 1) /(段界限颗粒读:4Kb 或者 1b)

例如:

16MB的段界限值 = 0x1000000 /(段界限颗粒读:4Kb 或者 1b - 1)= 0x0fff

段选择子

段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)

GDTR寄存器

在内存中建立完成GDT信息后,CPU会将GDT的内存地址 和 段界限 数据加载入GDTR寄存器

GDTR寄存器数据(48位):

GDTR定义数据(48位) = GDT全局描述符表的大小(16位) + GDT全局描述符表的地址(32位)

lgdt指令

lgdt GDTR定义数据

其中GDT全局描述符表数据格式如下

GDT全局描述符表 = GDT段描述符(64位) | GDT段描述符(64位) | GDT段描述符(64位) …

GDT段描述符 = 段基址 (8位)| 段描述符(4位) | 段界限(4位) | 段描述符(8位) | 段基址 (8位) | 段基址 (16位) | 段界限(16位)

其中,第一个GDT段的数据为空。

GDT临时分段

GDT临时段说明

现在已经进入了保护模式, 目前的改变

  • 可以访问1M以上的内存了
  • 可以使用32位的指令

问题:

由于以前的是实式下段寄存器寻址方式无法使用了,我们必须切换到使用GDT段方式来寻址

首要的任务就是先建立一个临时的GDT段,以便我们接下来的指令操作

目前准备建立3个段,如下:

Base, Limit, Attr

代码段:0x00000000, 0xfffff, 1100_1001_1010B = db 0x0000ffff, 0x00cf9a00

数据段:0x00000000, 0xfffff, 1100_1001_0010B = db 0x0000ffff, 0x00cf9200

vga显卡内存数据段:x000b8000, 0x00fff, 1100_1001_0010B

GDT解析宏

首先创建一个nasm宏,可以进行GDT解析

参数为GDT的Base, Limit, Attr,也就是段基址(32位),段界限(20位),段描述符(12位),然后生成GDT在内存中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;---------------------------------------------------------
; 描述符
; usage: Gdt_Descriptor Base, Limit, Attr : 段基址(32位),段界限(20位),段描述符(12位)
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
;---------------------------------------------------------
%macro Gdt_Descriptor 3
dw %2 & 0xFFFF
dw %1 & 0xFFFF
db (%1 >> 16) & 0xFF
db %3 & 0xFF
db ((%3 >> 4 ) & 0xF0 ) | ((%2 >> 16) & 0x0F )
db (%1 >> 24) & 0xFF
%endmacro

GDT 描述符属性定义

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
;--------------   gdt描述符属性  -------------
DESC_G_4K equ 1000_0000_0000b
DESC_D_32 equ 0100_0000_0000b
DESC_L equ 0000_0000_0000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0000_0000_0000b ; cpu不用此位,暂置为0
DESC_P equ 0000_1000_0000b
DESC_DPL_0 equ 000_0000b
DESC_DPL_1 equ 010_0000b
DESC_DPL_2 equ 100_0000b
DESC_DPL_3 equ 110_0000b
DESC_S_CODE equ 1_0000b
DESC_S_DATA equ 1_0000b
DESC_S_SYS equ 0_0000b
DESC_TYPE_CODE equ 1010b ;x=1可执行代码段,c=0普通,r=1可读,a=0已访问位a清0
DESC_TYPE_DATA equ 0010b ;x=0数据段,e=0向高位扩展,w=1可写,a=0已访问位a清0.

DESC_ATTR_CODE equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE
DESC_ATTR_DATA equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA


;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;-------------    选择子序列 --------------------------
SELECTOR_CODE equ 0x1<<3
SELECTOR_DATA equ 0x2<<3
SELECTOR_VGA equ  0x3<<3

定义GDT全局描述符表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;---------------------------------
;定义GDT全局描述符表
;code: 0x00000000 - 0x000FFFFF
;data: 0x00000000 - 0x000FFFFF
;vga: 0x000B8000 - 0x000BFFFF
;---------------------------------
Gdt_Addr:
dw 8*4-1 ;指定段上限为4(GDT全局描述符表的大小)
dd Gdt_Table_Addr ;GDT全局描述符表的地址
Gdt_Table_Addr:
Gdt_Descriptor 0,0,0
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_CODE ;可以执行的段
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_DATA ;可以读写的段
Gdt_Descriptor 0x000B8000, 0x00007FFF, DESC_ATTR_DATA ;vga段
dw 0

加载gdt

1
2
3
;---------------------------
;加载GDT
lgdt [Gdt_Addr]

代码

创建常量头文件

创建 boot.inc文件。用来配置常量

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
; boot.inc
;---------------------------------------------------------
; 描述符
; usage: Gdt_Descriptor Base, Limit, Attr : 段基址(32位),段界限(20位),段描述符(12位)
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
;---------------------------------------------------------
%macro Gdt_Descriptor 3
dw %2 & 0xFFFF
dw %1 & 0xFFFF
db (%1 >> 16) & 0xFF
db %3 & 0xFF
db ((%3 >> 4 ) & 0xF0 ) | ((%2 >> 16) & 0x0F )
db (%1 >> 24) & 0xFF
%endmacro



;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1000_0000_0000b
DESC_D_32 equ 0100_0000_0000b
DESC_L equ 0000_0000_0000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0000_0000_0000b ; cpu不用此位,暂置为0
DESC_P equ 0000_1000_0000b
DESC_DPL_0 equ 000_0000b
DESC_DPL_1 equ 010_0000b
DESC_DPL_2 equ 100_0000b
DESC_DPL_3 equ 110_0000b
DESC_S_CODE equ 1_0000b
DESC_S_DATA equ 1_0000b
DESC_S_SYS equ 0_0000b
DESC_TYPE_CODE equ 1010b ;x=1可执行代码段,c=0普通,r=1可读,a=0已访问位a清0
DESC_TYPE_DATA equ 0010b ;x=0数据段,e=0向高位扩展,w=1可写,a=0已访问位a清0.

DESC_ATTR_CODE equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE
DESC_ATTR_DATA equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA


;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;-------------    选择子序列 --------------------------
SELECTOR_CODE equ 0x1<<3
SELECTOR_DATA equ 0x2<<3
SELECTOR_VGA equ  0x3<<3

;----------- loader const ------------------
LOADER_SECTOR_LBA equ 0x1 ;第2个逻辑扇区开始
LOADER_SECTOR_NUM equ 9 ;读取9个扇区
LOADER_BASE_ADDR equ 0x8000 ;内存地址0x8000
;-------------------------------------------

loader.asm文件

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
;RAST LOADER [0x9000]
;Tab=4
[bits 16]

%include "boot/boot.inc"

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址


jmp Entry;
;---------------------------------
;定义GDT全局描述符表
;code: 0x00000000 - 0x000FFFFF
;data: 0x00000000 - 0x000FFFFF
;vga: 0x000B8000 - 0x000BFFFF
;---------------------------------
Gdt_Addr:
dw 8*4-1 ;指定段上限为4(GDT全局描述符表的大小)
dd Gdt_Table_Addr ;GDT全局描述符表的地址
Gdt_Table_Addr:
Gdt_Descriptor 0,0,0
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_CODE ;可以执行的段
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_DATA ;可以读写的段
Gdt_Descriptor 0x000B8000, 0x00007FFF, DESC_ATTR_DATA ;vga段
dw 0

;程序核心内容
Entry:

;----------------------
;禁止CPU级别的中断,进入保护模式时没有建立中断表
;----------------------
cli

;----------------------
;打开A20
;----------------------
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al

;----------------------
;加载GDT
;----------------------
lgdt [Gdt_Addr]

;----------------------
;进入保护模式
;----------------------
mov eax,cr0
or eax,0x1 ;设置第0位为1
mov cr0,eax

;程序挂起
Fin:
hlt ;让CPU挂起,等待指令。
jmp Fin



times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

测试

使用bochs执行

打好断点后,执行并查看gtd描述符数据是否正确。

info gdt

images/2_7_3.png

为什么需要刷新流水线

实模式和保护模式,地址位数不一样,实模式是16位寻址,保护模式是32位寻址。

实模式和保护模式的寻址方式也不一样,段寄存器内容也不一样。实模式中,CS和DS段寄存器是当前代码或者数据的基址段寄存器。而保护模式下,CS和DS段寄存器是GDT表的索引。

因此,进入保护模式下,需要尽快刷新CS,SS等段寄存器,否则将无法进行寻址。

立即跳转到32位模式,刷新流水线

进入保护模式后,需要马上跳转并刷新流水

定义代码段和数据段的选择子常量

CODE选择子: selector_code = 0x1<<3 + 000B

DATA 选择子:selector_data = 0x2<<3 + 000B

VGA 选择子: selector_vga = 0x3 <<3 + 000B

boot.inc文件定义选择子

1
2
3
4
;-------------    选择子 --------------------------
SELECTOR_CODE equ 0x1<<3
SELECTOR_DATA equ 0x2<<3
SELECTOR_VGA equ 0x3<<3

loader.asm文件 跳转并刷新流水,由16位模式进入32位代码模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;------------------------    
;刷新流水线,进入32位模式
[bits 16]

jmp dword SELECTOR_CODE:FlushPipeline


[bits 32]
;------------------
;刷新流水线
FlushPipeline:

mov ax,SELECTOR_DATA ; 可读写的32bit
mov ds,ax
mov es,ax
mov fs,ax
mov ss,ax
mov ax,SELECTOR_VGA
mov gs,ax

代码

loader文件内容如下:
loader.asm

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
98
99
100
101
102
103
104
105
106
107
108
;RAST LOADER [0x8000]
;Tab=4
[bits 16]

%include "boot/boot.inc"

;---------------------------------------

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址

jmp Entry

;---------------------------------
;定义GDT全局描述符表
;code: 0x00000000 - 0x000FFFFF
;data: 0x00000000 - 0x000FFFFF
;vga: 0x000B8000 - 0x000BFFFF
;---------------------------------
Gdt_Addr:
dw 8*4-1 ;指定段上限为4(GDT全局描述符表的大小)
dd Gdt_Table_Addr ;GDT全局描述符表的地址
Gdt_Table_Addr:
Gdt_Descriptor 0,0,0
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_CODE ;可以执行的段
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_DATA ;可以读写的段
Gdt_Descriptor 0x000B8000, 0x00007FFF, DESC_ATTR_DATA ;vga段
dw 0


;程序核心内容
Entry:

;------------------
;禁止CPU级别的中断
;------------------
cli

;------------------
;打开A20
;------------------
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al

;----------------------
;加载GDT
;----------------------
lgdt [Gdt_Addr]

;------------------
;进入保护模式
;------------------
mov eax,cr0
or eax,0x1 ;设置第0位为1
mov cr0,eax

jmp ProtectMode

;------------------------
;刷新流水线,进入32位模式
[bits 16]
ProtectMode:
jmp dword SELECTOR_CODE:FlushPipeline


[bits 32]
;------------------
;刷新流水线
FlushPipeline:
mov ax,SELECTOR_DATA ; 可读写的32bit
mov ds,ax
mov es,ax
mov fs,ax
mov ss,ax
mov ax,SELECTOR_VGA
mov gs,ax

PutHello:
; ---------------------------
; 打印hello
; ----------------------------
mov byte [gs:0x00],'h' ;输出字符
mov byte [gs:0x01],0x1F ;设置颜色(背景色蓝,前景色白)
mov byte [gs:0x02],'e'
mov byte [gs:0x03],0x1F
mov byte [gs:0x04],'l'
mov byte [gs:0x05],0x1F
mov byte [gs:0x06],'l'
mov byte [gs:0x07],0x1F
mov byte [gs:0x08],'o'
mov byte [gs:0x09],0x1F
mov byte [gs:0x0a],','
mov byte [gs:0x0b],0x1F
mov byte [gs:0x0c],'r' ;输出字符
mov byte [gs:0x0d],0x1F ;设置颜色(背景色蓝,前景淡紫)
mov byte [gs:0x0e],'a'
mov byte [gs:0x0f],0x1F
mov byte [gs:0x10],'s'
mov byte [gs:0x11],0x1F
mov byte [gs:0x12],'t'
mov byte [gs:0x13],0x1F

;程序挂起
Fin:
hlt ;让CPU挂起,等待指令。
jmp Fin

times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

images/2_8_1.png

保护模式

保护模式寻址方式

1. 实模式下寻址的缺陷

2.保护模式下寻址

实模式下使用的是段寄存器(16位) << 4 + 偏移地址方式来寻址。

保护模式和实模式下的完全不一样。

  1. 保护模式下寻址方式

    1)首先内存中建立一个GDT全局分段描述表。

    2)DS中不再是内存的段开始地址,而是GDT表的索引。

    3)寻址时,首先根据DS的高13位的值得到一个索引,然后查找到在GDT中对应的一个全局分段描述。再根据这个描述来定位到段的开始位置。

    4)找到GDT段之后,继续根据偏移地址,在GDT段内进行内存寻址。

  2. 段的跳转指令

    JMP 段选择子:偏移地址

1)加载段选择子到CS段寄存器
2)获取段选择子索引号,根据索引号查找GDT表,加载GDT段描述符到CS段寄存器的描述符缓存
3)加载偏移地址到EIP寄存器
4)根据 CS段寄存器的描述符缓存 和 EIP寄存器 寻址。

最后跳转位置是

内存地址:段基本地址(根据段选择子获取) + 偏移地址

保护模式下的段

  1. 段选择器

保护模式下,段寄存器CS,DS,ES,,FS,GS,SS,称之为段选择器。

段选择器中的数据称为段选择子

段选择子 : 描述符索引(13位) | TI | RPL

其中

  • 描述符索引:GDT描述符表中的描述符的索引号(从0开始:0,1,2,3…)
  • TI: TI = 0 表示GDT描述符, TI =1 表示LDT描述符
  • RPL: 请求权特级:

当我们跳转到段的时侯,实际上是段选择器赋值为以上格式的值即可。

images/003.png

实模式和保护模式的区别

在计算机加载完成后,在实模式执行完一些初始化和加载工作。然后CPU设置进入保护模式。可以使用16位的数据。。

为了突破实模式1M内存寻址的限制,使用到更多内存。于是出现了保护模式,保护模式下,通过开启A20总线,可以使用32位的寄存器操作,其实访问地址已经达到了1<<32=4G内存。

实模式与保护模式的最大区别就是寻址方式:

1)保护模式不再使用段寄存器 <<4 +偏移地址的方式寻址,通过建立分段表将内存分成段。寻址时先加载分段表进入不同的段位置,然后在当前段内继续进行内存寻址。

2)保护模式不能使用BIOS中断

进入保护模式

[TOC]

进入保护模式

进入保护模式的步骤:

  1. 关闭中断,打开地址线A20GATE,使得CPU可以访问1M以上的内存空间。
  2. 设置CR0寄存器,进入保护模式。
  3. 加载临时GDT
  4. 进入保护模式后,首先执行jmp指令。因为内存寻址方式改变,需要刷新指令流水线

打开A20Gate

1. A20Gate的作用

在实模式下,A20Gate是关闭的,意味着只能使用20根地址线,需要通过打开A20Gate,访问第21根以上的总线。

A20Gate关闭时侯的内存访问:

在8088 CPU下, 只能使用20根总线 .

因此,初始化时, A20Gate关闭式,使用20根总线. 所以寻址范围位 0x00000 ~ 0xFFFFF,总共1M的地址范围。

当访问的地址大于这个范围,高位的值将被截取掉,导致超出1M的地址访问会使得CPU回滚到1M内地址范围的现象

例如:

当使用 [0xFFFF :0xFFFF ] 内存地址,得到的地址位 0x10FFEF 。但是在实模式下,由于20根总线的限制,最高位的1是无效的,实际的访问地址回绕到 [0x0FFEF]。

A20Gate打开后的内存访问:

后期,80286使用24根总线,而80286有24根总线,80386有32位总线.

打开A20Gate, 可以使用到32位的地址总线,内存地址访问也达到了1<<32 的4G范围。

实际上开启A20Gate,总线的寻址能力达到了4G,但是cpu的内存访问能力因为16位段寄存器,和16位偏移地址的限制,并不能协调工作。

A20Gate打开后, 还需要进入保护模式, 建立GDT描述名, 进行段地址 和 内存的映射关系, 使用新的内存地址访问方式 . 突破cpu的内存访问限制。

2. 开启A20Gate

开启A20Gate,只要设置io端口0x92的第一位为1就可以了。

1
2
3
4
5
6
;------------------
;打开A20
cli ;禁止CPU级别的中断
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al

设置CR0寄存器,进入保护模式

CR0寄存器

images/2_1_3.png

CR0寄存器是一个32位的寄存器

第0位-PE位:
Protection Enable(保护使能)是CR0寄存器的第0位(bit 0)。当设置了PE标志时,启用保护模式;当清除PE标志时,启用实地址模式。该标志位并不直接启用分页机制,它仅启用段级别的保护。要启用分页,必须同时设置PE和PG标志。

第1位-MP位:
Monitor Coprocessor(监视协处理器)是CR0寄存器的第1位(bit 1)。它控制WAIT(或FWAIT)指令与TS标志(CR0的第3位)的交互作用。如果设置了MP标志,并且TS标志也被设置,那么WAIT指令将引发设备不可用异常(#NM)。如果清除了MP标志,则WAIT指令将忽略TS标志的设置。

第16位-WP位:
Write Protect(写保护)是CR0寄存器的第16位(bit 16)。当设置了WP标志时,阻止特权级程序对只读页面进行写操作;当清除WP标志时,允许特权级程序对只读页面进行写操作(不考虑U/S位的设置)。该标志有助于实现创建新进程(forking)时使用的写时复制(copy-on-write)方法,该方法在UNIX等操作系统中被使用。在软件可以设置CR4.CET之前,必须设置此标志,并且只要CR4.CET = 1,就不能清除该标志。

第18位-AM位:
Alignment Mask(对齐掩码)是CR0寄存器的第18位(bit 18)。当设置了AM标志时,启用自动对齐检查;当清除AM标志时,禁用对齐检查。对齐检查仅在满足以下条件时进行:AM标志被设置、EFLAGS寄存器中的AC标志被设置、CPL(当前特权级)为3,并且处理器处于保护模式或虚拟8086模式下。

第31位-PG位:
Paging(分页)标志位是CR0寄存器的第31位。当该位被设置时(为1时),启用分页机制;当该位被清除时(为0时),禁用分页机制。当分页被禁用时,所有线性地址都被视为物理地址。也就是说没有分页机制,段机制通过段基址加偏移后就是真实物理地址,如果PE标志(寄存器CR0的第0位)未设置,PG标志则没有影响;在PE标志被清除时设置PG标志会导致通用保护异常(#GP)。

设置CR0寄存器的最高位为0,最低位为1,则可以进入保护模式。

CR0寄存器的作用

  • 改变段寻址方式,使用段描述符方式寻址。
  • 实模式指令的操作数默认为16位,保护模式指令的操作数默认为32位。

代码:

1
2
3
4
5
;------------------
;进入保护模式
mov eax,CR0
or eax,0x00000001 ;设置第0位为1
mov CR0,eax

loader.asm完整代码如下

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
; RAST LOADER
;Tab=4
[bits 16]

;----------- loader const ------------------
LOADER_BASE_ADDR equ 0x9000 ;内存地址0x9000
;---------------------------------------

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址

jmp Entry


;程序核心内容
Entry:


;------------------
;禁止CPU级别的中断
;------------------
cli

;------------------
;打开A20
;------------------
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al


;------------------
;进入保护模式
;------------------
mov eax,cr0
or eax,0x1 ;设置第0位为1
mov cr0,eax



;程序挂起
jmp $ ;让CPU挂起,等待指令。

times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

使用bochs调试

在0x7c00打断点,输入c跳转执行

$ pb 0x7c00

$ c

输入显示切换模式命令

$ show mode

输入c继续执行

$ c

可以看到控制他输出:

00017609546: switched from ‘real mode’ to ‘protected mode’

说明系统成功的从实模式切换到保护模式

images/2_6_1

查看CR0的PE位: 值为1

$ creg

images/2_6_1

保护模式内存分配

内存区域 大小 数据内容 说明
0x7C00 - 0x7DFF 512B boot.bin 引导扇区的内存地址
0x8000 - 0x81FF 512B loader.bin loader内存位置
0x8200 - 0xC200 16KB kernel.bin kernel内存位置
0x10000 - 0x8FFFF 空白(执行loader程序)
0x90000 - 0x9FFFF 系统信息 内存信息
0xA0000 - 0xFFFFF 显示 显存地址(默认使用0xB8000 - 0xBFFFF)
0x100000-0x101000 4KB 页目录 1MB内存位置
0x101000-0x200000 页表 页表信息

Makefile

安装make

安装make

sudo apt-get install make

make -v

创建Makefile文件,并执行make命令

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
# tools
PLATFORM=Linux
NASM=nasm
BOCHS=bochs
BXIMG=bximage

# args
boot=boot
build=build

target: prepare img
$(BOCHS) -f bochsrc.me


img: $(build)/astraos.img
@echo "build img completed"

$(build)/astraos.img:$(build)/boot.bin $(build)/loader.bin
$(BXIMAGE) -mode=create -imgmode=flat -hd=16M -q $(build)/astraos.img
sleep 1
dd if=$(build)/boot.bin of=$(build)/astraos.img bs=512 count=1 conv=notrunc
dd if=$(build)/loader.bin of=$(build)/astraos.img bs=512 count=1 seek=1 conv=notrunc

$(build)/%.bin: $(boot)/%.asm
$(NASM) -f bin -o $(build)/$*.bin $(boot)/$*.asm

prepare: $(build)
@echo "prepare dir $(build)"
ifeq ($(build), $(wildcard $(build)))
@echo "build directory exist..."
else
mkdir -p $(build)
endif

clean:
@echo "clean dir $(build)"
rm -rf $(build)/*

platform:
@echo $(PLATFORM)

BA简介

LBA方式访问使用了data寄存器,LBA寄存器(总共3个),device寄存器,command寄存器来完成的。

LBA28和LBA48方式

LBA28方式:使用28位来描述一个扇区地址,最大支持128GB的硬磁盘容量。

LBA48方式:使用48位来描述一个扇区地址,最大支持144,000,000 GB的硬磁盘容量

1. CHS方式:

寄存器 端口 作用
data寄存器 0x1F0 已经读取或写入的数据,大小为两个字节(16位数据)
每次读取1个word,反复循环,直到读完所有数据
features寄存器 0x1F1 0
sector count寄存器 0x1F2 指定读取的扇区数
LBA low寄存器 0x1F3 扇区号
LBA mid寄存器 0x1F4 柱面的低8位
LBA high寄存器 0x1F5 柱面的高8位
device寄存器 0x1F6 第0-3位:磁头号
第4位:主盘值为0,从盘值为1
第5位:值为1
第6位:读取方式,LBA模式为1,CHS模式为0
第7位: 值为1lba地址的前4位(占用device寄存器的低4位)
command寄存器 0x1F7 指定读取或写入的命令,返回磁盘状态
读取扇区:0x20 第4位为0表示读写完成,否则要一直循环等待
写入扇区:0x30
磁盘识别:0xEC

LBA28方式:

读取时参数:

寄存器 端口 作用
data寄存器 0x1F0 已经读取的数据,大小为两个字节(16位数据)
每次读取1个word,反复循环,直到读完所有数据
features寄存器 0x1F1 读取时的错误信息
sector count寄存器 0x1F2 指定读取的扇区数
LBA low寄存器 0x1F3 lba地址的低8位(0~7位)
LBA mid寄存器 0x1F4 lba地址的中8位(0~7位)
LBA high寄存器 0x1F5 lba地址的高8位16~23位)
device寄存器 0x1F6 第0-3位: lba地址
第4位:主盘值为0,从盘值为1
第5位:值为1
第6位:读取方式,LBA模式为1,CHS模式为0
第7位: 值为1
command寄存器 0x1F7 指定读取或写入的命令,返回磁盘状态
读取扇区:0x20
写入扇区:0x30
磁盘识别:0xEC

写入时参数

寄存器 端口 作用
data寄存器 0x1F0 已经写入的数据,大小为两个字节(16位数据)
每次读取1个word,反复循环,直到读完所有数据
features寄存器 0x1F1 写入时的额外参数
sector count寄存器 0x1F2 指定读写入的扇区数
LBA low寄存器 0x1F3 lba地址的低8位(0~7位)
LBA mid寄存器 0x1F4 lba地址的中8位(0~7位)
LBA high寄存器 0x1F5 lba地址的高8位(16~23位)
device寄存器 0x1F6 第0-3位: lba地址
第4位:主盘值为0,从盘值为1
第5位:值为1
第6位:读取方式,LBA模式为1,CHS模式为0
第7位: 值为1
command寄存器 0x1F7 指定读取或写入的命令,返回磁盘状态
读取扇区:0x20
写入扇区:0x30
磁盘识别:0xEC

IDE通道1,读写0x1f0-0x1f7号端口

IDE通道2,读写0x170-0x17f号端口

**LBA48方式: **

写两次0x1f1端口: 0

写两次0x1f2端口: 第一次要读的扇区数的高8位,第二次低8位

写0x1f3: LBA参数的24~31位

写0x1f3: LBA参数的0~7位

写0x1f4: LBA参数的32~39位

写0x1f4: LBA参数的8~15位

写0x1f5: LBA参数的40~47位

写0x1f5: LBA参数的16~23位

写0x1f6: 75位,010,第4位0表示主盘,1表示从盘,30位,0

写0x1f7: 0x24为读, 0x34为写

磁盘寻址:LBA方式

LBA方式不考虑扇区的物理位置,而是根据逻辑位置来寻址的。

LBA方式不使用柱面-磁头-扇区方式定位,直接以从0开始,逐次递增的方式来定位逻辑扇区。

比如: 扇区0,扇区1,….

LAB寄存器

LAB使用硬盘控制器存储扇区定位信息。每个寄存器8位大小。

  • device寄存器:

  • LBA low寄存器

  • LBA mid寄存器

  • LBA high寄存器

例如LAB28,使用一个28位来表示扇区位置。

LAB28扇区地址 = device寄存器的低4位 + LBA high寄存器 + LBA mid寄存器 + LBA low寄存器

读取硬盘

1)sector count寄存器寄存器写入读取的扇区数
2)LBA low寄存器,LBA mid寄存器,LBA high寄存器写入lba地址
3)device寄存器写入lba地址和读取模式
4)command寄存器写入写入命令
5)读取两个字节数据,多次循环直到读取完扇区数据。

代码

boot.asm
引导文件,初始化屏幕后,读取硬盘并加载4个扇区到内存位置[0x90000]处。然后跳转到0x90000处执行指令。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
; GloxOS BOOT
;Tab=4
[bits 16]

org 0x7c00 ;指明程序的偏移的基地址

;----------- loader const ------------------
LOADER_SECTOR_LBA equ 0x1 ;第2个逻辑扇区开始
LOADER_SECTOR_COUNT equ 9 ;读取9个扇区
LOADER_BASE_ADDR equ 0x9000 ;内存地址0x9000
;-------------------------------------------

;引导扇区代码
jmp Entry
db 0x90
db "GLOXBOOT" ;启动区的名称可以是任意的字符串(8字节)

;程序核心内容
Entry:

; ---------------------------
;初始化寄存器
; ---------------------------
mov ax,0
mov ss,ax
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov gs,ax
mov sp,0x7c00

; ---------------------------
;清屏
; ---------------------------
mov ah,0x06 ;清除屏幕
mov al,0
mov cx,0
mov dx,0xffff
mov bh,0x17 ;属性为蓝底白字
int 0x10

; ---------------------------
; 光标位置初始化
; ---------------------------
mov ah,0x02 ;光标位置初始化
mov dx,0
mov bh,0
mov dh,0x0
mov dl,0x0
int 0x10

;------------------
;读取硬盘1-10扇区
;------------------
mov ebx,LOADER_SECTOR_LBA ;LBA扇区号
mov cx,LOADER_SECTOR_COUNT ;读取扇区数
mov di,LOADER_BASE_ADDR ;写入内存地址
call ReadDiskLBA16

jmp LOADER_BASE_ADDR

; ------------------------------------------------------------------------
; 读取磁盘: ReadDiskLBA16
; 参数:
; ebx 扇区逻辑号
; cx 读入的扇区数,8位
; di 读取后的写入内存地址
; ------------------------------------------------------------------------
ReadDiskLBA16:
;设置读取的扇区数
mov al,cl
mov dx,0x1F2
out dx,al

;设置lba地址
;设置低8位
mov al,bl
mov dx,0x1F3
out dx,al

;设置中8位
shr ebx,8
mov al,bl
mov dx,0x1F4
out dx,al

;设置高8位
shr ebx,8
mov al,bl
mov dx,0x1F5
out dx,al

;设置高4位和device
shr ebx,8
and bl,0x0F
or bl,0xE0
mov al,bl
mov dx,0x1F6
out dx,al

;设置commond
mov al,0x20
mov dx,0x1F7
out dx,al

.check_status:;检查磁盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .check_status ;磁盘数据没准备好,继续循环检查

;设置循环次数到cx
mov ax,cx ;乘法ax存放目标操作数
mov dx,256
mul dx
mov cx,ax ;循环次数 = 扇区数 x 512 / 2
mov bx,di
mov dx,0x1F0

.read_data:
in ax,dx ;读取数据
mov [bx],ax ;复制数据到内存
add bx,2 ;读取完成,内存地址后移2个字节

loop .read_data
ret


Fill0:
resb 510-($-$$) ; 处理当前行$至结束(1FE)填充0
db 0x55, 0xaa

boot.asm
被引导扇区加载到0x90000位置,执行输出hello in loader文字

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
; GloxOS LOADER
;Tab=4
[bits 16]

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址

;----------- loader const ------------------
LOADER_BASE_ADDR equ 0x9000 ;内存地址0x9000
;---------------------------------------
jmp Entry

;程序核心内容
Entry:


;---------------------------
;输出字符串
mov si,HelloMsg ;将HelloMsg的地址放入si
mov dh,1 ;设置显示行
mov dl,0 ;设置显示列
call PrintString ;调用函数


jmp $ ;让CPU挂起,等待指令



; ------------------------------------------------------------------------
; 显示字符串函数:PrintString
; 参数:
; si = 字符串开始地址,
; dh = 第N行,0开始
; dl = 第N列,0开始
; ------------------------------------------------------------------------
PrintString:
mov cx,0 ; BIOS中断参数:显示字符串长度
mov bx,si
.s1: ; 获取字符串长度
mov al,[bx] ; 读取1个字节到al
add bx,1 ; 读取下个字节
cmp al,0 ; 是否以0结束
je .s2
inc cx ; 计数器
jmp .s1
.s2: ; 显示字符串
mov bx,si
mov bp,bx
mov ax,ds
mov es,ax ; BIOS中断参数:计算[ES:BP]为显示字符串开始地址

mov ah,0x13 ; BIOS中断参数:中断模式
mov al,0x01 ; BIOS中断参数:输出方式
mov bh,0x0 ; BIOS中断参数:指定分页为0
mov bl,0x1F ; BIOS中断参数:显示属性,指定白色文字
int 0x10 ; 调用BIOS中断操作显卡。输出字符串
ret

; ------------------------------------------------------------------------
;准备显示字符串
HelloMsg: db "load loader file success!",0

times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

运行

创建build.sh脚本

1
2
3
4
5
6
7
8
#!/bin/bash

NASM=nasm
$NASM -f bin -o build/boot.bin boot/boot.asm
$NASM -f bin -o build/loader.bin boot/loader.asm
dd if=/dev/zero of=build/gloxos.img bs=512 count=2880
dd if=build/boot.bin of=build/gloxos.img bs=512 count=1 conv=notrunc
dd if=build/loader.bin of=build/gloxos.img bs=512 count=1 seek=1 conv=notrunc

创建run.sh脚本

1
2
3
4
#!/bin/bash

BOCHS=bochs
$BOCHS -f bochsrc.me

运行结果

2_5_1.png

软盘和硬盘

软盘

  • 软盘,目前已经淘汰。

目前主流的硬盘分为两种:

  • 机械硬盘
  • 固态硬盘

硬盘最早分为两种接口方式:

  • 并行接口(PATA),目前已经淘汰。
  • 串行接口(SATA)。

磁盘寻址两种方式:CHS和LBA

CHS方式, LAB28 和 LAB48

IO操作读取硬盘的三种寻址方式:

  • chs寻址方式 :小于8G (8064MB)
  • LBA28寻址方式:小于137GB
  • LBA48寻址方式:小于144,000,000 GB

CHS和LBA的转换

CHS地址可用以下公式转成LBA,

1
LBA = (C * H + H) * S + S - 1     

LBA可用以下公式对应到CHS:

1
2
3
C = LBA / (SPT * HPC)
H = (LBA / SPT ) % HPC
S = (LBA % SPT ) + 1

C = cylinder : 磁柱编号
H = head: 磁头编号
S = sector: 扇区编号
HPC=heads per cylinder,每个磁柱的磁头数
SPT=sectors per track,每磁道的扇区数

磁盘寻址:CHS方式

磁盘的三层定位结构分别为

  • 磁头(Head) :每张磁盘的正面和反面都有一个磁头,一个磁头对应着一张磁盘的一个面,因此,用第几个磁头就能表示数据在哪个盘面.
  • 柱面(Cylinder): 由所有磁盘中半径相同的同心磁道构成,在这一系列的磁道水质叠放在一起,就形成了一个柱面的形状。所以柱面数 = 磁道数。
  • 扇区(Sector): 一个扇区512字节

磁盘寻址,是指根据给出的方式找到扇区的位置。

按顺序根据柱面,磁头,扇区,这种方式称之为CHS方式。

例如: 磁盘的第一个扇区,位于 1柱面,1磁头,0扇区

CHS方式表示的容量上限

这种模式及下,支持的最大柱面数为1024,最大磁头数为16,最大扇区数为63,(每个扇区字节数为512Bit)因此最大访问硬盘容量为:

1024 x 16 x 64 x 512 = 528MB

虽然后面又拓展了large模式读取,但是没有解决根本问题。

所以后来有LBA的读取方式,可以描述更大容量。

软盘

一张软盘有80个柱面,2个磁头,18个扇区

软盘大小为 80x2x18x512=1440KB

较旧的硬盘驱动器,如 MFM 和 RLL 驱动器,将每个柱面划分为相等数量的扇区,并且 CHS 值与驱动器的物理构成相匹配。 CHS 值为 500 x 4 x 32 的驱动器的每个盘片每侧有 500 个磁道,两个盘片,每个柱面有 32 个扇区,总共有 32,768,000 字节(约 31 兆字节)。 大多数现代驱动器都有一个不构成柱面边界的剩余空间。 每个分区应始终在柱面边界处开始和结束。 只有一些最现代的操作系统可能会忽略这一规则,但这会导致一些兼容性问题,尤其是当用户想要在同一驱动器上启动多个操作系统时。

IDE 驱动器已经取代了 MFM 和 RLL 驱动器,并且在存储数据方面效率更高。 他们使用区域位记录 (ZBR),其中一个柱面中的扇区数随其在驱动器上的位置而变化。 靠近盘片边缘的柱面比靠近主轴的柱面包含更多扇区,因为在盘片边缘附近的给定磁道中有更多空间。 由于每个柱面的扇区数量不同,CHS 寻址系统不适用于这些驱动器。 IDE 驱动器可以在系统 BIOS 中配置为不超过驱动器容量的柱面、磁头和扇区的任何配置。 驱动器将给定的 CHS 地址转换为特定硬件配置的实际地址。

读取磁盘_CHS方式

BIOS读取磁盘

读取磁盘功能02H

读取磁盘也是调用BIOS:

中断命令: INT 13H

1)入口参数

寄存器 说明
AH 功能:02H读取扇区 02H
AL 扇区数
CH 柱面数
CL 扇区
DH 磁头
DL 驱动器号 00H-7FH:软盘驱动器号;80H-0FFH:硬盘驱动器号
ES:BX 缓冲区的地址 00H-7FH:软盘驱动器号;80H-0FFH:硬盘驱动器号

2)出口参数

读取成功:

寄存器 说明
AH 00H
AL 输的扇区数

读取失败:

寄存器 说明
AH 状态代码

定义磁盘读取函数

1. 读取一个扇区

1
2
3
4
5
6
.read_one_sector			
MOV AH,0x02 ;参数:读扇区
MOV AL,1 ;参数:读取扇区数
MOV BX,0 ;参数:
MOV DL,0x00 ;参数:设置读取驱动器为软盘
INT 0x13 ;调用BIOS中断操作磁盘:读取扇区
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
; ------------------------------------------------------------------------
; 读取一个扇区函数:ReadDisk0
; ------------------------------------------------------------------------
; 参数:ES:BX 缓冲区地址,CH柱面,DH磁头,CL扇区
; ------------------------------------------------------------------------
ReadDisk0:
MOV SI,0 ;初始化读取失败次数,用于循环计数

;为了防止读取错误,循环读取5次
;调用BIOS读取一个扇区
ReadFiveLoop:
MOV AH,0x02 ;BIOS中断参数:读扇区
MOV AL,1 ;BIOS中断参数:读取扇区数
MOV BX,0
MOV DL,0x00 ;BIOS中断参数:设置读取驱动器为软盘
INT 0x13 ;调用BIOS中断操作磁盘:读取扇区
JNC ReadEnd ;条件跳转,操作成功进位标志=0。则跳转执行ReadNextSector

inc si ;循环读取次数递增+1
CMP SI,5 ;判断是否已经读取超过5次
JAE LoadError ;上面cmp判断(>=)结果为true则跳转到DisplayError

MOV AH,0x00 ;BIOS中断参数:磁盘系统复位
MOV DL,0x00 ;BIOS中断参数:设置读取驱动器为软盘
INT 0x13 ;调用BIOS中断操作磁盘:磁盘系统复位
JMP ReadFiveLoop
;扇区读取完成
ReadEnd:
RET

2. 读取多个扇区

读取时要根据 扇区<磁头<柱面 的方式来读取。继续添加代码,读取18个扇区:(即完整的读取了一个柱面)。

代码如下:

3. 读取多个柱面

继续添加代码,读取10个柱面。

然后调用函数

1
2
3
4
5
6
7
8
9
10
;读取磁盘初始化
MOV AX,DISC_ADDR/0x10 ;设置磁盘读取的缓冲区基本地址为ES=0x820。[ES:BX]=ES*0x10+BX
MOV ES,AX ;BIOS中断参数:ES:BX=缓冲区的地址

MOV CH,0 ;设置柱面为0
MOV DH,0 ;设置磁头为0
MOV CL,2 ;设置扇区为2

ReadSectorLoop:
CALL ReadDisk0; ;读取一个扇区

CHS方式读取(使用LBA值转换后读取)

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
; ==============================================
; 读取磁盘:Func_readCHS2LBA
; 参数:
; ebx 扇区逻辑号
; cx 读入的扇区数,8位
; es 读取到内存单元的地址
; ==============================================
.reset
MOV AH,0x00 ;BIOS中断参数:磁盘系统复位
MOV DL,0x00 ;BIOS中断参数:设置读取驱动器为软盘
INT 0x13 ;调用BIOS中断操作磁盘:磁盘系统复位
ret

.readdisk
;初始化读取失败次数,用于循环计数
push cx
MOV cx,5

;为了防止读取错误,循环读取5次
;调用BIOS读取一个扇区
ReadFiveLoop:
MOV AH,0x02 ;BIOS中断参数:读扇区
MOV AL,1 ;BIOS中断参数:读取扇区数
MOV BX,0
MOV DL,0x00 ;BIOS中断参数:设置读取驱动器为软盘
INT 0x13 ;调用BIOS中断操作磁盘:读取扇区
JNC .readok ;条件跳转,操作成功进位标志=0。则跳转执行ReadNextSector

inc si ;循环读取次数递增+1
CMP SI,5 ;判断是否已经读取超过5次
JAE .readfail ;上面cmp判断(>=)结果为true则跳转到DisplayError

call .reset
loop ReadFiveLoop

;准备下一个扇区
.readok:

MOV AX,ES
ADD AX,0x0020
MOV EX,AX ;内存单元基址后移0x20。[EX+0x20:]
ADD CL,1 ;读取扇区数递增+1
CMP CL,18 ;判断是否读取到18扇区
JBE readdisk ;上面cmp判断(<=)结果为true则跳转到DisplayError

.readxx
;读取另一面磁头。循环读取柱面
MOV CL,1 ;设置柱面为0
ADD DH,1 ;设置磁头递增+1:读取下一个磁头
CMP DH,2 ;判断磁头是否读取完毕
JB ReadSectorLoop ;上面cmp判断(<)结果为true则跳转到DisplayError

MOV DH,0 ;设置磁头为0
ADD CH,1 ;设置柱面递增+1;读取下一柱面
CMP CH,10 ;判断是否已经读取10个柱面
JB readdisk ;上面cmp判断(<)结果为true则跳转到DisplayError

完整代码

最后,完整的boot.asm文件代码如下:

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
; RASTOS BOOT
[bits 16]

; ----------- loader const ------------------
LOADER_CYLINDER_NUM equ 10 ; 读取10个柱面
LOADER_SECTOR_CAP equ 18 ; 读取18个扇区
LOADER_SECTOR_CHS equ 0x1 ; 第2个逻辑扇区开始
LOADER_BASE_ADDR equ 0x9000 ; 内存地址0x9000
; -------------------------------------------


org 0x7c00 ; 指明程序的偏移的基地址

; 启动程序
jmp Entry
db 0x90
db "RATSBOOT" ; 启动区的名称可以是任意的字符串(8字节)

; 程序核心内容
Entry:

; ---------------------------
; 初始化寄存器
; ---------------------------
mov ax,0
mov ss,ax
mov ds,ax
mov es,ax
mov sp,0x7c00

; ---------------------------
; 清除屏幕
; ---------------------------
mov ah,0x06
mov al,0
mov cx,0
mov dx,0xffff
mov bh,0x17 ; 属性为蓝底白字
int 0x10

; ---------------------------
; 光标位置初始化
; ---------------------------
mov ah,0x02
mov dx,0
mov bh,0
mov dh,0x0
mov dl,0x0
int 0x10

; ---------------------------
; 输出字符串
; ---------------------------
mov si,BootMsg ; 将BootMsg的地址放入si
mov dh,0 ; 设置显示行
mov dl,0
call PutString ; 调用函数

; ---------------------------
; 读取磁盘
mov ax,LOADER_BASE_ADDR/0x10 ; 设置磁盘读取的缓冲区基本地址为ES=0x900。[ES:BX]=ES*0x10+BX
mov es,ax ; BIOS中断参数:ES:BX=缓冲区的地址 [0x900:0x0]

mov ch,0 ; 设置柱面为0
mov dh,0 ; 设置磁头为0
mov cl,1 ; 设置扇区为2

; ---------------------------
; 循环读取扇区,读取10个柱面
; ---------------------------
ReadSectorLoop:
call ReadDiskSector ; ;读取一个扇区

; 准备下一个扇区
.readNext:
mov ax,es
add ax,0x0020
mov es,ax ; 内存单元基址后移0x20(512字节)。[ES+0x20:]
add cl,1 ; 读取扇区数递增+1
cmp cl,LOADER_SECTOR_CAP ; 判断是否读取到18扇区
jbe ReadSectorLoop ; 上面cmp判断(<=)结果为true则跳转到DisplayError

; 读取另一面磁头。循环读取柱面
mov cl,1 ; 设置柱面为0
add dh,1 ; 设置磁头递增+1:读取下一个磁头
cmp dh,2 ; 判断磁头是否读取完毕
jb ReadSectorLoop ; 上面cmp判断(<)结果为true则跳转到DisplayError

mov dh,0 ; 设置磁头为0
add ch,1 ; 设置柱面递增+1;读取下一柱面
cmp ch,LOADER_CYLINDER_NUM ; 判断是否已经读取10个柱面
jb .readFin ; 上面cmp判断(<)结果为true则跳转到DisplayError
jmp .readNext

; 读取完成
.readFin:
mov si,SuccessMsg
mov dh,1
mov dl,0
call PutString ; 如果加载失败显示加载错误
ret


; ------------------------------------------------------------------------
; 读取一个扇区函数:ReadDiskSector
; 参数:
; es:bx = 缓冲区地址,
; ch = 柱面
; dh = 磁头
; cl = 扇区
; ------------------------------------------------------------------------
ReadDiskSector:
mov si,0 ; 初始化读取失败次数,用于循环计数

; 为了防止读取错误,循环读取5次
; 调用BIOS读取一个扇区
.readFiveLoop:
mov ah,0x02 ; BIOS中断参数:读扇区
mov al,1 ; BIOS中断参数:读取扇区数
mov bx,0
mov dl,0x00 ; BIOS中断参数:设置读取驱动器为软盘
int 0x13 ; 调用BIOS中断操作磁盘:读取扇区
jnc .readEnd ; 条件跳转,操作成功进位标志=0。则跳转执行ReadNextSector

add si,1 ; 循环读取次数递增+1
cmp si,5 ; 判断是否已经读取超过5次
jae .readError ; 上面cmp判断(>=)结果为true则跳转到readError

mov ah,0x00 ; BIOS中断参数:磁盘系统复位
mov dl,0x00 ; BIOS中断参数:设置读取驱动器为软盘
int 0x13 ; 调用BIOS中断操作磁盘:磁盘系统复位
jmp .readFiveLoop

; 读取完成
.readEnd:
ret

; 读取错误
.readError:
mov si,ErrorMsg
mov dh,1
mov dl,0
call PutString ; 如果加载失败显示加载错误
ret

; ------------------------------------------------------------------------
; 显示字符串函数:PutString
; 参数:
; si = 字符串开始地址,
; dh = 第N行,0开始
; dl = 第N列,0开始
; ------------------------------------------------------------------------
PutString:
mov cx,0 ; BIOS中断参数:显示字符串长度
mov bx,si
.s1: ; 获取字符串长度
mov al,[bx] ; 读取1个字节到al
add bx,1 ; 读取下个字节
cmp al,0 ; 是否以0结束
je .s2
inc cx ; 计数器
jmp .s1
.s2: ; 显示字符串
mov bx,si
mov bp,bx
mov ax,ds
mov es,ax ; BIOS中断参数:计算[ES:BP]为显示字符串开始地址

mov ah,0x13 ; BIOS中断参数:中断模式
mov al,0x01 ; BIOS中断参数:输出方式
mov bh,0x0 ; BIOS中断参数:指定分页为0
mov bl,0x1F ; BIOS中断参数:显示属性,指定白色文字
int 0x10 ; 调用BIOS中断操作显卡。输出字符串
ret

; ------------------------------------------------------------------------
; 准备显示字符串
BootMsg: db "GloxOS is booting!",0
SuccessMsg: db "load disk Sector success.",0
ErrorMsg: db "load disk Sector error.",0


Fill0:
resb 510-($-$$) ; 处理当前行$至结束(1FE)填充0
db 0x55, 0xaa

运行

创建build.sh脚本

1
2
3
4
5
6
#!/bin/bash

NASM=nasm
$NASM -f bin -o build/boot.bin boot/boot.asm
dd if=/dev/zero of=build/gloxos.img bs=512 count=2880
dd if=build/boot.bin of=build/gloxos.img bs=512 count=1 conv=notrunc

创建run.sh脚本

1
2
3
4
#!/bin/bash

BOCHS=bochs
$BOCHS -f bochsrc.me

运行结果如下:

2_1_1.png

上面代码的作用

首先boot.asm会被加载到内存并且执行.后面开始读取磁盘的10个柱面(10*18个扇区).
读取的扇区数据复制到 内存 0x8000 开始的位置.
然后打印输出”hello,rats os start”

显示字符串: 使用字符中断

1. 字符中断

当BIOS执行显示字符调用显示服务 INT 10H,AH=0EH,可以进行单个字符的显示

首先需要配置入口参数:

中断号:INT10

寄存器 说明
AH 功能:在Teletype模式下显示字符 0EH
AL 字符
BH 页码
BL 前景色(图形模式)

例如:

1
2
3
4
mov ah,0x0e				;BIOS中断参数:显示一个文字
mov byte al,'A'
mov bl,0x03 ;BIOS中断参数:指定字符颜色
int 0x10 ;调用BIOS中断操作显卡。输出字符

2. 显示一个字符

1
2
3
4
5
6
7
8
9
10
11
;------------------
;显示一个字符,si = 字符串文本地址
.putChar:
mov al,[si] ;将[di]指向的内存单元的一个字节放入AL。
inc si ;di指向下一个字节
cmp al,0 ;判断[di]中的字符值是否==0

je .putEnd ;为0字符则串结束
mov ah,0x0e ;BIOS中断参数:中断模式
mov bl,0x03 ;BIOS中断参数:指定字符颜色
int 0x10 ;调用BIOS中断操作显卡。输出字符

3.循环调用,显示字符串

通过中断来显示一个字符,我们可以通过循环操作字符中断的方式,来显示多个字符。

显示字符串代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; ------------------------------------------------------------------------
; 显示字串函数:PrintString
; 参数:
; si = 字符串文本地址
; ------------------------------------------------------------------------
PrintString:
; ------------------
; 显示一个字符,si = 字符串文本地址
.putChar:
mov al,[si] ; 将[di]指向的内存单元的一个字节放入AL。
inc si ; di指向下一个字节
cmp al,0 ; 判断[di]中的字符值是否==0

je .putEnd ; 为0字符则串结束
mov ah,0x0e ; BIOS中断参数:中断模式
mov bl,0x03 ; BIOS中断参数:指定字符颜色
int 0x10 ; 调用BIOS中断操作显卡。输出字符
jmp .putChar
.putEnd:
ret

4. 代码

这一段的代码如下:
boot.asm

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
; RASTOS BOOT
[bits 16]

org 0x7c00 ; 指明程序的偏移的基地址

jmp Entry ; 跳转到程序入口
db 0x90
db "RASTBOOT"

; ----------------------------
; 程序入口
; ----------------------------
Entry:

; ---------------------------
; 清除屏幕
; ----------------------------
mov ah,0x06
mov bh,0x07
mov al,0
mov cx,0
mov dx,0xffff
mov bh,0x17 ; 属性为蓝底白字
int 0x10

; ---------------------------
; 光标位置初始化
; ----------------------------
mov ah,0x02
mov bh,0
mov dx,0
int 0x10

; ---------------------------
; 输出字符串
; ---------------------------
mov si,HelloMsg ; 将HelloMsg的地址放入si
call PutString ; 调用函数

jmp $ ; 进入死循环,不再往下执行。


; ------------------------------------------------------------------------
; 字符串常量
; ------------------------------------------------------------------------
HelloMsg: db "Hello,RastOS!",0


; ------------------------------------------------------------------------
; 显示字串函数:PrintString
; 参数:
; si = 字符串文本地址
; ------------------------------------------------------------------------
PutString:
; ------------------
; 显示一个字符,si = 字符串文本地址
.putChar:
mov al,[si] ; 将[di]指向的内存单元的一个字节放入AL。
inc si ; di指向下一个字节
cmp al,0 ; 判断[di]中的字符值是否==0

je .putCRLF ; 为0字符则串结束
mov ah,0x0e ; BIOS中断参数:中断模式
mov bl,0x03 ; BIOS中断参数:指定字符颜色
int 0x10 ; 调用BIOS中断操作显卡。输出字符
jmp .putChar
.putCRLF:
mov al,13 ; 回车
mov ah,0x0e ; BIOS中断参数:中断模式
mov bl,0x03 ; BIOS中断参数:指定字符颜色
int 0x10 ; 调用BIOS中断操作显卡。输出字符

mov al,10 ; 换行
mov ah,0x0e ; BIOS中断参数:中断模式
mov bl,0x03 ; BIOS中断参数:指定字符颜色
int 0x10 ; 调用BIOS中断操作显卡。输出字符
jmp .putEnd
.putEnd:
ret

FillSector:
resb 510-($-$$) ; 处理当前行$至结束(1FE)的填充
db 0x55, 0xaa

运行

创建build.sh脚本

1
2
3
4
5
6
7
#!/bin/bash

NASM=nasm
mkdir build
$NASM -f bin -o build/boot.bin boot/boot.asm
dd if=/dev/zero of=build/astraos.img bs=512 count=2880
dd if=build/boot.bin of=build/astraos.img bs=512 count=1 conv=notrunc

创建run.sh脚本

1
2
3
4
#!/bin/bash

QEMU=qemu-system-x86_64
$QEMU -m 128 -rtc base=localtime -fda build/astraos.img

创建run.bat

1
2
set qume="C:/Program Files/qemu/qemu-system-x86_64w.exe"
%qume% -m 128 -rtc base=localtime -fda build/astraos.img

结果如图

(images/1_8_1.png

显示字符串:使用字符串中断

字符串中断

上面一篇通过循环调用字符中断显示字符串,这次通过AH=13H字符串中断来显示字符串。

1. 字符串中断

当BIOS执行显示字符串调用显示服务 INT 10H,AH=13H,可以进行单个字符的显示

首先需要配置入口参数:

中断号:INT10

寄存器 说明
AH 功能:在Teletype模式下显示字符 13H
AL 显示输出方式 —-
BH 页码
BL 属性,背景色和文字颜色
CX 字符串长度
DH, DL 坐标(行,列)
ES, BP 字符串的地址

AL=显示输出方式

0–字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变

1–字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变

2–字符串中含显示字符和显示属性。显示后,光标位置不变

3–字符串中含显示字符和显示属性。显示后,光标位置改变

显示字符串函数

我们设置字符串遇到0表示结束,然后将读取的内容调用BIOS中断进行显示。

此处使用汇编定义了一个显示字符串的函数。然后进行调用。

函数如下:

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
; ------------------------------------------------------------------------
; 显示字符串函数:PrintString
; 参数:
; si = 字符串开始地址,
; dh = 第N行,0开始
; dl = 第N列,0开始
; ------------------------------------------------------------------------
PrintString:
mov cx,0 ;BIOS中断参数:显示字符串长度
mov bx,si
.s1:;获取字符串长度
mov al,[bx] ;读取1个字节到al
add bx,1 ;读取下个字节
cmp al,0 ;是否以0结束
je .s2
add cx,1 ;计数器
jmp .s1
.s2:;显示字符串
mov bx,si
mov bp,bx
mov ax,ds
mov es,ax ;BIOS中断参数:计算[ES:BP]为显示字符串开始地址

mov ah,0x13 ;BIOS中断参数:显示文字串
mov al,0x01 ;BIOS中断参数:文本输出方式(40×25 16色 文本)
mov bh,0x0 ;BIOS中断参数:指定分页为0
mov bl,0x1F ;BIOS中断参数:指定白色文字
mov dl,0 ;列号为0
int 0x10 ;调用BIOS中断操作显卡。输出字符串
ret

程序代码

内容如下

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
; RASTOS BOOT
[bits 16]

org 0x7c00 ; 指明程序的偏移的基地址

; 引导扇区代码
jmp Entry
db 0x90
db "ASTRABOOT" ; 启动区的名称可以是任意的字符串(8字节)

; 程序核心内容
Entry:

; ---------------------------
; 清除屏幕
; ---------------------------
mov ah,0x06
mov al,0
mov cx,0
mov dx,0xffff
mov bh,0x17 ; 属性为蓝底白字
int 0x10

; ---------------------------
; 光标位置初始化
; ---------------------------
mov ah,0x02
mov dx,0
mov bh,0
mov dh,0x0
mov dl,0x0
int 0x10

; ---------------------------
; 输出字符串
; ---------------------------
mov si,HelloMsg ; 将HelloMsg的地址放入si
mov dh,0 ; 设置显示行
mov dl,0 ; 设置显示列
call PrintString ; 调用函数

jmp $ ; 进入死循环


; ------------------------------------------------------------------------
; 字符串常量
; ------------------------------------------------------------------------
HelloMsg: db "Hello,ASTRAOS!",0

; ------------------------------------------------------------------------
; 显示字符串函数:PrintString
; 参数:
; si = 字符串开始地址,
; dh = 第N行,0开始
; dl = 第N列,0开始
; ------------------------------------------------------------------------
PrintString:
mov cx,0 ; BIOS中断参数:显示字符串长度
mov bx,si
.s1: ; 获取字符串长度
mov al,[bx] ; 读取1个字节到al
add bx,1 ; 读取下个字节
cmp al,0 ; 是否以0结束
je .s2
inc cx ; 计数器
jmp .s1
.s2: ; 显示字符串
mov bx,si
mov bp,bx
mov ax,ds
mov es,ax ; BIOS中断参数:计算[ES:BP]为显示字符串开始地址

mov ah,0x13 ; BIOS中断参数:中断模式
mov al,0x01 ; BIOS中断参数:输出方式
mov bh,0x0 ; BIOS中断参数:指定分页为0
mov bl,0x1F ; BIOS中断参数:显示属性,指定白色文字
int 0x10 ; 调用BIOS中断操作显卡。输出字符串
ret


FillSector:
resb 510-($-$$) ; 处理当前行$至结束(1FE)的填充
db 0x55, 0xaa

结果如图

images/1_9_1.png

显示模式

计算机在加电自检之后,会将显示初始化为80 x 25的文本模式。此时,我们可以进行文本显示了。

而计算机的显示一般有2种模式,可以通过中断来修改显示模式

  • 文本模式
  • 图形模式

文本模式只能显示字符,一般通过BIOS中断修改。不过首先我们尝试修改显存的方式来显示字符。

而计算机的显示一般有2种模式:

  • 文本模式:开始地址地址:0xB8000
  • 图形模式: 开始地址地址:0xA0000

图形模式,存储的内容主要是颜色信息,而文字模式,存储的内容主要是文字信息。

比如,刚启动linux时,输出的一堆的启动信息就是,进入文本模式。而开启xwindows才会进入图形模式。

根据分辨率,可用的显示模式如下所列:

文本显示模式

2. 80 x 25文本模式

刚开始启动计算机时,系统默认进入文本模式。在计算机在加电自检完成之后,会默认将显示初始化为80 x 25的文本模式

在 80 x 25的文本模式,屏幕可以显示25行80列。显示地址段是位于0xB8000-oxBffff的地址段。

我们可以通过修改0xB8000-0xBffff地址段的值,来在屏幕上显示文本。

此模式下每2字节为一组,16位代表一个文字输出:

  • 高地址8位为颜色信息,低地址8位为文字信息

因此我们可以通过修改这段显示地址区域的值,从而来控制屏幕输出文字。

例如:80x25文本模式的颜色如下:

在 80 x 25的文本模式,显存地址是位于0xB8000-0xBffff。

例如:

0xb8000 存储字符,0xb8001存储颜色(包含前景色背景色)

背景色颜色(背景颜色),4位,分别为 KRGB,K为是否闪烁

颜色 颜色
0 黑色 4 红色
1 蓝色 5 紫色
2 绿色 6 黄色
3 青色 7 白色

前景色颜色(文字颜色),4位,分别为 IRGB

颜色 颜色
0 黑色 8 灰色
1 蓝色 9 淡蓝色
2 绿色 A 淡绿色
3 青色 B 淡青色
4 红色 C 淡红色
5 紫色 D 淡紫色
6 黄色 E 淡黄色
7 白色 F 亮白色

显示字符

1. 通过修改内存数据来显示字符

启动后实模式下-文本模式下的初始显存地址范围为[0xB8000-0xBFFFFF]。

显存地址的值对应屏幕的显示数据,我们可以修改显存值来改变屏幕显示。

我们使用段和偏移来表示这段显存信息,段基本地址为0xB800,偏移为0x0000到0xFFFF。

代码如下:

1
2
3
4
mov ax,0xb800
mov ds,ax ;配置显存段地址
mov byte [0x00],'r' ;输出字符,内存地址为 DS<<4 + 0x00
mov byte [0x01],0x17 ;设置颜色(背景色蓝,前景色白)

打印hello

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
; RASTOS BOOT
[bits 16]

org 0x7c00 ; 指明程序的偏移的基地址

jmp Entry ; 跳转到程序入口
db 0x90
db "RASTBOOT"

; ----------------------------
; 程序入口
; ----------------------------
Entry:

; ---------------------------
; 清除屏幕
; ----------------------------
mov ah,0x06
mov bh,0x07
mov al,0
mov cx,0
mov dx,0xffff
mov bh,0x17 ; 属性为蓝底白字
int 0x10

; ---------------------------
; 光标位置初始化
; ----------------------------
mov ah,0x02
mov bh,0
mov dx,0
int 0x10

; ---------------------------
; 打印hello
; ----------------------------
mov ax,0xb800
mov gs,ax ;显存段地址
mov byte [gs:0x00],'h' ;输出字符
mov byte [gs:0x01],0x1F ;设置颜色(背景色蓝,前景色白)
mov byte [gs:0x02],'e'
mov byte [gs:0x03],0x1F
mov byte [gs:0x04],'l'
mov byte [gs:0x05],0x1F
mov byte [gs:0x06],'l'
mov byte [gs:0x07],0x1F
mov byte [gs:0x08],'o'
mov byte [gs:0x09],0x1F
mov byte [gs:0x0a],','
mov byte [gs:0x0b],0x1F
mov byte [gs:0x0c],'r' ;输出字符
mov byte [gs:0x0d],0x1D ;设置颜色(背景色蓝,前景淡紫)
mov byte [gs:0x0e],'a'
mov byte [gs:0x0f],0x1D
mov byte [gs:0x10],'s'
mov byte [gs:0x11],0x1D
mov byte [gs:0x12],'t'
mov byte [gs:0x13],0x1D

Fin:
hlt
jmp Fin ; 进入死循环,不再往下执行。

Fill_Sector:
resb 510-($-$$) ; 处理当前行$至结束(1FE)的填充
db 0x55, 0xaa

运行如下:
./images/1_7_1.png

0%