从硬盘读取文件

伪指令 equ

1
app_lba_start equ 100

常数是用伪指令 equ 声明的,他的意思是“等于”。本句的意思是,用标号 app_lba_start 来代表数值 100。

和其他伪指令 db、dw、dd 不同,用 equ 声明的数值不占用任何汇编地址,也不再运行时占用任何内存位置。

MBR 分区

MBR 分区存储在第0面第0道(柱面)1扇区,只占512字节。每台计算机都有一个 BIOS 程序,从硬盘的0面0道1扇区来寻找MBR。

操作系统就是就是被这440字节拉起来的。

运行启动扇区外的文件

我们之前写的几段汇编程序,都是写在了这个BootLoader中。但毕竟 BootLoader 只有512字节,太小了,所以还是得将指令和数据写到更广阔的的空间去,BootLoader 找到它并将它加载到内存就可以了。

BootLoader 代码
Program 代码

回车与换行

ASCII 码表里,有128个字符,其中96个是可显示的。包括大小写字母、数字等等。32个控制字符不可显示,比如回车、换行、制表符等。

处理回车和换行,就是处理 0x0a,0x0d。

光标的当前位置,是通过读取显卡寄存器来实现。显卡的寄存器通过 0x3d4 把寄存器编号发下去,然后从 0x3d5 读取下发编号寄存器里的值。光标的位置由 0x0e(高8位)和 0x0f(低8位)两个寄存器里的值决定。高8位决定在第几行,低8位决定在第几列。

中断

内部软件中断和外部设备中断。

收到中断后,CPU对中断进行编号,从 0x00 一直到 0xff,总共256个号码。

0x00 - 0x07 是CPU预留的部分,用作异常或错误处理。

0x01 用于单步调试,0x03 用于打断点。我们用的 IDE 或者调试程序,就是通过它们实现的。

外部中断

0x08 - 0x0f 用于处理外部设备产生的中断。

8086 CPU提供了两个引脚,好让外部设备告诉 CPU 发生了什么事情。两个引脚相对于那么多外部设备,看起来确实不够用。

如同我们常用的 USB HUB 一样,这2个引脚中的1个,INTR 就接中断 HUB,通常使用 8259A 芯片。它可以拓展8个引脚,接8个设备,引脚名称分别是 IR0-7,对应 0X08 - 0X0F 中断号。而8个引脚可能也没办法覆盖全部设备,此时采取的策略是再接一个 8259A 芯片作为从片,这样就可以连接 15 个外部设备了,通常来说是够用的。统一编号为 IRQ0-15。

8259A 内部有一个8位的中断屏蔽寄存器,每一位对应一个引脚,引脚上过来的信号要想送到 CPU,至少不能被屏蔽。当某1位是0的时候,信号是不被屏蔽的。INTR 引脚上的中断,被称为可屏蔽中断。除去保留的 IRQ10-11 这两个引脚,INTR 供占用了 14 个中断号。

CPU 的 EFLAGS 寄存器有一个 IF位,它可以控制 INTR 上过来的信号:IF 是 1,则 INTR 来的中断不会被屏蔽;如果是 0 则 CPU 直接忽略。

另一个 NMI 引脚接收的信号是不可屏蔽的,8259A芯片管不着它,IF 位对它也不起作用。只有非常严重的事件,比如读取内存电路校验错误,或者 UPS 发来的停电通知等才会通过 NMI 引脚传达。这些事件 CPU 必须马上处理,不然后果很严重。NMI 引脚 占据了 0x02 中断号,没有进行细分。

INTR 和 NMI 这两个引脚从硬件层面区分了中断的紧急程度。NMI 的优先级比 INTR要高。INTR 上 IRQ 号码越小,优先级越高。

BIOS 中断

BIOS 保留的中断号,从 0x10 - 0x1A,用于提供显示、硬盘、键盘等输入输出。

自定义中断

256 个中断号除去上面的,还有 200 多个未被使用。这些属于自定义的部分。其中最著名的,当属 Linux 定义的 0x80 中断。

DOS 系统也提供了很多 DOS 中断。

中断向量表(IVT)

一共有 256 个中断号,会占据 256 * 4B 共 1KB 的空间。中断向量表在内存最开始的地方。

CPU 继续向前发展

80286 的保护模式。

80286 仍然是一个16位 CPU,段最大是64KB,段的大小占用了2个字节共16位;由于地址线总共有24条,因此段的起始地址占用24位。(目前,描述一个段用了40位,也就是5个字节)

在40位的基础上,又增加了一个字节(8位)来描述段的类型、是否可读写、拓展方式。这48位,6个字节,称为段描述符。

DPL:段的特权等级,从0b00 - 0b11共4级,00为最高级,11为最低级。

将很多个段描述符组织成一张表,叫描述符表(DT)

DT 并不是只有一张,操作系统有自己的 DT,程序也有自己的 DT。操作系统是为所有程序服务的,它里面的内容是全局的,叫全局描述符表(GDT);程序自己的局部描述符表叫 LDT。

GDT 和 LDT 会被保存到内存里,保存的位置我们可以决定。CPU 提供了两个寄存器,GDTR 和 LDTR。分别保存了 GDT 和 LDT 的起始地址。

可通过 lgdt 指令为 GDTR 赋值;lldt 指令为 LDTR 赋值。

由于程序有多个,每个程序有自己的 LDT,那么 LDT 就有多张。但是 LDTR 只有一个,每次只能存储一个 LDT 的起始地址,其他的怎么办?采取的办法就是,LDT 在 GDT 中注册(类似于二级索引???)。即,把每个 LDT 当成一个特殊的段,通过描述符来说明 LDT 的位置、大小、类型、特权级等信息。

段寄存器现在访问内存的方式:

RPL:请求特权等级。

有保护模式的情况下,访问段的方式:

  1. 给段寄存器一个16位的值,包含了段选择子、类型以及优先级(mov ax, 0x0008)

  2. 通过段地址:偏移地址的方式访问某个内存地址

  3. CPU 通过 TI 位的值,来确定去 GDT 还是 LDT 查询具体的段描述符

  4. 通过段寄存器的高13位,来获得段选择子的值,确定要访问的段

  5. 根据段选择子,找到段描述符。当 RPL 的值小于 DPL 的值,即允许访问。

  6. 取出24位的段地址。这里的段地址不需要左移,直接段地址:偏移地址访问内存即可。

80386 的保护模式

80386 的段描述符在 80286 的基础上修改如下

此时,一个段最大可以为 1MB(当 G 为 1的时候)

有些杂乱,这是由于历史原因造成的。

80386访问内存的方式

80386 的通用寄存器 eax/ebx/ecx/edx,以及ebp/esp/esi/edi,标志寄存器 Eflags 以及指令寄存器 EIP 都拓展到了32位,但是包括 cs/ds/ss/gs/fs 在内的段寄存器仍然是16位。

8086 使用的段地址:偏移地址的内存访问方式在 80386 这里有些鸡肋了。如果我们使用 esi 或 edi 作为索引,可以找到 4GB 的每一个字节的,就不需要段了。这种内存管理模式称为平坦模式(使用的是绝对地址),把段寄存器全部设置为0。

我们在分页内存管理前,也采用平坦模式。