[Make MyOS] Make a boot

第二天的内容本该是,学习一些汇编指令和 Makefile,在这之前我觉得有必要好好看看 CPU 结构,自古操作系统和组成原理就分不开啊

面向总线的计算机组成示意图。中央处理器,从主存储器中逐条进行取指令,分析指令和执行指令。计算机各组件通过总线连在一起,总线是一些平行导线的集合,用来传递地址,数据和控制信号,可以在 CPU 之外连接 CPU,存储器及 IO 设备;也可以在 CPU 内部连接 CPU 的各个组成部分。

面向总线的计算机组成

1 CPU 简介

控制单元:控制各个子系统的操作,控制是通过从控制单元发送到其他子系统的信号来进行

算术逻辑单元:对数据进行算数,逻辑和移位运算

寄存器:用来临时存放数据的高速独立的存储单元

1 数据寄存器

数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间。命名为 R1 到 Rn

2 指令寄存器

CPU 从内存中取出指令,存在指令寄存器中 (IR),解释并执行指令

3 程序计数器

保存着当前正在执行的指令,当前指令执行完成后,计数器将自动加 1,指向下一条指令的内存地址

通用 32 位 CPU 常用寄存器及其作用

  • 4 个数据寄存器 (EAX、EBX、ECX 和 EDX)
  • 2 个变址和指针寄存器 (ESI 和 EDI)
  • 2 个指针寄存器 (ESP 和 EBP)
  • 6 个段寄存器 (ES、CS、SS、DS、FS 和 GS)
  • 1 个指令指针寄存器 (EIP)
  • 1 个标志寄存器 (EFlags)
31~1615~87~0
AHAL
BHBL
CHCL
DHDL
BP
SP
SI
DI

32 位 CPU 有 4 个 32 位的通用寄存器 EAX、EBX、ECX 和 EDX。对低 16 位数据的存取,不会影响高 16 位的数据。这些低 16 位寄存器分别命名为:AX、BX、CX 和 DX,它和先前的 CPU 中的寄存器相一致。4 个 16 位寄存器又可分割成 8 个独立的 8 位寄存器 (AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每个寄存器都有自己的名称,可独立存取。程序员可利用数据寄存器的这种 “可分可合” 的特性,灵活地处理字 / 字节的信息。

寄存器 AX 称为累加器 (Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、除、输入 / 输出等操作,它们的使用频率很高;

寄存器 BX 称为基地址寄存器 (BaseRegister)。它可作为存储器指针来使用;

寄存器 CX 称为计数寄存器 (CountRegister)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用 CL 来指明移位的位数;

寄存器 DX 称为数据寄存器 (DataRegister)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放 I/O 的端口地址。

32 位 CPU 有 2 个 32 位通用寄存器 EBP 和 ESP。其低 16 位对应先前 CPU 中的 BP 和 SP,对低 16 位数据的存取,不影响高 16 位的数据。BP 为基指针 (BasePointer) 寄存器,用它可直接存取堆栈中的数据;SP 为堆栈指针 (StackPointer) 寄存器,用它只可访问栈顶。指针寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。

32 位 CPU 有 2 个 32 位通用寄存器 ESI 和 EDI。其低 16 位对应先前 CPU 中的 SI (源变址寄存器,suorce index) 和 DI (目的变址寄存器,destination index),对低 16 位数据的存取,不影响高 16 位的数据。它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。变址寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。

段寄存器是根据内存分段的管理模式而设置的。内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址。

CS——代码段寄存器 (CodeSegmentRegister),其值为代码段的段值;

DS——数据段寄存器 (DataSegmentRegister),其值为数据段的段值;

ES——附加段寄存器 (ExtraSegmentRegister),其值为附加数据段的段值;

SS——堆栈段寄存器 (StackSegmentRegister),其值为堆栈段的段值;

FS——附加段寄存器 (ExtraSegmentRegister),其值为附加数据段的段值;

GS——附加段寄存器 (ExtraSegmentRegister),其值为附加数据段的段值。

在 16 位 CPU 系统中,它只有 4 个段寄存器,所以,程序在任何时刻至多有 4 个正在使用的段可直接访问;在 32 位微机系统中,它有 6 个段寄存器,所以,在此环境下开发的程序最多可同时访问 6 个段。

32 位 CPU 把指令指针扩展到 32 位,并记作 EIP,EIP 的低 16 位与先前 CPU 中的 IP 作用相同。指令指针 EIP、IP (InstructionPointer) 是存放下次将要执行的指令在代码段的偏移量。在具有预取指令功能的系统中,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况。所以,在理解它们的功能时,不考虑存在指令队列的情况。

标志寄存器 (EFlags register)

20210905200924

进位标志 CF (CarryFlag) 主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么,其值为 1,否则其值为 0。使用该标志位的情况有:多字 (字节) 数的加减运算,无符号数的大小比较运算,移位操作,字 (字节) 之间移位,专门改变 CF 值的指令等。

奇偶标志 PF (ParityFlag) 用于反映运算结果中 “1” 的个数的奇偶性。如果 “1” 的个数为偶数,则 PF 的值为 1,否则其值为 0。利用 PF 可进行奇偶校验检查,或产生奇偶校验位。在数据传送过程中,为了提供传送的可靠性,如果采用奇偶校验的方法,就可使用该标志位。

辅助进位标志 AF (AuxiliaryCarryFlag)。在发生下列情况时,辅助进位标志 AF 的值被置为 1,否则其值为 0:在字操作时,发生低字节向高字节进位或借位时;在字节操作时,发生低 4 位向高 4 位进位或借位时。

零标志 ZF (ZeroFlag) 用来反映运算结果是否为 0。如果运算结果为 0,则其值为 1,否则其值为 0。在判断运算结果是否为 0 时,可使用此标志位。

符号标志 SF (SignFlag) 用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF 也就反映运算结果的正负号。运算结果为正数时,SF 的值为 0,否则其值为 1。

溢出标志 OF (OverflowFlag) 用于反映有符号数加减运算所得结果是否溢出。如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF 的值被置为 1,否则,OF 的值被清为 0。

64/128 位寄存器图

20210905201223

一般寄存器了解完了还得去了解寻址方式,这里就大致了解一下得了..

立即寻址 (直接操作立即数,即常数),直接寻址 (通过内存地址获得立即数),寄存器寻址 (通过寄存器获得立即数),寄存器间接寻址 (通过寄存器中内存地址获取立即数),变址寻址 (通过寄存器中内存地址加上一个偏移量得到新的内存地址获取立即数),基址变址寻址 (通过俩寄存器存的内存地址,一个是基址,另一个是变址,再加上一个偏移量得到内存地址获取立即数)

冯诺依曼结构:

1 计算机主要由运算器,控制器,存储器,I/O 设备等五大部件组成;

2 各部件的作用是

2.1 存储器存放数据和指令,两者表现形式上没有什么区别,但是计算机应能区分是数据还是指令;

2.2 控制器应能自动取出指令来执行;

2.3 运算器应能进行加减乘除基本算数运算,并且也能进行一些逻辑运算和附加运算 (移位运算);

2.4 操作人员可以通过 IO 设备与计算机交互

2 新的指令

这个时候再来看新一天的代码就会比较清晰了

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
; hello-os
; TAB=4
ORG 0x7c00 ; 指明程序的装载地址
; 下面的代码用于描述标准FAT12格式的软盘
JMP entry
DB 0x90
; 略...
entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
MOV ES,AX
MOV SI,msg ; 将标号msg的地址存入SI中
putloop:
MOV AL,[SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环

msg:
DB 0x0a, 0x0a ; 换行两次
DB "hello, world"
DB 0x0a ; 换行
DB 0
; 略...

ORG: 即 origin,在开始执行的时候告诉 nask,程序要从指定的地址开始,也就是要把程序装载到内存中的指定地址(启动区内容的装载地址:0x7c00-0x7dff)

JMP:即 jump,跳转指令;

entry::标签的声明,用于指定 JMP 指令的跳转目的地(入口);

MOV:即 move,赋值指令。

ADD:加法指令 CMP:即 compare,比较指令 JE:即 jump if equal,条件跳转指令之一,如果比较结果相等,则跳转到指定的地址;而如果比较结果不等,则不跳转,继续执行下一条指令 fin:是个标号,表示结束(finish)的意思。 INT:软件中断指令(interrupt),用来调用 BIOS 中的函数指令。暂时当作是函数调用来理解 HLT:即 halt,让 CPU 停止动作的指令,但并非彻底停止,而是进入待机状态。如果无 HLT 指令,CPU 会在 JMP fin 无限循环,所以不写 HLT 指令也可以,但这样会使 CPU 无意义的空转,CPU 负荷过大,耗费电能,所以加上 HLT 指令后使 CPU 处于休眠状态,降低能耗。

3 BIOS 中断向量表

对于这些本就定义好的东西,记录下来就好,这玩意还是啥时候用啥时候百度的好..

中断描述
INT 00hCPU: 除零错, 或商不合法时触发
INT 01hCPU: 单步陷阱, TF 标记为打开状态时, 每条指令执行后触发
INT 02hCPU: 非可屏蔽中断, 如 开机自我测试 时发生内存错误触发。
INT 03hCPU: 第一个未定义的中断向量, 约定俗成仅用于调试程序
INT 04hCPU: 算数溢出。通常由 INTO 指令在置溢出位时触发。
INT 05h在按下 Shift-Print Screen 或 BOUND 指令检测到范围异常时触发。
INT 06hCPU: 非法指令。
INT 07hCPU: 没有数学协处理器时尝试执行浮点指令触发。
INT 08hIRQ0: 可编程中断控制器每 55 毫秒触发一次,即每秒 18.2 次。
INT 09hIRQ1: 每次键盘按下、按住、释放。
INT 0AhIRQ2:
INT 0BhIRQ3: COM2/COM4。
INT 0ChIRQ4: COM1/COM3。
INT 0DhIRQ5: 硬盘控制器(PC/XT 下)或 LPT2。
INT 0EhIRQ6: 需要时由软碟控制器呼叫。
INT 0FhIRQ7: LPT1。
INT 10h显示服务 - 由 BIOS 或操作系统设定以供软件调用。
AH=00h 设定显示模式
AH=01h 设定游标形态
AH=02h 设定游标位置
AH=03h 获取游标位置与形态
AH=04h 获取光笔位置
AH=05h 设定显示页
AH=06h 清除或卷轴画面 (上)
AH=07h 清除或卷轴画面 (下)
AH=08h 读取游标处字符与属性
AH=09h 更改游标处字符与属性
AH=0Ah 更改游标处字符
AH=0Bh 设定边界颜色
AH=0Eh 在 TTY 模式下写字符
AH=0Fh 取得目前显示模式
AH=13h 写字符串
INT 11h返回设备列表。
INT 12h获取常规内存容量。
INT 13h低阶磁盘服务。
AH=00h 复位磁盘驱动器。
AH=01h 检查磁盘驱动器状态。
AH=02h 读扇区。
AH=03h 写扇区。
AH=04h 校验扇区。
AH=05h 格式化磁道。
AH=08h 取得驱动器参数。
AH=09h 初始化硬盘驱动器参数。
AH=0Ch 寻道。
AH=0Dh 复位硬盘控制器。
AH=15h 取得驱动器类型。
AH=16h 取得软驱中盘片的状态。
INT 14h串口通信例程。AH=00h 初始化串口。AH=01h 写出字符。AH=02h 读入字符。AH=03h 状态。
INT 15h其它(系统支持例程)。
AH=4FH 键盘拦截。
AH=83H 事件等待。
AH=84H 读游戏杆。
AH=85HSysRq 键。
AH=86H 等待。
AH=87H 块移动。
AH=88H 获取扩展内存容量。
AH=C0H 获取系统参数。
AH=C1H 获取扩展 BIOS 数据区段。
AH=C2H 指针设备功能。
AH=E8h, AL=01h (AX = E801h) 获取扩展内存容量(自从 1944 年引入的新功能),可获取到 64MB 以上的内存容量。
AH=E8h, AL=20h (AX = E820h) 查询系统地址映射。该功能取代了 AX=E801h 和 AH=88h。
INT 16h键盘通信例程。
AH=00h 读字符。
AH=01h 读输入状态。
AH=02h 读 Shift 键(修改键)状态。
AH=10h 读字符(增强版)。
AH=11h 读输入状态(增强版)。
AH=12h 读 Shift 键(修改键)状态(增强版)。
INT 17h打印服务。
AH=00h 打印字符。
AH=01h 初始化打印机。
AH=02h 检查打印机状态。
INT 18h执行磁带上的 BASIC 程序:“真正的” IBM 兼容机在 ROM 里内置 BASIC 程序,当引导失败时由 BIOS 调用此例程解释执行。(例:打印 “Boot disk error. Replace disk and press any key to continue...” 这类提示信息)
INT 19h加电自检之后载入操作系统。
INT 1Ah实时钟服务。
AH=00h 读取实时钟。
AH=01h 设置实时钟。
AH=02h 读取实时钟时间。
AH=03h 设置实时钟时间。
AH=04h 读取实时钟日期。
AH=05h 设置实时钟日期。
AH=06h 设置实时钟闹铃。
AH=07h 重置实时钟闹铃。
INT 1BhCtrl+Break,由 IRQ 9 自动调用。
INT 1Ch预留,由 IRQ 8 自动调用。
INT 1Dh不可调用:指向视频参数表(包含视频模式的数据)的指针。
INT 1Eh不可调用:指向软盘模式表(包含关于软驱的大量信息)的指针。
INT 1Fh不可调用:指向视频图形字符表(包含从 80h 到 FFh 的 ASCII 字符的数据)的信息。
INT 41h地址指针:硬盘参数表(第一硬盘)。
INT 46h地址指针:硬盘参数表(第二硬盘)。
INT 4Ah实时钟在闹铃时调用。
INT 70hIRQ8: 由实时钟调用。
INT 74hIRQ12: 由鼠标调用
INT 75hIRQ13: 由数学协处理器调用。
INT 76hIRQ14: 由第一个 IDE 控制器所呼叫
INT 77hIRQ15: 由第二个 IDE 控制器所呼叫

4 制作 boot

因为启动区只需要前 512 字节,所以只留下前面操作 512 字节的代码就好了,并将文件名改为ipl.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
; hello-o
; TAB=4
ORG 0x7c00
JMP entry
DB 0x90
DB "HELLOIPL"
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0,0,0x29
DD 0xffffffff
DB "HELLO-OS "
DB "FAT12 "
TIMES 18 DB 0
entry:
MOV AX,0
MOV SS,AX
MOV DS,AX
MOV ES,AX
MOV SP,0x7c00
MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI,1
CMP AL,0
JE fin
MOV AH,0x0e
MOV BX,15
INT 0x10
JMP putloop
fin:
HLT
JMP fin
msg:
DB 0x0a, 0x0a
DB "hello, world"
DB 0x0a
DB 0
TIMES 0x1fe-($-$$) DB 0
DB 0x55, 0xaa

执行命令nasm ipl.asm -o ipl.bin -l ipl.lst得到,其中 bin 文件只是将后缀名字改了,而 lst 文件是一个文本文件,记录着如何将指令翻译成机器语言的。然后书上说用作者写的工具edimg.exe制作 img 文件,在 Linux 显然没法弄了。但是咧,可以用dd if=ipl.bin of=myos.img bs=512 count=1 conv=notrunc,就弄好了,然后依然是执行qemu-system-i386 -drive file=myos.img,format=raw,if=floppy,还好,能跑起来.. 也就是说,不要后面那段 0 也是可以跑起来的,虽然不知道会不会有什么隐患..

扯完了这个,再看另一本书《一个 64 位操作系统的设计与实现》的 boot 程序

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
	org	0x7c00	          ; 开始的位置是一样的
BaseOfStack equ 0x7c00 ; equ是伪指令,可以理解位 BaseOfStack = 0x7c00
Label_Start:
mov ax, cs ; cs=0 所以和entry那块是一样的,ax,ds,es,ss置为0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack ; 和 MOV SP,0x7c00是一个道理
;======= clear screen
mov ax, 0600h ; 即AH=06h清除或卷轴画面(上)
mov bx, 0700h ; 滚动后屏幕放入的属性 00000111,白色,bit0~2,颜色;bit3,亮度;bit4~6,背景颜色;bit7,闪烁
mov cx, 0 ; 滚动范围的左上角行列号
mov dx, 0184fh ; 滚动范围的右上角行列号
int 10h
;======= set focus
mov ax, 0200h ; 设定光标位置 (0,0)
mov bx, 0000h ; 设定游标行数
mov dx, 0000h ; 列数
int 10h
;======= display on screen : Start Booting......
mov ax, 1301h ; AH=13打印字符串 AL=01 光标位置会放到字符串后面
mov bx, 000fh ; BH代表页码 BL表示字符属性参考AH=06
mov dx, 0000h ; 游标行列号
mov cx, 10 ; 字符串长度
push ax ; 真的就是入栈???
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage ; 放入字符串咯
int 10h
;======= reset floppy
xor ah, ah
xor dl, dl
int 13h ; 重置磁盘
jmp $
StartBootMessage: db "Start Boot"
times 510 - ($ - $$) db 0
dw 0xaa55

除了没有 FAT12 那个描述,其它基本上都是差不多的.. 这段程序通过

1
2
3
nasm my64.asm -o my64.bin -l my64.lst
dd if=my64.bin of=my64.img bs=512 count=1 conv=notrunc
qemu-system-i386 -drive file=my64.img,format=raw,if=floppy

也跑起来了..

5 Makefile

接下来是 Makefile 的一些介绍,推荐看经典啊

makefile 介绍 — 跟我一起写 Makefile 1.0 文档