以前每次开机,先是看到一个黑屏幕上显示一些字符,然后会进入windows经典的启动界面。对于要编写操作系统来说,这种表面的认识是不够的,得需要知道一下通电后,执行的入口是哪里,启动的流程是如何跑起来的。

先说结论:通电后,CPU的cs:ip寄存器被强制初始化为0xF000:0xFFF0。

接下来,为了理解CPU的cs:ip寄存器被初始化后的效果,需要学习一些准备知识:

  • 什么是分段访问
  • 什么是cs:ip寄存器
  • 什么是实模式
  • 实模式下的内存布局是如何的
  • 0xF000:0xFFF0地址有什么特别意义
  • cs:ip寄存器初始化为0xF000:0xFFF0后续操作是什么

什么是分段访问

8086的地址线有20位,也就是说8086可以寻址的范围是00000-FFFFF,1MB的内存。但是8086的寄存只有16位,直接使用一个寄存器只能寻址64KB的内存。

为了能访问到1MB的内存,8086引入了段的概念,把内存区域划分为一个一个的段:

用一个寄存器,存放段的起始位置,称为段基址,另外一个寄存器存放偏移量,称为段内偏移。起始位置+偏移量=最终的地址。这里需要注意的是,起始位置必须是20位的,否则两个16位的数字相加,区间为FFFF+FFFF=1FFFE,也是无法寻址20位地址的空间的。8086想的方法是存放段基址的寄存器左移4位实现20位,然后再加上偏移量。

所以最终物理地址的计算公式为:段基址*16+段内偏移

什么是cs:ip寄存器

大家知道CPU可以执行机器码,这些机器码包含了操作码和操作数或操作寄存器。

机器码是我们的程序编译后生成的,保存在持久化设备中,比如软盘,硬盘。但是CPU不会直接与硬盘交互,只会与内存交互,所以有一个过程,会把程序加载到内存中,然后CPU来读取,来执行。问题是CPU如何知道自己从内存的哪个位置读取机器码呢?答案就是cs:ip寄存器。

cs是代码段寄存器,存放代码段段基址,ip是指令寄存器,存放段内偏移。cs:ip永远保存下一条要被执行的机器码的地址。

只要给CPU一个cs:ip后,CPU取出机器码执行,同时会对ip加上机器码的长度,执行完当前机器码后,会根据cs:ip再次取出机器码,周而复始,直到关机断电。

我们无法直接操作ip寄存器,但是可以通过jmp等跳转相关指令修改cs:ip寄存器的值,实现分支等逻辑。

什么是实模式

8086中只有一种模式,80286中出现了保护模式,同时把8086的模式称为实模式。

实模式的特点是:

  • 实模式使用20位地址线,可以寻址1MB内存。
  • 实模式下寻址的地址如果超过20位,则会卷回到低地址,也就是取模的效果。
  • 在实模式下,所有的段都是可以读、写和执行的。

8086以及之后的CPU启动后进入的都是实模式。操作系统需要在实模式下引导系统启动。所以即使实模式是历史久远的落后模式,我们依然要学习如何在实模式下编程。

实模式下的内存布局是如何的

实模式下可以寻址1MB的内存,不过这个内存不是完全对应插在主机内存插槽上的内存。这个物理的内存称为DRAM。这1MB的区域只有一部分是对应到DRAM上的,其他的地址对应到BIOS的ROM,内卡的ROM。具体见下表:

起始 结束 大小 用途
FFFF0 FFFFF 16B BIOS入口地址,此地址也属于BIOS代码,同样属于顶部的640KB字节。只是为了强调其入口地址才单独贴出来。此处16字节的内容是跳转指令jmp f000:e05b
F0000 FFFEF 64KB-16B 系统BIOS范围是F0000~FFFFF共640KB,为说明入口地址,将最上面的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(中断向量表)

其中0~0x9FFFF对应到DRAM。

BIOS中也有只读存储器ROM,保存BIOS的程序,F0000~FFFFF这64KB空间对应的是BIOS ROM。

0xF000:0xFFF0地址有什么特别意义

前文提到,CPU在加电后,会强制初始化cs:ip寄存器为0xF000:0xFFF0,按分段寻址的公式计算:0xF000*16+0xFFF0=0xFFFF0。

这个地址对应的是什么数据呢?我们看下上面的实模式1MB内存布局的表格,可以看到F0000~FFFFF是BIOS程序的地址,而0xFFFF0则是指向最后16字节的地址。

也就是说,CPU启动后,会强制初始化cs:ip寄存器为0xF000:0xFFF0,是为了取到这个地址的执行去执行,而这个地址是BIOS程序的一个部分,也就是说,CPU启动后,就开始执行BIOS的自带的程序。

所以可以推论,CPU启动后,就开始执行BIOS程序,这是电脑设计者的目的,同时他们规定了1MB的最后16字节为BIOS程序的入口,基于这种规定,CPU才会做出强制初始化cs:ip寄存器为0xF000:0xFFF0的行为。

那这个0xFFFF0地址对应的指令具体是啥呢?

Bochs学习-安装配置篇有提到bochs启动后会自动暂停,那个时候就是CPU一条指令还未执行的时候,我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
➜  /Users/mazhibin/project/system/bochs-conf-file > bochs -q
========================================================================
Bochs x86 Emulator 2.6
Built from SVN snapshot on September 2nd, 2012
Compiled on May 5 2018 at 23:07:30
========================================================================
00000000000i[ ] reading configuration from bochsrc.txt
00000000000i[ ] installing sdl module as the Bochs GUI
00000000000i[ ] using log file bochsout.txt
Next at t=0
(0) [0x00000000fffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b ; ea5be000f0
<bochs:1>

可以看到,bochs显示了CPU接下来要执行的指令,也就是cs:ip寄存器指向的内存区域中存放的指令,地址是f000:fff0,这也印证了CPU在启动时,cs:ip的值会被设置为0xF000:0xFFF0。指令内容是jmp far f000:e05b,一个跳转指令,跳到了BIOS程序的入口,接下来BIOS程序开始检测内存,显卡的外设信息,初始化硬件,并在0x000~0x3FF内存区域中建立中断向量表。

BIOS程序执行完后,就会开始从可能的启动设备中启动操作系统,这个后面来继续学习。

总结

  • x86CPU启动后进入实模式
  • 实模式下使用20位的地址线,可以寻址1MB内存
  • 实模式下使用分段的方式访问1MB的空间,方法是两个寄存器,一个保存段基址,一个保存段内偏移,然后通过公式:段基址*16+段内偏移,得到20位的地址
  • CPU通电后,cs:ip寄存器会被强制更新为0xF000:0xFFF0,这个地址指向BOIS程序的入口
  • 所以CPU通电后,就会开始执行BOIS的程序,检测并初始化硬件,最后BIOS程序尝试从可能的启动设备中启动操作系统。

参考资料