计算机加电后发生了什么?代码 & 理论分析

本文最后更新于:2022年9月22日 晚上

计算机刚被加电的时候,RAM中空空如也,OS存储在磁盘中,CPU只能执行被加载到内存中的代码,那么操作系统的代码是如何被CPU执行的呢?

1.BIOS阶段

首先,CPU被设计为加电瞬间CS:IP指向0xF000:0xFFF0,也即是0xFFFF0的位置。此时是在实模式下,这个地址即是实际物理地址,ROM BIOS被设计为存储在这个位置,它的代码入口地址即是0xFFFF0。BIOS的代码地址范围:0xFE000~0xFFFFF,这些代码的作用是:

  • 进行硬件自检,检测CPU、显卡、内存、硬盘的信息
  • 在内存最开始的区域构建中断向量表(0x00000~0x003FF,1KB内存空间)
  • 构建BIOS数据区(0x00400~0x004FF)
  • 在上述数据区后面约57KB的位置加载8KB左右的、与中断向量表对应的中断处理程序

2.引导扇区(bootsect.s)

bootsect也被称为MBR(master boot record)

CPU收到int 0x19中断,在中断向量表中找到对应的中断服务程序的入口地址,并执行该中断服务程序对应的功能:将第一扇区(512个字节)的程序,也即是bootsect加载到内存中0x07C00地址处并跳转到此处

接下来,bootsect本身512个字节会被从0x7C00复制到内存的0x90000处

1
2
3
4
5
6
7
8
mov	ax,BYTE PTR BOOTSEG		;// 将ds段寄存器置为7C0h
mov ds,ax
mov ax,BYTE PTR INITSEG ;// 将es段寄存器置为9000h
mov es,ax
mov cx,256 ;// 移动计数值 = 256字 = 512 字节
sub si,si ;// 源地址 ds:si = 07C0h:0000h
sub di,di ;// 目的地址 es:di = 9000h:0000h
rep movsw ;// 重复执行,直到cx = 0;移动1个字

之后,bootsect触发0x13中断将setup.s程序加载到内存的0x90200处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
load_setup:
;// 以下10行的用途是利用BIOS中断INT 13h将setup模块从磁盘第2个扇区
;// 开始读到90200h开始处,共读4个扇区。如果读出错,则复位驱动器,并
;// 重试,没有退路。
;// INT 13h 的使用方法如下:
;// ah = 02h - 读磁盘扇区到内存;al = 需要读出的扇区数量;
;// ch = 磁道(柱面)号的低8位; cl = 开始扇区(05位),磁道号高2位(67);
;// dh = 磁头号; dl = 驱动器号(如果是硬盘则要置为7);
;// es:bx ->指向数据缓冲区; 如果出错则CF标志置位。
mov dx,0000h ;// drive 0, head 0
mov cx,0002h ;// sector 2, track 0
mov bx,0200h ;// address = 512, in INITSEG
mov ax,0200h+SETUPLEN ;// service 2, nr of sectors
int 13h ;// read it
jnc ok_load_setup ;// ok - continue
mov dx,0000h
mov ax,0000h ;// reset the diskette
int 13h
jmp load_setup

再往后,再次触发0x13中断将系统模块装载入内存0x10000地址处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ok_load_setup:
;/* 取磁盘驱动器的参数,特别是每道的扇区数量。
; 取磁盘驱动器参数INT 13h调用格式和返回信息如下:
; ah = 08h dl = 驱动器号(如果是硬盘则要置位7为1)。
; 返回信息:
; 如果出错则CF置位,并且ah = 状态码。
; ah = 0, al = 0, bl = 驱动器类型(AT/PS2)
; ch = 最大磁道号的低8位,cl = 每磁道最大扇区数(位0-5),最大磁道号高2位(位6-7)
; dh = 最大磁头数, 电力= 驱动器数量,
; es:di -> 软驱磁盘参数表。 */
mov dl,00h
mov ax,0800h ;// AH=8 is get drive parameters
int 13h
mov ch,00h
;// seg cs ;// 表示下一条语句的操作数在cs段寄存器所指的段中。
mov cs:sectors,cx ;// 保存每磁道扇区数。
mov ax,INITSEG
mov es,ax ;// 因为上面取磁盘参数中断改掉了es的值,这里重新改回。

由于这部分代码比较长,因此时间也比较久,屏幕上会打印出“loading system”进行提醒

1
2
3
4
5
6
7
8
9
10
11
;// Print some inane message   在显示一些信息('Loading system ... '回车换行,共24个字符)。

mov ah,03h ;// read cursor pos
xor bh,bh ;// 读光标位置。
int 10h

mov cx,27 ;// 共24个字符。
mov bx,0007h ;// page 0, attribute 7 (normal)
mov bp,offset msg1 ;// 指向要显示的字符串。
mov ax,1301h ;// write string, move cursor
int 10h ;// 写字符串并移动光标。

最后,确认根文件系统的设备号,并跳转到setup函数的0x90200地址处

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
;// 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0
;// 就直接使用给定的设备。否则就需要根据BIOS报告的每磁道扇区数来
;// 确定到底使用/dev/PS0(2,28)还是/dev/at0(2,8)。
;// 上面一行中两个设备文件的含义:
;// 在Linux中软驱的主设备号是2(参加第43行注释),次设备号 = type*4 + nr, 其中
;// nr为03分别对应软驱A、B、C或D;type是软驱的类型(2->1.2M或7->1.44M等)。
;// 因为7*4 + 0 = 28,所以/dev/PS0(2,28)指的是1.44M A驱动器,其设备号是021c
;// 同理 /dev/at0(2,8)指的是1.2M A驱动器,其设备号是0208

;// seg cs
mov ax,cs:root_dev
cmp ax,0
jne root_defined ;// 如果 ax != 0, 转到root_defined
;// seg cs
mov bx,cs:sectors ;// 取上面保存的每磁道扇区数。如果sectors=15
;// 则说明是1.2Mb的驱动器;如果sectors=18,则说明是
;// 1.44Mb软驱。因为是可引导的驱动器,所以肯定是A驱。
mov ax,0208h ;// /dev/ps0 - 1.2Mb
cmp bx,15 ;// 判断每磁道扇区数是否=15
je root_defined ;// 如果等于,则ax中就是引导驱动器的设备号。
mov ax,021ch ;// /dev/PS0 - 1.44Mb
cmp bx,18
je root_defined
undef_root: ;// 如果都不一样,则死循环(死机)。
jmp undef_root
root_defined:
;// seg cs
mov cs:root_dev,ax ;// 将检查过的设备号保存起来。

;// 到此,所有程序都加载完毕,我们就跳转到被
;// 加载在bootsect后面的setup程序去。

; jmp SETUPSEG:[0] ;// 跳转到9020:0000(setup程序的开始处)。
db 0eah
dw 0
dw SETUPSEG

3.setup

利用BIOS中断读取机器数据,将其保存到0x90000地址处(会覆盖掉bootsect代码),这些数据包括硬件参数表、光标位置、根设备号等等

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
;// setup.s负责从BIOS 中获取系统数据,并将这些数据放到系统内存的适当地方。
;// 此时setup.s 和system 已经由bootsect 引导块加载到内存中。
;// 这段代码询问bios 有关内存/磁盘/其它参数,并将这些参数放到一个
;// “安全的”地方:90000-901FF,也即原来bootsect 代码块曾经在
;// 的地方,然后在被缓冲块覆盖掉之前由保护模式的system 读取。

;// 以下这些参数最好和bootsect.s 中的相同!
INITSEG = 9000h ;// 原来bootsect 所处的段
SYSSEG = 1000h ;// system 在10000(64k)处。
SETUPSEG = 9020h ;// 本程序所在的段地址。


code segment
start:

;// ok, 整个读磁盘过程都正常,现在将光标位置保存以备今后使用。

mov ax,INITSEG ;// 将ds 置成INITSEG(9000)。这已经在bootsect 程序中
mov ds,ax ;// 设置过,但是现在是setup 程序,Linus 觉得需要再重新
;// 设置一下。
mov ah,03h ;// BIOS 中断10 的读光标功能号ah = 03
xor bh,bh ;// 输入:bh = 页号
int 10h ;// 返回:ch = 扫描开始线,cl = 扫描结束线,
mov ds:[0],dx ;// dh = 行号(00 是顶端),dl = 列号(00 是左边)。
;// 将光标位置信息存放在90000 处,控制台初始化时会来取。
mov ah,88h ;// 这3句取扩展内存的大小值(KB)。
int 15h ;// 是调用中断15,功能号ah = 88
mov ds:[2],ax ;// 返回:ax = 从100000(1M)处开始的扩展内存大小(KB)。
;// 若出错则CF 置位,ax = 出错码。

;// 下面这段用于取显示卡当前显示模式。
;// 调用BIOS 中断10,功能号ah = 0f
;// 返回:ah = 字符列数,al = 显示模式,bh = 当前显示页。
;// 90004(1 字)存放当前页,90006 显示模式,90007 字符列数。
mov ah,0fh
int 10h
mov ds:[4],bx ;// bh = display page
mov ds:[6],ax ;// al = video mode, ah = window width

;// 检查显示方式(EGA/VGA)并取参数。
;// 调用BIOS 中断10,附加功能选择-取方式信息
;// 功能号:ah = 12,bl = 10
;// 返回:bh = 显示状态
;// (00 - 彩色模式,I/O 端口=3dX)
;// (01 - 单色模式,I/O 端口=3bX)
;// bl = 安装的显示内存
;// (00 - 64k, 01 - 128k, 02 - 192k, 03 = 256k)
;// cx = 显示卡特性参数(参见程序后的说明)。
mov ah,12h
mov bl,10h
int 10h
mov ds:[8],ax ;// 90008 = ??
mov ds:[10],bx ;// 9000A = 安装的显示内存,9000B = 显示状态(彩色/单色)
mov ds:[12],cx ;// 9000C = 显示卡特性参数。

;// 取第一个硬盘的信息(复制硬盘参数表)。
;// 第1 个硬盘参数表的首地址竟然是中断向量41 的向量值!而第2 个硬盘
;// 参数表紧接第1 个表的后面,中断向量46 的向量值也指向这第2 个硬盘
;// 的参数表首址。表的长度是16 个字节(10)。
;// 下面两段程序分别复制BIOS 有关两个硬盘的参数表,90080 处存放第1 个
;// 硬盘的表,90090 处存放第2 个硬盘的表。
mov ax,0000h
mov ds,ax
lds si,ds:[4*41h] ;// 取中断向量41 的值,也即hd0 参数表的地址 ds:si
mov ax,INITSEG
mov es,ax
mov di,0080h ;// 传输的目的地址: 9000:0080 -> es:di
mov cx,10h ;// 共传输10 字节。
rep movsb

;// Get hd1 data

mov ax,0000h
mov ds,ax
lds si,ds:[4*46h] ;// 取中断向量46 的值,也即hd1 参数表的地址 -> ds:si
mov ax,INITSEG
mov es,ax
mov di,0090h ;// 传输的目的地址: 9000:0090 -> es:di
mov cx,10h
rep movsb


;// 检查系统是否存在第2 个硬盘,如果不存在则第2 个表清零。
;// 利用BIOS 中断调用13 的取盘类型功能。
;// 功能号ah = 15;
;// 输入:dl = 驱动器号(8X 是硬盘:80 指第1 个硬盘,81 第2 个硬盘)
;// 输出:ah = 类型码;00 --没有这个盘,CF 置位; 01 --是软驱,没有change-line 支持;
;// 02--是软驱(或其它可移动设备),有change-line 支持; 03 --是硬盘。
mov ax,1500h
mov dl,81h
int 13h
jc no_disk1
cmp ah,3 ;// 是硬盘吗?(类型= 3 ?)。
je is_disk1
no_disk1:
mov ax,INITSEG ;// 第2个硬盘不存在,则对第2个硬盘表清零。
mov es,ax
mov di,0090h
mov cx,10h
mov ax,00h
rep stosb

将位于0x10000地址处的内核程序复制到0x00000地址处(会覆盖掉终端向量表和BIOS数据区)

1
2
3
4
5
6
7
8
9
10
11
do_move:
mov es,ax ;// es:di -> 目的地址(初始为0000:0)
add ax,1000h
cmp ax,9000h ;// 已经把从8000 段开始的64k 代码移动完?
jz end_move
mov ds,ax ;// ds:si -> 源地址(初始为1000:0)
sub di,di
sub si,si
mov cx,8000h ;// 移动8000 字(64k 字节)。
rep movsw
jmp do_move

为了给后面的保护模式做准备,setup程序还要对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置

之后,设置 CPU 的控制寄存器 CR0(也称机器状态字),从而进入 32 位保护模式运行,并跳
转到位于 system 模块最前面部分的 head.s 程序继续运行

  • 打开A20,实现4GB寻址
  • 将CR0寄存器的第0位(PE)置零,将处理器工作方式设置为保护模式

4.head程序

之前提到system模块会被复制到0x00000开始的位置,head程序其实是system模块的一部分,而且是最开始的部分,0x00000其实也是head程序的起始地址。head.s 与前面bootsect.s和setup.s汇编不同,使用的是AT&T格式的汇编

  • 加载各个数据段寄存器,重新设置中断描述符表 idt,共 256 项,并使各个表项均指向一个只报错误的哑中断程序
  • 重新设置全局描述符表 gdt
  • 使用物理地址 0 与 1M 开始处的内容相比较的方法,检测 A20 地址线是否已真的开启
  • 设置管理内存的分页处理机制,将页目录表放在绝对物理地址 0 开始处紧随后面放置共可寻址 16MB 内存的 4 个页表,并分别设置它们的表项
  • 最后利用返回指令将预先放置在堆栈中的/init/main.c 程序的入口地址弹出,去运行 main()程序

实模式与保护模式

实模式:

  • 寻址
    • 段寄存器存放段的基地址,和偏移值共同确定内存地址,此内存地址即是真实物理地址,最大寻址空间是20位,1MB
  • 中断机制
    • 使用中断向量表,其实位置在0x00000,位置固定

保护模式:

  • 寻址
    • 段寄存器存放段描述符表中某一项的索引,索引值所指向的段描述符项包含以下信息:
      • 要寻址的内存段的基地址
      • 段的最大长度值
      • 段的访问级别等信息
    • 最大寻址空间是32位,4GB
  • 中断机制
    • 使用中断描述符表(IDT),可以由操作系统灵活安排,通过IDTR寄存器定位位置

计算机加电后发生了什么?代码 & 理论分析
http://gls.show/p/8b271bd/
作者
郭佳明
发布于
2022年9月14日
许可协议