《Linux内核设计的艺术》笔记
基于linux0.11,主要研究原理,对自己不清楚的地方会有一点个人补充,偶尔会穿插其他版本的对比。

内核版本和发行版本
linux内核和发行版不是一个概念。

linux内核是系统的心脏,是运行程序和管理像磁盘和打印机等硬件设备的核心程序,它提供了一个在裸设备与应用程序间的抽象层。

而发行版本是一个可以高效使用Linux 内核的操作系统,即它涵盖了Linux内核,此外还包含一些GNU程序库和工具,命令行shell,图形界面的X Window系统和相应的桌面环境,如KDE或GNOME,并包含数千种从办公套件,编译器,文本编辑器到科学工具的应用软件。典型的有CentOS、Ubuntu、RedHat、SUSE等。



Linux内核编译解释参考

参考2


相关内容:

实模式Real Mode
是Intel 80286和之后的80x86兼容CPU的操作模式(应该包括8086)。实模式的特性是一个20位的存储器地址空间,可以直接访问BIOS以及周边硬件,没有硬件支持的分页机制和实时多任务概念。从80286开始,所有的80x86CPU的开机状态都是实模式;8086等早期的CPU只有一种操作模式,类似于实模式。

ROM
只读寄存器,现在通常用闪存芯片做ROM,虽然闪存芯片在特定的条件下是可写的,但在谈到主板上存储BIOS的闪存的内存芯片时,业内人士把它看作ROM。

RAM
Random Access Memory,随机存取存储器,常见的内存条就是一类RAM。

CS
code Segment Register,代码寄存器,存在于CPU中,指向CPU当前执行代码在内存中的区域。

数据的存储
三极管的断电与通电,磁性物质的已被磁化与未被磁化,物质平面的凹与凸,都可以表示0与1。
硬盘( FLASH芯片)——硬盘就是采用磁性物质记录信息的,磁盘上的磁性物质被磁化了就表示1,未被磁化就表示0,因为磁性在断电后不会丧失,所以磁盘断电后依然能保存数据。
内存(RAM芯片) ——内存通电后,如果我要把“1010”这个信息保存在内存(现在画的“田”字)中,那么电子就会进入内存的储存空间里。电子是运动没有规律的物质,必须有一个电源才能规则地运动,内存通电时它很安守地在内存的储存空间里,一旦内存断电,电子失去了电源,就会露出它乱杂无章的本分,逃离出内存的空间去,所以,内存断电就不能保存数据了。




启动BIOS

加电的时候,计算机的内存中,准确的说是RAM中,没有任何程序。
软盘(现在已经硬盘替代)里有操作系统的程序,但CPU的逻辑电路被设计为只能运行内存中的程序。
如果想运行软盘中的操作系统,必须将程序加载到RAM中。

BIOS程序只能靠硬件方法启动。
Intel 80x86系列的CPU,包括新型号的CPU的硬件都设计为加电即进入16位实模式状态运行。


补充说明:
加电启动过程
加电后(也就是比如按下电源开关),电源就开始向主板和其他设备供电,此时电压还不太稳定。稳定之后CPU会跳转到0xFFFF0处开始执行指令。
但实际上BIOS在主机板上的一块ROM芯片上。
内存编址的时候,高地址一般给ROM。


将CPU硬件逻辑设计为加电瞬间强行将CS(指令寄存器,Code segment)的值置为0xF000、IP(instruction Pointer)的值置为0xFFF0,这样CS:IP就指向0xFFFF0这个地址位置。
这是一个纯硬件完成的动作。

BIOS程序的入口地址就是0xFFFF0,也就是说,BIOS程序的第一条指令就在这个位置。


BIOS在内存中加载

BIOS程序被固化在计算机主板上的一块很小的ROM芯片里。


补充说明:
关于内存的地址
电脑中一般安装有32MB、64MB、128MB内存(当然现在肯定不止这点),这些内存的每一个字节都被赋予了一个地址,以便CPU访问内存。
32MB的地址范围是0~1FFFFFFH,其中0~FFFFFH的低端内存非常特殊,最初的8086处理器能够访问的内存最大只有1MB,这1MB的低端640KB被称为基本内存。A0000H~BFFFFH保留给显示卡的显存使用,C0000H~FFFFFH则被保留给BIOS使用。其中系统BIOS一般占用了最后的64KB或者更多一点的空间,显卡BIOS一般在C0000H~C7FFFH处,IDE控制器的BIOS在C8000H~CBFFFH处。

BIOS
Basic Input\Output System 基本输入输出系统
它是一组程序,直接对计算机系统中的输入输出设备进行设备级、硬件级的控制。

BIOS和CMOS
BIOS为了提供对系统的最基本、最底层的支持,必须要设置和记录一些信息,比如系统时间、硬盘型号等,但这些信息不能放进BIOS所在的ROM芯片中,而是被放在一块可以随时读写的CMOS芯片里。
CMOS是制作芯片的一种工艺,主板上的CMOS芯片其实是一块RAM芯片。
CMOS芯片由主板上的一块纽扣电池供电,所以无论关机与否,信息都不会丢失。


不同的主机板所用的BIOS也有所不同。
随着BIOS程序的执行,屏幕上会显示显卡的信息、内存的信息等,说明BIOS程序在检测显卡、内存等。
在这个期间,有一项对启动(boot)操作系统很重要的工作,BIOS在内存中建立中断向量表和中断服务程序。


加载中断向量表和中断服务程序

0x00100是256字节 1024字节=4*256字节 = 1KB
以8KB,地址段为0xFE000~0xFFFFF的BIOS程序为例
BIOS程序在内存最开始的位置0x00000用1KB的内存空间(0x00000-0x003FF)构建中断向量表,在紧挨着它的内存空间用256字节构建BIOS数据区(0x00400~0x004FF),在大约57KB后的位置0x0E05B加载8KB左右的与中断向量表相应的若干中断服务程序。
中断向量表中有256个中断向量,每个中断向量占4字节,其中两个字节是CS的值,两个字节是IP的值。每个中断向量都指向一个具体的中断服务程序。


加载操作系统内核程序

现在要执行真正的boot操作了,把软盘中的操作系统程序加载至内存。对于Linux 0.11,计算机分三批加载内核代码。
第一批,BIOS中断int 0x19把第一扇区bootsect的内容加载到内存;第二第三批在bootsect的指挥下,分别把其后的4个扇区和随后的240个扇区的内容加载至内存。

接收中断,加载第一批程序

经过执行一系列BIOS代码之后,计算机完成了自检等操作。
将软盘(现在是硬盘)设置为启动设备,计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收到一个int 0x19中断。
CPU接收到这个中断后,会立即在中断向量表中找到int 0x19中断向量。接下来,中断向量把CPU指向0x0E6F2,这个位置就是int 0x19相对应的中断服务程序的入口地址。

这个中断服务程序的作用就是把软盘第一扇区中的程序加载到内存中的指定位置。

这个中断服务程序的功能是BIOS事先设计好的,代码是固定的,与操作系统无关。
这段BIOS程序所要做的就是 找到软盘 并加载第一扇区。
int 0x19中断向量所指向的中断服务程序,即启动加载服务程序,将软驱0号磁头对应盘面的0磁道1扇区的内容复制至内存0x07C00处。

bootsect
这个扇区里的内容就是Linux 0.11的引导程序,也就是bootsect,其作用就是陆续把软盘中的操作系统程序载入内存。这样制作的第一扇区就称为启动。第一扇区程序的载入,标志着Linux 0.11中的代码即将发挥作用了。

第一扇区中的程序由bootsect.s中的汇编程序汇编而成,现在内存中有了启动代码。

接下来需要执行bootsect把第二批、第三批代码载入内存。

注意
BIOS程序固化在主机板上的ROM中,是根据具体的主机板而不是操作系统设计的。
理论上,计算机可以安装任何合适其安装的操作系统。

下图是一个总览

注意
上图比如BOOTSEG=0x07C0、SETUP=0x9020这些都是寄存器里的值,不是真正在内存里的地址。
真正在内存里的地址需要寄存器联合使用,比如像CS:IP,联合使用才是真正在内存里的地址。后面还会再次说明。


加载第二部分内核代码setup

BIOS已经把bootsect也就是引导程序载入内存了,现在它的作用就是把第二批和第三批程序陆续加载到内存中。
bootsect首先做的工作就是规划内存。
操作系统本身使用的是汇编语言,只有靠操作系统的设计者把内存安排想清楚,确保不会出现代码数据相互覆盖的问题。
boot/bootsect.s linux 0.11版本
对内存位置进行设置,包括要加载的setup程序扇区数、被加载到的位置、内核被加载的位置等。

下面这张图是linux2.xxx的


已经和0.11版本在内核文件结构上发生了很大的变化。
下图是Linux-3.xxx

之前的两个汇编文件已经找不到了,发现了其他的疑似是启动的汇编文件。结果搜索了一下发现,kernel/arch/x86/boot/bioscall.S实现了c函数intcall(),所以对这一点我也很疑惑。
这个问题先放一下。


bootsect的复制
接下来bootsect文件启动程序将它自身从内存0x07C00处复制到内存0x90000处。
在这个复制过程中,ds(0x07C0)寄存器和si(0x0000)联合使用,构成源地址0x07C00;es(0x9000)寄存器和di(0x0000)联合使用,构成目的地址0x90000。

由于两头约定和定位识别,开始时bootsect是被迫加载到0x07C00位置,现在将自身移动到新的位置,说明操作系统开始根据自己的需要安排内存了。

从上图可以看到,复制后,代码还是接着之前运行到的位置接着运行。

bootsect复制到了新的地方,并且要在新的地方继续执行,因为代码的整体位置发生了变化,所以代码中的各个段也会发生变化。
前面已经修改了CS,还需要对DS、ES、SS和SP进行调整。

上面的代码用CS的值0x9000把DS、ES、SS、设置成和代码段寄存器CS相同的位置,并将栈顶指针SP指向偏移地址为0xFF00处。

bootsect复制完后,会继续执行bootsect.s里的代码,后面操作系统不需要完全依赖于BIOS,可以按照自己的意志把代码安排在自己想要的位置。在此期间,对SS和SP进行设置,为栈操作打下基础,程序可以执行更为复杂的数据运算类指令了。


补充说明
DS 数据段寄存器
ES 附加段寄存器
SP 栈顶指针寄存器
SS 栈基址寄存器
以上都是CPU里的一些寄存器

栈:C语言程序的运行时结构中,以先进后出机制运作的内存空间
堆:C中库函数malloc创建、free释放的动态内存空间


加载setup到内存
然后bootsect程序要执行第二步工作,将setup程序加载到内存中。
需要借助BIOS提供的int 0x13中断向量所指的中断服务程序来完成。

等bootsect执行完毕,setup程序就要开始工作了。


加载第三部分内核代码system模块

仍然使用BIOS提供的int 0x13中断,和前面setup的载入差不多,将system模块加载进内存。

至此,操作系统的代码已经全部加载到内存。
不过还要再确定一下根设备号。


注意 两次0x13中断加载setup和system都是由bootsect执行的,就是由操作系统自身的启动代码来完成的。而bootsect加载到内存的int 0x19中断是由BIOS执行的。
int 0x19的中断服务程序只负责把软盘的第一扇区的代码加载到0x07C00位置;而int 0x13的中断服务程序可以根据设计者的意图,把指定扇区的代码加载到内存的指定位置。


bootsect执行:

jmpi 0, SETUPSEG

跳转到0x90200处,就是setup程序加载的位置。CS:IP指向setup程序的第一条指令,setup程序将接着继续执行。

setup执行
setup开始执行,做的第一件事情就是利用BIOS提供的中断服务程序从设备上提取内核运行所需的机器系统数据。其中包括光标位置、显示页面等数据,并分别从中断向量0x14和0x46向量值所指的内存地址处获取硬盘参数表。

这些机器系统数据被加载到内存的0x90000~0x901FC位置,这些数据将在以后main函数执行时发挥重要作用。

BIOS提取的机器系统数据将覆盖bootsect程序所在的部分区域,这些数据后面需要用,在失去价值前不能被覆盖掉。

操作系统内核程序的加载工作已经全部完成



接下来,操作系统要使用计算机在32位保护模式下工作,在此期间要做大量的重建工作,并且持续工作到操作系统的main函数的执行过程中。


补充说明

保护模式
保护模式,是一种80286系列和之后的x86兼容CPU操作模式。保护模式有一些新的特色,设计用来增强多工和系统稳定度,像是 内存保护,分页 系统,以及硬件支援的 虚拟内存。大部分的现今 x86 操作系统 都在保护模式下运行,包含 Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本。
保护模式与实模式相对应。在80286以前,CPU只有实时模式,地址总线有20位,而内存地址是16位,也就是最多能够访问2^20=1M的内存空间。
在80286及以后,内存地址改为16位或32位,至少可以访问到2^32=4G的内存空间。但为了保证后续的CPU能够运行旧的CPU,只能保持向下兼容。因此,80286及以后的CPU首先进入实模式,然后通过切换机制再进入到保护模式。


关中断并且将system移动到内存地址起始位置0x00000

准备工作首先要关闭中断,将CPU的标志寄存器(EFLAGS)中的中断允许标志(IF)置0。这意味着,程序在接下来的执行过程中,无论是否发生中断,系统都不再对此中断进行响应。


补充说明
EFLAGS
标志寄存器,存在于CPU中,32位,包含一组状态标志、控制标志及系统标志。

关中断(cli)和开中断(sti) 操作将在操作系统代码中频繁出现,cli和sti总是在一个完整操作的两头出现,目的是避免中断在此期间的介入。接下来的代码将为操作系统进入保护模式做准备。此处即将进行实模式下中断向量表和保护模式下中断描述符表IDT的交接工作。cli和sti保证了IDT可以完整创建,以避免不可预料中断的进入造成IDT创建不完整或者新老中断机制混用。


接下来,setup程序做了一个影响深远的动作,将位于0x10000的内核程序复制至内存地址起始位置0x00000处。
0x00000这个位置原来存放着由BIOS建立的中断向量表及BIOS数据区。这个复制动作将BIOS中断向量表和BIOS数据区完全覆盖,使它们不复存在。直到新的中断服务体系构建完毕之前,操作系统不再具备相应并处理中断的能力。所以前面要关中断。

这样做的效果

  • 废除BIOS的中断向量表,等于废除了BIOS提供的实模式下的中断服务程序。
  • 收回刚刚结束使用寿命的程序所占内存空间。
  • 让内核代码占据内存物理地址最开始的、天然的、有利的位置。

通过废除BIOS中断向量表,废除16位的中断机制。


设置中断描述符表和全局描述符表

setup程序对中断描述符寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。
对应方式如下图。

GDT
Global Descriptor Table 全局描述符表
系统中唯一的存放段寄存器内容(段描述符)的数组,配合程序进行保护模式下的段寻址。


补充说明:
32位的中断机制和16位的中断机制,在原理上有比较大的差别。
最明显的是16位的中断机制用的是中断向量表,中断向量表的起始位置在0x00000处,这个位置是固定的;
32位的中断机制用的是中断描述符表(IDT),位置是不固定的,可以由操作系统的设计者根据设计要求灵活安排,由IDTR来锁定其位置。
GDT是保护模式下管理段描述符的数据结构,对操作系统自身的运行以及管理、调度进程有重大意义。
因为,此时此刻内核尚未真正运行起来,还没有进程,所以现在创建的GDT第一项为空,第二项为内核代码段描述符,第三项为内核数据段描述符,其余项皆为空。
IDT虽然已经设置,实为一张空表,原因是目前已关中断,无需调用中断服务程序。此处反映的是数据“够用即得”的思想。

创建这两个表的过程可理解为是分两步进行的:
1)在设计内核代码时,已经将两个表写好,并且把需要的数据也写好。
2)将专用寄存器(IDTR、GDTR)指向表。
此处的数据区域是在内核源代码中设定、编译并直接加载至内存形成的一块数据区域。专用寄存器的指向由程序中的lidt和lgdt指令完成。

值得一提的是,在内存中做出数据的方法有两种:
1)划分一块内存区域并初始化数据,“看住”这块内存区域,使之能被找到;
2)由代码做出数据,如用push代码压栈,“做出”数据。
此处采用的是第一种方法。


打开A20,实现32位寻址
打开A20地址线后CPU可以进行32位寻址,最大寻址空间为4GB。虽然物理内存只能支持16MB但是线性寻址空间已经是4GB。

实模式下CPU寻址范围为0~0xFFFFF,共1 MB寻址空间,需要0~19号共20根地址线。进入保护模式后,将使用32位寻址模式,即采用32根地址线进行寻址,第21根(A20)至第32根地址线的选通控制将意味着寻址模式的切换。


为保护模式下执行head.s做准备

为了建立保护模式下的中断机制,setup程序将对可编程中断控制器8259A进行重新编程

8259A:专门为了对8085A和8086/8088进行中断控制而设计的芯片,是可以用程序控制的中断控制器。单个的8259A能管理8级向量优先级中断,在不增加其他电路的情况下,最多可以级联成64级的向量优先级中断系统。

CPU在保护模式下,int 0x00~int 0x1F被Intel保留作为内部(不可屏蔽)中断和异常中断。
如果不对8259A进行重新编程,上述中断将被覆盖。
setup程序通过下面代码的前两行将CPU工作方式设为保护模式。将CR0寄存器第0位(PE)置1,即设定处理器工作方式为保护模式。

CR0寄存器:0号32位控制寄存器,存放系统控制标志。第0位为PE(Protected Mode Enable,保护模式使能)标志,置1时CPU工作在保护模式下,置0时为实模式。


CPU工作方式转变为保护模式,一个重要的特征就是要根据GDT决定后续执行哪里的程序。
前面对GDT的设置都是setup事先安排好的默认设置。
接下来就是setup程序跳转到head程序。
大概就是一句汇编的jump语句。

jmpi 0, 8

这一行代码中的0是段内偏移(段指的应该是代码段),8是保护模式下的段选择符,用于选择描述描述符表和描述符表项以及所要求的特权级。8是1000,最后两位00表示内核特权级,与之相对的用户特权级是11;第三位的0表示GDT,如果是1,则表示LDT;1000的1表示所选的表,这里是GDT的1项,来确定代码段的段基址和段限长等信息。

下图是原书中保护模式开启前后的指令寻址方式对比示意图

其实目前为止我也没能完全理解这个图。

一点补充
全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。


head.s开始执行

在执行main函数之前,先执行三个由汇编代码生成的程序,bootsect、setup、head。之后才执行由main函数开始的c语言编写的内核程序。

head程序的加载方式是先将head.s汇编成目标代码,将用C语言编写的内核程序编译成目标代码,然后链接成system模块。也就是说system模块里既有内核程序也有head程序。并且head程序在前,内核程序在后。

System模块加载到内存后,setup将system模块复制到0x00000位置,由于head在system前面,实际上head程序就在0x00000这个位置。

head程序为调用main函数做准备工作。
并且,用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,就是在0x00000那个位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖。
这意味着head程序自己将自己废弃,main函数开始执行。


标号_pg_dir标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置0x000000。head程序马上就要在此处建立页目录表,为分页机制做准备。这是内核能够掌控用户进程的基础之一。

现在head程序正式开始执行,一切都是为适应保护模式做准备。
上图中,其本质就是让CS的用法从实模式转变到保护模式。在实模式下,CS本身就是代码段基址。在保护模式下,CS本身不是代码段基址,而是代码段选择符。
前面提到的jmpi 0, 8这句代码使CS和GDT的第二项关联,并且使代码基址指向0x000000。

现在开始,要将DS、ES、FS和GS等其他寄存器从实模式转变到保护模式。
执行完对应的代码后,DS、ES、FS、和GS中的值都成0x10。0x10也应该看成二进制的00010000,最后三位和前面那个jmpi 0, 8是一个套路。最后两位表示内核特权级,从后数第3位表示选择GDT,第4、5两位是GDT的2项,也就是第3项(从0开始的)。也就是4个寄存器用的是同一个全局描述符,它们的段基址、段限长、特权级都是相同的。
影响段限长的关键字段段值是0x7FF,段限长就是8MB。

SS现在也要转变为栈段选择符,栈顶指针也成为32位的esp

        lss _stack_start,%esp

在kernel/sched.c中,stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 }这行代码将栈顶指针指向user_stack数据结构的最末位置。这个数据结构是在kernel/sched.c中定义的,如下所示:

long user_stack [ PAGE_SIZE>>2 ]

补充说明
设置段寄存器指令(Load Segment Instruction)
该组指令的功能是把内存单元的一个“低字”传递给指令中指定的16位寄存器,把随后的一个“高字”传给相应的段寄存器(DS,ES,FS,GS,和SS)。
指令格式如下:

        LDS/LES/LFS/LGS/LSS   Mem, Reg

指令LDS(Load Data Segment Register)和LES(Load Extra Segment Register)在8086 CPU中就存在,而LFS和LGS、LSS(Load Stack Segment Register)是80386及其以后CPU中才有的指令。若Reg是16位寄存器,则Mem必须是32位指针;若Reg是32位寄存器,则Mem必须是48位指针,其低32位给指令中指定的寄存器,高16位给指令中的段寄存器。


0x10将SS设置为与前面4个段选择符的值相同,段基址都是指向0x000000,段限长都是8MB,特权级都是内核特权级,后面的压栈动作就要在这里进行。

特别值得一提的是,现在刚刚从实模式转变到保护模式,段基址的使用方法和实模式差别非常大,要使用GDT产生段基址,前面讲到的那几行设置段选择符的指令本身都是要用GDT寻址的。现在就能清楚地看出,如果没有setup程序在16位实模式下模拟32位保护模式而创建的GDT,恐怕前面这几行指令都无法执行。

以上都是head对GDT的设置。


接下来,head程序对IDT进行设置。


中断描述符

中断描述符为64位,包含了其对应中断服务程序对段内偏移地址OFFSET、所在段选择符SELECTOR、描述符特权级DPL、段存在标志P、段描述符类型TYPE等信息,供CPU在程序中需要进行中断服务时找到相应的中断服务程序。
其中,第0~15位和第48~63位组合成32位的中断服务程序的段内偏移地址(OFFSET);第16~31位为段选择符(SELECTOR),定位中断服务程序所在段;第47位为段存在标志(P),用于标识此段是否存在于内存中,为虚拟存储提供支持;第45~46位为特权级标志(DPL),特权级范围为0~3;第40~43位为段描述符类型标志(TPYE),中断描述符对应的类型标志为0111(0xE),即将此段描述符标记为“386中断门”。


这是重建保护模式下的中断服务体系的开始。程序先让所有的中断描述符默认指向ignore_int这个位置,之后还要对IDT寄存器的值进行设置。

构造IDT,使中断机制的整体架构先搭建起来(实际的中断服务程序挂接则在main函数中完成),并使所有中断服务程序指向同一段只显示一行提示信息就返回的服务程序。从编程技术上讲,这种初始化操作,既可以防止无意中覆盖代码或数据而引起的逻辑混乱,也可以对开发过程中的误操作作出及时的提示。IDT有256个表项,实际只使用了几十个,对于误用未使用的中断描述符,这样的提示信息可以提醒开发人员注意错误。


现在head程序要废除已有的GDT,并在内核中的新位置重新创建GDT。

其实这个GDTR还是没有搞特别明白,之后再说吧。

为什么要废除原来的GDT重新设置一套呢。在原来GDT所在的位置是设计代码时在setup.s里面设置的数据,将来这个setup模块所在的内存位置会在设计缓冲区时被覆盖。如果不改变位置,将来GDT的内容肯定会被缓冲区覆盖掉,从而影响系统的运行。这样一来,将来整个内存中唯一安全的地方就是现在head.s所在的位置了。

GDT的位置和内容发生了变化,特别要注意最后的三位是FFF,说明段限长不是原来的8MB,而是16MB。所以要再次对一些段选择符进行重新设置,包括DS、ES、FS、GS及SS,主要是段限长变为16MB。

现在栈顶指针esp指向user_stack数据结构的外边缘,就是内核栈的栈底。后面需要压栈的时候可以最大限度地使用栈空间。
设置esp的代码:

lss    _stack_start,%esp

A20地址线是否打开影响保护模式是否有效,所以要检查一下。

确定A20地址线已经打开之后,head程序如果检测到数学协处理器(x87协处理器)存在,则将其设置为保护模式工作状态。

head程序将为调用main函数做最后的准备。这是head程序执行的最后阶段,也是main函数执行前的最后阶段。

head程序将L6标号和main函数入口地址压栈,栈顶为main函数地址,目的是使head程序执行完后通过ret指令就可以直接执行main函数。

orl    $2,%eax        # set MPmovl   %eax,%cr0call   check_x87jmp    after_page_tables…after_page_tables:pushl  $0              # These are the parameters to main :-)pushl  $0pushl  $0pushl  $L6             # return address for main, if it decides to.pushl  $_mainjmp setup_paging
L6:jmp    L6              # main should never return here, but…

在这些压栈动作完成后,head程序将跳转至setup_paging:去执行,开始创建分页机制。

先将页目录表放在物理内存的起始位置,从内存开始的5页空间内容全部清零(每页4KB),为初始化页目录和页表做准备。
这个动作起到了用一个页目录表和4个页表覆盖head程序自身所占内存空间的作用。

head程序将页目录表和4个页表所占物理内存空间清零后,设置页目录表的前4项,使之分别指向4个页表。

head程序设置完页目录表后,Linux 0.11在保护模式下支持的最大寻址地址为0xFFFFFF(16 MB),此处将第4个页表(由pg3指向的位置)的最后一个页表项(pg3 + 4902指向的位置)指向寻址范围的最后一个页面,即0xFFF000开始的4 KB字节大小的内存空间。
然后开始从高地址向低地址方向填写4个页表,依次指向内存从高地址向低地址方向的各个页面。继续设置页表。将第4个页表(由pg3指向的位置)的倒数第二个页表项(pg3-4 + 4902指向的位置)指向倒数第二个页面,即0xFFF000~0x1000(0x1000即4 KB,一个页面的大小)开始的4 KB字节内存空间。

最终,从高地址向低地址方向完成4个页表的填写,页表中的每一个页表项分别指向内存从高地址向低地址方向的各个页面。

这些工作完成后,内存中的布局如图

head程序已将页表设置完毕了,但分页机制的建立还没有完成,还需要设置页目录表基址寄存器CR3,使之指向页目录表,再将CR0寄存器设置的最高位(31位)置为1。


补充说明:

PG(Paging)标志
CR0寄存器的第31位,分页机制控制位。当CPU的控制寄存器CR0第0位PE(保护模式)置为1时,可设置PG位为开启。当开启后,地址映射模式采取分页机制。当CPU的控制寄存器CR0第0位PE(保护模式)置为0时,设置PG位将引起CPU发生异常。

CR3寄存器
3号32位控制寄存器,其高20位存放页目录表的基地址。当CR0中的PG标志置位时,CPU使用CR3指向的页目录表和页表进行虚拟地址到物理地址的映射。


xorl   %eax,%eax              /* pg_dir is at 0x0000 */movl   %eax,%cr3              /* cr3-page directory start */movl   %cr0,%eaxorl    $0x80000000,%eaxmovl   %eax,%cr0              /* set paging (PG) bit */

两行代码的动作是将CR3指向页目录表,意味着操作系统认定0x0000这个位置就是页目录表的起始位置;后3行代码的动作是启动分页机制开关PG标志置位,以启用分页寻址模式。两个动作一气呵成。到这里为止,内核的分页机制构建完毕。

        xorl        %eax,%eax             /* pg_dir is at 0x0000 */

回过头来看,开始将system模块移动到0x00000处,然后在内存的起始位置建立内核分页机制,最后就是上面的这行代码,认定页目录表在内存的起始位置。三个动作联合起来为操作系统中最重要的目的——内核控制用户程序奠定了基础。这个位置是内核通过分页机制能够实现线性地址等于物理地址的唯一起始位置。


head程序执行最后一步:ret
通过跳入main函数程序执行。

将压入的main函数的执行入口地址弹出给EIP。


补充说明:

  • EIP(instruction pointer):EIP寄存器,用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。

  • EBP:栈底指针,pointer to data on the stack(in the SS segment)

  • ESP:栈顶指针,stack pointer(in the SS segment)


call指令会将EIP的值自动压栈,保护返回现场,然后执行被调函数的程序。等到执行被调函数的ret指令时,自动出栈给EIP并还原现场,继续执行call的下一行指令。这是通常的函数调用方法。
对操作系统的main函数来说,这个方法就有些怪异了。main函数是操作系统的。如果用call调用操作系统的main函数,那么ret时返回给谁呢?难道还有一个更底层的系统程序接收操作系统的返回吗?操作系统已经是最底层的系统了,所以逻辑上不成立。那么如何既调用了操作系统的main函数,又不需要返回呢?
这个方法的妙处在于,是用ret实现的调用操作系统的main函数。既然是ret调用,当然就不需要再用ret了。不过,call做的压栈和跳转的动作谁来做呢?操作系统的设计者做了一个仿call的动作,手工编写代码压栈和跳转,模仿了call的全部动作,实现了调用setup_paging函数。注意,压栈的EIP值并不是调用setup_paging函数的下一行指令的地址,而是操作系统的main函数的执行入口地址_main。这样,当setup_paging函数执行到ret时,从栈中将操作系统的main函数的执行入口地址_main自动出栈给EIP,EIP指向main函数的入口地址,实现了用返回指令“调用”main函数。

将压入的main函数的执行入口地址弹出给CS:EIP,这句话等价于CPU开始执行main函数程序 。

Linux笔记整理(1)系统的加载和main函数执行准备相关推荐

  1. linux加载u盘乱码怎么转换,Linux系统下加载U盘设备时文件乱码的有效解决方法

    很少情况会在Linux系统下使用U盘,但是最近有朋友在Linux系统下加载U盘设备的时候发现U盘内的文件出现了乱码现象,这该怎么办呢?很多朋友对Linux系统又不太熟悉,不知道该怎么操作,没关系,让小 ...

  2. linux u盘 慢_u盘加载较慢 建议优化 - 卡饭网

    U盘加载速度十分缓慢的原因及解决方法 U盘加载速度十分缓慢的原因及解决方法 很多朋友在使用U盘的时候都遇到过电脑接入U盘后,加载读取文件的速度十分的缓慢,总是要等上一段时间才能完全读取,这是怎么回事呢 ...

  3. linux内核及其模块的查询,加载,卸载 lsusb等

    http://blog.sina.com.cn/s/blog_53e81e2a0100zkxi.html 1,/sbin/update-modules文件,他是一个linux通用的模块管理脚本程序. ...

  4. 安装win7和centos6.7双系统 引导加载安装位置问题

    我在安装win7和centos6.7双系统时,根据网上的教程选择的是把引导加载安装到了centos系统的根分区中了,没有分boot分区,这样时无法启动centos的,所以在win7中安装easybcd ...

  5. Linux下编译、链接、加载运行C++ OpenCV的两种方式及常见问题的解决

    Linux下编译.链接.加载运行C++ OpenCV的两种方式及常见问题的解决 在Linux下安装完OpenCV C++之后(还没有安装的读者请参考Ubuntu 18.04 安装OpenCV C++) ...

  6. 【Android 插件化】基于插件化的恶意软件的加载策略分析 ( 自定义路径加载插件 | 系统路径加载插件 | 用户同意后加载插件 | 隐藏恶意插件 )

    文章目录 一.自定义路径加载插件 二.系统路径加载插件 三.用户同意后加载插件 四.隐藏恶意插件 一.自定义路径加载插件 插件化应用中 , 宿主应用 加载 插件 APK , 需要获取该插件 APK 文 ...

  7. 【转】Linux如何在系统启动时自动加载模块

    1.Linux安装驱动程序 tar zxf ixgbe-<x.x.x>.tar.gz cd ixgbe-<x.x.x>/src/ make install modprobe & ...

  8. 【OS学习笔记】十 实模式:实现一个程序加载器-程序加载器如何将用户程序加载到内存并执行

    上一篇文章学习了以下内容: 用一种不同的分段方法,从另一个不同的的角度理解处理器的分段内存访问机制 使用循环和条件转移指令来优化主引导扇区代码 点击链接查看上一篇文章:点击链接查看 对于主引导扇区部分 ...

  9. Linux用户程序的编译链接与加载启动过程

    Linux用户程序的编译链接与加载启动过程 rtoax 2021年3月 1. 程序的编译链接 1.1. 介绍 如果我们打开维基百科的 链接器 页,我们将会看到如下定义: 在计算机科学中,链接器(英文: ...

最新文章

  1. Eigen矩阵运算的混淆问题
  2. 马斯克嘲笑「元宇宙」的想法,并给年轻人5条鸡汤
  3. 高级转录组分析和R数据可视化(2020.2,课程推迟,可先报名,时间另行告知)
  4. AI:2020年6月23日北京智源大会演讲分享之智能信息检索与挖掘专题论坛——09:55-10:40刘欢教授《Challenges in Combating Disinformation》
  5. hive增量表和全量表_hive 拉链表 实现全量数据 增量更新
  6. D-query SPOJ - DQUERY (莫队算法裸题)
  7. 不是bug!百度集好运卡奖品追加8000个金猪
  8. winform 自定义控件属性在属性面板中显示的问题
  9. 深度学习优化算法入门:二、动量、RMSProp、Adam
  10. 非线性优化_曲线拟合_Ceres_最小二乘法示例
  11. JavaScript 常用技巧收集
  12. springboot启动报错@Bean definition illegally overridden by existing bean definition
  13. 数据库事务(Transaction)详解
  14. 延时关机命令 linux,Ubuntu自动定时关机的方法
  15. 计算机考研909考试大纲,山东大学2019年909数据结构考研大纲
  16. 调用百度汇率api 获取各国的汇率值
  17. CVPR 2021 论文大盘点-去阴影、去反光、去高光、去伪影篇
  18. 能复活超级英雄的除了时间宝石,还有量子计算机?
  19. 如果NBA也像JAVA一样面试
  20. 《惢客创业日记》2020.01.14(周二)从新学习《长征》

热门文章

  1. 建模大神是如何制作出可爱戴着眼镜的卡通女孩角色呢
  2. java 判断两个经纬度差异_计算两个经纬度点的实际距离
  3. java socket 读取文件_java中ServerSocket读取文件流不是分行读取
  4. windows下Graphviz安装及入门教程(附下载链接)
  5. (转)少儿编程这么火, 究竟学哪一种语言最靠谱?这篇文章说透了!
  6. android apk 微信登入_微信第三方登录(Android 实现)
  7. python 给word动态添加水印
  8. 2018蓝桥杯 题解
  9. 结果页要求用户复制链接进行分享 而不是直接调用浏览器分享API的原因
  10. 软件工程基于场景建模 习题