引导启动程序—bootsect

1.简介

冯·诺依曼存储程序思想

存储程序的主要思想:将程序和数据存放到计算机内部的存储器中,计算机在程序的控制下一步一步进行处理

计算机由五大部件组成:输入设备、输出设备、存储器、运算器、控制器

==取指执行(取指、{间指}、执行、{中断} 大括号可选 )==

image-20211122152739894

打开电源,计算机执行的第一句指令什么? 指针IP及其指向的内容

对于X86PC机而言: (1)x86 PC刚开机时CPU处于实模式 (2)开机时,CS=0xFFFF; IP=0x0000 (3)寻址0xFFFF0(ROM BIOS映射区) (4)检查RAM,键盘,显示器,软硬磁盘 (5)将磁盘0磁道0扇区读入0x7c00处 (6)设置cs=0x07c0,ip=0x0000

注:实模式和保护模式对应,实模式的寻址CS:IP(CS左移4位+IP), 和保护模式(32位汇编模式下)不一

0x7c00处存放的代码:从磁盘引导扇区读入的512个字节

1.引导扇区就是==启动设备的第一个扇区==(开机时按住del键可进入启动设备设置界面,可 以设置为光盘启动!)

2.启动设备信息被设置在CMOS中(CMOS: (64B-128B)。用来存储实时钟和硬件配置信息。)

因此,硬盘的第一个扇区上存放着开机后执行的第一段我们可以控制的程序

==操作系统的故事从这里开始…==

image-20211122155624183

2.操作系统启动流程

bootsect.s和setup.s是实时模式下运行的16位代码程序,采用近似Intel语法,而head.s使用GUN汇编格式(AT&T),并且运行到保护模式下。

当PC的电源打开后,CPU自动进入实模式,并从地址0XFFFF0开始执行程序代码,这个地址通常是ROM-BIOS中的地址,PC机的BIOS将执行某些系统检测,并在物理地址0处开始初始化中断向量。然后,它将可以启动设备的第一个扇区(磁盘引导扇区,512byte)读入内存绝对地址0x7C00处,并跳转到这个地方,启动设备通常是软驱或者硬盘。

Linux最前面部分(boot/bootsect.s),它将由BIOS读入内存绝对地址0x7C00处,当它被执行时就会把自己移动到内存绝对地址0X90000处,并把启动设备中后2kb字节代码(boot/setup.s)读入到内存0x90200处,而内核的其他部分(system模块)则被读入到内存地址0x10000开始处

setup.s将会把system模块移动到物理内存起始位置处,这样system模块中代码的地址就等于实际的物理地址,便于对内核代码和数据操作。

从机器加电开始执行顺序 在这里插入图片描述 启动引导时内核在内存中的位置和移动: 在这里插入图片描述

整个系统从地址0x10000移至0x0000处,进入保护模式并跳转至系统的余下部分(在0x0000chu)。此时所有的32位运行方式的设置启动被完成:IDT,GDT和LDT被加载,处理器和协处理器也确认,分页工作也设置完成。

最终调用init/main.c中main()程序

3.引导扇区代码: bootsect.s

bootsect.s代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第一个扇区)

在PC机加电ROM BIOS自检后,ROM BIOS会把引导扇区代码bootsect加载到内存地址0x7C00开始处并执行之。在bootsect代码执行期间,它会将自己移动到内存绝对地址0x90000开始处并继续执行。

该程序的主要作用是首先把从磁盘第2个扇区开始的4个扇区的setup模块(由 setup.s编译而成)加载到内存紧接着bootsect后面位置处(0x90200),然后利用BIOS中断 0x13 ,取磁盘参数表中当前启动引导盘的参数,接着在屏幕上显示“Loading system.”字符串。

再把磁盘上setup模块后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数判别出盘的类型和种类并保存其设备号于 root_dev( 引导块的508地址处),最后长跳转到setup程序的开始处(0x00200)执行setup程序。

各源文件位置 在这里插入图片描述

1.由BIOS读入内存绝对地址0x7C00处,当它被执行时就会把自己移动到内存绝对地址0X90000处

ds:si

es:di

 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
; bootsect启动程序将它自身从内容0x07c00(BOOTSEG)处复制至内存0x9000(INITSEG)处
entry start            ;关键字entry告诉链接器"程序入口"
start:
	mov	ax,#BOOTSEG    ;BOOTSEG = 0x07c0 赋值给ax,
	mov	ds,ax          ;源地址
	mov	ax,#INITSEG    ;INITSEG = 0x9000 赋值给bx
	mov	es,ax		   ;目标地址
	mov	cx,#256        ;循环次数,每次循环完次数减一
	sub	si,si          ;清零
	sub	di,di          ;清零
	rep				   ;rep是repeat,rep配合 movw(movsb) 就是多次复制直到cx=0为止 复制的次数放在cx中
	movw               ;用于把内容从ds:si 复制es:di  以字位单位
	jmpi	go,INITSEG ;间接跳转 即程序跳到9000:0 去继续执行  CS=INITSEG,IP=go(偏移地址)

; 从这里开始cpu已经跳到内存0x90000去执行,
; BIOS把引导扇区加载到0x7c00处并把执行权交给引导程序,(ss=0x00,sp=0xfffe)
; 将ds,es,ss,都设置成移动后代码所在段(0x9000)
go:	mov	ax,cs          ;ax = cs = INITSEG = 0x9000
	mov	ds,ax          ;数据段地址
	mov	es,ax          ;附加段地址


! put stack at 0x9ff00. ;将堆栈指针sp指向0x9fff00(0x9000:0xff00)
	mov	ss,ax           ;栈段地址
; 保证栈指针sp只要指向远大于512byte字节偏移(即地址0x90200)
; 因为在0x90200后要存放setup程序,大约为4个扇区 sp指向大于(0x200+0x200*4+堆栈大小)
	mov	sp,#0xFF00		! arbitrary value >>512  

! load the setup-sectors directly after the bootblock.
; 在bootsect程序紧跟着加载setup程序
! Note that 'es' is already set up.
; es在移动代码时设置好了指向目的地址0x9000

2.利用BIOS中断 INT 0x13 将setup模块从磁盘第2个扇区开始读到0x90200,然后取磁盘参数表中当前启动引导盘的参数,接着在屏幕上显示“Loading system.”字符串

 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
; 这一段主要是利用BIOS中断 INT 0x13 将setup模块从磁盘第2个扇区开始读到0x90200,
; 一个四个扇区,如果读错,则复位驱动器,并重试
; INT 0x13 使用方法:
; 读扇区:
; ah = 0x02 --读磁盘扇区到内存
; al = 需要读出的扇区数量
; ch = 磁道(柱面)号低8位
; cl = 开始扇区(位0-5),磁道号高两位(位6-7)
; dh = 磁头号
; dl = 驱动器号(如果是硬盘则位7要置位)
; es:bx-->指向数据缓存区 ,如果出错则CF标志置位,ah中是出错码
load_setup:
	mov	dx,#0x0000		! drive 0, head 0
	mov	cx,#0x0002		! sector 2, track 0
	mov	bx,#0x0200		! address = 512, in INITSEG
	mov	ax,#0x0200+SETUPLEN	! service 2, nr of sectors
	int	0x13			! read it
; JNC:Jump Not Carry 没进位时跳转 正确读取时CF=0
	jnc	ok_load_setup		! ok - continue

; 读取出错,对驱动器0进行读操作 并重新读取加载setup程序
	mov	dx,#0x0000
	mov	ax,#0x0000		! reset the diskette
	int	0x13
	j	load_setup      ! jmp指令  返回到重新加载setup处

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track
; 取磁盘驱动器的参数,特别是每道的扇区数量
; 取磁盘驱动器的参数 INT 0x13调用格式和返回信息:
; 调用格式:
; ah = 0x08        dl = 驱动器号(如果是硬盘则位7要置位)
; 返回信息:
; 如果出错,则CF值位,并且ah = 状态码
; ah = 0, al = 0  bl = 驱动器类型(AT/PS2)
; ch = 磁道(柱面)号低8位
; cl = 开始扇区(位0-5),磁道号高两位(位6-7)	
; dh = 最大磁头数 ,   dl = 驱动器数量
; es:di--->软驱磁盘参数表
	mov	dl,#0x00
	mov	ax,#0x0800		! AH=8 is get drive parameters
	int	0x13
	mov	ch,#0x00

; 这条指令表示下一条指令的操作数在cs段寄存器所指的段中
	seg cs
; 保持每磁道扇区数 (cx = 每磁道扇区数)
	mov	sectors,cx
	mov	ax,#INITSEG
; 由于上面取磁道参数中断改掉了es的值,这里重新复制, 	
	mov	es,ax

! Print some inane message
; 显示信息:"'Loading system ...'回车换行" 包括回车换行一共24个字符
; BIOS中断0x10功能号 ah= 0x03,读光标的位置
; 输入:bh = 页号
; 返回:ch = 扫描开始线,cl = 结束开始线,dh = 行号(0x00顶端),dl=列号(0x00最左边)
; BIOS中断0x10功能号 ah= 0x13,显示字符串
; 输入:al=放置光标的方式及规定属性,0x01--表示使用bl中的属性,光标停在字符串结尾处
; es:bp 此寄存器指向要显示字符串起始位置处
; cx = 显示的字符串字符数
; bh = 显示页面号 bl = 字符属性 dh = 行号,dl=列号
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh           ! 首先读光标的位置,返回光标位置值在dx
	int	0x10            !dh = 行(0-23)  dl = 列(0-79) 显示字符串使用

	mov	cx,#24          ! 显示24个字符
	mov	bx,#0x0007		! page 0, attribute 7 (normal)
	mov	bp,#msg1		! es:bp 寄存器指向要显示字符串起始位置处
	mov	ax,#0x1301	 	! write string, move cursor 
	int	0x10

! ok, we've written the message, now
! we want to load the system (at 0x10000)
; 将system加载到0x10000
	mov	ax,#SYSSEG
	mov	es,ax		! segment of 0x010000 
	call	read_it      ; 读磁盘上system模块,es为输入参数
	call	kill_motor   ; 关闭驱动器马达,这样就可以知道驱动器的状态

3.检测使用的根文件系统

 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
; 检测使用的根文件系统
; 如果已经指定了设备并且不等于0,就直接使用给定的设备,否则就需要报道的每磁道扇区数来
; 确定是使用/dev/PS0(2,28) 还是/dev/at0(2,8)
; 在Linux中软驱的主设备是2,次设备 = type*4 + nr 
; type是软驱的类型(2-->1.2MB,7-->1,44MB)
; nr(0-3)对应软驱A,B,C,D
; /dev/PS0(2,28)--->1.44mb A驱动器,设备号0x21c  7*4+0 =28
; /dev/at0(2,8)--->1.2MB A驱动器,设备号0x0208
	seg cs
	mov	ax,root_dev     !取508,509 byte处的根设备号,root_dev定义在这里
	cmp	ax,#0
	jne	root_defined    !判断是否被定义,每定义跳到定义处
	seg cs
	mov	bx,sectors
	mov	ax,#0x0208		! /dev/ps0 - 1.2Mb
	cmp	bx,#15			! 判断每磁道扇区数是否等于15 ,sectors等于15则是1.2Mb驱动器
	je	root_defined
	mov	ax,#0x021c		! /dev/PS0 - 1.44Mb
	cmp	bx,#18          ! sectors等于18则是1.44Mb驱动器
	je	root_defined

; 如果都不一样,则死循环(死机)
undef_root:
	jmp undef_root

root_defined:
	seg cs
	mov	root_dev,ax      !将检测到的设备号保存到root_dev

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
; 到这里,所有的程序都加载完毕,然后跳转到加载到bootsect后面的setup程序

	jmpi	0,SETUPSEG    !!!!!!本程序结束

4.剩余的程序两个子程序,(read_it)用于读取system模块,(kill_moter)用于关闭软件的马达,就不一一介绍了。

4.完整的bootsect.s源码:

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
SYSSIZE = 0x3000
;SYS_SIZE是要加载的系统模块长度,单位是节,16 bytes为1节
;0x3000字节 = 196kB
!
;操作系统启动流程
!	bootsect.s		(C) 1991 Linus Torvalds
!
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts. 
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.


;伪指令, .globl用于定义随后的标识符是外部或者全局的
.globl begtext, begdata, begbss, endtext, enddata, endbss  !全局标识符,供ld86链使用
.text                                                      !正文段
begtext:                                                   !标号 代表其所在的位置,通常指明一个跳转命令的目标地址
.data                                                      !数据段
begdata:
.bss                                                       !未初始化数据段
begbss:         
.text                                                      !正文段

SETUPLEN = 4				! nr of setup-sectors
							!setup程序的扇区数值

BOOTSEG  = 0x07c0	   		! original address of boot-sector
							!BIOS加载bootsect代码的原始段地址

INITSEG  = 0x9000			! we move boot here - out of the way
							!将bootsect移动到这里

SETUPSEG = 0x9020			! setup starts here
							!setup程序从这里开始

SYSSEG   = 0x1000			! system loaded at 0x10000 (65536).
							!system模块加载到0x010000(64kb).

ENDSEG   = SYSSEG + SYSSIZE		! where to stop loading
								! 停止加载的段地址

! ROOT_DEV:	0x000 - same type of floppy as boot.
; 根文件系统设备使用与引导时相同的软驱设备
!		0x301 - first partition on first drive etc
; 根文件系统设备在第一个硬盘的第一个分区上
ROOT_DEV = 0x306
; 设备号0x36指定根文件系统时第2个硬盘的第一个分区
; 设备号命名方式:
; 设备号 = 主设备号*256 + 次设备号 (dev_no = (major<<8)+minor)
; 主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道
; 0x300 - /dev/hd0  --代表整个第一个硬盘
; 0x30(1-4) - /dev/hd(1-4)  --代表第一个盘的1-4个分区
; 0x305 - /dev/hd5  --代表整个第二个硬盘
; 0x30(6-9) - /dev/hd(6-9)  --代表第二个盘的1-4个分区

; bootsect启动程序将它自身从内容0x07c00(BOOTSEG)处复制至内存0x9000(INITSEG)处
entry start            ;关键字entry告诉链接器"程序入口"
start:
	mov	ax,#BOOTSEG    ;BOOTSEG = 0x07c0 赋值给ax,
	mov	ds,ax          ;源地址
	mov	ax,#INITSEG    ;INITSEG = 0x9000 赋值给bx
	mov	es,ax		   ;目标地址
	mov	cx,#256        ;循环次数,每次循环完次数减一
	sub	si,si          ;清零
	sub	di,di          ;清零
	rep				   ;rep是repeat,rep配合 movw(movsb) 就是多次复制直到cx=0为止 复制的次数放在cx中
	movw               ;用于把内容从ds:si 复制es:di  以字位单位
	jmpi	go,INITSEG ;间接跳转 即程序跳到9000:0 去继续执行  CS=INITSEG,IP=go(偏移地址)

; 从这里开始cpu已经跳到内存0x90000去执行,
; BIOS把引导扇区加载到0x7c00处并把执行权交给引导程序,(ss=0x00,sp=0xfffe)
; 将ds,es,ss,都设置成移动后代码所在段(0x9000)
go:	mov	ax,cs          ;ax = cs = INITSEG = 0x9000
	mov	ds,ax          ;数据段地址
	mov	es,ax          ;附加段地址


! put stack at 0x9ff00. ;将堆栈指针sp指向0x9fff00(0x9000:0xff00)
	mov	ss,ax           ;栈段地址
; 保证栈指针sp只要指向远大于512byte字节偏移(即地址0x90200)
; 因为在0x90200后要存放setup程序,大约为4个扇区 sp指向大于(0x200+0x200*4+堆栈大小)
	mov	sp,#0xFF00		! arbitrary value >>512  

! load the setup-sectors directly after the bootblock.
; 在bootsect程序紧跟着加载setup程序
! Note that 'es' is already set up.
; es在移动代码时设置好了指向目的地址0x9000



; 这一段主要是利用BIOS中断 INT 0x13 将setup模块从磁盘第2个扇区开始读到0x90200,
; 一个四个扇区,如果读错,则复位驱动器,并重试
; INT 0x13 使用方法:
; 读扇区:
; ah = 0x02 --读磁盘扇区到内存
; al = 需要读出的扇区数量
; ch = 磁道(柱面)号低8位
; cl = 开始扇区(位0-5),磁道号高两位(位6-7)
; dh = 磁头号
; dl = 驱动器号(如果是硬盘则位7要置位)
; es:bx-->指向数据缓存区 ,如果出错则CF标志置位,ah中是出错码
load_setup:
	mov	dx,#0x0000		! drive 0, head 0
	mov	cx,#0x0002		! sector 2, track 0
	mov	bx,#0x0200		! address = 512, in INITSEG
	mov	ax,#0x0200+SETUPLEN	! service 2, nr of sectors
	int	0x13			! read it
; JNC:Jump Not Carry 没进位时跳转 正确读取时CF=0
	jnc	ok_load_setup		! ok - continue

; 读取出错,对驱动器0进行读操作 并重新读取加载setup程序
	mov	dx,#0x0000
	mov	ax,#0x0000		! reset the diskette
	int	0x13
	j	load_setup      ! jmp指令  返回到重新加载setup处

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track
; 取磁盘驱动器的参数,特别是每道的扇区数量
; 取磁盘驱动器的参数 INT 0x13调用格式和返回信息:
; 调用格式:
; ah = 0x08        dl = 驱动器号(如果是硬盘则位7要置位)
; 返回信息:
; 如果出错,则CF值位,并且ah = 状态码
; ah = 0, al = 0  bl = 驱动器类型(AT/PS2)
; ch = 磁道(柱面)号低8位
; cl = 开始扇区(位0-5),磁道号高两位(位6-7)	
; dh = 最大磁头数 ,   dl = 驱动器数量
; es:di--->软驱磁盘参数表
	mov	dl,#0x00
	mov	ax,#0x0800		! AH=8 is get drive parameters
	int	0x13
	mov	ch,#0x00

; 这条指令表示下一条指令的操作数在cs段寄存器所指的段中
	seg cs
; 保持每磁道扇区数 (cx = 每磁道扇区数)
	mov	sectors,cx
	mov	ax,#INITSEG
; 由于上面取磁道参数中断改掉了es的值,这里重新复制, 	
	mov	es,ax

! Print some inane message
; 显示信息:"'Loading system ...'回车换行" 包括回车换行一共24个字符
; BIOS中断0x10功能号 ah= 0x03,读光标的位置
; 输入:bh = 页号
; 返回:ch = 扫描开始线,cl = 结束开始线,dh = 行号(0x00顶端),dl=列号(0x00最左边)
; BIOS中断0x10功能号 ah= 0x13,显示字符串
; 输入:al=放置光标的方式及规定属性,0x01--表示使用bl中的属性,光标停在字符串结尾处
; es:bp 此寄存器指向要显示字符串起始位置处
; cx = 显示的字符串字符数
; bh = 显示页面号 bl = 字符属性 dh = 行号,dl=列号
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh           ! 首先读光标的位置,返回光标位置值在dx
	int	0x10            !dh = 行(0-23)  dl = 列(0-79) 显示字符串使用

	mov	cx,#24          ! 显示24个字符
	mov	bx,#0x0007		! page 0, attribute 7 (normal)
	mov	bp,#msg1		! es:bp 寄存器指向要显示字符串起始位置处
	mov	ax,#0x1301	 	! write string, move cursor 
	int	0x10

! ok, we've written the message, now
! we want to load the system (at 0x10000)
; 将system加载到0x10000
	mov	ax,#SYSSEG
	mov	es,ax		! segment of 0x010000 
	call	read_it      ; 读磁盘上system模块,es为输入参数
	call	kill_motor   ; 关闭驱动器马达,这样就可以知道驱动器的状态

! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.

; 检测使用的根文件系统
; 如果已经指定了设备并且不等于0,就直接使用给定的设备,否则就需要报道的每磁道扇区数来
; 确定是使用/dev/PS0(2,28) 还是/dev/at0(2,8)
; 在Linux中软驱的主设备是2,次设备 = type*4 + nr 
; type是软驱的类型(2-->1.2MB,7-->1,44MB)
; nr(0-3)对应软驱A,B,C,D
; /dev/PS0(2,28)--->1.44mb A驱动器,设备号0x21c  7*4+0 =28
; /dev/at0(2,8)--->1.2MB A驱动器,设备号0x0208
	seg cs
	mov	ax,root_dev     !取508,509 byte处的根设备号,root_dev定义在这里
	cmp	ax,#0
	jne	root_defined    !判断是否被定义,每定义跳到定义处
	seg cs
	mov	bx,sectors
	mov	ax,#0x0208		! /dev/ps0 - 1.2Mb
	cmp	bx,#15			! 判断每磁道扇区数是否等于15 ,sectors等于15则是1.2Mb驱动器
	je	root_defined
	mov	ax,#0x021c		! /dev/PS0 - 1.44Mb
	cmp	bx,#18          ! sectors等于18则是1.44Mb驱动器
	je	root_defined

; 如果都不一样,则死循环(死机)
undef_root:
	jmp undef_root

root_defined:
	seg cs
	mov	root_dev,ax      !将检测到的设备号保存到root_dev

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
; 到这里,所有的程序都加载完毕,然后跳转到加载到bootsect后面的setup程序

	jmpi	0,SETUPSEG    !!!!!!本程序结束


; 下面是两个子程序,(read_it)用于读取system模块,(kill_moter)用于关闭软件的马达
! This routine loads the system at address 0x10000, making sure
! no 64kB boundaries are crossed. We try to load it as fast as
! possible, loading whole tracks whenever we can.
!
! in:	es - starting address segment (normally 0x1000)
!
; 该子程序将系统模块加载到内存地址0x10000处,并确定没有跨越64kb内存边界
; 尽快可能的加载,每次加载整条磁道的数据
; 输入:es开始内存地址段值(一般0x1000)

; .word定义一个字内存
; (1+SETUPLEN)表示开始已经读进一个引导扇区和setup程序所占的扇区数SETUPLEN 
sread:	.word 1+SETUPLEN	! sectors read of current track 当前磁道中已读扇区数
head:	.word 0			! current head  当前磁头号
track:	.word 0			! current track 当前磁道号

read_it:
	mov ax,es
	test ax,#0x0fff
die:	jne die			! es must be at 64kB boundary
	xor bx,bx		! bx is starting address within segment



rp_read:
	mov ax,es
	cmp ax,#ENDSEG		! have we loaded all yet?
	jb ok1_read
	ret


ok1_read:
	seg cs
	mov ax,sectors
	sub ax,sread
	mov cx,ax
	shl cx,#9
	add cx,bx
	jnc ok2_read
	je ok2_read
	xor ax,ax
	sub ax,bx
	shr ax,#9


ok2_read:
	call read_track
	mov cx,ax
	add ax,sread
	seg cs
	cmp ax,sectors
	jne ok3_read
	mov ax,#1
	sub ax,head
	jne ok4_read
	inc track


ok4_read:
	mov head,ax
	xor ax,ax


ok3_read:
	mov sread,ax
	shl cx,#9
	add bx,cx
	jnc rp_read
	mov ax,es
	add ax,#0x1000
	mov es,ax
	xor bx,bx
	jmp rp_read

read_track:
	push ax
	push bx
	push cx
	push dx
	mov dx,track
	mov cx,sread
	inc cx
	mov ch,dl
	mov dx,head
	mov dh,dl
	mov dl,#0
	and dx,#0x0100
	mov ah,#2
	int 0x13
	jc bad_rt
	pop dx
	pop cx
	pop bx
	pop ax
	ret
	
bad_rt:	mov ax,#0
	mov dx,#0
	int 0x13
	pop dx
	pop cx
	pop bx
	pop ax
	jmp read_track  

/*
 * This procedure turns off the floppy drive motor, so
 * that we enter the kernel in a known state, and
 * don't have to worry about it later.
 */

 ; 这个程序用于关闭软件马达,这样进入内核就可以知道它所处的状态
kill_motor:
	push dx
	mov dx,#0x3f2         !软件控制卡的数字输出寄存器(DOR)端口,只写
	mov al,#0
	outb
	pop dx
	ret

sectors:
	.word 0

msg1:                           ! 调用BIOS中断显示信息
	.byte 13,10                 ! 回车换行的ascii码
	.ascii "Loading system ..." ! 显示字符串
	.byte 13,10,13,10           !一个24个字符

; 表示下面语句从地址508(0x1fc)开始,所以root_dev在启动扇区的第508开始的2个字节中
.org 508
root_dev:
	.word ROOT_DEV               !存放根文件系统所在设备号(init/main.c中会用)

; 下面是启动盘具有有效引导扇区的标志,在BIOS程序加载引导扇区时识别使用,
; 它必须位于引导扇区的最后两个字节中
boot_flag:
	.word 0xAA55

.text
endtext:
.data
enddata:
.bss
endbss:

操作系统加载—setup

1.简介

作用:setup.s是操作系统加载程序,它的作用是利用ROM BIOS中断读取系统数据,并将这些数据保存到0x90000开始的位置处(覆盖了原来bootsect程序所在的地方)

读取到数据保存的位置: 在这里插入图片描述

将读取到的数据保存后,==setup将system模块从0x10000-0x8ffff整块移动到内存绝对地址0x00000处==。 然后加载中断描述符表寄存器(idir)和全局描述符表寄存器(gdtr),开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20-0x2f。再设置cpu的控制寄存器CR0(机器状态字),从而进入32位保护模式,并跳到system模块最前面部分的head.s程序继续运行。

为了能让head.s在32位保护模式下运行,在程序中临时设置中断描述符(IDT)和全局描述符表(GDT),并在GDT中设置当前内核代码段的描述符和数据段的描述符。

GDT 段描述符存放在描述符表中。描述符表其实就是内存中描述符项的一个阵列。

描述符表有两类:全局描述符表(Global descriptor table-GDT)和局部描述符表(Local descriptor table−LDT)。

处理器是通过使用GDTR和LDTR寄存器来定位GDT表和当前的LDT表。

这两个寄存器以线性地址的方式保存了描述符表的基地址和表的长度。

指令Igd和sgd用于访问GDTR寄存器; 指令Hdt和slut用于访问LDTR寄存器。 lgd使用内存中一个6字节操作数来加载GDTR寄存器。头两个字节代表描述符表的长度,后4个字节是描述符表的基地址。但,访问LDTR寄存器的指令lut所使用的操作数却是一个2字节的操作数,表示全局描述符表GDT中一个描述符项的选择符。该选择符所对应的GDT表中的描述符项应该对应一个局部描述符表。

在这里插入图片描述

setup设置的GDT描述符项,代码段描述符的值是0x00C09A0000007FF,

表示代码段的限长是 8MB(=(0x7F+1)∗4KB, 这里加1是因为限长值是从0开始算起的,段在线性地址空间中的基址是0,段类型值009A表示该段存在于内存中、段的特权级别为0、段类型是可读可执行的代码段,段代码是32位的并且段的颗粒度是4KB。

数据段描述符的值是0x00C0920000007FF,表示数据段的限长是8MB…段在线性地址空间中的基址是0。段类型值0x92表示该段存在于内存中、段的特权级别为0、段类型是可读可写的数据段、段代码是32位的并且段的颗粒度是4KB。

逻辑地址的选择符部分用于指定一描述符,它是通过指定一描述符表并且索引其中的一个描述符项完成的。

段选择符格式:

在这里插入图片描述

其中索引值用于指定描述符表中8192(2**13)个描述符中的一个。

处理器将该索引值乘上8,并加上描述符表的基地址即可访问表中指定的段描述符。

表指示器(Table Indicator - TD)用于指定选择符所引用的描述符表。值为0表示指定GDT表,值为1表示指定当前的LDT表。请求者特权级(R capaestor’sPrivalege Level-RPL)用于保护机制。

由于GDT表的第一项(索引值为0)没有被使用,因此一个具有索引值0和表指示器值也为0的选择符(也即指向GDT的第一项的选择符)可以用作为一个空(null)选择符。当一个段寄存器(不能是 CS或SS)加载了一个空选择符时,处理器并不会产生一个异常。但是若使用这个段寄存器访问内存时就会产生一个异常。对于初始化还未使用的段寄存器以陷入意外的引用来说,这个特性是很有用的。

在进入保护模式之前,我们必须首先设置好将要用到的段描述符表,例如全局描述符表GDT。然后使用指令lgdt把描述符表的基地址告知CPU(GDT表的基地址存入g知寄存器)。再将机器状态字的保护模式标志置位即可进入32位保护运行模式。

Linux 0.11硬盘设备号

在Linux中,硬盘的主区号是3,其他设备的主设备号分别为:

1–内存 2–磁盘 3–硬盘 4–ttyx 5–tty 6–并行口 7–非命名管道

一个硬盘可以有1-4个分区,可以依据分区的不同用次设备号进行指定分区,所以:设备号=主设备号*256+次设备号

在这里插入图片描述

磁盘

一个磁盘由多个盘片(如下图中的 0 号盘片)叠加而成。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面

在这里插入图片描述

每个盘片被划分为一个个磁道,每个磁道又划分为一个个扇区

在这里插入图片描述

柱面

每个盘面对应一个磁头。所有的磁头都是连在同一个磁臂上的,因此所有磁头只能“共进退”。所有盘面中相对位置相同的磁道组成柱面

在这里插入图片描述

磁盘的物理地址

可用(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”

可根据该地址读取一个“块”,操作如下:

① 根据“柱面号”移动磁臂,让磁头指向指定柱面;

② 激活指定盘面对应的磁头;

③ 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写

2.源码分析

1.setup完成OS前的初始化和设置

1)保存光标的位置 2)得到扩展内存的大小 3)得到显示卡当前的显示模式 4)检测显示方式 5)读取硬盘参数表信息

硬盘基本参数表(INT 0x41)

在中断向量表中,int 0x41的中断向量位置(4*0x41=0x0000:0x0140)存放的不是中断程序的地址入口,而是第一个硬盘参数表的信息,0x46存放第二个硬盘参数表

硬盘参数表信息:

在这里插入图片描述

2.将整个system模块移动到0x00000处

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
; bootsect引导程序将system模块移动到(0x10000)处,
; 并把自己移动到(0x90000)处,把setup加载在它后面
; 下面这段程序将整个system模块移动到0x00000处,
; 即把从0x10000到0x8ffff的内存数据块整块的向内存地址低端移动了0x10000的位置

	mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000  ! 判断代码是否移动完成
	jz	end_move    ! 移动完成则跳转
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000   ! 循环移动,循环次数,每次循环完次数减 移动0x8000字
	rep				! 用于把内容从ds:si 复制es:di  以字节单位
	movsw           ! rep是repeat,rep配合 movw(movsb) 就是多次复制直到cx=0为止 复制的次数放在cx中
	jmp	do_move

移动后内存存放数据:

在这里插入图片描述

3.跳转到绝对地址0x00000处

在跳转之前还要进行相应的设置

1)加载段描述符,设置全局描述符表和中断描述表 2)开启A20地址线,为了能够访问和使用1MB以上的物理内存 3)重新对中断进行编程

进入保护模式:jmpi 0,8

1
2
3
4
5
6
7
;进入保护模式,只是跳转到绝对地址0x00000处

; 加载机器状态字(控制寄存器CR0),将0位置1,CPU切换到保护模式
	mov	ax,#0x0001	! protected mode (PE) bit 保护模式比特位(PE)
	lmsw	ax		! This is it!             加载状态寄存器
	;段选择符8表示请求特权0级,使用GDT第二个段描述符
	jmpi	0,8		! jmp offset 0 of segment 8 (cs)  跳转至cs段偏移地址位0处(system已经移动到0x00000处)

setup程序完整代码

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
!
!	setup.s		(C) 1991 Linus Torvalds
!
! setup.s is responsible for getting the system data from the BIOS,
! and putting them into the appropriate places in system memory.
! both setup.s and system has been loaded by the bootblock.
!
! This code asks the bios for memory/disk/other parameters, and
! puts them in a "safe" place: 0x90000-0x901FF, ie where the
! boot-block used to be. It is then up to the protected mode
! system to read them from there before the area is overwritten
! for buffer-blocks.
!
; setup从BIOS中获取数据,并将这些数据保存到0x90000开始的位置处(0x90000-0x901FF覆盖了原来bootsect程序所在的地方)
; 此时setup和system已经由bootsect引导块加载到内存中
; 
! NOTE! These had better be the same as in bootsect.s!

INITSEG  = 0x9000	! we move boot here - out of the way 原来bootsect所在段
SYSSEG   = 0x1000	! system loaded at 0x10000 (65536).   system所在0x10000处
SETUPSEG = 0x9020	! this is the current segment          本程序所在段地址

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.
; 保存光标的位置
; 使用BIOS中断取屏幕当前光标的位置(列,行),保存到内存(0x90000)处,2个byte
; 控制台初始化程序会到此处读取该值
; BISO 中断0x10 功能号 ah = 0x30 ,读光标的位置
; 输入:bh=页号
; 返回:返回:ch = 扫描开始线,cl = 结束开始线,dh = 行号(0x00顶端),dl=列号(0x00最左边)
	mov	ax,#INITSEG ! this is done in bootsect already, but...
	mov	ds,ax
	mov	ah,#0x03	! read cursor pos 功能号 ah = 0x30 ,读光标的位置
	xor	bh,bh
	int	0x10		! save it in known place, con_init fetches
	mov	[0],dx		! it from 0x90000.  将ds设置成0x90000(INITSEG)

! Get memory size (extended mem, kB)
; 得到扩展内存的大小
; 利用BIOS中断0x15 功能号 ah= 0x88取系统所含扩展内存大小并保存到0x90002处
; 返回: ax= 0x10000(1M)处开始的扩展内存大小,若出错CF置位,ax=出错码
	mov	ah,#0x88
	int	0x15
	mov	[2],ax      !扩展内存的大小保存到0x90002处

! Get video-card data:
; 得到显示卡当前的显示模式
; 调用BIOS中断0x10,功能号 ah = 0x0f
; 返回:ah=字符列数,al=显示模式,bh=显示当前页数

	mov	ah,#0x0f
	int	0x10
	mov	[4],bx		! bh = display page
	mov	[6],ax		! al = video mode, ah = window width

! check for EGA/VGA and some config parameters
; 检测显示方式
; 调用BIOS中断0x10, 功能号 ah=0x12,bl=0x10
	mov	ah,#0x12
	mov	bl,#0x10
	int	0x10
	mov	[8],ax      ! 0x90008 =ax
	mov	[10],bx     ! 0x9000A = 安装的显示内存,0x9000B = 显示状态
	mov	[12],cx		!0X9000C = 显卡特性参数

! Get hd0 data
; 取第一个硬盘信息
; 第一个硬盘参数表的首地址是中断向量0x41的向量值
; 第二个紧跟着对应着中断向量0x46
; 下面两个程序分别复制BIOS有关硬盘参数表,
; 第一个硬盘存放在0x90080,第二个硬盘存放在0x90090
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]         !取中断向量0x41对应的地址 ,hd0参数表的地址--> ds:si
	mov	ax,#INITSEG         
	mov	es,ax
	mov	di,#0x0080          !传输的目的地址(0x9000:0x0080) -->es:di
	mov	cx,#0x10            ! 循环次数,每次循环完次数减一,共传输16个字节
	rep						! rep是repeat,rep配合 movw(movsb) 就是多次复制直到cx=0为止 复制的次数放在cx中
	movsb                   ! 用于把内容从ds:si 复制es:di  以字节单位

! Get hd1 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]         !取中断向量0x41对应的地址 ,hd0参数表的地址--> ds:si
	mov	ax,#INITSEG 
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	rep
	movsb


! Check that there IS a hd1 :-)
; 检测是否有第二个硬盘,如果没有则把第2个清零
; 利用BIOS中断调用0x13的取盘的类型,功能号 ah =0x15
	mov	ax,#0x01500
	mov	dl,#0x81         ! dl = 驱动器号(0x8X是硬盘,0x81是第一个硬盘,0x82是第二个硬盘)
	int	0x13
	jc	no_disk1         ! 第二个不存在
	cmp	ah,#3			 ! ah =类型码 指硬盘
	je	is_disk1         ! 存在

; 第二个硬盘不存在,对第二个硬盘表清零
no_disk1:
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	mov	ax,#0x00
	rep
	stosb
; 第二个硬盘存在,进入保护模式,从此开始不允许中段
is_disk1:

! now we want to move to protected mode ...

	cli			! no interrupts allowed !

! first we move the system to it's rightful place
; bootsect引导程序将system模块移动到(0x10000)处,
; 并把自己移动到(0x90000)处,把setup加载在它后面
; 下面这段程序将整个system模块移动到0x00000处,
; 即把从0x10000到0x8ffff的内存数据块整块的向内存地址低端移动了0x10000的位置

	mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000  ! 判断代码是否移动完成
	jz	end_move    ! 移动完成则跳转
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000   ! 循环移动,循环次数,每次循环完次数减 移动0x8000字
	rep				! 用于把内容从ds:si 复制es:di  以字节单位
	movsw           ! rep是repeat,rep配合 movw(movsb) 就是多次复制直到cx=0为止 复制的次数放在cx中
	jmp	do_move

! then we load the segment descriptors
; 加载段描述符,设置全局描述符表和中断描述表

end_move:
	mov	ax,#SETUPSEG	! right, forgot this at first. didn't work :-)
	mov	ds,ax
; lidt指令用于加载中断描述符表(IDT)寄存器
; 中断描述符表中每一个8个字节对应每个中断发生时所需要的中断程序地址入口
	lidt	idt_48		! load idt with 0,0   
; lgdt指令用于加载全局描述符表(GDT)寄存器
; 全局描述符表中每个描述符项(8字节)描述了保护模式下数据段和代码段的信息
	lgdt	gdt_48		! load gdt with whatever appropriate

! that was painless, now we enable A20
; 开启A20地址线,为了能够访问和使用1MB以上的物理内存
	call	empty_8042    ! 测试8042状态寄存器,等待输入缓冲器空,
	mov	al,#0xD1		  ! command write 0xD1命令码表示写数据到8042的P2端口
	out	#0x64,al

	call	empty_8042    !等待输入缓冲器空,看命令是否被接受
	mov	al,#0xDF		  ! A20 on
	out	#0x60,al
	call	empty_8042    !若此时输入缓冲器为空,则表示A20线也选通 

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.

; 重新对中断进行编程

	mov	al,#0x11		! initialization sequence
	out	#0x20,al		! send it to 8259A-1  发送到8259A主芯片
;   0x00eb直接使用机器码表示两条相对跳转指令,起延时作用
	.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
	out	#0xA0,al		! and to 8259A-2   再发送到8259A从芯片
	.word	0x00eb,0x00eb
;   系统硬件中断号被设置成0x20开始
	mov	al,#0x20		! start of hardware int's (0x20)
	out	#0x21,al		!送主芯片ICW2命令字,设置起始中断,要送奇端口
	.word	0x00eb,0x00eb

	mov	al,#0x28		! start of hardware int's 2 (0x28)
	out	#0xA1,al        !送主芯片ICW2命令字,从芯片的起始中断号

	.word	0x00eb,0x00eb
	mov	al,#0x04		! 8259-1 is master
	out	#0x21,al        !ICW3
	.word	0x00eb,0x00eb
	mov	al,#0x02		! 8259-2 is slave
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x01		! 8086 mode for both
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0xFF		! mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.

; 进入保护模式,只是跳转到绝对地址0x00000处

; 加载机器状态字(控制寄存器CR0),将0位置1,CPU切换到保护模式
	mov	ax,#0x0001	! protected mode (PE) bit 保护模式比特位(PE)
	lmsw	ax		! This is it!             加载状态寄存器
	;段选择符8表示请求特权0级,使用GDT第二个段描述符
	jmpi	0,8		! jmp offset 0 of segment 8 (cs)  跳转至cs段偏移地址位0处(system已经移动到0x00000处)

! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.

; 检差键盘命令队列是否为空
; 只有当输入缓冲器为空(键盘控制器状态寄存器位1 = 0)才可以进行写命令
empty_8042:
	.word	0x00eb,0x00eb      !延时作用
	in	al,#0x64	! 8042 status port
	test	al,#2		! is input buffer full?
	jnz	empty_8042	! yes - loop
	ret

; GDT全局描述符表开始处,描述符表由多个8字节长的描述符项组成,
; 3个描述符项
; 第一项没有作用,但是必须存在
; 第二项是系统代码段描述符
; 第三项是系统数据段描述符
gdt:
	.word	0,0,0,0		! dummy  第一个描述符 不用

; 在GDT表这里的偏移量是0x80,它是内核代码段选择符的值
	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

; 在GDT表这里的偏移量是0x10,它是内核数据段选择符的值
	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386

; 加载中断描述符表寄存器(idtr)
; 这里设置一个长度为0的空表
idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L

; 加载全局描述符表寄存器(gdtr)
; GDT表长度为2kb
gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	! gdt base = 0X9xxxx
	
.text
endtext:
.data
enddata:
.bss
endbss:

内核引导程序—head

1.简介

head.s 程序在被编译生成目标文件后会与内核其他程序一起被链接成 system 模块,它位于 system 模块的最开始部分。system模块将被放置在磁盘上setup模块之后的扇区,从磁盘上第6个扇区开始放置。

注:这段程序处于绝对地址0x00000处。

程序进入保护模式,程序采用AT&T语法格式。

Linux AT&T汇编语法简介:

添加链接描述

作用:head.s程序:设置中断描述符表项(哑中断);检查A20;测试是否有协处理器;初始化内存页目录表;跳转到main.c执行内核初始化

head完成后完成了内存页目录和页表的设置,并重新设置了内核实际使用的中断描述符表idt和全局描述符表GDT,还为软盘驱动程序开辟了1KB字节的缓冲区。

32位下寻址

将实模式下的段寄存器当作保护模式下的段描述符的指针使用,此时段寄存器中存放的是一个描述符在描述符表中的偏移地址寄存器,而当前描述符表的基地址则保存在描述符表寄存器中。

head程序结束后内存映像

在这里插入图片描述

.align

1
2
3
4
5
.align  2
完整格式 :.align val1,val2,val3
val1 是需要对齐的值
val2 填充字节指定的值
val3 指明最大用于填充或跳过的直接数

.align是汇编语言指示符。其含义是边界对齐调整。”2”表示把随后的代码或数据的偏移位置调整到地址值最后 2 比特位为零的位置,即按 4(=2^2)字节方式对齐内存地址。不过现在 GNU as 直接写出对齐的值而非 2 的幂次。使用该指示符的目的是为了提高 32 位 CPU 访问内存中代码或数据的效率。

.ORG

ORG伪指令用来表示起始的偏移地址,紧接着ORG的数值就是偏移地址的起始值。ORG伪操作常用来指定数据的存储地址,有时也用来指定代码段的起始地址

fill

fill伪指令的格式是 .fill repeat,size,value

表示产生 repeat 个大小为 size 字节的重复拷贝。size 最大是 8,size 字节的值是 value.

按位异或 xor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
1. 使某些特定的位翻转
例如要使 EAX  b1 位和 b2 位翻转:
      EAX = EAX ^ 00000110
      
2.不使用临时变量就可以实现两个值的交换
例如 a=11110000b=00001111,要交换ab的值
a = a^b   //a=11111111
b = b^a   //b=11110000
a = a^b   //a=00001111


3.在汇编语言中经常用于将变量置零
xor eaxeax

4.快速判断两个值是否相等
例如判断两个整数ab是否相等,可通过下列语句实现:
return ((a ^ b) == 0)

LSS指令

1
2
3
格式:LSS r32,m16:32  #用内存中的长指针加载 SS:r32
m16:32表示一个内存操作数,这个操作数是一个长指针,由2部分组成:16位的段选择
子和32位的偏移。

2.源码分析

1.启动32位程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
startup_32:
	movl $0x10,%eax  	;0x10 GDT中的偏移值(一个描述符表项的选择符)
						;请求特级权0(位0-1 =0),GDT(位2=0),选择表中第2项(位3-15=2)
						; 正好指向数据段描述符项
	mov %ax,%ds         ; 设置ds,es,fs,gs为setup中构造的数据段的选择符 =0x10      
	mov %ax,%es			;并将堆栈放置在stack_start(指针)指向的user_stack数组区
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp  ;_stack_start-->ss:esp,设置系统堆栈
						   ;移动到任务0执行(init/main.c),该栈就被用做任务0和任务1共同使用的用户栈
	call setup_idt         ; 调用设置中断描述符表子程序
	call setup_gdt         ; 调用设置全局描述符表子程序
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs		# 因为修改了gdt,所以重新加载这些寄存器
	mov %ax,%gs

2.加载各个段寄存器,重新设置中断描述符表,共256项,并使各个表项均指向一个只报错误的哑中断程序,重新设置全局描述符表

1)IDT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
;设置中断描述符表(IDT)字程序setup_idt
 ; 将中断描述符表设置具有256个项,并都指向ignore_int中断门,然后加载中断描述符表寄存器
 ; 中断描述符表中的项为8个字节,称为门描述符
setup_idt:
	lea ignore_int,%edx  # 将ignore_int有效地址值赋值给edx
	movl $0x00080000,%eax # 将选择符0x0008赋值给eax的高16位
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea _idt,%edi               # _idt 是中断描述符表的地址
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)			# eax -> [edi] 将哑中断门描述符存入表中
	movl %edx,4(%edi)           # edx -> [edi+4]
	addl $8,%edi				# edi + 8 -> edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr              #加载中断描述符表寄存器
	ret

2)GDT

1
2
3
4
# 设置全局描述符表项
setup_gdt:
	lgdt gdt_descr    # 加载全局描述符寄存器
	ret

3.对比物理地址0与1M开始处的内容是否相同,如果相同那么没有开启A20地址线,进入死循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;测试A20地址线是否开启
	;方法:向内存地址0处写入任意一个数值,看内存地址是否是这个数值
	;如果是就一直比较下去,产生死循环,
	;表示A20线没有选通,内核就不能使用1MB以后的内存空间
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled  
					# 1-->标号,表示活动位置计数的当前值,并可以作为指令的操作数
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b           # Nb:引用先前最近的标号 'b':backwards 向后
					# Nf:引用下一个标号     'f':forwards  向前
					# 这里‘1b’表示:向后跳转到标号1去
					# 如果是5f,则是向前跳转到标号5去

4.测试PC机是否含有数据协处理器芯片,并在控制寄存器CR0中设置相应的标志位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
; 检测486数学协处理器芯片是否存在
 ; 方法:修改控制寄存器CR0,然后执行一条协处理器指令,若出错,则不存咋
 ; 需要设置协处理器仿真位(EM)位2,并复位协处理器存在标志(MP)位2
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP   并复位协处理器存在标志(MP)
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

5.设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处紧随其后放置共可寻址16MB内存的4个页表,并分别设置它们的表项

6.最后利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,运行main()程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 下面几个入栈操作为了跳转到init/main.c中main()函数做准备
# 前面三个入栈0值分别表示envp,argv指针和argc的值
after_page_tables:
	pushl $0		# These are the parameters to main :-)  main函数参数 envp
	pushl $0		# argv指针
	pushl $0		# argc
	pushl $L6		# return address for main, if it decides to. 模拟调用main时首先将返回地址入栈的操作
	pushl $_main    # _main--->main
	jmp setup_paging

# main函数到不了这里 ,到标号L6这里,是一个死循环
L6: 
	jmp L6			# main should never return here, but
				    # just in case, we know what happens.

3.head完整源码

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*
 *  linux/boot/head.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  head.s contains the 32-bit startup code.
 *
 * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
 * the page directory will exist. The startup code will be overwritten by
 * the page directory.
 */
 ; head程序含有32启动程序代码
 ; 32启动程序代码是从绝对地址0x00000处开始
 ; 页目录也在该内存,以后启动代码将会被覆盖
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:                ; 页目录将会放在在这里
startup_32:
	movl $0x10,%eax  	;0x10 GDT中的偏移值(一个描述符表项的选择符)
						;请求特级权0(位0-1 =0),GDT(位2=0),选择表中第2项(位3-15=2)
						; 正好指向数据段描述符项
	mov %ax,%ds         ; 设置ds,es,fs,gs为setup中构造的数据段的选择符 =0x10      
	mov %ax,%es			;并将堆栈放置在stack_start(指针)指向的user_stack数组区
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp  ;_stack_start-->ss:esp,设置系统堆栈
						   ;移动到任务0执行(init/main.c),该栈就被用做任务0和任务1共同使用的用户栈
	call setup_idt         ; 调用设置中断描述符表子程序
	call setup_gdt         ; 调用设置全局描述符表子程序
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs		# 因为修改了gdt,所以重新加载这些寄存器
	mov %ax,%gs

	lss _stack_start,%esp

	;测试A20地址线是否开启
	;方法:向内存地址0处写入任意一个数值,看内存地址是否是这个数值
	;如果是就一直比较下去,产生死循环,
	;表示A20线没有选通,内核就不能使用1MB以后的内存空间
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled  
					# 1-->标号,表示活动位置计数的当前值,并可以作为指令的操作数
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b           # Nb:引用先前最近的标号 'b':backwards 向后
					# Nf:引用下一个标号     'f':forwards  向前
					# 这里‘1b’表示:向后跳转到标号1去
					# 如果是5f,则是向前跳转到标号5去
					
/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
 ; 检测486数学协处理器芯片是否存在
 ; 方法:修改控制寄存器CR0,然后执行一条协处理器指令,若出错,则不存咋
 ; 需要设置协处理器仿真位(EM)位2,并复位协处理器存在标志(MP)位2
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP   并复位协处理器存在标志(MP)
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
 ; fninit和fstsw是协处理器的指令
check_x87:
	fninit           # 向协处理器发出初始化指令
	fstsw %ax        # 将协处理器状态字复制给ax
	cmpb $0,%al		 # 初始化后状态字应该位0,否则协处理器不存在
	je 1f			/* no coprocessor: have to set bits */ #跳转到标号位1处(前面)
	movl %cr0,%eax   # 如果存在则跳到标号位1处,否则改写cr0
	xorl $6,%eax		/* reset MP, set EM */ ;设置协处理器仿真位(EM)
	movl %eax,%cr0
	ret
# .align 是汇编语言指示符,用于存储边界对齐调整
# 2表示把随后的代码和数据的偏移位置调整到地址值最后2比特位为0的位置(2*2)
# 即按四字节方式对齐内存地址
# 使用该指示符目的是为了提高32位cpu访问内存中代码或数据的速度和效率
.align 2
# 287协处理码,将80287设置为保护模式
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */ 
	ret

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
 ;设置中断描述符表(IDT)字程序setup_idt
 ; 将中断描述符表设置具有256个项,并都指向ignore_int中断门,然后加载中断描述符表寄存器
 ; 中断描述符表中的项为8个字节,称为门描述符
setup_idt:
	lea ignore_int,%edx  # 将ignore_int有效地址值赋值给edx
	movl $0x00080000,%eax # 将选择符0x0008赋值给eax的高16位
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea _idt,%edi               # _idt 是中断描述符表的地址
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)			# eax -> [edi] 将哑中断门描述符存入表中
	movl %edx,4(%edi)           # edx -> [edi+4]
	addl $8,%edi				# edi + 8 -> edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr              #加载中断描述符表寄存器
	ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */

 # 设置全局描述符表项
setup_gdt:
	lgdt gdt_descr    # 加载全局描述符寄存器
	ret

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */
 # 将内核的内存页表直接放在页目录后,使用4个表来寻址16MB的物理地址
.org 0x1000            # 从偏移地址0x1000处开始是第一个页表(偏移0开始将存放页表目录)
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000            # 定义下面的内存数据从偏移地址0x5000开始

/*
 * tmp_floppy_area is used by the floppy-driver when DMA cannot
 * reach to a buffer-block. It needs to be aligned, so that it isn't
 * on a 64kB border.
 */
 # 当DMA(直接存储器访问)不能访问缓冲块时,则_tmp_floppy_area内存块就可以供软盘驱动程序使用
 # 需要保证地址对齐
_tmp_floppy_area:
	.fill 1024,1,0    # 保留1024项,每一项一个字节,填充数值为0


# 下面几个入栈操作为了跳转到init/main.c中main()函数做准备
# 前面三个入栈0值分别表示envp,argv指针和argc的值
after_page_tables:
	pushl $0		# These are the parameters to main :-)  main函数参数 envp
	pushl $0		# argv指针
	pushl $0		# argc
	pushl $L6		# return address for main, if it decides to. 模拟调用main时首先将返回地址入栈的操作
	pushl $_main    # _main--->main
	jmp setup_paging

# main函数到不了这里 ,到标号L6这里,是一个死循环
L6: 
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

/* This is the default interrupt "handler" :-) */
# 下面是默认的中断'向量句柄''
int_msg:
	.asciz "Unknown interruptnr"     # 定义‘未知的指定’
.align 2                               # 按4字节方式对齐内存地址

# 中断处理过程
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds                            # 这里请注意ds,es,fs,gs等虽然是16位的寄存器
	push %es                            # 但仍然会以32位的形式入栈,即需要占用4个字节的栈空间
	push %fs                            # 以上用于保存寄存器
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg 
	call _printk                        # 该函数在 kernel/printk.c 中
	popl %eax                           # 清理参数
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret                                # 中断返回(把中断调用是压入栈的CPU标志寄存器值也弹出)


/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
 # 下面这段子程序通过控制CR0的标志位(PG位31)来启动对内存的分页处理功能,并设置各个表项的内容	
.align 2                    # 按4字节方式对齐内存地址边界
setup_paging:
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */#对(1页目录+4页页表)清零
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */ #目录页从0x00开始

	cld;rep;stosl

	# 设置页目录表中的项,内核中有4个页表需要设置4项
	movl $pg0+7,_pg_dir		/* set present bit/user r/w */# pg0+7:0x000010007,页目录表中的第一项
	movl $pg1+7,_pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,_pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,_pg_dir+12		/*  --------- " " --------- */

	# 填写4个页表中所有内容:4(页表)*1024(项)=4096(项)
	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b

	# 设置页目录表基址寄存器CR3的值(保存的页目录表的物理地址),指向页目录表,
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax

	# 设置启用分页处理,(cr0的标志PG,位31)
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */


# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret
# 返回指令的作用将main程序压入栈中的地址弹出,并转到init/main.c去运行
.align 2
.word 0

# 加载中断描述符表寄存器idtr的lidt指令
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long _idt
.align 2
.word 0

# 加载全局描述符表寄存器gdtr的lgdt指令
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long _gdt		# magic number, but it works for me :^)

	.align 3

_idt:	.fill 256,8,0		# idt is uninitialized

# 全局表,前4项是空项,代码段描述符,数据段描述符,系统调用段描述符
_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

初始化程序—main(1)

(1)小结

1.bootsect.s程序的主要功能:将setup.s和system模块加载到内存中,并且将自身移动到0x90000处,然后控制权交给setup.s程序

2.setup程序:利用BIOS获取硬件参数并保存(main.c会用到的);将system移动到0x00000;描述符表寄存器设置;硬件中断设置;设置CR0进入32位保护模式,控制权交给head.s

3.head.s程序:初步初始化中断描述符表项(哑中断);检查A20;测试是否有协处理器;初始化内存页目录表;跳转到main.c执行内核初始化

(2)功能描述

系统执行完boot/head.s程序就会将执行权交给main.c

main.c首先利用setup.s取得的系统参数设置系统的根文件设备号以及一些内存全局变量,这些变量指明了主内存的开始地址,系统所拥有的内存容量和作为高速缓存区的末端地址。若定义了虚拟盘,则主内存将适当减少

如同系统功能

在这里插入图片描述

高速缓冲区还要扣除被显存和ROM BIOS占用的部分。

高速缓存区是用于磁盘等块设备临时存放数据的地方,以1k字节为数据块单位。

主内存区的内存由内存管理模块mm通过分页机制进行管理分配,以4k字节为一个内存页单位

内核程序可以自由访问高速缓冲区的数据,但需要通过mm才能使用分配到的内存页面

内核进行所有的方面的硬件初始化工作。包括陷阱门,块设备,字符设备,tty,还包括人工设置的第一个任务(task 0).

所有的初始化工作完成后程序就设置中断允许标志以开启中断,并切换到任务0中允许。

在整个内核初始化完成后,内核将执行权切换到用户模式(任务0),

CPU从0特权级切换到了第3个特权级,然后此时main函数工作就在任务0中,最后系统第一次调用进程创建函数fork(),创建出一个用于运行init() 的子进程(init进程)

内核初始化流程

在这里插入图片描述

1.main.c程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到任务0(进程0)中运行,并使用fork/调用首次创建出进程1(inti进程),并在其中调用min()函数。在该函数中程序将继续进行应用环境的初始化并执行shell登录程序。而原进程0则会在系统空闲时被调度执行,因此进程0通常也被称为idle进程。此时进程0仅执行pause()系统调用,并又会调用调度函数。

2.init 函数的功能可分为4个部分:

①安装根文件系统;

②显示系统信息;

③运行系统初始资源配置文件re中的命令;

④执行用户登录shell程序。

3.代码首先调用系统调用setup(),用来收集硬盘设备分区表信息并安装根文件系统。在安装根文件系统之前,系统会先判断是否需要先建立虚拟盘。若编译内核时设置了虚拟盘的大小,并在前面内核初始化过程中已经开辟了一块内存用作虚拟盘,则内核就会首先尝试把根文件系统加载到内存的虚拟盘区中。

4.然后inii打开一个终端设备tty0,并复制其文件描述符以产生标准输入stdin、标准输出stdout和错误输出water设备。内核随后利用这些描述符在终端上显示一些系统信息,例如高速缓冲区中缓冲块总数、主内存区空闲内存总字节数等。

5.接着init又新建了一个进程(进程2),并在其中为建立用户交互使用环境而执行一些初始配置操作,即在用户可以使用shell命令行环境之前,内核调用/bin/sh程序运行了配置文件etc中设置的命令。文件的作用与DOS系统根目录上的AUTOEXEC.BAT文件类似。这段代码首先通过关闭文件描述符D,并立刻打开文件/te陵rù,从而把标准输入stdm定向到etc/re文件上。这样,所有的标准输入数据都将从该文件中读取。然后内核以非交互形式执行/bin/sh,从而实现执行/etc.re文件中的命令。当该文件中的命令执行完毕后,/bin/sh就会立刻退出。因此进程2也就随之结束。

6.init函数的最后一部份用于在新建进程中为用户建立一个新的会话,并运行用户登录shell程序,/bin/sh。在系统执行进程2中的程序时,父进程(mit进程)一直等待着它的结束。随着进程2的退出,父进程就进入到一个无限循环中。在该循环中,父进程会再次生成一个新进程,然后在该进程中创建一个新的会话,并以登录shell方式再次执行程序/bin/sh,以创建用户交互shell环境。然后父进程继续等待该子进程。登录shell虽然与前面的非交互式shell是同一个程序/bin/sh,但是所使用的命令行参数( argV[ ])不同。登录shell的第0个命令行参数的第1个字符一定是一个减号比。这个特定的标志会在/bin/sh执行时通知它这不是一次普通的运行,而是作为登录shell运行bin/sh的。从这时开始,用户就可以正常使用Linux命令行环境了,而父进程随之又进入等待状态。此后若用户在命令行上执行了exit或Ilogount命令,那么在显示一条当前登录shell退出的信息后,系统就会在这个无限循环中再次重复以上创建登录shell进程的过程。

7.任务1中运行的init()函数的后两部分实际上应该是独立的环境初始化程序mit等的功能。

3.内联函数

由于创建新进程是通过完全复制父进程的代码段和数据段,因此在首次使用fork()创建进程的时候,为了确保新进程 用户态栈中没有进程0的多余信息,要求进程0在创建首个新进程(进程1)之前,不要使用其用户态栈,即要求任务0不要调用函数。

所以在main程序移动到任务0执行后,任务0中的代码fork()不能以函数形式进行调用,从而引入了gcc函数内嵌形式来执行这个系统调用。

static inline _syscall0(int ,fork) —>内联函数 通过声明一个内联函数,可以让gcc把函数的代码集成调用到它的代码中 省去函数调用的内存,提高代码执行速度 注;这里的0表示后面为参数,若有一个参数则是_syscall1

-syscall0()是unistd.h中内嵌宏代码,以嵌入式汇编形式调用Linux的系统调用中断 int 0x80,也即是int fork()创建进程系统调用

gcc语法文章详情:

添加链接描述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define _syscall0(type,name) 
type name(void) 
{ 
long __res;                     // 声明一个寄存器
__asm__ volatile ("int $0x80"   //调用系统中断 0x80
	: "=a" (__res)              //将返回值-->eax(_res) 输出寄存器
	: "0" (__NR_##name));     //输入为系统中断调用号_NR_name 输入寄存器
if (__res >= 0) 
	return (type) __res; 
errno = -__res; 
return -1; 
}

(4)CMOS

PC机的CMOS内存大小为128或者64字节内存块,是系统实时时钟芯片RTC的一部分,保存时间和日期信息,存放的格式是BCD码

要访问CMOS需要通过端口0x70(地址端口),0x71(数据端口)

CMOS64字节信息:

image-20230801232102695

初始化程序—main(2)

一、源码分析

系统初始化程序init/main.c主要功能是对系统进行初始化,并切换到用户模式下执行登录程序

(1)库文件介绍

 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
//  宏定义__LIBRARY__为了包括在unistd.h中内嵌汇编代码
#define __LIBRARY__
// *.h所在头文件默认目录在include/
// unistd.h是标准符号常数与类型文件,定义了各种符号常数和类型,并声明了各种函数
// 若定义了_LIBRARY_,则还会包含系统调用号和内嵌汇编代码如syscall0
#include <unistd.h>
// 时间类型头文件,定义了tm结构和关于时间的函数原型
#include <time.h>


#include <linux/tty.h>   //定义了tty_io,串口通信方面的参数,常数
#include <linux/sched.h> //调度,定义了任务结构体task_struct,第一个初始任务的数据
#include <linux/head.h>  //定义了段描述符的简单结构和几个选择符常量
#include <asm/system.h>  //以宏的形式定义了有关描述符参数设置或修改描述符,中断门等汇编程序
#include <asm/io.h>      //以嵌入式汇编程序形式定义了对io端操作的函数

#include <stddef.h>     //标准定义头文件,定义了NULL,TYPE,MEMBER
#include <stdarg.h>		//标志参数头文件,以宏的形式定义了变量参数列表
						//一个类型:va_list,三个宏:va_start,va_arg,va_end
						//vsprintf,vprintf,vfprintf
#include <unistd.h>		//标准符号常数与类型文件,定义了各种符号常数和类型,并声明了各种函数
#include <fcntl.h>      //文件控制头文件,用于文件及其描述符的操作控制常数符号的定义	
#include <sys/types.h>  //类型头文件,定义了基本的系统数据类型

#include <linux/fs.h>   //文件系统头文件,定义了文件表结构(file,buffer_head,m_inode)

(2)CMOS读取时间

 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
//读取CMOS实时时钟信息
//outb_p端口输出宏定义,inb_p端口输入宏定义
//0x70是写地址端口号 0x80|addr是要读取CMOS内存地址,0x71是度数据端口号
#define CMOS_READ(addr) ({ 
outb_p(0x80|addr,0x70); 
inb_p(0x71); 
})


//将BCD码转化为二进制数值
//(val)&15取BCD码第四位也就是个位,val)>>4右移四位只剩高四位也就是十位
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)


//该函数取CMOS实时中信息作为开机时间,并保存到全局变量startup_time
static void time_init(void)
{
	struct tm time;                         //时间结构体

	do {                                    //从CMOS内存列表中读取时间
		time.tm_sec = CMOS_READ(0);         //秒值(BCD码形式)
		time.tm_min = CMOS_READ(2);
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);
	} while (time.tm_sec != CMOS_READ(0));
	BCD_TO_BIN(time.tm_sec);                 //转换位2进制数值
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;
	startup_time = kernel_mktime(&time);      //计算开机时间
}

(3)内核所有初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//内核进行所有方面的初始化
	mem_init(main_memory_start,memory_end); //主内存区初始
	trap_init();                            //陷阱门(硬件中断向量)初始化
	blk_dev_init();                         //块设备初始化
	chr_dev_init();                         //字符设备初始化
	tty_init();                             //tty初始化
	time_init();                            //设置开机启动时间
	sched_init();                           //调度程序初始化(加载任务0的tr,ldtr)
	buffer_init(buffer_memory_end);         //缓冲管理初始化,建内存链表
	hd_init();                              //硬盘初始化
	floppy_init();                          //软盘初始化
	sti();                                  //所有初始化完成,开启中断

	//下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行
	move_to_user_mode();                    //切换到用户模式

(4)切换到用户模式

1
2
3
4
5
//下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行
	move_to_user_mode();                    //切换到用户模式
	if (!fork()) {		/* we count on this going ok */
		init();         //在新建的子进程中运行
	}

二、完整源码

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */
//  宏定义__LIBRARY__为了包括在unistd.h中内嵌汇编代码
#define __LIBRARY__
// *.h所在头文件默认目录在include/
// unistd.h是标准符号常数与类型文件,定义了各种符号常数和类型,并声明了各种函数
// 若定义了_LIBRARY_,则还会包含系统调用号和内嵌汇编代码如syscall0
#include <unistd.h>
// 时间类型头文件,定义了tm结构和关于时间的函数原型
#include <time.h>

/*
 * we need this inline - forking from kernel space will result
 * in NO COPY ON WRITE (!!!), until an execve is executed. This
 * is no problem, but for the stack. This is handled by not letting
 * main() use the stack at all after fork(). Thus, no function
 * calls - which means inline code for fork too, as otherwise we
 * would use the stack upon exit from 'fork()'.
 *
 * Actually only pause and fork are needed inline, so that there
 * won't be any messing with the stack from main(), but we define
 * some others too.
 */

/*
在内核空间创建进程将会导致没有写时复制,
为了保证不使用任务0的用户栈:
main在移动到用户模式(到任务0)后执行内嵌方式的fork()和pause()
在执行move_to_user_mode()后,main就以任务0的方式运行
任务0是所有子进程的父进程
当它创建子进程时,由于任务1属于内核空间,无写时复制
任务0的用户空栈就是任务1的用户栈,即他们共用一个栈空间
因此在运行任务0时不要对堆栈有任何的操作
但在再次执行fork()并执行过execve()后,被加载的程序也不属于内核空间
*/

// 定义内联函数
static inline _syscall0(int,fork) // 实际上是 int fork()创建进程系统调用
static inline _syscall0(int,pause) //int pause()系统调用 :暂停进程的执行,直到收到一个信息
static inline _syscall1(int,setup,void *,BIOS) // int setup(void *,BIOS)系统调用  用于linux初始化
static inline _syscall0(int,sync) // int sync()系统调用:更新文件系统

#include <linux/tty.h>   //定义了tty_io,串口通信方面的参数,常数
#include <linux/sched.h> //调度,定义了任务结构体task_struct,第一个初始任务的数据
#include <linux/head.h>  //定义了段描述符的简单结构和几个选择符常量
#include <asm/system.h>  //以宏的形式定义了有关描述符参数设置或修改描述符,中断门等汇编程序
#include <asm/io.h>      //以嵌入式汇编程序形式定义了对io端操作的函数

#include <stddef.h>     //标准定义头文件,定义了NULL,TYPE,MEMBER
#include <stdarg.h>		//标志参数头文件,以宏的形式定义了变量参数列表
						//一个类型:va_list,三个宏:va_start,va_arg,va_end
						//vsprintf,vprintf,vfprintf
#include <unistd.h>		//标准符号常数与类型文件,定义了各种符号常数和类型,并声明了各种函数
#include <fcntl.h>      //文件控制头文件,用于文件及其描述符的操作控制常数符号的定义	
#include <sys/types.h>  //类型头文件,定义了基本的系统数据类型

#include <linux/fs.h>   //文件系统头文件,定义了文件表结构(file,buffer_head,m_inode)

static char printbuf[1024]; //静态字符串数组,用作内核显示信息的缓存

extern int vsprintf();         //格式化输出到一字符串中
extern void init(void);        //初始化
extern void blk_dev_init(void);//块设备初始化
extern void chr_dev_init(void);//字符设备初始化
extern void hd_init(void);	   //硬盘初始化
extern void floppy_init(void); //软驱初始化
extern void mem_init(long start, long end);       //内存管理初始化
extern long rd_init(long mem_start, int length);  //虚拟盘初始化
extern long kernel_mktime(struct tm * tm);        //计算系统开机启动时间(s)
extern long startup_time;                         //内核启动时间(s)

/*
 * This is set up by the setup-routine at boot-time
 */
//这些数据在内核引导期间由setup设置
//将指定的线性地址强行转换为给的数据类型的指针,并获取指针所指的内容
#define EXT_MEM_K (*(unsigned short *)0x90002)       //1MB以后的扩展内容大小
#define DRIVE_INFO (*(struct drive_info *)0x90080)   //硬盘参数表的32字节内容
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)   //根文件系统所在设备号

/*
 * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
 * and this seems to work. I anybody has more info on the real-time
 * clock I'd be interested. Most of this was trial and error, and some
 * bios-listing reading. Urghh.
 */

//读取CMOS实时时钟信息
//outb_p端口输出宏定义,inb_p端口输入宏定义
//0x70是写地址端口号 0x80|addr是要读取CMOS内存地址,0x71是度数据端口号
#define CMOS_READ(addr) ({ 
outb_p(0x80|addr,0x70); 
inb_p(0x71); 
})


//将BCD码转化为二进制数值
//(val)&15取BCD码第四位也就是个位,val)>>4右移四位只剩高四位也就是十位
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)


//该函数取CMOS实时中信息作为开机时间,并保存到全局变量startup_time
static void time_init(void)
{
	struct tm time;                         //时间结构体

	do {                                    //从CMOS内存列表中读取时间
		time.tm_sec = CMOS_READ(0);         //秒值(BCD码形式)
		time.tm_min = CMOS_READ(2);
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);
	} while (time.tm_sec != CMOS_READ(0));
	BCD_TO_BIN(time.tm_sec);                 //转换位2进制数值
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;
	startup_time = kernel_mktime(&time);      //计算开机时间
}

static long memory_end = 0;                  //机器具有的物理内存容量
static long buffer_memory_end = 0;           //高速缓冲区末端地址
static long main_memory_start = 0;           //主内存(将用于分页)开始的位置

struct drive_info { char dummy[32]; } drive_info; //用于存放硬盘参数表信息


//内核初始化主程序,初始化结束以后将以任务0(idle任务位空闲任务)的身份运行
void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
//此时中断被关闭,做完必要的设置后就将其开启

 	ROOT_DEV = ORIG_ROOT_DEV;                     //保存根设备号 -->ROOT_DEV

 	drive_info = DRIVE_INFO;                      //保存0x90080磁盘参数表内容

	memory_end = (1<<20) + (EXT_MEM_K<<10);       //机器内存数--->memory_end ,内存大小=1MB+扩展内存(k)*1024字节
	memory_end &= 0xfffff000;                     //忽略不到4kb(1页)的内存数
	if (memory_end > 16*1024*1024)                //大于16MB 则等于16MB
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024)                //内存>12MB,高速缓冲末端=4mb
		buffer_memory_end = 4*1024*1024;          //高速缓冲末端地址--->buffer_memory_end
	else if (memory_end > 6*1024*1024)             //内存>6MB,高速缓冲末端=4mb
		buffer_memory_end = 2*1024*1024;           
	else                                           //否则高速缓冲末端=1mb
		buffer_memory_end = 1*1024*1024;

	main_memory_start = buffer_memory_end;        //主内存开始地址=高速缓冲末端


//如果在Makefile中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘(主内存减少)
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif

//内核进行所有方面的初始化
	mem_init(main_memory_start,memory_end); //主内存区初始
	trap_init();                            //陷阱门(硬件中断向量)初始化
	blk_dev_init();                         //块设备初始化
	chr_dev_init();                         //字符设备初始化
	tty_init();                             //tty初始化
	time_init();                            //设置开机启动时间
	sched_init();                           //调度程序初始化(加载任务0的tr,ldtr)
	buffer_init(buffer_memory_end);         //缓冲管理初始化,建内存链表
	hd_init();                              //硬盘初始化
	floppy_init();                          //软盘初始化
	sti();                                  //所有初始化完成,开启中断

	//下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行
	move_to_user_mode();                    //切换到用户模式
	if (!fork()) {		/* we count on this going ok */
		init();         //在新建的子进程中运行
	}
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
	//运行任务0
	//pause()系统调用会把任务0转换成可中断等待状态,再执行调度函数
	//若调度函数发现系统中没有没有其他任务可以运行就会切换到任务0,而不依赖任务0的状态	
	for(;;) pause();
}

//printf()产生格式化信息输出到标准设备stdout(1),在屏幕上显示
// 参数fmt:指定输出将采用的格式
static int printf(const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));
	va_end(args);
	return i;
}

//读取并执行/etc/rc文件所使用的命令行参数和环境参数
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };

//运行登录shell时所使用的命令行参数和环境参数
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

//在main()中进行了系统初始化,包括内存管理,各种硬件设备和驱动设备
//而init()运行在任务0第一次创建的子进程,对第一个要执行的shell程序的环境进行初始化
//然后以登录shell方式加载该程序并执行
void init(void)
{
	int pid,i;

	//setup()用于读取硬盘参数包含分区表信息并加载虚拟信息和安装根文件系统设备
	setup((void *) &drive_info);

	(void) open("/dev/tty0",O_RDWR,0); //终端控制台
	(void) dup(0);
	(void) dup(0);
	//打印缓冲区块数和总字节数,每块1024字节,以及主内存区空闲内存字节数
	printf("%d buffers = %d bytes buffer spacenr",NR_BUFFERS,
		NR_BUFFERS*BLOCK_SIZE);
	printf("Free mem: %d bytesnr",memory_end-main_memory_start);

	//创建一个子进程(任务2) -->返回值为 =0,父进程-->返回值 = 子进程pid

	//创建失败
	if (!(pid=fork())) {
		close(0);
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);
		execve("/bin/sh",argv_rc,envp_rc);
		_exit(2);
	}

	//父进程
	if (pid>0)
		while (pid != wait(&i))  //等待子进程结束。&i存放返回状态信息的位置
			/* nothing */;

	//上一个进程结束,下面循环在创建一个子进程
	while (1) {
		//创建失败
		if ((pid=fork())<0) {
			printf("Fork failed in initrn");
			continue;
		}
		//新的子进程
		if (!pid) {
			close(0);close(1);close(2);
			setsid();                    		//创新会话期
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh",argv,envp));
		}
		while (1)
			if (pid == wait(&i))
				break;
		printf("nrchild %d died with code %04xnr",pid,i);
		sync();                                 //同步操作,刷新缓冲区
	}
	_exit(0);	/* NOTE! _exit, not exit() */
	//_exit() 终止一个函数属于sys_exit系统调用
	//exit() 终止一个函数,属于库函数,它会先执行一些清除操作,然后调用_exit()
}