当前位置: 首页 > news >正文

linux0.11学习之启动主线要点(一)

引言

操作系统对于每位信息从业人员的重要性是不言而喻的,其中linux0.11对每个学习电子信息的人更显得致关重要,本文主要对linux0.11操作系统的关键运行逻辑做要点梳理,并对难理解的代码做一定阐释,其他细节之处有所忽略请大家参考更多资料。

bootsect.s加载

开机启动,移动自身

因为bios一般是直接放在bios的Rom中,计算机硬件开机启动的第一件事情就会去加载0x07c00 处的bios内容,当加载完成bootsect的相关内容后, 那么开机第一件事情要做什么呢?就是将自己移动到0x90000,刚加载的时候x86的cpu都默认采用16位模式且linux0.11默认的寻址范围是1M, 整个机器寻址范围为0x00000-0xfffff,完整的寻址地址 =段基地址<<4+偏移地址,其代码定义的地址如下:

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
SYSSEG   = 0x1000			! system loaded at 0x10000 (65536).

从上面可以出实际的地址和基地址,偏移地址之间的关系。此外,因为操作位数是16位且搬运256次,故而总共搬运512个字节,依照此种逻辑,bootsect将 自己搬运到0x90000处的代码如下:


_start:mov	ax,#BOOTSEGmov	ds,axmov	ax,#INITSEGmov	es,axmov	cx,#256sub	si,sisub	di,direpmovw

图解也一并给出:
在这里插入图片描述

setup代码读取并载入

当上述代码执行完毕后,就开始要读取setup的相关的操作,其中的关键语句如下:

	jmpi	go,INITSEG
go:	mov	ax,cs  mov	ds,axmov	es,ax

go语句所在之处就是被搬运到0x90000处的代码,jmpi 到0x90000 处开始执行加载setup的相关逻辑。

load_setup:mov	dx,#0x0000		! drive 0, head 0mov	cx,#0x0002		! sector 2, track 0mov	bx,#0x0200		! address = 512, in INITSEGmov	ax,#0x0200+SETUPLEN	! service 2, nr of sectorsint	0x13			! read itjnc	ok_load_setup		! ok - continuemov	dx,#0x0000mov	ax,#0x0000		! reset the disketteint	0x13j	load_setup

上述代码开始加载磁盘中setup代码,从磁盘的第二个扇区加载。此处的SETUPLEN为4,故总共加载4个扇区数量,值得一提的是此处采用0x13中断加载磁盘上的setup代码,这也是非常科学的设计思路,同步读取磁盘耗时多,采用中断可大幅度提升系统运行效率,此外系统还采用0x10 显示中断来满足相关的字符串显示需求,下面为bootsect 整体运行完毕后的一个图解:
在这里插入图片描述

加载系统代码

setup代码加载完成后,陆续加载system代码到内存的0x10000处:需要连续加载240个扇区:

! ok, we've written the message, now
! we want to load the system (at 0x10000)mov	ax,#SYSSEGmov	es,ax		! segment of 0x010000call	read_itcall	kill_motor

read_it 函数中有比较详细的加载过程,此处不过多赘述,棋加载如下图所示意:
在这里插入图片描述
当加载system需要加载到0x10000的地址,此类做法是可避免bios 在16位时所建立的一些bios栈和中断向量表数据遭到覆盖而影系统接下来的运行。

setup代码执行

当bootsect万成自身移动,加载了系统的setup和system相关代码后,就开始跳转到setup代码执行,下面一句16位汇编指令是致关重要的。

	jmpi	0,SETUPSEG

通过上述指令可直接将cpu从bootsect执行跳转到setup执行,setup主要任务有三件:

  • 1、第一件要务是需要读取各个硬件的参数,如内存大小,显卡参数,其他硬盘和光驱等设备参数。
  • 2、移动system模块到0x00000处。
  • 3、从16进位入到保护模式(从16位向32位转变)

参数获取

参数获取主要是通过bios中断来获取,如0x10 为视频中断,用于在启动时打印信息。而0x15 为查询扩展内存大小、获取移动磁盘参数或者其他功能,需要根据子中断号来决定,当AH=0x88时,为获取内存参数,当AH=0f时,为获取移动盘参数,0x10也是一样的道理,具体可参考相关文献及数据手册,此处不过多赘述。

搬运system

首先可以思考一下为什么要搬运system代码,当移动代码的过程中的要点是什么?个人理解有如下三类起因:

  • 1、代码紧凑,回收内存空间,在那个硬件资源稀缺的时代,这是非常有价值的做法。
  • 2、逻辑严密,让各类有关内核的重要数据都放在内存最低位,更有利于软硬深度结合。
  • 3、为从实际模式到保护模式做充足的准备。
    知晓搬运system代码至0x00000的重要性,那就开始整个搬运工程的重难点阐述,首先在搬运system代码的过程中重点要注意的是需要关闭cpu中断,因为此时此刻系统并不具备处理其他中断的能力,下面是搬运代码的部分片段。
! now we want to move to protected mode ...cli			! no interrupts allowed !! first we move the system to it's rightful placemov	ax,#0x0000cld			! 'direction'=0, movs moves forward
do_move:mov	es,ax		! destination segmentadd	ax,#0x1000cmp	ax,#0x9000jz	end_movemov	ds,ax		! source segmentsub	di,disub	si,simov 	cx,#0x8000repmovswjmp	do_move

上述代码的简易图解如下:
在这里插入图片描述
do_move函数都是比较重要且通俗易懂,此处就不做其他说明。经过上面的代码的执行,系统成功的将system.s移动到0x00000处,下面是整个引导过程中内存内容全局移动图解,图片是来自于猿友资料,此处引用一下,非常感谢。
在这里插入图片描述

从实模式向保护模式转变

8259中断控制芯片重新映射

当转变为保护模式时,由于0x00-0x1f的中断地址被intel 设置成不可屏蔽中断,故需要重新设置映射8259的中断向量地址,在实模式下0x00-0x1f号中断将被映射到0x20-0x3f中,保证相关中断能正常执行。

打开A20,跳转到system

打开A20地址线,系统的理论寻址范围从1M变成4G,虽然此时系统实际能操作的内存只有16M左右,但系统是完全具备4G的内存寻址能力。当转变过程执行完成后就开始使用系统临时构建的gdt,下面就开始从setup跳转到system所在的head.s去执行:

! 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.mov	ax,#0x0001	! protected mode (PE) bitlmsw	ax		! This is it!jmpi	0,8		! jmp offset 0 of segment 8 (cs)

jmpi 0, 8 将直接转到gdt表中,将gdt表中的第一项内容按规定的位数载入到相关寄存器,此处从第0项开始,其数据项如下图所示:
在这里插入图片描述
因为是word 占两个字节,且每项数据占64bit(8byte)所以jmpi 0, 8 将跳转到上图所示的第一项,其每个位数显示的内容如下:
在这里插入图片描述
代码段地址为0x00000000,即system的地址,如此设置正好跳转到system的首行代码,上述是通过GDT表来实现跳转,GDT表的作用是通过GDT固定的64位数据来实现相关寄存器赋值。
这里面值得一提的是关于jmpi 指令中的8的理解:

理解一

8要把他理解成二进制1000,其中00表示特权级(00表示内核级,11表示用户级别),第三位表示是使用GDT还是LDT(0表示GDT,1表示LDT), 第四位(0表示采用GDT或者LDT表的第0项,1表示采用第一项),同理如果第五不为0,那么应当和第四项结合使用(10表示GDT或者LDT表的第2项),这个是可以依次类推的。(参考杨力祥老师书)

理解二

不用上述那么麻烦,因为GDT表的数据站8位,jmpi 0, 8 直接跳到第一项数据所在的位置,同理,jmpi 0, 16 则直接跳转到临时GDT表的第二项位置。(参考李老师的教程)
理解一可能更为详细一些,上图所属的GDT数据项位第1项数据,表示的是将所有寄存器的内容设置成宇system 代码段相关,这样就可以实现代码段访问了,下面开始介绍head.s 中的相关代码。

system模块执行

system代码是由head.s和部分system模块中的c和汇编代码共同混合编译而来。而head.s的相关代码被编译放在了0地址处,总共大小为:25k+184b,而上文说了,jmpi 指令将跳到system的0地址处执行,即从head.s的第一行代码执行,但是为什么head.s是第一段呢?这个是和makefile编译链接树有密切关系,有兴趣可以参考哈工大李老师的操作系统教程,此处不过多赘述。

初始化新建的I/GDT表

前面使用了gdt表,为什么需要从新建立并初始化GDT?目前重新建立GDT是为了让保护模式下的GDT实际运转有效,而在setup中手动代码建立的GDT表是临时需要的,其段限长为0x7ff4KB=8MB,其主要目的是为了过渡到system代码里面来,并且实现保护模式,新建立的GDT表的段限长度为0xfff4KB=16MB。下面是系统开始建立gdt,ldt的相关代码:

startup_32:movl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss stack_start,%espcall setup_idtcall setup_gdt
...
setup_gdt:lgdt gdt_descrret
gdt_descr:.word 256*8-1		# so does gdt (not that that's any.long gdt		# magic number, but it works for me :^).align 8
...	
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 */

通过设置ds,es,fs,gs 为0x10,ds为数据段寄存器,目前数据段寄存器已经指向0x10,按照本人的理解其效果是类似于 jmpi 0, 16,然后根据GDT表跳转到GDT所指向的数据段所在的地址,而后开始初始化新的gdt表,总共256项目,前4项是固定数据,后252项暂时用64位的0来填充。
idt也是类似的操作模式,具体请参考相关书籍,此处不再深究。

打开A20并设置CR0

cr0是一个非常重要的寄存器,cr0又称为数学协助处理器, 其结构和关键位如下所示。
在这里插入图片描述
通过以下代码可以快速设置cr0寄存器:

	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabledmovl %eax,0x000000	# loop forever if it isn'tcmpl %eax,0x100000je 1b
/** 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.*/movl %cr0,%eax		# check math chipandl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */orl $2,%eax		# set MPmovl %eax,%cr0call check_x87

如果没有开启保护模式,则CPU 的地址线是 20 位,最大寻址空间是 1MB (0xFFFFF)。当地址超过 1MB 时,会回绕到起始位置。例如:

  • 访问 0x100000 会回绕到 0x000000。
  • 访问 0x100001 会回绕到 0x000001。
    可根据地址是否生效来判断到底有没有打开A20,如果打开了,就不会有上述的绕到起始位置,这样做的目的就是强制打开A20,当A20 设置完毕后就可以考虑设置PG,PE等状态来设置启动分页并和保护标志位。

设置分页并跳转到main

main相关参数压栈

下面是为开始跳转到main做最后的准备,这里比较有意思的是该段汇编代代码。

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# just in case, we know what happens.

为什么说有意思呢,首先这段代码连续压了三次0,然后压了$L6,最后压了main的入口地址,这样设计必然是为了某个函数返回后自动跳到main的入口去,一般情况下main函数式不返回任何数,而如果当某些情况下返回,那就直接返回到L6了,而L6是一个无限循环,这样就保证系统可控,而压入栈的其他三个零则表示main的三个参数:

pushl $0	# envp (环境变量指针)
pushl $0	# argv (命令行参数数组指针)  
pushl $0	# argc (参数计数)

分页设置

在上述代码手动设置完main函数的栈之后,系统开始执行分页初始化,其代码如下:

.align 2
setup_paging:movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */xorl %eax,%eaxxorl %edi,%edi			/* pg_dir is at 0x000 */cld;rep;stoslmovl $pg0+7,pg_dir		/* set present bit/user r/w */movl $pg1+7,pg_dir+4		/*  --------- " " --------- */movl $pg2+7,pg_dir+8		/*  --------- " " --------- */movl $pg3+7,pg_dir+12		/*  --------- " " --------- */movl $pg3+4092,%edimovl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */std
1:	stosl			/* fill pages backwards - more efficient :-) */subl $0x1000,%eaxjge 1bxorl %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 */ret			/* this also flushes prefetch-queue */

页目录与页表项清零并设置属性

1、 此处略说,首先初始化页表和页目录,页目录有1024项,其他的4张页表也有1024项,总共有10245项,赋值给ecx,这样就确定了清除页帧的大小,直接循环向前清零直到10245个项全部完成了;
2、 后面就是设置每个页表的属性参数,7应该看成0111,其中:

  • Bit 0 § - Present: 1,表示页表存在于内存中。
  • Bit 1 (R/W) - Read/Write: 1,表示可读可写。
  • Bit 2 (U/S) - User/Supervisor: 1,表示用户模式可访问。
    下面是页目录的简易图解:
    在这里插入图片描述
    当页表目录中将页表有关的的属性参数设置成功后,就开始设置页表项,总共初始化的页地址范围是0x01000~0x4000,需要全部清零,执行完成下面的代码后,整个页表项的结果就如下所示:
    在这里插入图片描述
    3、设置CR0的页保护位的同时将页目录的地址赋给页指向寄存器CR3,如此页的初始化工作就算基本完毕了。

跳转main

上文已经讲述了在after_page_tables执行时候首先压入了5个参数,其对应关系如下所示:

ESP     -->  _main 的地址
ESP+4   -->  L6 的地址
ESP+8   -->  0 (argc)
ESP+12  -->  0 (argv)
ESP+16  -->  0 (envp)
返栈跳转

当setup_paging代码段执行完成ret后, 执行过程回到了压参序列之后,此时,ESP 正指向我们之前压入的 _main 的地址, _main 的地址被ret指令弹出,所以 CPU 跳转到了 _main 函数,同时,ESP 现在指向了下一个值,即我们之前压入的 L6 的地址。

参数获取

main 函数访问参数:当 main 函数的代码试图访问它的第一个参数 argc 时,编译器生成的代码会去访问 [ebp+8] 的位置。而那里正好是我们之前压入的 0(模拟的 argc),同理,访问 argv 会去 [ebp+12],访问 envp 会去 [ebp+16],这样我们手动压入栈的所有参数都被正确的应用了,至此操作系统就成功的跳转到init模块中的main函数了。

总结

本章内容小猿为大家通俗易懂的阐述了linux0.11启动的整个过程,对中间的重难点内容进行了重点说明,下章节将继续深入学习main后面的系统进程建立和切换的相关要点,也是以简单明了的方式向大家阐述相关内容。

参考资料

哈工大 李治军老师 操作系统
国科大 杨力祥老师 linux内核设计的艺术 第2版
其他网络资料不再给出连接。

http://www.dtcms.com/a/427322.html

相关文章:

  • Invoke-customs are only supported starting with Android O (--min-api 26)
  • 安卓基础组件014--button圆角 背景色 边框
  • 【Android】浅谈kotlin协程应用
  • 比价网站源码整站程序梦幻西游网页版app
  • dz做网站虚拟主机可以干什么
  • Windows10,11自带的Hyper-V虚拟机开启及使用方法
  • QCustomPlot 系列总结:从入门到精通的完整指南与资源整理
  • RK3566鸿蒙开发板规格书Purple Pi OH
  • 大模型落地深水区:企业 AI 转型的实践路径与价值突破
  • 金顺广州外贸网站建设图片模板网站
  • LinuxC++——etcd-cpp-api精简源代码函数参数查询参考
  • [特殊字符]️ Spring Cloud Eureka 三步通:搭建注册中心 + 服务注册 + 服务发现,通俗易懂!
  • 打工人日报#20250930
  • 六安网站建设网络服务中国都在那个网站上做外贸
  • 软件工程实践团队作业——团队组建与实践选题
  • YDWE编辑器系列教程二:物体编辑器
  • 软考-系统架构设计师 NoSQL数据库详细讲解
  • 钢铁厂设备健康监测系统:AIoT技术驱动的智慧运维革命​
  • Elasticsearch集群监控信息(亲测)
  • TARA (威胁分析与风险评估) 学习笔记
  • 网站集成微信登陆如何选择大良网站建设
  • 鸿蒙:使用Image组件展示相册图片或rawfile图片
  • ubuntu系统当前的时间和时区
  • 图解式部署:WSL2 中 Dify 本地化安装与访问故障排查指南
  • ABAP+新值过长,仅可以传输255个元素
  • 顺序队列与环形队列的基本概述及应用
  • 数组的三角和
  • 文创产品设计的目的和意义岳阳优化营商环境
  • Spring Boot 内置日志框架 Logback - 以及 lombok 介绍
  • 网站优化塔山双喜百度关键词快速排名方法