实模式

[TOC]

实模式分段机制

1. 实模式

在32位系统中,CPU有2种工作模式,分别为实模式和保护模式。当电脑一开始启动时,就默认进入实模式下。

2. 为什么要分段

8086cpu有20根地址总线。使用20根CPU总线的寻址范围就为 1<<20 = 1M 。

而段寄存器(例如DS段寄存器),只有16位大小。如果只使用单独的段寄存器寻址,使用寻址范围为1<<16 = 64kb 。

CPU总线总线寻址能力:1<<20 = 1M
段寄存器寻址能力 : 1<<16 = 64kb

此时出现一个矛盾点,64kb寻址范围是无法完整访问1M内存空间的。需要引入分段机制来解决这个问题,使用一个基址段寄存器,配合一个偏移地址,来配合使用共同寻址。从而达到20位的寻址能力(归根结底还是段寄存器不够大,导致单独一个段寄存器不够用的)。使用DS段值+偏移地址进行计算,换算成20位总线地址。

1
2
CPU总线寻址: 20位寻址能力 
段寄存器址: 段寄存器(16位) << 4 + 偏移地址(16位) = 总线寻址能力(20位)

使用一个段寄存器存储基址,配合一个数据寄存器存储偏移地址,来配合使用共同寻址,刚好达到20位(1M)寻址能力。

在这种模式下,CPU提供了3个段寄存器:

段寄存器:

  • 代码段寄存器CS:用于指令寻址
  • 数据段寄存器DS:用于数据寻址
  • 附加段寄存器ES:用于其他寻址、

偏移地址:

- 寄存器:  偏移地址使用寄存器时,只能为BX,SI,DI,BP中之一.
- 立即数: 使用立即数作为为偏移地址

3. 代码段和数据段

一个可执行程序一般包含代码段和数据段两部分。比如显示字符串的功能,将字符串文本数据和显示函数放在2个位置。当程序执行时,可以设置一个段地址为段基址,其他的段内地址为段基址+段内偏移地址。这样来决定一条指令的位置。

  1. 代码段

    因为处理器是自动从一个开始地址中取出指令开始执行,如果没有指令进行跳转的话。则依次取出下一条指令继续执行。而这些完成某个工作的指令集中在内存的连续一段区域,称为代码段。

    内存中指令位置:

    [CS段寄存器 : IP指令指针寄存器]

  2. 数据段

    程序操作的数据也集中一起,放在内存的连续一段区域,称为数据段。

    内存中数据位置:

    [DS段寄存器:偏移地址]

指令执行

段寄存器赋值

  1. 段寄存器赋值
    因为intel处理器不允许直接将段寄存器进行’立即数’赋值,因此如果使用段寄存器的话,必须先将’立即数’放到通用寄存器,然后复制通用寄存器的数据到段寄存器中。

    比如:
    ​ MOV 通用寄存器 , 0x7c00
    ​ MOV 段寄存器,通用寄存器
    或者:
    ​ MOV 段寄存器,内存地址

  2. 段寄存器范围

    实模式下,段的寄存器为16位,范围为 0x0000 ~ 0xFFFF,当超出0xFFFF就会进位,继续回到 0x0000的值。

  3. 段内地址赋值

    当我们使用一个段地址时,可以使用一下方式

    1
    MOV [0x1000:0x000b], 0x100

    其中0x1000为段地址,0x000b为偏移地址,0x100为操作数的值。

    这个表达式意思为设置地址 DS * 0xf + 0x000b 的值为0x100。

    当然,不过也可以不指定 0x1000,那么偏移会默认以DS中的值为基点,计算偏移后的地址

    比如

    1
    MOV [0x000b], 0x100
1
* 关于[]表名这个是地址,而不是操作数。如果在右边不加[]时为操作数,而加入[]则表示一个地址,当运算会以地址指向的数据来进行计算。

指令执行

执行指令,需要使用到段寄存器:CS段寄存器 和 IP指令指针寄存器

CPU每次执行内存中指令,都需要一个内存地址。这个地址为内存中的指令代码地址。

CPU是根据寄存器来获取到这个地址并且执行这个地址的指令的。使用到2个寄存器来获取指令地址:CS和IP

  1. 指令执行

1.首先CPU根据CS和IP获取到内存地址,将内存中对应的指令放入指令缓冲器等待执行。
2.之后IP寄存器的值会自动增加,使得CS和IP的地址指向内存中的下一条指令。
3.使用JMP指令可以修改CS,IP的值

例如,执行CS:0x0003H,IP:0x0016H地址的指令

1
JMP 0x0003:0x0016    ;跳转到内存 0x0003H<<4 + 0x0016H处

指令的内存地址
指令的内存地址使用:CS段寄存器:IP指令指针寄存器 来表示。
例如:

0x0003:0x0016 ;代表内存 0x0003<< 4 + 0x0016处的指令。

数据访问

1. 内存单元:

内存单元,一个内存单元的大小是一字节:1B(1BYTE)。

内存单元的地址:

内存单元表示方式为:

[基本地址 : 偏移地址]

计算方式:

内存单元地址 = 基本地址 << 4+偏移地址

2. 内存单元的数据

内存单元的数据,大小为16B,内存单元的数据,是根据[内存单元地址]来获取的。

CPU是根据DS段地址和偏移地址来定位内存单元的地址/

访问内存单元的数据使用

[段寄存器值 : 偏移地址]

来表示根据

得到的内存地址处的内存单元 = 段寄存器值 << 4 + 偏移地址。

如果不指定段寄存器,CPU执行时默认会取DS段寄存器值进行计算。

默认使用DS段寄存器
内存单元的数据: [偏移地址] = [DS段寄存器:偏移地址]

3. 访问内存数据(16位)

访问内存,需要使用到段寄存器:DS段寄存器

内存单元读写

读取内存单元的数据到寄存器中:
例如

1
2
mov ds,0x10000
mov ax,[0x10] ;ax = [0x10000:0x10]

段寄存器不能使用常量立即数赋值,所以必须使用一个中间的数据寄存器来操作

例如读取[0x10000H:0x0016H]的数据到al寄存器中

1
2
3
mov bx,0x10000
mov ds,bx ;ds = 0x10000
mov al,[0x0016] ;al = [0x10000:0x0016WW]

访问范围

实模式下,偏移地址也是16位的,限制位 0x0000 ~ 0xffff。所以如果访问超出这个范围的内存地址,只使用偏移地址是不行的。

如下:
[偏移地址] 寻址范围: 0x0000 ~ 0xffff
[段寄存器值 :偏移地址] 寻址范围 : 0x0_0000 ~ 0xf_ffff

实模式下内存访问范围

在实模式下:

内存单元的数据大小为1个字节 (16B).

基本地址是使用一个16位的段寄存器的值,而偏移地址是一个16位的值。

如果没有显式说明段寄存器,默认基本地址使用DS段寄存器。

访问范围 0x0000 ~ 0x10FFEF (0xFFFF << 4 + 0xFFFF )

当程序访问0x100000~0x10FFEF这一段地址时,因为其逻辑上是正常的,CPU并不会认为其访问越界而产生异常,但这段地址确实没有实际的物理地址与其对应,会截取掉最高的一位,进行回绕访问。

自制系统实模式内存分配

首先我们需要对程序在内存中分配的位置有个大概的定义。
首先是实模式下的1M空间的内存,分配

这1M大小的内存区域位并不完全位于通常的内存条中,会被BIOS以及显卡等占据一部分。

其中0x000000x9FFFF位置是属于内存条的地址。
0xA0000
0xEFFFF提供给外围设备使用,例如显卡等。
而0xF0000~0xFFFFF是属于BIOS的ROM地址。

内存地址 空间大小 用途
0x00000 ~ 0x003FF 1KB 中断向量表
0x00400 ~ 0x004FF 256B BIOS数据区
0x00500 ~ 0x07BFF 大概30KB 可用区域
0x07C00 ~ 0x07DFF 512B MBR引导加载位置
0x07E00 ~ 0x9FBFF 大概608KB 可用区域
0x9FC00 ~ 0x9FFFF 1KB BIOS扩展数据区

操作系统文件加载
boot文件512字节:加载到0x07C00 ~ 0x07DFF

内存地址 空间大小 用途
boot 0x07C00 ~ 0x07DFF 1个扇区(512字节)
loader0 0x90000 ~ 0x907FF 1个扇区(512字节)
loader 0x10000 4个扇区(2048字节)

运行后应该是进入黑屏无光标的页面(区别是没有光标的哦)。

RASTOS磁盘分布配置

0x0000 - ox01ff 引导扇区,1个扇区,512 B
0x0200 - 0x09ff loader文件,4个扇区,2048 B
0x0a00 - 0x39ff 内核文件,24个扇区,12288 B

Bochs

工具

bochs: bochs

ubuntu安装配置Bochs

  1. 安装bochs

  2. 创建工程目录

创建工程目录并进入

  1. 新建并修改配置文件

在工程目录下新建bochsrc.me文件

$ vim bochsrc.me

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
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
cpu: cpuid_limit_winnt=0

memory: guest=512, host=256


romimage: file=$BXSHARE/BIOS-bochs-latest, options=fastboot

vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest


mouse: enabled=0

pci: enabled=1, chipset=i440fx

private_colormap: enabled=0


floppya: 1_44=/dev/fd0, status=inserted



ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=1, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata2: enabled=0, ioaddr1=0x1e8, ioaddr2=0x3e0, irq=11
ata3: enabled=0, ioaddr1=0x168, ioaddr2=0x360, irq=9



ata0-master: type=disk, mode=flat,path="build/rastos.img"


boot: disk

floppy_bootsig_check: disabled=0

log: bochsout.txt


panic: action=ask
error: action=report
info: action=report
debug: action=ignore, pci=report # report BX_DEBUG from module 'pci'


debugger_log: -

parport1: enabled=1, file="parport.out"


sound: driver=default, waveout=/dev/dsp. wavein=, midiout=

speaker: enabled=1, mode=sound, volume=15

脚本文件
build.sh

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

NASM=nasm
BOXIMAGE=bximage

mkdir build

$NASM -f bin -o build/boot.bin boot/boot.asm

$BOXIMAGE -func=create -imgmode=flat -hd=16M -q build/rastos.img
dd if=/dev/zero of=build/rastos.img bs=512 count=2880
dd if=build/boot.bin of=build/rastos.img bs=512 count=1 conv=notrunc

run.bat

1
2
3
4
Rem bat
@echo bochs start
set bochs="C:/Program Files (x86)/Bochs-2.6.9/bochsdbg.exe"
%bochs% -f bochsrc.me

images/2_1_1.png

Bochs使用

1. 运行

进入工程目录
输入 bochs命令运行

进入选择命令行,输入6启动模拟器

2. 创建硬盘镜像

bximage -mode=create -hd=128M -imgmode=flat -q icyos.img

3. 根据配置文件运行

命令如下:

1
bochs -f bochsrc.me

4. Bochs调试

命令 说明
blist 显示所有断点信息
pb [物理地址] 设置断点,以物理地址方式
vb [虚拟地址] 设置断点,以虚拟地址方式
lb [线性地址] 设置断点,以线性地址方式
d [断点号] 删除断点 ,断点号根据blist查询
c 继续执行,跳到下一个断点/
s [N] 单步执行
n 单步执行(跳过call函数内部 )
q 退出

显示信息

命令 说明
show mode 显示模式切换
show int 显示中断
show call 显示call调用
trace on 显示指令反编译
info ivt 显示ivt(中断向量表)信息
info idt 显示idt(中断描述符表)信息
info gdt 显示gdt信息
info ldt 显示ldt信息
info tss 显示tss信息
info tab 页表映射
reg 通用寄存器信息 + 标志寄存器 + eip寄存器信息
sreg 段寄存器信息
creg 控制寄存器信息
dreg 调试寄存器信息
print-stack N 堆栈信息

内存信息

命令 说明
xp /nuf [物理地址] 显示物理地址处内容,例如:xp /100 0xa0000
x /nuf [线性地址] 显示线性地址处内容
setpmem
page

加载器

  1. boot.bin文件,16位汇编文件,引导扇区,系统的入口
  2. loader.bin文件,16位汇编文件,loader文件,负责从16位实模式进入32位保护模式,并且加载32位ELF格式文件 loader.bin。
  3. loaderELF.bin文件,32位ELF文件,使用C语言实现,运行在32位保护模式下。

loader.bin位于引导的第1.5阶段。
loaderELF.bin位于引导的第2阶段。
两个称为loader加载器

按照正常的逻辑,BIOS载入执行引导扇区后,主引导记录就应该去查询并执行内核文件了。

但是存在着下面几个问题:

  1. 没有文件系统,就算磁盘有内核的内容,引导代码也不知道应该从哪里加载内核内容执行。

  2. 主引导记录只有512KB,没有办法做更多的操作

因此需要二阶段的引导,流程如下:

主引导记录继续加载执行一段的第二阶段引导程序,而这段内容可能占据几个扇区。

第二阶引导程序会构建文件系统,然后根据文件系统查找和执行内核。

第二阶引导程序我们称之为loader。加载器。

读取加载器

磁盘的1扇区为mbr扇区 ,我们规定其后面2-32扇区为loader内容的扇区。流程如下:

BIOS加载执行引导扇区。

引导扇区访问磁盘,并读取磁盘的2-62扇区。

复制磁盘的2-62扇区到内存的0x90000位置,并执行。

1. loader文件的位置

1)内存分配

在boot引导完成后,当前系统的内存分配如下:

内存 0x7c00-0x7dff :引导扇区

内存 0x8000-N : 读取的磁盘内容 ( 其中的 0xc200-0xc3ff 是loader文件)

2)loader在内存的位置

在前面的引导程序里面已经将磁盘的10个柱面加载到了内存单元的0x8000处。而在ima文件中程序的起始位置(也就是引导扇区)在磁盘中的位置是 0x4200。

同样在内存也是从 0x8000 往后的 0x4200 位置。

所以loader的内容在内存单元的开始位置为:0x8000+0x4200=0xc200位置。

说明:

整个磁盘文件包括引导扇区512+磁盘文件sys。loader在sys文件的0x4200位置,引导扇区在此之后。

需要的工具

qemu: qemu

qemu的windows版本:QEMU for Windows – Installers (64 bit)

BIOS中断

BIOS中断简介

计算机刚启动时,进入实模式下,此时操作系统跟硬件(例如键盘鼠标显卡等)交互通过BIOS进行的。通过调用中BIOS中断的方式来访问硬件设备。

BIOS中断就不详细介绍了。

BIOS中断大全

查询相应的中断API可以根据BIOS中断大全:BIOS中断大全)

BIOS中断指令

1
int  中断号

BIOS的中断向量表

中断向量表位置

中断向量表位于BIOS的 0x0000 - 0x03FF 地方,大小为 1k。

中断号 说明: int 中断号
0x00 DIVIDE ERROR
0x01 SINGLE STEP
0x02 NON-MASKABLE INTERRUPT
0x03 BREAKPOINT
0x04 INT0 DETECTED OVERFLOW
0x05 BOUND RANGE EXCEED
0x06 INVALID OPCODE
0x07 PROCESSOR EXTENSION NOT AVAILABLE
0x08 IRQ0
0x09 IRQ1
0x0a IRQ2
0x0b IRQ3
0x0c IRQ4
0x0d IRQ5
0x0e IRQ6
0x0e IRQ7
0x10 VIDEO 显示
0x11 GET EQUIPMENT LIST 设备列表
0x12 GET MEMORY SIZE 内存大小
0x13 DISK 磁盘
0x14 SERIAL 串行口服务
0x15 SYSTEM 系统
0x16 KEYBOARD 键盘
0x17 PRINTER 打印机
0x18 CASETTE BASIC
0x19 BOOTSTRAP LOADER 时钟
0x1a TIME
0x1b KEYBOARD - CONTROL-BREAK HANDLER
0x1c TIME - SYSTEM TIMER TICK
0x1d SYSTEM DATA - VIDEO PARAMETER TABLES
0x1e SYSTEM DATA - DISKETTE PARAMETERS
0x1f SYSTEM DATA - 8x8 GRAPHICS FONT
0x70 IRQ8 - CMOS REAL-TIME CLOCK
0x71 IRQ9 - REDIRECTED TO INT 0A BY BIOS
0x72 IRQ10 - RESERVED
0x73 IRQ11 - RESERVED
0x74 IRQ12 - POINTING DEVICE
0x75 IRQ13 - MATH COPROCESSOR EXCEPTION
0x76 IRQ14 - HARD DISK CONTROLLER OPERATION COMPLETE
0x77 IRQ15 - SECONDARY IDE CONTROLLER OPERATION

BIOS中断清屏

清屏是通过BIOS中断,来滚动屏幕,达到清屏的效果。

1. BIOS中断滚屏

中断 int 10h,AH = 06H / 07H

寄存器 说明
AH 功能编码 向上滚屏:06H,向下滚屏 : 07H
BH 空白区域的缺省属性
AL 滚动行数 0:清窗口
CH、CL 滚动区域左上角位置:Y坐标,X坐标
DH、DL 滚动区域右下角位置:Y坐标,X坐标

例如:使用蓝底白字清屏

1
2
3
4
5
6
7
8
;---------------------------
;清除屏幕
mov ah,0x06
mov al,0
mov cx,0
mov df,0xffff
mov bh,0x17 ;属性为蓝底白字
int 0x10

2. BIOS中断设置光标位置:

中断 int 10h

功能描述:用文本坐标下设置光标位置

入口参数:

寄存器 说明
AH 功能编码 设置光标位置:02H
BH 显示页码
DH,DL 行,列 (Y坐标,X坐标)

例如:设置光标到第一行第一列

1
2
3
4
5
6
;---------------------------			
;光标位置初始化
mov ah,0x02
mov bh,0
mov dx,0
int 0x10

实现

1. 代码

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
; 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

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

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

构建并运行

构建
创建build.sh脚本

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

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

rastoos.ima镜像文件。

运行
创建run.sh脚本

1
2
3
4
#!/bin/bash

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

window下创建run.bat

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

执行run.sh脚本

结果如图

./images/1_5_1.png

工具

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

所需工具

汇编编译器:nasm: https://www.nasm.us/

文本编辑器:vscode: https://code.visualstudio.com/

vscode插件:x86 and x86_64 Assembly

vscode插件:hexdump

1.安装WSL和ubuntu

在windows下安装并启用WSL,在WSL中安装ubuntu22版本

2. 安装nasm编译器

ubuntu下使用命令安装

sudo apt-get install nasm

计算机启动过程

1 计算机启动过程

1.1 加电自检

计算器通电后,首先加载执行BIOS代码,由BIOS对系统硬件执行自检查。

通常完整的POST自检将包括对CPU,640K基本内存,1M以上的扩展内存,ROM,主板,CMOS存储器,串并口,显示卡,软硬盘子系统及键盘进行测试,一旦在自检中发现问题,系统将给出提示信息或鸣笛警告。

BIOS检测硬件是否正常,然后开始初始化一些硬件设备。

1.2 加载引导扇区

  1. 首先BIOS查找启动盘顺序。BIOS检查到硬件正常并与CMOS中的设置相符后,按照CMOS中对启动设备的设置顺序检测可用的启动设备。

  2. BIOS将相应MBR扇区(也就是启动设备的第一个扇区,为硬盘的0面0磁道1扇区,扇区大小为512字节)读入内存地址为0000:7C00H处。

  3. BIOS检查扇区格式,判断扇区是否以0xAA55结束,BIOS认为这个扇区是一个可用的引导扇区。否则去尝试其他的启动设备。如果没有启动设备满足要求则显示”NO ROM BASIC”然后死机。

1.3 执行指令

加载完成后,BIOS将控制权交给CPU,CPU开始执行0x0000:7c00处,也就是被加载了引导扇区的地方的代码。

cpu加载内核文件,启动相关操作系统。

2 BIOS

2.1 BIOS的作用

BIOS的英文全称为Basic Input Output System,是位于计算机主板上只读ROM芯片上的程序。

BIOS映射的地址为ROM的0xF0000~0xFFFFF位置。

其中0x00000~0x9FFFF位置是属于内存条的地址,0xF0000~0xFFFF是属于BIOS的ROM地址。

主引导扇区

主引导扇区又称为MBR,是操作系统的起点,当计算机启动后,会加载并执行位于硬盘上的第一个扇区做为引导扇区。

1. 主引导扇区格式

  • 主引导扇区必须位于硬盘开始位置的512字节。

    例如在1.44M软盘上,位于(柱面=0,磁头=0,扇区1)的位置

  • 主引导扇区大小必须是512字节。

  • 主引导扇区必须是以 0x55, 0xAA 为结尾的。

2. 主引导扇区执行过程

主引导扇区由BIOS进行加载并执行。

BIOS系统首先会读取 0 柱面 0 磁道 1 扇区,将其内容加载到内存地址的 0x0000:0x7c00处。然后BIOS跳转到内存的0x7c00处开始执行指令。

至于为什么必须加载到 0x7c00 的地方,当时就这么规定的吧,没啥特殊的地方,计算机读取指令开始时,会从 0x7c00处读取。(其实还是有点特殊的地方,参见http://www.ruanyifeng.com/blog/2015/09/0x7c00.html)

编写一个主引导扇区

下面我们编写一个最简单的主引导扇区,并且在虚拟机中加载运行。

示例代码

创建boot目录,并新建文件boot/mbr.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; RAST BOOT
[bits 16]

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

; 引导扇区代码
jmp Entry
db 0x90
db "RASTABOOT"

; 程序核心内容
Entry:
jmp $ ; 让CPU挂起,等待指令。

; 扇区格式
Fill0:
resb 510-($-$$) ; 当前行$至结束(1FE) 之间的内容填充0
db 0x55, 0xaa

简单解释下上面的汇编代码:

[bits 16] 表示运行在16位模式下(实模式下是16位模式)

org 0x7c00: 指定一个基地址,用来声明计算程序的偏移地址,后面的地址跳转都会以此为基础(编译器会把所有程序用到的 段内偏移地址自动加上org后跟的数值)

jmp Entry:这句是正式开始执行的代码,表示跳转到后面 Entry标记的位置执行指令。

db 0x90, db "GLOXBOOT": 填充内容,并无实际作用。不是系统指令,也不会被执行。

jmp $: $ 代表当前行首的地址,所以会一直循环执行此指令。

resb 510-($-$$) : 当前行$至结束(1FE) 之间的内容填充0

db 0x55, 0xaa:填充 0x55, 0xaa作为主引导扇区结束标识。

编译代码

使用 nasm 编译器编译 boot.asm 文件

nasm -f bin -o build/mbr.bin boot/boot.asm

然后,使用 linux 命令创建一个1.44M大小,字节都为0的 rast.img 镜像文件

dd if=/dev/zero of=build/rastos.img bs=512 count=2880

将引导扇区文件,添加到rastos.img 镜像中

dd if=build/boot.bin  of=build/rastos.img bs=512 count=1 conv=notrunc

创建完成,至于dd命令的用法,自己查找学习,在此不做详述了。

总结,完整的 build.sh 执行脚本如下

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

NASM=nasm

mkdir build

$NASM -f bin -o build/boot.bin boot/boot.asm

dd if=/dev/zero of=build/rastos.img bs=512 count=2880

dd if=build/boot.bin of=build/rastos.img bs=512 count=1 conv=notrunc

在虚拟机中执行

  1. 使用vitualbox虚拟机

    创建一个虚拟电脑,rastos

  2. 加载并运行镜像

    使用vitualbox创建一个系统,添加一个软盘驱动器。

    添加一个软盘驱动器。

    使用软盘驱动器加载镜像文件rastos.img。

    images/1_4_1.png

  3. 启动系统

    点击显示运行系统

    images/1_4_2.png

    顺利启动,可以发现进入一个黑屏界面。并且光标一直闪烁。

二进制编码表

英文字符的二进制码(16进制表示):

英文 十六进制 英文 十六进制 英文 十六进制 英文 十六进制
A=65 41 H 48 O 4F V 56
B 42 I 49 P 50 W 57
C 43 J 4A Q 51 X 58
D 44 K 4B R 52 Y 59
E 45 L 4C S 53 Z 60
F 46 M 4D T 54
G 47 N 4E U 55

查看二进制文件

以二进制的方式查看rastos.img文件内容

使用xxd命令分析二进制文件(以16进制方式输出文件的前512字节)

xxd -l 512 build/rastos.img

如图:

images/1_3_3.png

1机器语言与汇编语言

1.2 汇编语言简介

人们通过编写汇编指令,然后使用汇编编译器编译成机器语言。汇编语言比只有0和
1组成的机器语言更容易阅读识别

比如

操作:寄存器BX的内容送到AX中

1
2
1000100111011000              // 机器指令
mov ax,bx // 汇编指令

2 常见的汇编编译器

汇编语言目前主要有2种风格式

  1. intel风格:intel风格
  2. AT&T风格:多数在linux系统上使用。

汇编编译器有以下几种

  1. gas编译器: 与gcc搭配食用,不用详细说了。
  2. masm: 微软开发的编译器,可以忽略掉。
  3. nasm: 目前看比较优雅,我比较喜欢的一个。

安装nasm编译器

ubuntu下使用命令安装

sudo apt-get install nasm

NASM汇编编译器

1. nasm命令用法:

1
> nasm.exe -f {formart} -o 生成文件 源文件

可以使用查看支持生成的文件的{formart}

2. NASM分段

段(Section): 每个汇编程序都是由段来组成的,有以下的段:

  1. data:用来声明初始化的数据或常量
  2. bss:用来声明未初始化的变量
  3. text:用来存放代码

NASM汇编指令

1. 内存单元

名称 长度 说明
byte 8 位 字节
word 16 位
dword 32 位 双词
内存单元表示:[]

任何被[]包含的变量都是地址。[]表示一个内存单元。内存单元的段地址为ds中的存储数据,偏移地址为[]里面表示的数据。

例如:

1
mov [gs,0x10]   0x3F

注释: ;

例如

1
mov ax,0x02    ;ax=0x02

数据类型

  1. 数据类型,基本的数据类型如下

    byte : byte 字节,8位
    word : word 字,16位
    dword : double word 双字,32位
    qword : quadword 四字,64位

2. 伪指令

什么是伪指令

伪指令(Pseudo Instruction)是用于对汇编过程进行控制的指令,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。

**1) 地址: [] **

地址的三种表现方式:

  1. [立即数],例如 [0xf0]
  2. [存储器],例如 [ds]
  3. [存储器 + 立即数],例如 [ds,0xf0]

任何不被[]包含的变量都是地址。[]表示一个内存单元。内存单元的段地址为ds中的存储数据,偏移地址为[]里面表示的数据。

**1)当前指令开始地址:$
被称为当前位置计数器

在汇编程序对源程序进行汇编的过程中,使用地址计数器来保证当前正在汇编的指令地址。地址计数器值可用$来表示,汇编语言也允许用户直接用$来引用地址计数器的当前值,因此,ORG $+5可表示从当前地址开始跳过5个字节存储单元,在指令和伪指令中,也可直接用$表示地址计数器的当前值。

jmp $进入了一个无限循环。

**2)当前段开始地址:$$ **

例如 $-$$ 代表 当前指令开始地址 - 当前段开始地址

3)写字节:DB,DW,DD

DB, DW,DD 输出(1个字节,2个字节,4个字节)

说明:

  • DB:写出一个字节(BYTE)。会按照字节输出
  • DW :输出一个字(WORD=2BYTE).也就是输出2个字节(16位)。
  • DD :输出两个字(DoubleWord=4BYTE).也就是输出4个字节(32位)

**4)填充:RESB, RESW, RESD

RESB, RESW, RESD 填充(1个字节,2个字节,4个字节)

说明:

RESB 510-($-$$) 意思即是510字节位置(0x1fe) 减去这一行现在的字节位置:132。这些都填入0x00

$表示当前行被汇编后的地址,也就是当前行的地址。$$代表一个section节的开始地址。也就是这个section的开始地址。

也可以使用times 510-($-$$) db 0来表示上面的意思。

5)重复指令或数据:times

说明:

重复指令或数据
例如:

times 510-($-$$) db 0

imes前缀引起指令被汇编多次。其中$$表示是该程序的初始代码段的地址,故该指令将会被执行510-($-$$)次。也就是用0来填充剩下的空间,达到510字节。

6)org指令:指定偏移量

格式:ORG 偏移地址

例如:org 07c00h

ORG伪指令用来指出其后的程序段或数据块存放的起始地址的偏移量。汇编程序汇编时把语句中表达式的值作为起始地址,连续存放ORG语句之后的程序和数据,直到出现一个新的ORG指令。若省略ORG语句,则从本段起始地址开始连续存放。偏移地址的范围为(0x0000=0xffff)

在大多数情况下,不需要用ORG语句设置位置指针。由于段定义语句是段的起点,它的偏移地址为0000H,以后每分配一个字节,位置指针自动加1,所以每条指令都有确定的偏移地址。只有程序要求改变这个位置指针时,才需要安排ORG语句。通常ORG语句可以出现在程序中任何位置上。

使用org 07c00h是因为

  1. 首先DS是默认为0的
  2. 引导扇区会被加载在07c00h处,也就是磁盘中的代码和内存的代码有一个偏移量为07c00h
    当我们设置了org之后,后面的偏移都会默认在0x7c00的基础上。也就是说,如果不加org的话,我们在代码中指定指令时就必须制定 偏移 + 07c00h 来达到相同的效果了。

7) EQU 定义常量

one equ 1 使用one常量代表1 ​

8) 注释 ;

1 数据的描述

对于计算机来说,能识别的只有0和1两个数字。使用0和1这种二进制,位数足够,其实就可以描述无限可能性的数据、

例如穿孔纸带是早期计算机的储存介质,它将程序和数据转换二进制数码:带孔为1,无孔为0,经过光电输入机将数据输入计算机。

./images/1_1_1.jpg

内存中的内容是0和1表示的。
内存内容可以具体分为两类:
1)数据内容
2)指令内容

而指令内容,就是具体提供给CPU执行的内容。

2 十进制,二进制,十六进制

十进制,二进制,十六进制

十进制(Decimal )
是一种计数方法,即满十进一,十进制计数法是日常使用最多的计数方法,每相邻的两个计数单位都为十的计数法则。十进制一般用字母D表示。

例如:100、88、36753

二进制(Binary )
是在数学和数字电路中以2为基数的记数系统,它只有两个符号:0(代表零)和1(代表一)。这一系统是现代‌计算机和依赖计算机的设备里所使用的数制。

例如 :1101、11

十六进制 (hex)
是一种基数为16的计数系统,通常用数字0、1、2、3、4、5、6、7、8、9和字母A、B、C、D、E、F(a、b、c、d、e、f)表示、

c语言及其他相近的语言使用字首“0x”,开头的“0”令解析器更易辨认数,而“x”则代表十六进制。

例如:0xFF

2. 进制转换

使用linux的bc命令进行进制转换

10进制转2进制
echo ‘obase=2;ibase=10;1000’|bc
输出:1111101000

10进制转16进制
echo ‘obase=16;ibase=10;1000’|bc
输出:3E8

2进制转10进制
echo ‘obase=10;ibase=2;1000’|bc
输出:8

16进制转10进制
echo ‘obase=10;ibase=16;1000’|bc
输出:4096

1 CPU的功能模块

CPU从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来

  1. 控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和操作控制器OC(Operation Controller)

  2. 运算单元:是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。

  3. 存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方

简单的描述下CPU是如何进行计算的。

例如计算2个数的和:

  1. 首先,CPU根据指令寄存器,定位到内存中当前需要执行的指令内容的位置

  2. CPU的控制器从内存读取两条MOV指令,执行指令将内存的值赋值到两个不同的寄存器中。

  3. 然后。CPU的控制器从内存读取一条ADD指令,计算两个寄存器中数的和值。

  4. 计算的结果保存在第三个寄存器中,并将此寄存器的结果返回到内存中。

以上过程就包含了CPU的寻址,计算,读写这三个主要功能

2 CPU总线

  1. 地址总线

CPU是通过地址总线来进行内存地址寻址的。8086cpu有20根地址总线,可以访问 1M (1<<20 )的内存空间。

CPU地址总线的位数决定了CPU的寻址能力。

  1. 数据总线

CPU通过数据总线来进行内存和其他器件之间的数据传递。8086有16根数据总线,每次可以读写16位数据。

数据总线的宽度决定了CPU和外界的数据传输速度。

  1. 控制总线

CPU通过控制总线发出各种控制命令,用来操作外部设备。

3 CPU寄存器

说明:关于CPU寄存器的话,网上很多资料。这边只做一个总结。

http://zh.wikipedia.org/wiki/%E5%AF%84%E5%AD%98%E5%99%A8

要编写操作系统,首先要熟悉CPU的结构,这样才能控制CPU完成自己想要的操作。CPU是由很多个寄存器构成的,通过汇编语言操作寄存器的数据,从而进行CPU的执行和运算。

CPU寄存器组可以分为通用寄存器组,段寄存器组,指令指针寄存器 和 标志寄存器。

images/1_2_1.png)

16位cpu的寄存器组

8086 有14个16位寄存器,这14个寄存器按其用途可分为(1)通用寄存器-包括数据寄存器和指针寄存器、(2)指令指针寄存器、(3)标志寄存器和(4)段寄存器等4类。

CPU数存器组:

总共4个数据寄存器,2个指针寄存器以及2个变址寄存器,如下:

  • 4个数据寄存器(AX、BX、CX和DX)
  • 2个指针寄存器(SP和BP)
  • 2个变址寄存器(SI和DI)

通用寄存器

名称 所属寄存器组 作用
通用寄存器
AX 累加寄存器(accumulator) 累加寄存器
1.作为数据寄存器
2.累加器可用于乘、除、输入/输出等操作
BX 基址寄存器(base) 基址寄存器。
1.作为数据寄存器
2.可作为基址寄存器来使用,配合段寄存器寻址
CX 计数寄存器(count) 1. 计数寄存器:在循环和字符串操作时,要用它来控制循环次数
2.位移寄存器:在位操作中,当移多位时,要用CL来指明移位的位数
DX 数据寄存器(data) 1.在进行乘、除运算时,它可作为默认的操作数参与运算
2.用于存放I/O的端口地址
指针寄存器
SI 变址寄存器(source index) 源变址寄存器
DI 变址寄存器(destination index) 目标变址寄存器
指令指针寄存器
SP 栈指针寄存器(stack pointer) 栈指针寄存器
BP 基数指针寄存器(base pointer) 基址指针寄存器
IP 指令指针寄存器 指向当前需要取出的指令字节。IP指向的是指令地址的段内地址偏移量
段寄存器组
CS 代码段寄存器(code segment) 用于代码段内存位置寻址
DS 数据段寄存器(data segment ) 用于数据段内存位置寻址
SS 栈段寄存器(stack segment) 用于堆栈内存位置寻址
ES 附加段寄存器(extra segment)
标志寄存器
FR 标志寄存器(Flags Register) 存放条件标志、控制标志寄存器

通用寄存器: AX,BX,CX,DX

大小为16位又可再拆分高低位使用,大小为8位:AH,AL,BH,BL,CH,CL,DH,DL。

寄存器(16位) AX BX CX DX
高位(8位) AH BH CH DH
低位(8位) AL BL CL DL

1. 累加寄存器 AX -accumulator,累加寄存器。

作用:累加器可用于乘、除、输入/输出等操作,它们的使用频率很高

2. 基址寄存器 BX -base 基址寄存器。存储器指针

作用:它可作为存储器指针来使用;

3. 计数寄存器 CX -counter,计数寄存器。串操作,循环操作计数寄存器,其中的CL低位为移位操作计数器

作用:在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数

4. 数据寄存器 DX -data,数据寄存器。乘除运算

作用:在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址

5. 指针寄存器(16位)

6. 指令指针寄存器(16位)

7. 段寄存器组(16位)

8. 标志寄存器(每个标志占一位)FLAGS

32位cpu的寄存器组

CPU数存器组:

后来出现的32位处理器在16位的基础上延伸到32位:

总共有:
4个32位的数据寄存器:EAX、EBX、ECX和EDX。而这4个32位寄存器的低位和原先16位的一样,命名为:AX、BX、CX和DX。
2个指针寄存器:名称为ESP和EBP,
2个变址寄存器:名称为ESI和EDI。

1. 通用寄存器组(32位)

★名称 ★作用
位数 32位 16位 8位(高) 8位(低)
通用寄存器组
累加寄存器 eax ax ah al
基址寄存器 ebx bx bh bl
计数寄存器 ecx cx ch cl
edx dx dh dl
指针寄存器组
源变址寄存器 esi si
目标变址寄存器 edi di
指针寄存器组
栈寄存器组
栈指针寄存器 esp sp
基址指针寄存器 ebp bp
指令指针寄存器
指令指针寄存器 eip ip
段寄存器组
代码段寄存器 cs
数据段寄存器 ds
栈段寄存器 ss
附加段寄存器 es
附加段寄存器 fs
附加段寄存器 gs
标志寄存器
标志寄存器 cflags

64位cpu的寄存器组

CPU数存器组:

★名称 所属寄存器组 ★作用
**通用寄存器组
RAX 数据寄存器(accumulator) 累加寄存器,1.作为数据寄存器
2.累加器可用于乘、除、输入/输出等操作
RBX 数据寄存器(base) 基址寄存器。1.作为数据寄存器
2.可作为基址寄存器来使用,配合段寄存器寻址
RCX 数据寄存器(count) 1. 计数寄存器:在循环和字符串操作时,要用它来控制循环次数
2.位移寄存器:在位操作中,当移多位时,要用CL来指明移位的位数
RDX 数据寄存器(data) 1.在进行乘、除运算时,它可作为默认的操作数参与运算
2.用于存放I/O的端口地址
指针寄存器
RSP 栈指针寄存器(stack pointer) 栈指针寄存器
RBP 基数指针寄存器(base pointer) 基址指针寄存器
指令指针寄存器
RSI 变址寄存器(source index) 源变址寄存器
RDI 变址寄存器(destination index) 目标变址寄存器
RIP 指令指针寄存器

还新增了8个64位的寄存器:
R8 R9 R10 R11 R12 R13 R14 R15

1 指令执行

1.1 指令寻址

cs寄存器,ip寄存器

每次从 [cs,ip] 指向的内存位置取一条指令执行,执行后ip值增加,指向下一条指令的内存位置

CPU执行时,会一直的从内存单元中读取指令,坊到指令缓冲器执行。而取指令时,是根据CS寄存器和IP寄存器来获取一个地址[CS:IP]。这个地址指向内存单元中的代码段。CPU从中读取一条指令执行,然后IP指向下一条指令,继续读取执行。如此循环。

[CS+IP]:CS=段地址,IP=偏移地址。CPU一直从[CS+IP]指向的内存单元处读取指令。

1.2 访问数据
当CPU需要获取一个数据时,这时候代码段中能够获得的是DS寄存器和一个偏移地址。
[DS*0x10 + 偏移地址],指定地址时候可以不显示的写出DS。
这个地址指向内存单元中的数据。CPU根据这个地址获取到需要的数据。
基本地址:DS存储的是基本地址,但是在程序中段寄存器DS一般可以不显示说明。只用 [偏移地址] 来表示内存单元的地址。
偏移地址:是一个常数,可以直接使用数字来表示,比如:[553]。也可以使用BX,BP,SI,DI(其他的寄存器不能表示内存地址)中的值来表示。比如[BX]
[DS+BX]:DS=段地址,BX=偏移地址,内存单元地址为DS*0x10+BX。计算其寻址能力根据可指定的内存范围大概为0-1M之间。

2.3 指令跳转

当CPU执行完一块内存中的代码段时,使用JMP命令来修改CS寄存器和IP寄存器的值。从而控制计算机指令执行的跳转。

jmp:jmp指令转移就是修改cs,ip寄存器的指向。来完成跳转的功能。JMP导致[CS:IP]指向新的内存单元,CPU从新的内存单元读取指令。

汇编指令

以nasm为例

1. 赋值操作

  • mov

mov ax, 0x1

1)将值赋给寄存器:mov 寄存器,寄存器|内存单元|立即数

1
2
3
mov ax,0x0018H  		;ax = 0x0018H
mov ax,[0x0c200H] ;ax = [ds:0x0c200H]
mov ax,bx ;ax = bx

1)将值赋给内存单元:mov 内存单元,寄存器|内存单元|立即数

1
2
3
mov [0x0a200H],0x0018H   	;ax = 0x0018H
mov [0x0a200H],[0x0c200H] ;ax = [ds:0x0c200H]
mov [0x0a200H],bx ;ax = bx

2. 算术操作

下面是算术操作指令的简单列表:

指令 说明
add 整数加
sub 减法
mul 乘法(无符号)
IMUL 乘法(有符号)
DIV 除法(无符号)
IDIV 除法(有符号)
INC 自增
DEC 自减
NEG 取反
CMP 比较大小

1)加法: add 寄存器,寄存器|立即数

add ax,0x0017H 	;ax=ax+0x0017H
add ax,bx			;ax=ax+bx

2)减法: sub 寄存器,寄存器|立即数

sub ax,0x0017H ;ax=ax-0x0017H
sub ax,bx ;ax=ax-bx

3)乘法(无符号): mul 寄存器.

使用寄存器: AX存放目标操作数,操作后AX和DX存放结果。

    mov eax 0x0100H
    mul 寄存器

4) 除法: div 寄存器|内存单元(除数).

使用寄存器: AX和DX存放被除数,操作后AX和DX存放商和余数。
​ div 寄存器|内存单元

5)自增1:inc 寄存器

inc ax

6) 自减1:dec 寄存器

dec ax

7)取补码:nec

3. 转移操作

指令 说明
JE 如果相等则跳转
JNE 如果不相等则跳转
JZ 如果为 0 则跳转
JNZ 如果不为 0 则跳转
JC 如果进位 则跳转
JNC 如果不进位 则跳转
JG 如果第一个操作数比第二个大则跳转
JGE 如果第一个操作数比第二个大或者相等则跳转
JA 与 JG 指令相同,只不过比较的是无符号数则跳转
JAE 与 JGE 指令相同,只不过比较的是无符号数则跳转

4. 跳转指令

jmp指令

cs寄存器,ip寄存器

通过修改 [cs,ip] 内容,达到跳转效果。

  • jmp near 短转移 : [ ip <- new ip val]
  • jmp far 远转移 : [ cs<- new cs val, ip <- new ip val]
  • jmp word 段间转移,可从32位跳转64位指令

call指令

cs寄存器,ip寄存器,栈寄存器

1)压栈当前指令位置 [ss, sp] <- [cs, ip]

  1. 跳转到函数执行

3)返回,出栈当前指令位置 [cs, ip] <- [ss, sp]

4)继续执行指令

  • call
  • call far
  • call world

ret指令

cs寄存器,ip寄存器,栈寄存器

3)返回,出栈当前指令位置 [cs, ip] <- [ss, sp]

4)继续执行指令

loop指令

cx寄存器

判断 [cs] 寄存器值是否为0,为0停止循环,不为0继续循环。

每次执行 [cs内容]=cx-1

5. 栈操作

栈寄存器

首先CPU进行出栈入栈操作时,需要先分配一段栈空间的内存,来存储数据使用。

这段内存的位置由栈段寄存器SS和栈指针寄存器SP来指定。

1
栈顶地址 = 栈段寄存器SS << 4 + 栈指针寄存器SP

每次进行出栈入栈操作时,SS和SP会更新得到新的栈顶位置。

我们可以通过指定 栈段寄存器SS和栈指针寄存器SP的值来初始化栈空间分配。

栈顶地址: [ss:sp] (默认 [ss:0xf] )

栈空间 : [ss:0] -> [ss:0xf]

push指令

push:入栈,将寄存器的数据入栈

ss寄存器,sp寄存器

通过修改 [ss,sp] 内容,改变栈顶位置

[cs, sp] <- 栈顶位置

执行后, 栈值增加,指向新的地址

[cs,sp+2] <- 栈顶位置

pop指令

pop: 出栈,数据存储到寄存器中

ss寄存器,sp寄存器

通过修改 [ss,sp] 内容,改变栈顶位置

执行后, 栈值减少,指向上一个地址

[cs,sp-2] <- 栈顶位置

其他指令

CLI : Clear Interupt

STI :Set Interupt

CLD : Clear Director

STD :Set Director

REP:Repeat

MOVSB: Move String Byte

0%