从01开始 从01开始
首页
  • 计算机科学导论
  • 操作系统
  • 计算机网络
  • 关于本站
  • 网站日记
  • 赞赏支持
  • 如何搭建一个博客
  • 如何搭建一个邮箱
GitHub (opens new window)

peterjxl

人生如逆旅,我亦是行人
首页
  • 计算机科学导论
  • 操作系统
  • 计算机网络
  • 关于本站
  • 网站日记
  • 赞赏支持
  • 如何搭建一个博客
  • 如何搭建一个邮箱
GitHub (opens new window)
  • 计算机科学导论

  • 操作系统

    • 我的操作系统学习笔记

      • 学习操作系统之前
      • 从操作系统启动开始
        • 打开电源之后……
        • 准备Linux源码
        • 引导扇区代码:bootsect.s
        • setup.s
        • head.s
        • main.c
        • 谈点题外话:操作系统是怎么生成的
        • 参考
      • 操作系统接口
      • 操作系统历史与学习任务
      • 如何高效管理CPU:并发和进程
      • 如何支持多进程
      • 用户级线程
    • 我的操作系统实验笔记

    • 操作系统
  • Linux

  • 编译原理

  • 计算机网络

  • 数据库

  • 计算机基础学习路线
  • 计算机基础
  • 操作系统
  • 我的操作系统学习笔记
2022-10-23
目录

从操作系统启动开始

# 1. 从操作系统启动开始

‍

‍

我们从计算机的启动开始,讲解开机的时候,计算机内部到底发生了什么。这是大家每次开机都会看到的画面,也是第一幅画面,从这里开始合情合理;并且安排了实验一,控制计算机的启动。

image​

‍

# 打开电源之后……

打开电源之后,计算机就开始工作了,那么计算机是如何工作的呢?

我们简单回顾下计组里的内容:计算机是在一个计算模型下设计出来的,也就是说计算机是计算模型的一种实现,最著名的模型就是图灵提出的图灵机(可以回顾计算机简史)。

图灵机是怎么定义的呢?实际上图灵机借鉴了人在计算的过程:例如我们计算3+2,我们用眼睛看到3+2后,就知道要做加法,然后在脑海里运算,得到结果是5,然后将答案写出。而图灵机也是这样,当图灵机在纸带上读到3+2这条指令后,就知道要做加法,并且用运算器 得出结果,并写回到纸带上。

计算机的工作原理无非4个字:取指执行。大家一定一定要牢记这4个字。CPU从内存中取出指令,如果是加法指令,就执行加法; 如果是乘法指令,就执行乘法。这就是通用图灵机。同样的,如果我们将应用程序放到内存里,例如浏览器,那么CPU就运行浏览器;如果是Word,CPU就运行Word。

‍

一开始内存是空的,CPU从哪里取指呢?第一条指令是多少?PC指针的初值是多少?这个由硬件设计者决定的。

这就要了解一下硬件的知识。以x86结构为例,刚一上电的时候,内存中有一部分是固化的ROM,叫做ROM BIOS。BIOS全称Basic Input Output System,基本输入输出系统。也就是说,总得有一个基本的输入输出,如果什么都没有,CPU是做不到取指执行的。那么这一段指令做了什么事情呢?

  1. 以x86为例,一上电的时候,CPU处于实模式
  2. CS寄存器 = 0xFFFF, IP = 0x0000
  3. 因此CPU就会去执行 0xFFFF:0x0000处的指令,也就是内存 0xFFFF0处的ROM BIOS的指令
  4. BIOS处的指令首先会检查外设,例如CPU,键盘,显示器,硬盘等
  5. 然后会将磁盘0磁道0扇区的内容读到内存的0x7C00处
  6. 最后设置CS = 0x7C00, IP = 0x0000,开始执行引导扇区里的代码。

‍

关于第4步的补充:如果有给电脑加过内存条的经历应该知道,加内存条后的第一次开机,计算机会在屏幕上提示 The amount of system memory has changed. 也就是计算机检测到内存总量变化了,是否继续开机:   

‍

关于第5步的补充:在计组里我们学过,一个扇区是512个字节的。而0磁道0扇区就是操作系统的引导扇区,因此就是将引导扇区里的指令读到内存里,并开始执行。这也就是操作系统的第一段代码。

‍

# 准备Linux源码

接下来我们会讲Linux的部分源码,同学们可以先准备下,接下来我们仅仅会挑部分重点代码进行解读。

Linux 0.11 版本的代码地址:http://oldlinux.org/Linux.old/Linux-0.11/sources/system/linux-0.11.tar.Z

也可以在我的Gitee上下载:https://gitee.com/peterjxl/learn-os

本段代码很小,不到500k; 代码里也有详细的注释。

我们接下来讲的bootsect.s,setup.s 和head.s 的路径:linux-0.11\boot\。该目录下也就只有这几个文件

‍

‍

# 引导扇区代码:bootsect.s

引导扇区里的代码:是一段汇编代码,文件名是bootsect.s。为什么第一段代码是汇编代码呢?因此我们要对计算机进行精细的控制,而C语言的代码编译之后,它的内存位置是人为不可控的(比如自动分配栈),而汇编就不一样了,汇编中的每一条指令最后都变成了这都变成了真正的机器指令,所以你可以对它进行完整的控制。**而在引导过程中,你当然要对他进行完整的控制,绝对不能有任何差错或者细小的出入的控制。**在操作系统中有很多地方都要实现这样精细的控制。我们会挑bootsect.s的一些关键代码来解读

‍

‍

‍

# 第一件事:将代码挪到0x9000处执行

SETUPLEN = 4			! nr of setup-sectors
BOOTSEG  = 0x07c0		! original address of boot-sector
INITSEG  = 0x9000		! we move boot here - out of the way
SETUPSEG = 0x9020		! setup starts here
entry start
start:
	mov	ax,#BOOTSEG
	mov	ds,ax
	mov	ax,#INITSEG
	mov	es,ax
	mov	cx,#256
	sub	si,si
	sub	di,di
	rep	movw
	jmpi	go,INITSEG		!段间跳转 cs=INITSEG, ip=go
go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax

‍

我们来解读这段代码

1 ~ 4行可以理解为定义了一个变量,值是我们后续会用到的地址

第5行:关键字entry告诉链接器“程序入口” 从start 标号开始

第6行:CPU开始执行第5行的指令(上一句表明了程序的入口)

‍

接下来执行完这两句后,ds = 0x07c0 :

mov	ax,#BOOTSEG
mov	ds,ax

‍

‍

同理,执行完这两句后,es = 0x9000 :

mov	ax,#INITSEG
mov	es,ax

‍

‍

接下来做什么呢?就是将bootsect.s里的代码,全部挪到内存 0x9000处:使用的是rep指令,复制256个字,也就是512字节,刚好一个扇区,从0x7c00开始的一个扇区的代码。为什么要将本段代码移动到9000,我们后续再讲

	mov	cx,#256
	sub	si,si
	sub	di,di
	rep	movw

‍

接下来执行一个跳转指令:

jmpi	go,INITSEG		!段间跳转 cs=INITSEG, ip=go

也就是将0x9000作为基址,go作为偏移地址。我们可以推理出来,既然bootsect.s已经挪到 0x9000处, 我们就应该跳转到 0x9000处,继续往下执行bootsect.s处的代码,且我们可以看到go标号就在 跳转指令的下一行,所以其实就是继续往下执行bootsect.s。

‍

# 第二件事:加载setup模块

接下来bootsect.s 做了什么事情呢?我们继续看一些关键的代码:

go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax
	mov	ss,ax
	mov	sp,#0xFF00		! arbitrary value >>512 
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	ok_load_setup		! ok - continue
	mov	dx,#0x0000
	mov	ax,#0x0000		! reset the diskette
	int	0x13
	jmp	load_setup

当执行到go处的代码时,CS = 9000,而go标号处 到 load_sectup标号处的代码,将段寄存器DS,ES,SS的值都设置为了9000。因此目前所有段寄存器的值都是 0x9000

下一步,就是用0x13号中断 ,将setup模块从磁盘加载到内存里。因为我们一开始只将引导扇区读入进来了,而操作系统还有很多内容要读进来。

‍

‍

我们来解读下load_setup代码:

在执行13号中断之前,我们要先传参给中断例程,其中

  • ah是功能号 02表示读磁盘。 00表示磁盘系统复位
  • al 是读取扇区的数量,因为ax = 0x0200 + SETUPLEN,因此al = 4,表示读取4个扇区
  • ch 是柱面号, 这里表示读取0柱面号,
  • cl 是要读取的扇区。这里是2,表明读2号扇区(这里需要说明一下,其实扇区是从1开始编号的,而第一个扇区是引导扇区,因此读下一个扇区就是2号扇区;而之前说的0磁道0扇区指的是 0号逻辑扇区,其对应的是1号物理扇区。如果不理解也没关系,记住是读下一个扇区即可)
  • dh 是磁头号, dl是驱动器号 这里都是0,表明还是读0磁道 0号驱动器
  • es:bx 内存地址,用于存放磁盘里的内容

传递参数后,就执行13号中断,下一行代码是 jnc ok_load_setup​如果正常读入了,就跳转到 ok_load_setup处执行代码。而如果有异常,就将磁盘系统复位,继续尝试读取sectup扇区(无条件跳转 jmp load_setup​)。

‍

因此load_setup就是读取4个扇区的内容到 0x90200 处。十六进制是200,转换成十进制就是512,因此就是sectup模块在内存的地址就是紧挨着bootsect.s 。内存示意图如下:

内存地址 内存的内容
0xFFFF0 ROM BIOS区
.......... .............
0x90200 setup模块
0x90000~0x901FF bootsect.s模块
.............. ....................

‍

‍

# 第三件事:显示开机画面并读system模块

读入setup模块后,接下来做什么呢?setup模块只有4个扇区,操作系统当然不只这么少代码,因此读入了setup模块后,继续读入操作系统的system模块。我们来看看ok_load_setup的关键代码:

ok_load_setup:
	mov	ch,#0x00
	mov	sectors,cx
	mov	ax,#INITSEG
	mov	es,ax


	mov	ah,#0x03
	xor	bh,bh
	int	0x10	; ah 功能号为3,表示读取光标位置

	mov	cx,#24
	mov	bx,#0x0007
	mov	bp,#msg1
	mov	ax,#0x1301
	int	0x10	; ah功能为为13,作用是显示字符串,es:bp是串地址,cx是串长度

	mov	ax,#SYSSEG
	mov	es,ax		! segment of 0x010000
	call	read_it   ; 这里就是读入system模块
	jmpi	0,SETUPSEG  ; 跳转到0x09020:0000处执行代码,也就是执行setup.s


; ...........这里省略一些代码, 以下这段代码在bootsect.s的244行.......
msg1:
	.byte 13,10
	.ascii "Loading system ..."
	.byte 13,10,13,10

‍

我们来分析下上述代码做了什么:

  1. 首先是用 int 0x10读取光标的位置,
  2. 然后将字符串 "Loading system ..."显示到光标的位置上
  3. 然后执行reat_it函数读入system模块,读到内存 0x10000处,这里我们就不继续展开了
  4. 最后跳转到setup.s处执行,
  5. 至此,bootsect.s 代码执行结束。

‍

其实,第二步就是我们看到的开机画面了,只不过当时显示的比较简单和粗糙。因此,我们可以做实验一了,就是修改一下计算机的开机画面,例如改成 “Hello World!” 。这里也说一下大致的思路:首先修改下msg处的字符串,然后数一下要显示的字符个数,然后修改cx,重新汇编bootsect.s即可。

‍

‍

‍

# bootsect.s小结

bootsect.s做了以下事情:

  1. 将自己挪到0x90000处
  2. 读入setup模块
  3. 读入system模块,并在屏幕上打印 “Loading System”
  4. 将控制权交给setup模块

‍

‍

具体代码执行逻辑:

  1. 通过汇编的rep movw指令,将自己的代码复制到0x9000:0000 处 512字节(256个字,所以cx=256)
  2. 执行跳转指令 jmp go 9000 (go是标号,会赋值给IP, 9000会赋值给CS,作为段基址)。其实就是顺序执行,只不过因为在第一步里将自己挪到了9000,所以CS:IP也指到那里去
  3. 目前bootset.s的段基址是9000,然后长度是256个字,引导扇区在内存里的结束地址是90200H
  4. 然后根据13号中断,从第二个扇区开始(第一个扇区是bootsect.s),读取setup的4个扇区的内容(al存放扇区数量),也就是2kb
  5. setup的代码放到哪里?紧接着bootsect.s,也就是从90200H开始, es:bx就是指向90200H
  6. 然后在屏幕上显示loding system(通过int 10h中断)
  7. 然后call read_it 读入system模块,读入到0x10000处
  8. 然后执行 jmpi 0, SETUPSEGMENT,也就是将代码转到setup的内存开始执行指令
  9. 至此,bootsect.s结束

‍

‍

# setup.s

目前操作系统还在启动中,还需要精细的控制,因此setup也是一段汇编。首先我们根据文件名可以联想到:setup应该是完成OS启动前的一些设置。我们还是挑部分重点代码来看。

‍

# 第一件事:获取硬件参数

start:
	mov	ax,#INITSEG ; INITSEG 在setup.s里的第17行 定义为0x9000
	mov	ds,ax   ; ds = 0x9000
	mov	ah,#0x03
	xor	bh,bh
	int	0x10	 ; 10号中断的3号功能 读光标位置
	mov	[0],dx	; 取出光标位置 放到0x9000处


	mov	ah,#0x88
	int	0x15
	mov	[2],ax   ; 获取扩展内存大小,并放到0x9000处

	mov	ah,#0x0f
	int	0x10	; 获取显卡参数  放到0x9000处
	mov	[4],bx
	mov	[6],ax

	mov	ah,#0x12
	mov	bl,#0x10
	int	0x10    ; 获取根设备号  放到0x9000处
	mov	[8],ax
	mov	[10],bx
	mov	[12],cx

‍

我们可以解读下这段代码:首先获取光标,内存,显卡等参数(后面还有很多代码获取硬件参数,这里不表),并放到0x90000处(此时bootsect.s已经不在用到,可以被覆盖)

内存地址 名称
0x90000 光标位置
0x90002 扩展内存数
0x9000c 显卡参数
0x901FC 根设备号

其中,获取扩展内存数是非常重要的。

  1. 什么是扩展内存:早期计算机中,地址总线只有20位,因此只能寻址1M以内的内存;而如今的计算机,都是8G,16G内存起步的,那么通常把1M以后的这些内存就叫扩展内存
  2. 为什么获取内存很重要:操作系统,就是帮我们管理硬件的,而内存就是一个重要的硬件。要管理好内存,首先得知道内存的多大。

我们再谈谈,为什么这段代码的文件名是setup.s ? 要管理好硬件,就得设置一些数据结构去保存这些信息。就好比学校里管理学生,就会有一个学生信息表来存储学生的信息,方便查询、修改和删除等;不仅仅是内存,还有光标,显卡参数等,获取完这些信息后先存起来,后面会形成一些数据结构来保存这些信息,这就是为什么叫setup.s 。(实际上操作系统开机就做了2件事,第一件事就是读取操作系统到内存里,第二件事就是setup,初始化,我们后面会慢慢体会到这句话)

‍

‍

# 第二件事:挪动system模块

在获取完硬件参数后的代码如下:

	cli	; 不允许中断
	mov	ax,#0x0000
	cld	
do_move:
	mov	es,ax
	add	ax,#0x1000
	cmp	ax,#0x9000
	jz	end_move
	mov	ds,ax
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000
	rep     ; 将system模块挪到0地址处!
	movsw
	jmp	do_move

do_move,我们可以猜测是移动的意思,那么移动什么呢?

首先 ES被置成0;

在第一次循环中,DS = 1000,然后设置cx,因此就是将 DS:SI 也就是1000:0000处的代码,挪到ES:DI 也就是 0000:0000处。而在bootsect.s 里,1000:0处的代码就是system模块。因此本段代码的作用就是循环,将system模块挪到 0 地址处。(每次复制system模块的 0x8000个字节到0地址处,然后ax自增,判断是否复制完了,没有则继续循环)

那么接下来我们就可以回到之前留下的一个问题:为什么bootsect.s 会将自己从 0x7c00处挪到9000处?因为要给system模块腾出空间。system模块很长,会覆盖到0x7c00处,如果正在执行的代码被覆盖了,肯定是不行的。同时,当时的system模块不会太大,不会覆盖到0x90000处的bootsect和setup模块 (当时system模块不会超过0x80000字节(即512kb),Linux0.11内核只有14000行左右,大概325KB大小)。操作系统诞生之初,功能还比较简单,代码也不会太多。

‍

之后,system模块就会一直在0地址处。而system之后的内存,我们就可以用于运行我们自己的程序了,例如浏览器,Word等。

‍

‍

# 第三件事:进入保护模式,将控制权交给操作系统

setup模块该做的事都差不多了,接下来就是将控制权交给操作系统了,但在这之前,还有一件非常重要的事去做:进入保护模式。

我们知道,早期计算机只支持1M的内存,这指的是早期的寻址方式,只支持1M。早期使用的是段基址 + 偏移地址这样的方式寻址的,这种方式不能满足 4G内存的寻址,因此,我们要切换到一个新的寻址模式。因此CPU接下来会从16位寻址模式(也叫实模式)切换到32位寻址模式(也叫保护模式)。

那么CPU是怎么切换寻址模式呢?根据一个寄存器:CR0 。 如果这个寄存器的最后一位是0,CPU就会用16位模式;如果是1,就用保护模式(其实就是换一条电路去寻址)。

mov	ax,#0x0001
lmsw	ax
jmpi	0,8	

在setup.s第192行,有个lmsw ax,就是将ax的值赋给CR0寄存器,然后接下来的指令,就是用32位寻址模式了。

‍

那么32位寻址模式怎么寻址呢?这就要提到一个非常著名概念叫 GDT(全局描述表Global Descriptor Table),GDT表里面存放的才是基址。 当然这也是硬件帮我们实现的寻址方式(因为硬件快)。如何用GDT寻址?

在16位模式下,代码寻址是用CS:IP 实现的,而在32位模式下,CS不再左移4位产生一个地址,而是用作选择子,换句话说就是CS的内容是GDT表的下标,对应的GDT表项的内容,才是段基址。

因此,32位寻址模式是这样工作的:首先根据CS取出GDT表的内容作为基址,IP还是作为偏移地址,因此来产生一个新的地址,示意图:

image​

‍

同样的,保护模式下,中断例程的寻址方式也发生了变化:仿照GDT表,新建了一个IDT表(中断描述符表Interrupt Descriptor Table),int n 就用n进行查表取出中断例程的地址,然后执行:

image​

‍

‍

‍

那GDT表的内容是什么呢?没有内容的话,CS选了也没意义,因此SETUP里也定义了GDT表(setup.s 的 205行,下面就是IDT表)

gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386
idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L
gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	! gdt base = 0X9xxxx

我们可以看到有很多 word指令,一个word就是16位,而GDT表一个表项占8字节(64位),因此每4个word就是 一个GDT的表项(其中,第一个表项为空不使用)

每个表项的组成如下:

image​

而GTD的下标如何确定呢?依次为0,8,16………… 我们以setup.s 的表为例:

GDT表下标 GDT表内容
16 .word 0x07FF 0x0000 0x9200 0x00C0
8 .word 0x07FF 0x0000 0x9A00 0x00C0
0 .word 0,0,0,0

‍

GDT的相关部分我们介绍完了,我们回到本小结的开始 ,jmpi 0,8​这条指令是如何确定跳转到哪呢?

mov	ax,#0x0001
lmsw	ax
jmpi	0,8	

由于CS是8,因此我们去查8这个下标的内容:

.word	0x07FF  0x0000  0x9A00  0x00C0

‍

而这几个word是如何存放到GDT表的呢?


.word	0x07FF  0x0000  0x9A00  0x00C0
在内存中,从高地址到 低地址极速 0x00C0 9A00  0000  07FF

用二进制展开来就是
0x00C0: 0000 0000 1100 0000
0x9A00: 1001 1010 0000 0000
0x0000: 0000 0000 0000 0000
0x07FF: 0000 0111 1111 1111

因此,我们可以这样将07FF放到0~15的地方,然后0x0000放到16 ~ 31的地方,然后0x9A00的后两个字节,00,放到16~23这个地方

image​

放完后,我们可以看到,段基址就是0。因此,jmpi 0,8​其实就是跳到0地址处去执行。至此,set up的工作到此就完成。

‍

‍

‍

‍

‍

# setup.s 小结

  1. 因为操作系统就是管理硬件的,因此首先得知道硬件的情况:读了一些硬件参数并存到内存里
  2. 把system挪到0地址处,将来操作系统运行的时候,system模块会一直存在那里
  3. 然后启动了保护模式(通过修改CR0寄存器),最后运用应用了32位的汇编指令JMPI 0, 8 跳到了0地址处去执行
  4. 0地址就是system模块,因此后面就是操作系统运行起来了

‍

# head.s

system模块的第一个部分是head.s ,head.s 做了什么呢?

‍

# 初始化GDT, IDT

我们可以看看其关键的代码:还是初始化了GDT和IDT表(之前的setup.s 里建立的GDT只是临时用于跳转而已),现在操作系统是真正的开始工作了,所以还要再次建立这个表

startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs 	; 指向gdt的0x10项(数据段)
	lss _stack_start,%esp  ; 设置系统栈

	call setup_idt  ; 初始化 IDT表
	call setup_gdt  ; 初始化 GDT表

还有其他的一些内容,这里不表。

‍

# 三种汇编格式

我们可以看到,这里的汇编和之前的汇编代码的格式有点不同,因为现在是32位保护模式,用了32位的汇编。我们对汇编的格式做个简单的介绍

(1) as86汇编:能产生16位代码的Intel 8086(386)汇编

mov ds, ax,   ;  ax → ds, 目标操作数在前

‍

(2) GNU as汇编:产生32位代码,使用AT&T系统 V语法(AT&T美国电话电报公司,包含贝尔实验室等,1983年AT&T UNIX支持组发布了系统V)

movl var, %eax  ; (var) → eax
movb -4(%ebp), %al ; 取出一字节

‍

‍

(3) 内嵌汇编,gcc编译x.c会产生中间结果 as汇编文件x.s

__asm__(“汇编语句”
: 输出
: 输入
: 破坏部分描述);

//例如
__asm__(“movb 
%%fs:%2, %%al”  //%2表示addr,
:”=a”(_res)  //a表示使用eax,并编号%0
:”0”(seg),”m”(*(addr))  //0或空表示使用与相应输出一样的寄存器  m表示使用内存
);

‍

其实操作系统确实是一个复杂的工程,光汇编就用了3种:16位汇编(bootsect.s和setup.s),32位汇编(head.s),还有内嵌汇编(在后面讲到的C语言代码里,因为有些指令还是要精细的控制)。这里不展开讲这3种汇编的语法,这些不是本堂课的主线,如果等学完了三种汇编,黄花菜都凉了,因此待后续讲到的时候再简单的说一说和查找资料即可

‍

# 跳转到main

当head.s 执行完后,接下来就是执行 main.c 代码了。

after_page_tables:
	pushl $0	;These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6	; return address for main, if it decides to.
	pushl $_main
	jmp setup_paging
L6:	jmp L6	 	; main should never return here, but just in case, we know what happens.

setup_paging:
	; …………这里省略一些设置页表代码……
	ret	

如何从汇编 跳去执行 C语言的main函数呢?怎么做到的?

我们知道,汇编执行子程序的话,可以通过跳转指令;

C语言执行函数(子程序)的话,用的是调用函数的语句,例如调用方法b(假设需要传参),就用 b(int a, int b)​ 即可。但其实, C语言最后还是会翻译成汇编,然后才被CPU执行。而传参,可以靠栈来实现。

因此,本段汇编代码的一开始几条压栈语句,就是传参给CPU;然后将main函数的地址压到栈中;当setup_paging执行ret后,就回执行函数main了!(执行ret指令后,会将栈里的内容取出作为下一个要执行的代码的地址)

‍

# 如果main返回了……

最后我们来看一个奇怪的语句:

L6:	jmp L6	 	; main should never return here, but just in case, we know what happens.

这段代码不就是死循环吗?为什么要设置成死循环?

其实,操作系统是一个永远不停止的程序。如果一旦main函数停止了,就会跳转到这里,然后死循环,也就是我们常见的死机………… 注释是linus加的,也挺有意思

‍

‍

# head.s小结

head.s做了什么?

  1. 建立GDT和IDT表(用的是32位的汇编),设置页表等(后面会讲)
  2. 执行main.函数(通过汇编跳转到main函数,本质上就是调整PC指针而已)

‍

‍

# main.c

代码路径:init/main.c

我们看一些关键的代码

void main(void)
{	
	// ........省略部分代码	........
	mem_init(main_memory_start,memory_end);
	trap_init();
	blk_dev_init();
	chr_dev_init();
	tty_init();
	time_init();
	sched_init();
	buffer_init(buffer_memory_end);
	hd_init();
	floppy_init();
	sti();
	move_to_user_mode();
	if (!fork()) {
		init();
	}
	// ........省略部分代码	........
}

首先,为什么main函数的参数是void? 其实三个参数分别是envp,argv,argc,但目前版本的main没有使用,且我们在head.s 里可以看到,在push main函数之前,都压栈了3个0,所以这里是没有问题的

其次,我们可以看到,有很多的函数,并且都是带init字眼的,这些就是初始化内存,中断,时钟,硬盘,显示器等(Linux0.11不支持鼠标)

每一个都可以说很久很久,我们这里简单说说mem_init()​,其他的都类似

‍

‍

# ​mem_init()​

​mem_init()​ 顾名思义就是初始化内存的。前面我们提到操作系统就是管理硬件的,内存是一个重要的硬件,因此本函数就是初始化一些数据结构用来保存内存的信息,例如哪些被使用了,哪些是空闲的。

我们看看mem_init()​的部分代码(在linux-0.11\mm\memory.c 中 399行)

#define USED 100
#define PAGING_PAGES (PAGING_MEMORY>>12)
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

void mem_init(long start_mem, long end_mem)
{
	int i;

	HIGH_MEMORY = end_mem;
	for (i=0 ; i<PAGING_PAGES ; i++)
		mem_map[i] = USED;
	i = MAP_NR(start_mem);
	end_mem -= start_mem;
	end_mem >>= 12;
	while (end_mem-->0)
		mem_map[i++]=0;
}

‍

我们先看看参数,start_mem, end_mem是main函数里传参的,而main函数是这样调用的:

static long main_memory_start = 0;
static long memory_end = 0;
//..........省略部分代码
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
//..........省略部分代码
mem_init(main_memory_start,memory_end);

其实内存的大小,在setup.s 里就已经存储到了 0x90000处,通过main函数里读取,然后传个mem_init​函数。

‍

接下来 mem_init​里做什么呢?首先有一个全局变量mem_map,里面的值如果是0,表明内存没有被使用,如果是100,表明是已经被使用了。

所以在 mem_init​首先将 0地址处(也就是自己system模块)的代码标记为已使用;然后将剩余的内存表明为未使用

image​

来看看是怎么标记的。第14行的代码,用了右移运算符,右移12位其实就是除以2的12次方,也就是除以4k(在内存里我们是用页来管理的,后续会详细说)。

这样的话大家可以看到所谓内存初始化就是形成这样一个表格,用这个表格来表示内存中哪些地方是使用的,哪些地方是没使用的,所以后面是没使用的,而前面是使用的。

‍

‍

‍

# main.c小结

  1. 很多很多的初始化,每个都可以讲很久
  2. 这里讲下memory的初始化,建立一个数组,每个数组项就是一页内存,然后置为0,表示没使用过

‍

‍

‍

# 谈点题外话:操作系统是怎么生成的

我们可以画下操作系统在磁盘里的逻辑示意图:

image​

第一个部分存放bootsect.s,第二个部分存放setup模块,第三个部分存放system模块。这个顺序是不能变的,如果有一点点差错,都会死机。那么,操作系统是如何从源代码,编译成我们想要的样子呢?这就得提到make file。

我们平时写C语言的时候,都是由IDE帮我们编译并运行的,不用关心程序运行在内存的哪里;而如果做操作系统,一切的事情都要自己控制。除了要写源码之外,还要确定如何编译操作系统生成镜像(这里镜像可以简单理解为操作系统安装包,英文名Image),也就是make file

初步讲下如何确保磁盘里的第一段代码是bootsect,第二段是setup呢?通过makefile,并且有很多依赖文件,例如head.s,main.s,驱动等等。把这些汇编成.o文件,然后链接起来生成system。

‍

我们可以看到Linux根目录下有个 Makefile文件,我们可以看看里面的关键内容:

Image: boot/bootsect boot/setup tools/system tools/build
 tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) > Image

我们略略说一下,最后操作系统镜像Image,是依赖于bootsect的,还依赖于setup,system,还有很多工具类(tools),最后将这些代码链接起来,生成镜像,然后就可以运行这个镜像了。

‍

‍

# 操作系统启动小结

我们总结下这堂课:

  1. bootsect将操作系统从磁盘读进来,
  2. 而setup获得了一些参数,启动了保护模式,
  3. head初始化了GDT和IDT表,初始化了一些页表,然后跳到mian
  4. mian里又是很多int初始化函数,比如初始化内存,中断,时钟,硬盘,显示器

‍

以下截图来自《Linux内核完全剖析基于0.11内核》第六章

image​

‍

‍

图6-2清晰地显示出Liux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。先看图例

image​

‍

‍

‍

我们本堂课主要讲了引导扇区的代码做了什么事,其实我们可以换一个角度想想,bootsect.s应该做什么?

在刚一上电的时候,操作系统一定还在硬盘上,而计算机的工作原理是取指执行,因此就要运行操作系统,就必须将操作系统读到内存里。所以bootsect.s 应该就是将操作系统从磁盘读入到内存里。

当操作系统读入到内存后,操作系统就要管理硬件,因此需要初始化,读取硬件的参数,形成一些关键重要的数据结构来管理内存(例如mem_map管理内存)

所以,计算机在启动的时候就做了两件事,第一件事情把操作系统读到内存里,而第二步就是初始化操作系统运行需要的一些参数。

‍

‍

# 参考

操作系统(哈工大李治军老师)32讲(全)超清_哔哩哔哩_bilibili (opens new window) 第2课,第3课

《Linux内核完全剖析基于0.11内核》第6.1节 (本书好像已经不再发版了,当当上都是二手书。)

Linux v0.11源码:http://oldlinux.org/Linux.old/Linux-0.11/sources/system/linux-0.11.tar.Z

bootsect.s,setup.s 和head.s 的路径:linux-0.11\boot\。该目录下也就只有这几个文件

‍

在GitHub上编辑此页 (opens new window)
上次更新: 2022/11/3 23:27:41
学习操作系统之前
操作系统接口

← 学习操作系统之前 操作系统接口→

Theme by Vdoing | Copyright © 2022-2022 粤ICP备2022067627号-1 粤公网安备 44011302003646号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式