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

【linux V0.11】boot

文章目录

  • 前言
    • 磁盘结构
  • 内存布局
  • bootsect.s
    • 来源
    • 内容
  • setup.s
    • 描述符表
  • head.s

记录一下自己第一次看Linux内核的过程,大致也是囫囵吞枣过一遍。
参考书籍《Linux内核完全注释》

前言

磁盘结构

10分钟学懂磁盘的结构(盘片、磁道、扇区、柱面)
在这里插入图片描述

名称解释
Cylinder(柱面)相同半径的一组磁道,从 0 开始编号
Head(磁头)对应盘面(Platter),即上下不同磁盘片的一面,从 0 开始编号
Sector(扇区)磁道上被划分出的小块,从 1 开始编号

由上,可用CHS 地址(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。
在操作系统或文件系统使用逻辑寻址(如 LBA 或扇区编号)。
在 LBA(Logical Block Addressing):
扇区从 0 开始编号;
(C=0, H=0, S=1) → LBA 0;
(C=0, H=0, S=2) → LBA 1;
以此类推。
所以有时候会有不同的说法,比如第0扇区是逻辑第0扇区,也是(0,0,1)的第一个物理扇区。

内存布局

在这里插入图片描述
(查看完整的实模式内存布局)

bootsect.s

来源

BIOS会从磁盘第0扇区加载bootsect到内存0x07C00。

内容

  • 将自己从 0x7c00 移动到 0x90000
    BIOS 把 bootsect 放在 0x7c00,但这块内存空间太小,而且后续要加载其他模块(如 setup 和内核),所以 bootsect 会把自己移动到更高的地址
  • 使用 BIOS 中断加载 setup 模块
    bootsect 使用 BIOS 的中断服务(int 0x13)从磁盘第 1~4 扇区上读取下一个模块:setup
  • 继续使用 BIOS 中断加载 system(Linux 内核)
  • 跳转到 setup 模块继续执行

setup.s

setup.s程序结束后内存中程序示意图

  • 收集 BIOS 提供的系统信息
    • 光标位置 :保存当前屏幕光标位置,存储在 0x90000 ,用于后续终端初始化;
    • 内存大小 :通过 BIOS 中断获取扩展内存大小(单位 KB),存入 0x90002 处;
    • 显示模式 :获取视频显示信息(模式(如文本模式或图形模式)、窗口宽度等),存入 0x90004 和 0x90006;
    • 显卡信息:获取 EGA/VGA 等配置参数,存入偏移 0x90008、0x9000a、0x9000c;
    • 硬盘参数 :读取硬盘 0 (存入偏移 0x90080)和 硬盘 1 (存入偏移 0x90090)的参数表;
    • 检查是否存在第二个硬盘(hd1),如果硬盘 1 不存在,就清空之前写入的硬盘参数。 ;
  • 关闭中断,准备进入保护模式
  • 将内核(system)从0x10000 ~ 0x90000移动到0x00000 ~ 0x80000
  • 加载 GDT (全局描述符表)和 IDT(中断描述符表)
    设置 IDT和 GDT;
    准备进入保护模式所需的段机制和中断处理。
  • 开启 A20 地址线
    开启 A20 地址线,允许访问超过 1MB 的内存;
    A20 是第 21 条地址线(从 A0 开始数);
    它对应的是物理地址的第 21 位(bit 20) ;
    如果 A20 被禁用,那么任何对超过 1MB 地址的访问都会被“回绕”到低地址区(比如 0x100000 会被当作 0x00000);
  • 重新编程 8259A 中断控制器
    重映射中断向量,避免冲突;
    把中断映射到 0x20~0x2F;
    设置主从 PIC 控制器。
  • 切换到保护模式
    设置 CR0.PE 位,进入保护模式;
  • 跳转到system(真正的系统内核起点

描述符表

描述符表由多个8字节长的描述符项组成。

mov	ax,#0x0001	! protected mode (PE) bit
lmsw	ax		! This is it!
jmpi	0,8		! jmp offset 0 of segment 8 (cs) !跳转至cs段8,偏移0处。

我们已经将system模块移动到0x00000开始的地方,所以这里的偏移地址是0。 这里的段值8已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。
段选择符长度为16位(2字节);
位0~1表示请求的特权级0~3,Linux操作系统只用到两级:0级(系统级)和3级(用 户)级;
位2用于选择全局描述符表(0)还是局部描述符表(1);
位3~15是描述符表项的索引,指出选择第几项描述符。
所以段选择符8(0000 0000 0000 1000)表示请求特权级0、使用全局描述符表中的第1项,该项指出代码的基地址是0,因此这里的跳转指令就会去执行system中的代码。

gdt:/*第0个描述符,不用但得存在*/.word	0,0,0,0/*第1个描述符,系统代码段描述符,这里在gdt表中的偏移量为0x08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值*/.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb).word	0x0000		! base address=0.word	0x9A00		! code read/exec.word	0x00C0		! granularity=4096, 386/*第2个描述符,这里在gdt表中的偏移量是0x10,当加载数据段寄存器(如ds等)时,使用的是这个偏移值。*/.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb).word	0x0000		! base address=0.word	0x9200		! data read/write.word	0x00C0		! granularity=4096, 386idt_48://lidt指令的操作数。6字节。前2字节是idt表限长,后4字节是idt表所处的基地址。.word	0			! idt limit=0.word	0,0			! idt base=0Lgdt_48://lgdt指令的操作数.word	0x800		! 全局表长度为2KB,因为每8B组成一个段描述符项,所以表中共可有256项。.word	512+gdt,0x9	! 4字节构成的线性地址:0x0009<<16+0x0200+gdt,即0x90200+gdt

head.s

  • startup_32是从绝对地址0x00000000开始的,这里也同样是页目录将存在的地方,因此这里的启动代码将被页目录覆盖。
_pg_dir:
startup_32:movl $0x10,%eax			#设置段寄存器指向 GDT 中的 0x10 描述符(即内核数据段)。mov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss _stack_start,%esp	#设置堆栈指针 esp 指向 _stack_start。call setup_idt			#调用 setup_idt 和 setup_gdt 初始化中断描述符表和全局描述符表。call setup_gdtmovl $0x10,%eax			#重新加载所有段寄存器,确保使用新 GDT 的段选择符。mov %ax,%dsmov %ax,%es	mov %ax,%fsmov %ax,%gslss _stack_start,%esp

setup_idt

/* 默认的中断“向量句柄” :-) */
int_msg:.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:pushl %eaxpushl %ecxpushl %edxpush %dspush %espush %fsmovl $0x10,%eax	#使ds,es,fs指向gdt表中的数据段mov %ax,%dsmov %ax,%esmov %ax,%fspushl $int_msg	#把调用printk函数的参数指针(地址)入栈call _printk	#_printk是 printk 编译后模块中的内部表示法popl %eaxpop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret
# 中断描述符表中的项虽然也是8B组成,但其格式与全局表中的不同,被称为门描述符(Gate Descriptor)。
# 它的0~1,6~7字节是偏移量,2~3B是选择符,4~5B是一些标志。
setup_idt:lea ignore_int,%edx			#将ignore int的偏移值→edx寄存器,这是偏移地址的低16位 ,后面还会用 %dx 来构造完整偏移movl $0x00080000,%eax		# 将选择符0x0008置入eax的高16位中。movw %dx,%ax				#偏移值的低16位置入eax的低16位中。此时eax含有门描述符低4B的值。movw $0x8E00,%dx			#此时edx含有门描述符高4B的值。lea _idt,%edi	 			#将 _idt 的地址加载到 %edi,作为当前写入的目标位置mov $256,%ecx				#设置循环次数为 256,因为 IDT 有 256 个条目
rp_sidt:						#循环填充整个 IDTmovl %eax,(%edi)			#第一次写入 %eax(低32位)到当前位置movl %edx,4(%edi)			#第二次写入 %edx(高32位)到当前位置+4addl $8,%edi				#然后 %edi 增加 8,移动到下一个条目dec %ecxjne rp_sidt					#直到 256 次完成lidt idt_descr				#加载 IDT 描述符 ret

setup_gdt

setup_gdt:lgdt gdt_descr				#加载 GDT 描述符 ret
  • 测试A20地址线是否已经开启
    采用的方法是向内存地址0x000000处写入任意一个数值,然后看内存地址0x100000处是否也是这个数值。如果一直相同的话,就一直比较下去,即死循环、死机,表示地址A20线没有选通,结果内核就不能使用1MB以上内存。
  • 检查数学协处理器芯片是否存在
    方法是修改控制寄存器CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。
  • 将参数压栈,调用 setup_paging,然后跳转到 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.

分页设置函数

通过设置控制寄存器cr0的标志(PG位31)来启动对内存的分页处理功能,并设置各个页表项的内容,以恒等映射前16MB的物理内存。分页器假定不会产生非法的地址映射(即在只有4MB的机器上设置出大于4MB的内存地址)。注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能直接使用大于1MB的地址。所有“一般”函数仅使用低于1MB的地址空间,或者是使用局部数据空间,地址空间将被映射到其他一些地方去———mm(内存管理程序)会管理这些事的。对于那些有多于16MB内存的机器,代码就在这里,可对它进行修改。实际上,这并不太困难的。通常只需修改一些常数等。我把它设置为16MB,因为我的机器再怎么扩充都不能超过这个界限(当然,我的机器很便宜的)。我已经通过设置某类标志来给出需要改动的地方(搜索“16MB”),但我不能保证做这些改动就行了。

每个页表长为4KB字节,而每个页表项需要4个字节,因此一个页表共可以存放1024个表项,如果一个表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。页表项的格式为:项的前0~11位存放一些标志,如是否在内存中(P位0)、读写许可(R/W位1)、普通用户还是超级用户使用(U/S位2)、是否修改过(是否脏了)(D位6)等;表项的位12~31是页框地址,用于指出一页内存的物理起始地址。

setup_paging:				movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */ #首先对5页内存(1页目录+4页页表)清零。xorl %eax,%eaxxorl %edi,%edi			/* pg_dir is at 0x000 */cld;rep;stosl#共有4个页表,所以只需设置4项。页目录项的结构与页表中项的结构一样,4个字节为1项。#″$pg0+7″表示:0x00001007,是页目录表中的第1项。则第1个页表所在的地址=0x00001007&0xfffff000=0x1000;#第1个页表的属性标志=0x00001007&0x00000fff=0x07,表示该页存在、用户可读写。movl $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		/*  --------- " " --------- */#填写4个页表中所有项的内容,共有:4(页表)∗1024(项/页表)=4096项(0-0xfff),#即能映射物理内存4096∗4KB=16MB。每项的内容是:当前项所映射的物理内存地址+该页的标志(这里均为7)。#使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的位置是1023∗4=4092。#因此最后一页的最后一项的位置就是$pg3+4092。movl $pg3+4092,%edi#最后1项对应物理内存页面的地址是0xfff000,加上属性标志7,即为0xfff007.movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */ std						#方向位置位,edi值递减(4B)。
1:	stosl			/* fill pages backwards - more efficient :-) */subl $0x1000,%eax		#每填写好一项,物理地址值减0x1000。jge 1b					#如果小于0则说明全填写好了。#设置页目录基址寄存器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	#添上PG标志。movl %eax,%cr0		/* set paging (PG) bit */ret			/* this also flushes prefetch-queue */#在改变分页处理标志后,要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。#该返回指令的另一个作用是将堆栈中的main程序的地址弹出,并开始运行/init/main.c程序。#本程序到此真正结束了。

此时system模块在内存中的详细映像如图
在这里插入图片描述

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

相关文章:

  • 多生产者多消费者问题(操作系统os)
  • SpringCloud之Hystrix
  • 【DOCKER】-4 dockerfile镜像管理
  • linux网络存储——freeNAS的安装配置
  • Spring Cloud分布式配置中心:架构设计与技术实践
  • MFC/C++语言怎么比较CString类型 第一个字符
  • 读文章 Critiques of World model
  • Java(集合)
  • aspnetcore Mvc配置选项中的ModelMetadataDetailsProviders
  • SAP-ABAP:SAP库存管理核心增强:IF_EX_MB_DOCUMENT_BADI 深度解析
  • 交换类排序的C语言实现
  • Hello, Tauri!
  • 基于Android的景点旅游信息系统App
  • 使用aiohttp实现高并发爬虫
  • uni-app开发的页面跳转全局加载中
  • 基于HarmonyOS的智能灯光控制系统设计:从定时触发到动作联动全流程实战
  • C++ 中常见的字符串定义方式及其用法
  • 1111自己
  • 基础分类模型及回归简介(一)
  • 体验RAG GitHub/wow-rag
  • 前端同学,你能不能别再往后端传一个巨大的JSON了?
  • 引用(C++)
  • python的微竞网咖管理系统
  • ⽂本预处理(一)
  • volatile 关键字
  • Codeforces Round 787 (Div. 3)(A,B,C,D,E,F,G)
  • DO,VO,DTO.....
  • (二十四)-java+ selenium自动化测试-三大延时等待
  • UI前端与数字孪生融合案例:智慧城市的智慧停车引导系统
  • 苍穹外卖Day4