1. 背景
想了解进入startup_32之前,CPU执行了哪些操作,但是找不到跳转到它的call或jmp指令:

2. 主体过程
为避免介绍内容过于杂乱,本文尽量紧密围绕最主体、最关键的过程,不深究过于细节、相对独立、不影响整体理解的环节和疑问。

2.1. 加载内核
硬件设计层面会保证,CPU刚加电时处于实模式、仅开启段式内存管理,并且CS寄存器=0xffff、IP寄存器=0,从而执行0xffff0处的BIOS程序(主板出厂时烧入代码,断电不挥发),将bootsect(内核加载程序,位于主引导扇区),加载到0x7c00内存地址处并执行,bootsect先将自己移动到0x90000处,然后进一步将setup和内核文件,分别加载到0x90200和0x10000处,并执行setup(内核启动程序)。
分析这块时,我产生过这些疑问:
(1) bootsect不移动自己,直接在0x7c00区域执行时,就加载setup和内核,存在什么问题?
(2) BIOS为什么不直接将bootsect加载到0x90000处?
(3) 0xffff0距离0x100000只有16个字节,容不下几条指令,所以0xffff0指向的是不是一条跳转指令,如果是,为什么不让CS:IP在加电时,直接指向要跳转的地方?
简单查了资料:(1)、(2)、(3)大致都是为了提高兼容性,让A的内部细节,对B透明。这样,a1、a2对执行条件的特殊要求,就可以由它们自己处理,不影响对外的约定。
(4) BIOS代码执行期间,是如何让CS:IP地址(比如0xffff0),指向BIOS,而又让BIOS指令访问的地址(比如0x7c00),指向内存的?
deepseek的解释是:通过设置PCI配置寄存器,可以让读操作,访问BIOS,写操作访问内存。
对于这个解释,我个人是这样理解的:举个例子,执行“movl 0xffff0, %eax”,从0xffff0地址读取内容(包括从CS:IP读取指令),会被PCI路由到BIOS,而执行”mov %eax, 0xffff0”,往0xffff0地址写入内容,会被PCI路由到内存。这样,BIOS就可以先在内存中,复制一个自己的影子,然后修改PCI配置,让地址完全路由到内存,后续使用BIOS,实际使用的都是BIOS影子。
2.2. 启动内核
setup程序最主要的目标,是开启CPU的保护模式,并跳转到内核的入口startup_32处执行。
这里,又有两个疑问:
(1) 内核的加载和启动,为什么要开发bootsect、setup两个程序,而不是开发一个程序统一完成?
BIOS只从MBR读取loader程序,而在同一个程序里实现加载和启动,大小会超过256字节,MBR装不下,就只装了bootsect程序,再由它间接加载并启动体积较大的setup程序。
(2) 整个过程中,怎么没见到grub?
grub可以理解为更强大的bootsect+setup,实质上已经是一个小型操作系统,有自己的内核、驱动、应用程序,除了必要的加载和启动内核功能,还支持丰富的用户交互命令。
2.3. 内核执行
内核最初的入口是startup_32,它最主要的目标,是开启CPU的页式内存管理,并调用start_kernel()函数。
2.4. CPU状态的变化
以上实际上是一个边搭桥、边过桥的过程:一边加载,一边执行;一边准备,一边逐步开启CPU保护模式和页式内存管理。

2.4.1. 实模式+仅开启段式内存管理
bootsect+setup可正常执行(过桥):
(1) BIOS为实模式提供了中断向量表,所以int指令可以正常执行;
(2) CPU按照”CS/SS << 4 + IP/SP”或”DS << 4 + 包含在指令中的偏移值”计算地址,仅依赖寄存器,不依赖内存上的管理数据,所以指令的执行和寻址也正常。
setup切换保护模式(搭桥):
setup事先构造全局段描述符表(gdt),并执行lgdt指令,设置GDTR寄存器=gdt,然后开启保护模式。
2.4.2. 保护模式+仅开启段式内存管理
startup_32可正常执行(过桥):
切换到保护模式后,CPU硬件层对段寄存器的使用逻辑,会发生改变,段基址不再从中直接获取,而是从它指向的段描述符,间接获取,这在setup执行时,已经准备好了。
startup_32开启页式内存管理(搭桥):
startup_32事先构造目录表swapper_pg_dir,并准备充足的目录表项、页表项,然后设置cr3寄存器=swapper_pg_dir,最后开启页式内存管理。
2.4.3. 保护模式+开启页式内存管理
start_kernel()可正常执行(过桥):
开启页式内存管理后,CPU硬件层的寻址逻辑,又会发生改变,对于i386 CPU,指令中的地址,不光要经过段式映射,还要经过页式映射,才能得到最终的物理地址,页式映射依赖的目录表和页表(包括表项),这些都由startup_32准备好了。
CPU的内核态和用户态切换:
保护模式下,CPU硬件层,开始区分ring0~ring3权限。由于内核先于用户程序执行,也就是先抢到ring0权限,它在跳转到用户程序执行之前,就有权将CPU权限改为ring3,而用户程序只能穿过门,回到内核代码的同时,才能将CPU权限重新提升到ring0,因此所有依赖ring0权限的操作,只能请求内核帮忙完成。这样,内核就利用CPU提供的硬件特性,将ring0权限掌握在自己手里,用户程序永远只能在ring3权限下执行。另外,从用户程序跳入内核代码执行,仍然是在推进这个进程的目标,并没有导致进程切换(和跳入.so动态库执行类似),只是执行进程的CPU状态改变了而已,这其实也是内核的本质。
3. 代码分析
linux-2.4.0/arch/i386
|- boot
| |- bootsect.S // bootsect
| |- setup.S // setup
| |- compressed
| |- head.S
|- kernel // vmlinux
|- head.S // startup_32
|- main.c // start_kernel()
3.1. bootsect.S(bootsect程序)
有些操作并不用关注,主要的有三个:
(1) 将自己从0x7c00迁移到0x90000,并跳转到新区域,继续执行一下条指令;
(2) 将ds、ss段寄存器,设置为0x90000;
(3) 加载setup和内核文件(按小内核文件加载),并执行setup程序。

3.2. setup.S(setup程序)
真正需要关注的也不多:
(1) 确保自己位于0x90200位置,如果不在,执行自迁移;
(2) 如果是大内核文件,将bootsect加载到0x10000的那部分,转移到0x100000;
(3) 设置中断向量表、全局段描述符表;
(4) 切换到保护模式;
(5) 跳转到startup_32执行。

setup迁移到0x90000区域过程:
暂不清楚,为什么不用考虑从高往低迁移的情况。

向startup_32的跳转指令,写的比较有意思:
这是为了操作数,可以根据小内核/大内核的实际情况,进行动态设置,这同时也是代码中找不到startup_32被调用的一方面原因,另一方面是因为,这个时候还不能用0xC0010000或0xC0100000地址,访问到startup_32处的代码。

3.3. head.S(内核入口:startup_32)
这个文件,不是我自己分析的,因为《Linux内核源代码情况分析》10.2节,已经介绍的很清楚了。个人觉得比较有意思的就是390和391两行,给人一种极限过弯的感觉。

4. 打破神秘
(1) loader程序,为什么使用汇编编写?
第一,用于控制程序的体积,比如bootsect;第二,有些操作,C语言没有对应的语法成分,比如:int中断指令、将”jmpi __KERNEL_CS:code32”拆成opcode和操作数等;第三,内核还有很多其它.S文件,或嵌入到.c文件中的汇编代码,主要用于提高执行性能。
(2) 一堆没有生命的器材,组装一起,是怎么产生智能的?
个人认为,智能表面是一种感受,实质是一堆精密的物理反应。
扣动扳机,手枪完成一系列精密的动作,最终发射一枚子弹,这个过程远比跳水运动员,完成一次高分跳水,还要复杂,按道理已经很智能了,但是从感受上讲,它又跟按下电源,灯就亮了一样简单,并没有什么智能。
如果从穿孔纸带时代(甚至更早前手动插线连接电子管),开始去体会计算机,就并没有什么神秘了:手搓一个简单的工具,再用简单的工具,制造脱离手搓的工具,并进一步制造更多、更高级的工具。计算机,只是在交互方式上,做了更多的硬件设计,更贴近人类的习惯,所以显得智能。编程以及AI,也是通过物理的方式,让程序和数学公式,反过来参与进这些物理反应而已。
最后于 9分钟前
被jmpcall编辑
,原因: