内核实战之VGA你好世界

实战演练实模式启动,加入多系统启动支持,并转入保护模式

前往git拉取代码learnNasm

CPU运行架构

所谓指令集,就是机器码的集合,规定了机器码所能干的事情,使用打孔机/晶体管执行.指令集是具体的一套编码方式,微架构是指令集的物理实现方式

汇编代码相当于机器码的助记符,需要使用汇编编译器编译成机器码

c语言则需要使用编译器编译为汇编代码/机器代码

java/php语言则需要使用相应解释器运行在一个虚拟机里面

所谓云原生,则是不需要虚拟机,直接解释成机器码,用来提高性能

CISC复杂指令集

CISC包括一个丰富的微指令集,这些微指令简化了在处理器上运行的程序的创建。指令由汇编语言所组成,把一些原来由软件实现的常用的功能改用硬件的指令系统实现,编程者的工作因而减少许多,在每个指令期同时处理一些低阶的操作或运算,以提高计算机的执行速度,这种系统就被称为复杂指令系统。

在CISC指令集的各种指令中,其使用频率却相差悬殊,大约有20%的指令会被反复使用,占整个程序代码的80%。而余下的80%的指令却不经常使用,在程序设计中只占20%。

广泛应用于PC领域,intel的x86系列产品被自己命名为IA32指令集

amd的x64也是intel的x86授权的,但是硬件是它自己实现的,被称为x86_64或者amd64,也叫做Intel 64

Intel在之前已在Itanium处理器上使用了自家的64位IA-64技术,虽然说Intel 64也是64位,但两者并不兼容,即IA-64的软件不能直接在Intel 64上运行。Intel 64所用的x86-64是IA-32指令集的延伸,而IA-64则是另一款独立的架构,没有任何IA-32的影子。虽然IA-64可通过模拟来运行IA-32的指令,但指令在运行前需经转换,才能在IA-64上运行,导致其速度变慢。由于x86-64是从IA-32派生而来,因此运行IA-32与64位程序的表现也显得绰绰有余。

RISC精简指令集

RISC(精简指令集计算机) 设计方案,如它的名字所蕴涵的那样,有一个简化的指令集,该指令集提高处理器的效率但是需要有更复杂的外部程序。RISC结构优先选取使用频最高的简单指令,避免复杂指令;将指令长度固定,指令格式和寻地方式种类减少;以控制逻辑为主,不用或少用微码控制等措施来提高运算速度。

RISC设计方案是根据John Cocke在IBM所做的工作形成的。John Cocke发现大约20%的计算机指令完成大约80%的工作。因此,基于RISC的系统通常比CISC系统速度快。它的80/20规则促进了RISC体系结构的发展。

MIPS,ARM,Power,C6000都采用精简指令集

CPU运行模式

在16位CPU系统中,它只有4个段寄存器,所以,程序在任何时刻至多有4个正在使用的段可直接访问.传统的16位处理器启动时就是在实模式,也就是纯裸的,没有任何支持。

实模式: 前4个段寄存器CS、DS、ES和SS与CPU中的所对应的段寄存器的含义完全一致,内存单元的逻辑地址仍为”段值:偏移量”的形式。为访问某内存段内的数据,必须使用该段寄存器和存储单元的偏移量。

在32位微机系统中,它有6个段寄存器,所以,在此环境下开发的程序最多可同时访问6个段。32位CPU有两个不同的工作方式:实方式和保护方式。

32位处理器为了兼容16处理器,把开机时的实模式也兼容了,所以即使是16位的操作系统,放到32位处理器上,仍然能运行。

所以开机的时候CPU就是实模式,然后32位操作系统又将实模式切换成保护模式。如果发现是16位的操作系统,就直接运行在实模式。

模式进化

在8086/8088时代,处理器只存在一种操作模式(Operation Mode),当时由于不存在其它操作模式,因此这种模式也没有被命名。自从80286到80386开始,处理器增加了另外两种操作模式——保护模式和系统管理模式SMM(System Management Mode),因此,8086/8088的模式被命名为实地址模式RM(Real-address Mode)。

前世今生

实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器。所以为了能够通过这些16位的寄存器去构成20位的主存地址,必须采取一种特殊的方式。

随着CPU的发展,CPU的地址线的个数也从原来的20根变为现在的24/32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位。所以实模式下的内存地址计算方式就已经不再适合了。所以就引入了现在的保护模式,实现更大空间的,更灵活也更安全的内存访问。

CPU复位(reset)或加电(power on)的时候以实模式启动,处理器以实模式工作。在实模式下,内存寻址方式和8086相同,32位的x86 CPU用做高速的8086。在实模式下,所有的段都是可以读、写和可执行的。实模式的"实"更多地体现在其地址是真实的物理地址

保护模式是处理器的本机模式,在这种模式下,处理器支持所有的指令和所有的体系结构特性,提供最高的性能和兼容性。对于所有的新型应用程序和操作系统来说,建议都使用这种模式。为了保证PM的兼容性,处理器允许在受保护的,多任务的环境下执行RM程序。这个特性被称做虚拟8086模式(Virtual -8086 Mode),尽管它并不是一个真正的处理器模式。Virtual-8086模式实际上是一个PM的属性,任何任务都可以使用它。

实模式寻址方式

8086CPU数据总线为16位,也就是一次最多能取216=64KB数据,这个数据也解释了实模式下为什么每个段最大只有64KB。但刚才还说了其地址总线为20位,这样它能寻址的能力其实是220=1MB,这也就是实模式下CPU的最大寻址能力。既然它有1MB寻址能力,那怎么用16位的段寄存器表示呢?

这就引出了分段的概念,8086CPU将1MB存储空间分成许多逻辑段,每个段最大限长为64KB(但不一定就是64KB)。这样每个存储单元就可以用“段基地址+段内偏移地址”表示。段基地址由16位段寄存器值左移4位表达,段内偏移表示相对于某个段起始位置的偏移量

20221015185312

实模式下对应的寄存器一般都有固定的使命,对于寻址来说,基本只能使用上面对应的寄存器。

保护模式寻址方式

在保护模式下,可以用各种通用寄存器(除了esp不能当作变址寄存器),并且偏移量也变成了32位

20221015190406

保护模式下的寻址方式明显比实模式下灵活了许多。注意一下,只能使用这里面出现过的寄存器(比如EAX包括BL,但是BL无论在实模式下还是在保护模式下都没有出现,则自然不能用来进行寻址)

依照设计的规格,所有的x86 CPU都是在实模式下开机,来确保传统操作系统的向前兼容性。在任何保护模式的特性可用前,他们必须要由某些程序手动地切换到保护模式。

在现今的计算机,这种切换通常是由操作系统在开机时候必须完成的第一件任务的一个。它也可能当CPU在保护模式下运行时,使用虚拟86模式来运行设计运行在实模式下的代码。

切换保护模式

现代操作系统都是运行在保护模式下(Intel x86系列CPU)。计算机启动时,默认的工作模式是实模式,为了让内核能运行在保护模式下,Bootloader需要从实模式切换到保护模式,切换步骤如下:

  • 准备好GDT(Global Descriptor Table)
  • 关中断
  • 加载GDT到GDTR寄存器
  • 开启A20,让CPU寻址大于1M
  • 开启CPU的保护模式,即把cr0寄存器第一个bit置1
  • 跳转到保护模式代码

20221017091059

GDTR是一个6字节的寄存器,有4字节表示GDT表的基地址,2字节表示GDT表的大小,即最大65536(实际值是65535,16位最大值是65535),每个表项8字节,那么GDT表最多可以有8192项。

实模式的寻址总线是20bits,为了让寻址超过1M,需要开启A20,可以通过以下指令开启

in al, 0x92
or al, 2
out 0x92, al

把上述步骤完成之后,我们就进入保护模式了。在保护模式下我们要使用GDT通过GDT Selector完成,它是GDT表项相对于起始地址的偏移

实模式上代码

boot

还是这个表,我们找到两个可用区域,可以把setup加载到这里

起始 结束 大小 用途
FFFF0 FFFFF 16B BIOS 入口地址,此地址也属于BIOS 代码,同样属于顶部的640KB 字节。只是为了
强调其入口地址才单独贴出来。此处16 字节的内容是跳转指令jmp f000:e05b
F0000 FFFEF 64KB-16B 系统 BIOS 范围是 F0000~FFFFF 共 64KB,为说明入口地址,将最上面的 16字节从此处去掉了,所以此处终止地址是 0XFFFEF
C8000 EFFFF 160KB 映射硬件适配器的 ROM 或内存映射式 I/O
C0000 C7FFF 32KB 显示适配器 BIOS
B8000 BFFFF 32KB 用于文本模式显示适配器
B0000 B7FFF 32KB 用于黑白显示适配器
A0000 AFFFF 64KB 用于彩色显示适配器
9FC00 9FFFF 1KB EBDA(Extended BIOS Data Area)扩展 BIOS 数据区
7E00 9FBFF 622080B 约 608KB 可用区域
7C00 7DFF 512B MBR 被 BIOS 加载到此处,共 512 字节
500 7BFF 30464B 约 30KB 可用区域
400 4FF 256B BIOS Data Area(BIOS 数据区)
000 3FF 1KB Interrupt Vector Table(中断向量表)

这里使用了16H中断读取键盘,让用户选择多系统启动

就是软盘和硬盘的第二扇区了

;[ORG  0x7c00]
section maintext vstart=0x7c00
BOOT_MAIN_ADDR equ 0x7E00
;extern print
[BITS 16]
global boot_start
boot_start:
; 设置屏幕模式为文本模式,清除屏幕
mov ax, 3
int 0x10

mov si, strHello
call print

mov si, strSelect
call print

mov ah,0x10
int 0x16
cmp al,'0'

jz .readf
call readDisk
mov si, selhd
jmp .end

.readf
call readFlopy
mov si, selfp

.end
call print
jmp BOOT_MAIN_ADDR

; 如何调用
; call readFlopy ; 2 调用
readDisk:
pusha
mov ecx, 2 ; 从硬盘哪个扇区开始读
mov bl, 1 ; 读取的扇区数量

; 0x1f2 8bit 指定读取或写入的扇区数
mov dx, 0x1f2
mov al, bl
out dx, al

; 0x1f3 8bit iba地址的第八位 0-7
inc dx
mov al, cl
out dx, al

; 0x1f4 8bit iba地址的中八位 8-15
inc dx
mov al, ch ; 取中8位
out dx, al

; 0x1f5 8bit iba地址的高八位 16-23
inc dx
shr ecx, 16
mov al, cl
out dx, al

; 0x1f6 8bit
; 0-3 位iba地址的24-27
; 4 0表示主盘 1表示从盘
; 5、7位固定为1
; 6 0表示CHS模式,1表示LAB模式
inc dx
mov al, ch
and al, 0b11101111
out dx, al

; 0x1f7 8bit 命令或状态端口
inc dx
mov al, 0x20
out dx, al

; 验证状态
; 3 0表示硬盘未准备好与主机交换数据 1表示准备好了
; 7 0表示硬盘不忙 1表示硬盘忙
; 0 0表示前一条指令正常执行 1表示执行出错 出错信息通过0x1f1端口获得
.read_check:
mov dx, 0x1f7
in al, dx
and al, 0b10001000 ; 取硬盘状态的第3、7位
cmp al, 0b00001000 ; 硬盘数据准备好了且不忙了
jnz .read_check

; 读数据
mov dx, 0x1f0
mov cx, 256
mov edi, BOOT_MAIN_ADDR
.read_data:
in ax, dx
mov [edi], ax
add edi, 2
loop .read_data

popa
ret

; 如何调用
; call readFlopy ; 2 调用
readFlopy:
pusha
mov dh, 0 ; 0 磁头
mov dl, 0 ; 驱动器编号

mov ch, 0 ; 0 柱面
mov cl, 2 ; 2 扇区

mov bx, BOOT_MAIN_ADDR ; 数据往哪读

mov ah, 0x02 ; 读盘操作
mov al, 1 ; 连续读几个扇区

int 0x13

cmp ah,0
jz .doneDisl
mov si, error_msg
call print
jmp $

.doneDisl:
popa
ret

; 如何调用
; mov si, msg ; 1 传入字符串
; call print ; 2 调用
print:
pusha
mov ah, 0x0e
mov bh, 0
mov bl, 0b01000011 ;红色/青色
.loop:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .loop
.done:
popa
ret

error_msg db "read error!", 10, 13, 0

strHello db "Hello,Boot!", 10, 13, 0

strSelect db "floppy:0", 10, 13, "hard:1",10, 13,0

selfp db "selct floppy!", 10, 13,0

selhd db "selct hard disk!", 10, 13,0

times 510 - ($ - $$) db 0
db 0x55, 0xaa

硬盘setup

;[ORG  0x7c00]
section maintext vstart=0x7E00
BOOT_MAIN_ADDR equ 0x7E00
;extern print
[BITS 16]
global setup_start
setup_start:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov si, ax

mov si, strHello
call print

jmp $

; 如何调用
; mov si, msg ; 1 传入字符串
; call print ; 2 调用
print:
push bx
mov ah, 0x0e
mov bh, 0
mov bl, 0b01000011 ;红色/青色
.loop:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .loop
.done:
pop bx
ret

strHello db "Hello,Setup hard!", 10, 13, 0
times 510 - ($ - $$) db 0
db 0x55, 0xaa

20221024000120

软盘setup

;[ORG  0x7c00]
section maintext vstart=0x7E00
BOOT_MAIN_ADDR equ 0x7E00
;extern print
[BITS 16]
global setup_start
setup_start:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov si, ax

mov si, strHello
call print

jmp $

; 如何调用
; mov si, msg ; 1 传入字符串
; call print ; 2 调用
print:
push bx
mov ah, 0x0e
mov bh, 0
mov bl, 0b01000011 ;红色/青色
.loop:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .loop
.done:
pop bx
ret

strHello db "Hello,Setup floopy!", 10, 13, 0
times 510 - ($ - $$) db 0
db 0x55, 0xaa

实模式访问32位寄存器

当32位CPU以16位的实模式运行时,并不是变成纯粹的16位的CPU,其可以看作更为强大的16位的CPU,但其本质仍然是32位,因此仍然具备处理32位操作数的能力。

  • 在16位实模式下,默认访问数据的大小是8位或者16位的;控制转移和内存访问时,偏移量也是16位的;
  • 处理器在16位实模式下运行时,可以使用32位的寄存器,执行32位运算;
  • 使用伪指令[bits 16][bits 32]设置指令前缀0x66和0x67
  • 指令前缀0x66用来选择非默认值的操作数大小,0x67用来选择非默认值的地址大小

汇编代码执行nasm -f elf32 test.asm生成二进制文件,再使用objdump -s -d查看机器码

test0:
inc ax ;66 40
inc eax ;40

[BITS 16]
test1:
inc ax ;40
inc eax ;66 40

[BITS 32]
test2:
inc ax ;66 40
inc eax ;40

由机器码可知,32位cpu操作16位时,默认会添加前缀66,这是保护模式下需要的.保护模式默认访问32位寄存器

当指定[BITS 16]伪指令后,则结果相反,这正是实模式下需要的,实模式默认访问16位寄存器

也就是说同一个机器码40,保护模式下访问的是eax,实模式下访问的是ax;

所以,要想正确地执行指令,在启动阶段的实模式里,我们的汇编代码需要添加[BITS 16];进入保护模式后,我们的代码需要添加[BITS 32]

保护模式上代码

大家在github上自己看吧....

static int s_nihaoshijie[]={
//你
27,64,10,100
,20,83,20,135
,46,64,33,89
,40,77,78,77
,78,77,73,90
,55,87,55,132
,55,132,40,132
,42,89,32,121
,69,99,78,121
//好
,99,64,91,105
,91,105,113,123
,87,80,112,80
,112,80,89,132
,119,69,155,69
,155,69,139,84
,139,84,138,132
,138,132,123,132
,117,98,159,98
//世
,166,84,237,84
,198,64,198,110
,224,64,224,110
,198,110,224,110
,177,67,177,131
,177,131,234,131
//界
,253,67,253,96
,253,67,308,67
,308,67,308,96
,253,82,308,82
,281,67,281,96
,253,96,308,96
,277,96,246,116
,283,96,315,114
,267,112,251,134
,294,112,294,134
};

void kernel_main(void) {

init_palette(); /* 设定调色板 */

int size = sizeof(s_nihaoshijie)/sizeof(uint)/4;

while(1)
{
for(int i=0;i<size;i++)
{
draw_line_radom(
s_nihaoshijie[i*4+0],s_nihaoshijie[i*4+1]
,s_nihaoshijie[i*4+2],s_nihaoshijie[i*4+3]
,5);
}
}
}