初入操作系统

环境搭建

使用VSCODE连接虚拟机Ubuntu20.04,在VSCODE中编程,在Ubuntu中运行操作系统

安装ssh-server

在Ubuntu中安装ssh,通过vscode远程连接

安装

sudo apt install openssh-server

运行

sudo service ssh start

修改配置后重启

/etc/init.d/ssh restart

检查是否运行

sudo systemctl status ssh

VS CODE连接Ubuntu

下载相关插件,remote:

添加ssh,利用==ip a==命令重看ubuntu地址以进行连接

如果连接不成功需要修改ssh配置文件

利用VIM进行修改

vim /etc/ssh/sshd_config

修改内容为

#PermitRootLogin without-password
PermitRootLogin yes

编写代码

编写一个简单的例子让屏幕显示hello world!

org 7c00h; 将程序加载到07c00h处,即程序起始地址
mov ax, cs
mov ds, ax;初始化数据段ds
mov es, ax;初始化附加段寄存器

;调用函数,显示字符Hello World!
call DispStr
;死循环,不让操作系统结束
jmp $

DispStr:
  mov ax, BootMessage ;字符串首地址送入ax
  mov bp, ax;将字符串首址送入基址寄存器,字符串地址为es:bp
  mov cx, 16;设置字符串长度
  mov ax, 1301h;10h的13号中断,此时通过AH=13传入,AL=1,表示目标字符串仅仅包含字符,属性在BL中包含,移动光标
  mov bx, 000ch ;BH表示视频区页数
  mov dl, 0 ;DL表示在第几列显示(0为第一列)
  int 10h ;10H中断,int中断指令,10h中断类型
  ret
;等价于BootMessage db "Hello World!" 
BootMessage: db "Hello World!" 
; $$当前节(section)的开始地址,$-$$ 即表示本行距离程序开始处的相对地址
; times 重复汇编,即0填充直到程序有510字节,加上后面两个字节,即程序拥有512个字节
times 510 - ($ - $$) db 0
dw 0xaa55

电源打开时进行加电自检,然后寻找启动盘,如果选择软盘启动,那么计算机就会检查软盘的0面0磁道1扇区,如果发现它以0xAA55结束,则BIOS则会认为他是一个引导扇区。

除此之外,他还需要一段不少于512B的执行码

可以通过对bochs的设置以解除限制

创建镜像并在bochs中打开

在Ubuntu中安装所需软件

sudo apt-get install bochs vgabios bochs-x bximage

创建makefile自动化安装过程

# 将汇编代码进行编译
boot.bin: boot.asm 
    nasm -f bin boot.asm -o boot.bin
master.img: boot.bin
    # 创建虚拟软盘
    bximage -q -fd=1.44M master.img
    # 将操作系统写入软盘
    dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc

.PHONY: clean
clean:
    rm -rf *.bin
    rm -rf *.img

.PHONY:bochs
bochs: master.img
    bochs -q

bochs说明

执行上述代码后会在目录中生成一个bochsrc文件,这是bochs的配置文件

修改配置文件:

display_library: x, options="gui_debug"
boot: disk
#由bximage决定
ata0-master: type=disk, path="master.img", mode=flat

利用bochs启动操作系统

bochs -q

他会在本目录下查找bochsrc文件

bximage说明

-func=...     operation to perform (create, convert, resize, commit, info)
-fd=...       create: floppy image with size code
-hd=...       create/resize: hard disk image with size in megabytes (M)
              or gigabytes (G)
-imgmode=...  create/convert: hard disk image mode
-b            convert/resize: create a backup of the source image
              commit: create backups of the base image and redolog file
-q            quiet mode (don't prompt for user input)
--help        display this help and exit

相关概念

主引导记录

BIOS:Basic Input Output System

BIO程序会检查计算机硬件是否满足运行条件,这叫做“硬件自检”(Power-On Self-Test),即POST。

BIOS根据“启动顺序”去加载,当读。取到第一个扇区最后两个字节是55AA后,表明该设备可以启动,否则启动下一个设备。

主引导记录(Master boot record,缩写为MBR)【512B】,告诉计算机到哪一个位置寻找操作系统

分区可以安装多个操作系统,并决定将控制权交给哪个分区。

  • 代码【446B】

  • 硬盘分区表【64B = 4 * 16B】,即最多可以使用4个主分区,每个主分区大小16B

    • 第一个字节:如果是0x80,表示这是激活分区,只能有一个激活分区
    • 2~4个字节:主分区的第一个扇区的物理位置
    • 5个字节:主分区类型
    • 6~8个字节:主分区最后一个扇区的物理位置
    • 9~12个字节:主分区第一个扇区的逻辑地址
    • 13~16个字节:主分区的扇区总数,4个字节,即最大2^32^*512B = 2TB
  • 魔数【2B】:0xaa55 = 0x55, 0xaa

启动硬盘

卷引导记录

上面被激活的分区就叫做“卷引导记录”(Volume boot record,缩写为VBR),作用就是告诉计算机操作系统所在分区的位置

扩展分区与逻辑分区

随着硬盘越做越大,4个分区已经明显不够了,于是规定有且仅有一个主分区可以被定义成”扩展分区“,所谓的扩展分区就是里面可以有多个”逻辑分区“。计算机会读取扩展分区里面的第一个扇区,其叫做“扩展引导记录”(Extended boot record,缩写为EBR),里面是一张64字节的分区表,但最多只有两项,即最多只有两个逻辑分区。

然后又继续读第二个逻辑分区的第一个扇区,里面有第三个逻辑分区的位置,以此类推,但是很少以这种方式启动操作系统,如果操作系统确实安装在扩展分区,则一般使用启动管理器的方式启动。

启动管理器

在计算机读取“主引导记录”后,不再将控制权交给某一分区了,而是运行实现安装好的“启动管理器”(boot loader),目前最流行的是GRUB

操作系统

之后控制权转给操作系统后,内核就会被加载到内存。

以Linux系统为例,先载入/boot目录下面的kernel。内核加载成功后,第一个运行的程序是/sbin/init。它根据配置文件(Debian系统是/etc/initab)产生init进程。这是Linux启动后的第一个进程,pid进程编号为1,其他进程都是它的后代。

然后,init线程加载系统的各个模块,比如窗口程序和网络程序,直至执行/bin/login程序,跳出登录界面,等待用户输入用户名和密码。

相关寄存器

在8086CPU中寄存器16个,每个寄存器只能存放2B即16bit,而在80386中添加了许多寄存器,同时结构发生了些许变化,它们大部分都存放32bit数据,但仍有一些16bit

通用寄存器

可以存放数据,除此此外也都被叫做专用寄存器

数据寄存器
  • AX (Accumulator):累加寄存器,也称之为累加器;

    它被叫做累加器,是因为在进行除法和乘法时充当被乘数和被除数,如果在32位乘除法中,它表示数据的低16位,而DX则表示高16位。

    如果是8位除法,AL会存商,AH会表示余数

    如果是16位除法,AX存商,DX存余数

    如果是8位乘法,结果16位,存在AX中

    如果是16位乘法,结果32位,低位保存在AX,高位保存在DX

  • BX (Base):基地址寄存器;

    主要用来寻址,存放的是偏移地址

  • CX (Count):计数器寄存器;

    通常用来存放循环次数

  • DX (Data):数据寄存器;

都是16位寄存器,每个寄存器都可当作两个单独的8位寄存器

除上面之外,其他寄存器都不可这样用

指针寄存器
  • SP (Stack Pointer):堆栈指针寄存器;

  • BP (Base Pointer):基指针寄存器;

主要用于堆栈中,堆栈主要用于汇编的函数中。

BP函数的基址,SP函数栈顶地址

变址寄存器:

  • SI (Source Index):源变址寄存器;
  • DI (Destination Index):目的变址寄存器;

主要用于指示数据在存储单元在段内的偏移量

控制寄存器

  • IP (Instruction Pointer):指令指针寄存器;
  • FLAG:标志寄存器;

段寄存器

  • 代码段寄存器CS
  • 栈寄存器SS
  • 数据寄存器DS
  • 附加寄存器ES
  • 32位,80386中的寄存器
    • 标志段寄存器FS
    • 全局段寄存器GS

保护模式与实模式

实模式

在8086CPU中,它的地址线共有20根,这一位其访存范围是2^20^即1M,但是其内部寄存器都是16位,这意味着存放的地址范围是2^16^,即无法访问到所有空间。

于是它给出的方案是使用两个寄存器共同表示整个范围。一个寄存器(段寄存器)表示段地址,另一个寄存器(BX)表示偏移地址。段地址左移4位(即将段内容*16)就形成了基地址,而偏移地址16位,两者共同组成了真实的物理地址。所以一个段的大小是2^16^,起始地址也必须能够被其整除。

于是真实的物理地址就是:

==物理地址(physicaladdress)=段值(segment) * 16 + 偏移(offset)==

这种模式编程使用物理地址,所见即所得。

在汇编语言编程时,我们通常会使用各种段,它们的段地址会被存放在各个寄存器中

  • 数据段,存放在DS中
  • 代码段,存放栈CS
  • 栈段,存放栈SS
CS:IP

这两个寄存器指向了可执行文件的起始地址,执行的顺序执行就只需要改变IP中的数据。

如果是这样的话,那么我们仅仅改变这两个寄存器的值,就能执行其它代码了

SS:SP

这两个寄存器指向了栈的栈顶,在真实环境中,我们是通过内存来模拟栈的使用,对栈的访问就是对内存的访问,栈的PUSH于POP就是在对SP进行改变。

DS与ES

DS存放数据段,当我们觉得大小不够时,可以使用ES,可以当作其是一个扩张

需要注意的时,段的概念是由CPU体现的,而不是真正在内存中进行了段的分割。实际上由于使用的是真实物理地址,所以各个程序分配的内存是不定长。

在实模式下有些严重的缺陷就是程序安全性无法保证,且访存空间小,只能执行单任务,在现代计算机中它的存在只是为了引出保护模式。

实模式的内存布局

起始地址 结束地址 大小 用途
0x000 0x3FF 1KB 中断向量表
0x400 0x4FF 256B BIOS 数据区
0x500 0x7BFF 29.75 KB 可用区域
0x7C00 0x7DFF 512B MBR 加载区域
0x7E00 0x9FBFF 607.6KB 可用区域
0x9FC00 0x9FFFF 1KB 扩展 BIOS 数据区
0xA0000 0xAFFFF 64KB 用于彩色显示适配器
0xB0000 0xB7FFF 32KB 用于黑白显示适配器
0xB8000 0xBFFFF 32KB 用于文本显示适配器
0xC0000 0xC7FFF 32KB 显示适配器 BIOS
0xC8000 0xEFFFF 160KB 映射内存
0xF0000 0xFFFEF 64KB-16B 系统 BIOS
0xFFFF0 0xFFFFF 16B 系统 BIOS 入口地址

显卡在文本模式下的显示规则

对于一个字符通常有输入码、内码、字模码。其中字模码定义了一个字符在屏幕上显示的点阵坐标。在所有pc1上工作的显卡,在加电初始化后都会之中初始化到80*25的文本模式(25行,每行80个字符)。

从0xB8000这个地址开始,每2个字节表示屏幕上显示的一个字符。第一个字节就是内码ASCLL码,第二个字节控制字符颜色和属性的控制信息。

截屏2022-02-22 18.24.11

除了显存单元,还有显示控制单元,它们被编址到独立的I/O空间中,需要特殊的指令读取。这些寄存器有非常多个,工程师给出的解决方案是使用0x3D4来存寄存器索引,0x3D5来设置对应寄存器的值。

保护模式

在保护模式下,32条地址线全部有效,访存范围有4G,并且扩有分段管理和可选的分页管理,支持多任务,以及进程保护。

保护模式下的段与实模式下的段是完全不同的。

GDT与LDT的关系:

img

GDT

与实模式不同,段寄存器存放的不再是基地址,而是一个索引,索引指向的是一张表中的表项,而这张表就是==GDT全局描述表==也称段描述表,表项被称为==段描述符==,索引被称为==段选择子==。

在整个系统中,GDT只有一张(一个处理器对应一张),GDT可以被存放在任何位置,但CPU必须知道它的位置,于是在Intel设计中,提供了一个寄存器(GDTR)用于存放GDT的入口地址,我们可以==LGDT==指令将GDT的入口地址送入GDTR。

段描述符

上面是低32位,下面是高32位:

img

之所以感觉特别零散是为了兼容CPU80286

  • 段基址,32位

  • 段界限,20位,其单位是字节或4K字节

  • G,粒度单位,即段界限的单位,0是以字节位,1是以4KB为单位

  • S,描述符的类型,0是系统段,1是代码段或数据段

  • DPL,描述符的特权级,0为最高级,刚进入保护模式时执行的代码具有最高级0级,这是从处理器继承而来,这些通常是系统代码。不同级别的程序是相互隔离的,有些指令只能使用0级特权指令。

  • P,段存在位,描述对应段是否存在,但程序结束后,就应当将该位置0,如果访问时该位是0就会产生中断。

  • D/B,默认操作数大小/默认堆栈大小。主要作用是兼容16位保护模式的程序。0表示偏移地址与操作数是16位,1表示32位。

    • 对于代码段标志D,0使用16位的IP,1使用32位的EIP
    • 对于栈段标志B,使用的是SP,还是ESP
  • L,64位代码段标志,留给64位处理器,暂时设置为0

  • TYPE,用于指示描述符的子类型,或者说类别。

    img

有人会好奇代码段不可读,那处理器怎么取的指令,事实上这个描述符不针对处理器,它只针对其他程序。

根据Intel的要求,第一个段描述符的必须是全0

GDTR

48588_128261824694v4

段选择子

  • 描述符索引(index),13位,意味着有8K个段描述符
  • 表指示符(TI),1位,0代表GDT的选择子,LDT代表的是LDT的选择子
  • 请求特权级(RPL),2位

上面的各个关系就是:

==GDTR + 段选择子 * 8 = 段描述符==

段选择子*8是因为段描述符共64位8B,而该CPU寻址方式是字节寻址。

找到了段描述符就找到了段基址,这时候加上BX中的偏移地址就得到了真实地址

LDT

LDT局部描述表,每一个程序都有这样一个表。GDT通常描述的是系统段,而LDT描述的是程序段。LDT组成和GDT类似,两者的选择子也是类似的,只是TI位的不同罢了。LDT只是一个可选的数据结构,使用它很方便的同时也增加了程序的复杂性,如果想要内核保持简洁性并且具有良好的可移植性,那么最好不要使用它。

特权级与调用门

CPL、DPL和RPL

CPL是当前执行的程序或任务的特权级。它被存储在CS和SS的第0位和第1位上。它代表的是当前代码所在段的特权级别,其只有两个取值,0/3,分别代表用户态和内核态

DPL表示段或门的特权级。它被存储在段描述符或者门描述符的DPL字段中。DPL将会和CPL以及段或者门选择子的RPL相比较,根据段或者门类型的不同,DPL将会区别对待。

RPL是通过段选择子的第0和第1位表现出来的。RPL是代码中根据不同段跳转而确定,以动态刷新CS里的CPL,在代码段选择符中。RPL相当于附加的一个权限控制,只有当RPL>DPL的时候,才起到实际的限制作用。

一致代码段

简单来说就是操作系统拿出来共享的代码段。对于一致性代码来说,特权级高的程序不允许访问特权级低的数据:即是说核心态不允许调用用户态的数据;特权级低的程序可以访问到特权级高的数据.但是特权级不会改变:用户态还是用户态。

非一致代码段

被操作系统保护起来的一段代码,向不同特权级的非一致代码段转移都会引起保护异常,除非使用任务门或者调用门。

每当调用门用于把程序控制转移到一个更高级别的非一致性代码段时,CPU会自动切换到目的代码段特权级的堆栈去。每个任务只能定义最多4个栈,分别对应4个特权级。每个栈都位于不同的段中,并且使用段选择符和段中偏移值指定。

调用门

调用门用于在不同特权级之间实现受控的程序控制转移,通常仅用于使用特权级保护机制的操作系统中。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LGT中,但是不能安装在IDT(中断描述符表)中。它主要是定义了目标代码对应段的选择子、入口地址的偏移和一些属性等。结构跟代码段以及数据段描述符大不相同。结构如下图所示:

img

通过调用门访问代码段:

img

从实模式跳转到保护模式

请看下面的代码:

%include "pm.inc"

org 07c00h
jmp LABEL_BEGIN

; GDT定义                    base, limit, attr
[SECTION .gdt]
; 空描述符
LABEL_GDT:              Descriptor 0, 0, 0
; 代码段描述符
LABEL_DESC_CODE32:      Descriptor 0,SegCode32Len-1, DA_C+DA_32
; 指向显存的段描述符
LABEL_DESC_VIDEO:       Descriptor 0B8000h,0ffffh ,DA_DRW

;
GdtLen equ $-LABEL_GDT ;GDT长度

GdtPtr dw GdtLen-1 ;GDT界限
dd 0 ;GDT基址

;段选择子
SelectorCode32 equ LABEL_DESC_CODE32-LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT

[SECTION .s16]
[BITS 16] ;指名这是16位代码段
LABEL_BEGIN:
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov sp,0100h

    mov ax,0003h
    int 10h ;清屏

        ; 声明栈段和sp,初始化32位代码段描述符
    xor eax,eax
    mov ax,cs
    shl eax,4
    add eax,LABEL_SEG_CODE32
    mov word [LABEL_DESC_CODE32+2],ax
    shr eax,16
    mov byte [LABEL_DESC_CODE32+4],al
    mov byte [LABEL_DESC_CODE32+7],ah

    xor eax,eax
    mov ax,ds
    shl eax,4
    add eax,LABEL_GDT
    mov dword [GdtPtr+2],eax

        ; 加载GDT,将GDT基址调入GDTR
    lgdt [GdtPtr]

        ; 关中断
    cli

        ; 打开A20地址线
    in al,92h
    or al,00000010b
    out 92h,al

        ; 开启保护模式
    mov eax,cr0
    or eax,1
    mov cr0,eax

        ; 跳转到32位保护模式下打印p
        ; 由于jmp在16位模式下,要跳转到32位模式下的地址,所以注意该指令
    jmp dword SelectorCode32:0

; 这是32位代码段
[SECTION .s32]
[BITS 32]

; 打印p
LABEL_SEG_CODE32:
    mov ax,SelectorVideo 
    mov gs,ax
    mov edi,(80*11+5)*2
    mov ah,0ch
    mov al,'p'
    mov [gs:edi],ax
    jmp $

SegCode32Len equ $-LABEL_SEG_CODE32

使用Freedos引导

操作系统的引导程序可能会超过512B,这个时候有一种解决方法就是将部分引导放在引导程序,利用引导程序去加载完整的引导程序,对于我们来说这个时候来编写这个引导程序可能会有点复杂,我们可以使用Freedos提供的引导程序。

安装Freedos

在bochs官网中下载Freedos,解压后有4个文件,将a.img复制到自己的工作目录并更名为freedos.img。

修改bochsrc:

floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=master.img, status=inserted
boot: a

启动bochs,并在安装的Freedos中格式化B:盘

修改自己引导程序的汇编源码启动地址为0100h并进行重编译

nasm boot.asm -o boot.com

将结果复制到软盘上

sudo mount -o loop master.img /mnt/floppy/
sudo cp boot.com /mnt/floppy/
sudo umount /mnt/floppy/

在Freedos执行如下命令

dir B:
B:\boot.com

页式存储

graph LR
逻辑地址 --> 分段机制 --> 线性地址 --> 分页机制 --> 物理地址

使用的是两级页表结构,PDE(一级页表),PTE(二级页表):

img

PDE(页目录表)结构:

img

PTE(页表)结构:

img

  • P——bit0,存在位标志,表示当前条目所指向的页或页表是否在物理内存中;

  • R/W——bit1,读写位标志,指定一个页或者一组页的读写权限。R/W=0表示只读,R/W=1可读可写;

  • U/S——bit2,用户/超级用户标志,指定一个页或者一组页的特权级;

  • PWT——bit3,用于控制对单个页或者页表的Write-back或者Write-through缓冲策略;

  • PCD——bit4,用于控制对单个页或者页表的缓冲;  

  • A——bit5,表示页或者页表是否被访问;

  • D——bit6,表示页或者页表是否被写入;

    AD位共同决定页面的换入换出策略

  • PS——bit7,决定页的大小;

  • G——bit8,指示全局页;

  • AVL——保留字段;

可以通过cr0的PG位开启分页机制,最近被使用的页面都会被保存在TLB中,当cr3被加载时,所有的TLB都会自动失效,除非页或页表条目的G位被设置。

cr3

img

中断和异常

保护模式和实模式下的中断是有很大不同的。实模式下的中断向量表已经被IDT(中断描述表)替代。注意IDT第一个描述符不是NULL。

IDT描述符可以是以下三个:

  • 中断门描述符
  • 陷进门描述符
  • 任务门描述符(不常用)

截屏2022-02-22 13.13.03

异常类型

  • Fault(错误):可被更正的异常,一旦被更正程序将不失连续性的继续执行。当中断发生,处理器会保存中断之前的代码,异常处理程序返回的地址是发生中断的指令,而不是之后的指令。
  • Trap(陷阱):这种异常发生Trap的指令执行之后立即被报告,异常处理程序返回的是发生trap指令之后的那条指令
  • Abort(终止):无需精确的地址,不允许程序继续执行。

中断类型

  • 外部中断:硬件产生的中断
    • 不可屏蔽中断(NMI): CPU引脚NMI接收
    • 可屏蔽中断: CPU引脚INTR接收
  • 内部中断:指令int n产生的中断

8259A芯片

这里写图片描述

对于可屏蔽中断它是通过中断控制器8259A建立的,可以将其视作中断机制对所有外围设备的一个代理,这个代理不但可以根据优先级在多个中断同是发生时选择合适的中断,而且可以通过对寄存器的设置来屏蔽或打开相应中断。注意一般使用两个8259A进行串联,每个芯片有8个端口,于是中断可以有15个端口以供使用。由于主8259A的IRQ2被从8259A占用,为了保证兼容性,从8259A的的IRQ9会被软件重定向到主8259A上,这就保证了其兼容性。

一旦芯片进行了初始化就进入了操作状态,它可以响应外部设备设备产生的中断,修改命令字还可以修改中断处理方式,芯片选择优先级最高的中断请求作为中断服务对象,并通过INT引脚通知CPU中断的到来,D0~D7将当前的中断号送往CPU,CPU根据中断号获取中断向量值,并执行中断服务程序。CPU收到INT信号后会回送一个INTA信号。

这里写图片描述
  • IMR:保存中断屏蔽字

中断优先级

固定优先级

优先级固定,IR0最高,IR7最低

自动循环优先级

当先正在处理的优先级变为最低,其下一级自动升为最高。

  • 普通自动循环方式,初始最低由系统指定
  • 特殊自动循环方式,初始最低由用户指定(OCW2)

中断嵌套方式

  • 普通嵌套:屏蔽同级
  • 特殊完全嵌套:不屏蔽同级

在保护模式下,IRQ0~IRQ7对应的向量号是08h~0Fh,但是在保护模式下,这些向量号已经被占用,所以需要重新设置。IRQ8~IRQ15对应的向量号是28h~2Fh

对8259A的设置是通过对相应端口写入ICW(Initialization Command Word)来实现的。

主8259A对应端口地址是20h和21h,从8259A对应端口地址是A0h和A1h。

  • 往端口20h或A0h写入ICW1
  • 往端口21h或A1h写入ICW2
  • 往端口21h或A1h写入ICW3
  • 往端口21h或A1h写入ICW4

以上4步顺序不能颠倒

0~31号中断是CPU使用和保留的,用户可以使用的中断从32号开始,32对应的就是IQR0