linux0.11源码分析第二弹——setup.s内容

时间:2024-12-19 12:56:45

???? 前言

    继上篇博客分享了boot文件的内容后,本篇博客进而来到第二个文件: setup.s ,对应了《linux源码趣读》的第5~8回。这部分的功能主要就是做了 三件事 ,第一件事是做代码搬运和临时变量存放,第二件事是突破寻址瓶颈,第三件事是进入保护模式,具体如何操作以及为什么要做这些,本篇博客回一一解答,希望各位给个三连,拜托啦,这对我真的很重要!!!

目录

  • ???? 前言
  • ????代码搬运和临时变量存放
    • ????前期准备
    • ????system代码存放
  • ????段描述符
  • ????突破寻址瓶颈
  • ????进入保护模式
  • 其他部分
  • ????总结
  • ????参考资料

????代码搬运和临时变量存放

????前期准备

    首先做了一些准备工作,比如调用0x10中断的03功能, 读取光标位置 ,在linux中源码如下:

start:

; ok, the read went well so we get current cursor position and save it for
; posterity.

	mov	ax, #0x9000	; this is done in bootsect already, but...
	mov	ds,ax
	mov	ah,#0x03	; read cursor pos
	xor	bh,bh
	int	0x10		; save it in known place, con_init fetches
	mov	[0],dx		; it from 0x90000.

    调用的方法就是设置ah为0x03,同时用int指令。结果会保存在dx寄存器中,高8位存储行号,低8位存储列号。然后mov [0], dx,再结合ds段寄存器是0x9000,结果就是就是把光标位置存储在0x9000这个内存地址。

    再之后就是 获取一些信息 。罗列出的代码如下所示,分别为内存信息,显卡显示模式,检查显示方式,获取第一块和第二块磁盘信息。

; Get memory size (extended mem, kB)

	mov	ah,#0x88
	int	0x15
	mov	[2],ax

; Get video-card data:

	mov	ah,#0x0f
	int	0x10
	mov	[4],bx		; bh = display page
	mov	[6],ax		; al = video mode, ah = window width

; check for EGA/VGA and some config parameters

	mov	ah,#0x12
	mov	bl,#0x10
	int	0x10
	mov	[8],ax
	mov	[10],bx
	mov	[12],cx

; Get hd0 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0080
	mov	cx,#0x10
	rep
	movsb

; Get hd1 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	rep
	movsb

    经过上面的操作,最后的内存存放内容如下所示。

在这里插入图片描述

    接下来 关闭中断 ,因为后面要把原本是BIOS写好的中断向量表覆盖掉,即原来内存中0 ~ 0x3FF的位置,要存放系统编译后的文件,因此此时是不能允许中断进来的,禁止终端采用如下指令:

cli

????system代码存放

    接下来就是要把系统代码的240个扇区从上一个boot文件设置的0x10000~0x90000处统统移动到 0 ~ 0x80000 处。

mov	ax,#0x0000
	cld			; 'direction'=0, movs moves forward
do_move:
	mov	es,ax		; destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000
	jz	end_move
	mov	ds,ax		; source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000
	rep
	movsw
	jmp	do_move

    对这段代码而言,将sidi设置为了0,然后调用rep movsw进行复制,复制次数是0x80000次。现在来说,内存的分布情况如下图所示:

在这里插入图片描述

????段描述符

    首先需要知道为什么需要 段描述符 。其实这个是一个x86架构的历史遗留问题,现在的CPU 几乎都支持32位或64位模式了,但仍然需要解决一下16位实模式下的CPU这个历史遗留问题,于是就出现了模式转换,从实模式切换到保护模式。

    所谓的实模式,即CPU可以访问任何物理地址,只能访问20根地址线所能达到的1M的大小。保护模式则可以突破这个限制,并引入了分页管理的概念,对内存地址进行了保护。关于具体区别,其中也有很深的渊源,日后可以单独出篇博客,这里还是回归主线,只是简单讲一下实模式与保护模式的寻址区别。

    实模式的寻址比较简单粗暴,采用: 段基址+偏移地址 的方式。保护模式的寻址模式则相对复杂很多:首先根据段寄存器(ds,es,ss,cs)存储的值作为 段选择子 ,段选择子去全局描述符表(GDT)中找 段描述符 ,在段描述符中取出基地址,最后加上偏移地址构成最终的物理地址。

    这个GDT存发在内存的哪个地方呢?答案是gdtr寄存器。这个寄存器是一个48位的寄存器,结构如下所示:

在这里插入图片描述
    具体内存中是怎么实现的呢,如下所示:

lgdt	gdt_48		; load gdt with whatever appropriate

gdt_48:
	.word	0x800		; gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	; gdt base = 0X9xxxx

    这段代码一点一点来分析,使用lgdt指令可将后面的 gdt_48放入到GDTR寄存器中,后面是解释gdt_48标签的内容。第一行表示的是GDT界限,即多少个段描述符,0x800换算十进制是2048,一个段描述符是64位,8个字节(待会细说),因此一共能存 256个GDT 。第二行是GDT的内存起始地址,段基址是0x90000,512是0x200,因此最后存放的位置就是0x90200+gdt

    gdt标签又在哪里呢,代码先放一放,先来介绍一下段描述符,其结构如下所示:

在这里插入图片描述
    还记得上文提到的保护模式吗,保护模式是会做区分的,因此此时csds这些不同的段终于可以有各自的意义了。段描述符可以给代码段,数据段,其区分的方式是看其中的S位,1是代码段或数据段。

    好了,接下来我们可以回到源码,看刚刚我们要看的gdt标签处的代码了,如下所示:

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

    来分析这段代码,首先给了四个0,因此 最开始是一个空的描述符 ,然后看第二大段,第二大段最开始给了0x07ff,对应上图的段限长,0x7ff2047,每个段至少占用一个字节,因此是2048字节,即2MB,然后0x0000是段基地址,随后的0x9A00涉及的位比较多,参考上面的表涉及到了S位,TYPE位,Base位,展开二进制后可以发现S位是1,因此 该段表示代码段 ,后面的0x00c0则对应上图剩下的位。对于第二段同理,可以看到可以代表是数据段。

    上面看不懂没关系,只需要关注下面的逻辑关系即可,下面这张图也是段选择子中描述符索引的内容,比如段选择子为1,表示代码段:
在这里插入图片描述


    再来回顾一下寻址方式,这个很重要,先去段寄存器拿段选择子,然后根据段选择子到GDT中拿段基地址,最后加上偏移得到物理地址,流程如下所示:

在这里插入图片描述

    截止目前为止,内存中的结构如下所示:

在这里插入图片描述

????突破寻址瓶颈

    上文有提到,实模式下是20位寻址线,即只可访问1MB的空间,这里突破寻址瓶颈顾名思义就是突破1MB的寻址空间。至于为什么到了如今32位,甚至64位的今天,还需要保留这个20位寻址呢,那只能说是历史遗留问题,为了兼容。如果不突破这个20位寻址限制,那即使有32位寻址线,仍然会收到20位寻址线的限制。

    换到具体代码中的操作如下所示:

mov	al,#0xD1		; command write
out	#0x64,al
mov	al,#0xDF		; A20 on
out	#0x60,al

    开启A20门有几种方式,第一种是键盘控制,第二种是I/O端口0x92来处理,第三种是使用int 15来处理。这里之前关闭了中断,代码里是用的键盘控制。具体是向端口 0x64 发送 0xD1,其通常用于重置键盘控制器的状态,使其能够正确地处理后续的命令。然后,向端口 0x60 发送 0xDF来开启A20。

????进入保护模式

    想要开启保护模式很简单,只需要将cr0寄存器的位0置1即可,在汇编中可以采用指令lmsw写入,实际代码如下所示:

mov	ax,#0x0001	; protected mode (PE) bit
lmsw	ax		; This is it;
jmpi	0,8		; jmp offset 0 of segment 8 (cs)

    在代码的最后,使用jmpi指令跳转到一个新的位置。那么这个位置是什么呢?这里有个注意点,此时已经是变为保护模式了,寻址方式和实模式是不一样的。该指令将cs置为8,ip指针置为0,8展开二进制并对应下面段选择子结构可以发现,段描述符索引为1,对照上文中全局描述符表可以发现,是代码段描述符,段基址为0,偏移也是0,所以最终跳转的位置还是0地址,即整个system这个大模块。
在这里插入图片描述

其他部分

    在这里重新编程了中断,可以不用看,代码如下:

mov	al,#0x11		; initialization sequence
out	#0x20,al		; send it to 8259A-1
.word	0x00eb,0x00eb		; jmp $+2, jmp $+2
out	#0xA0,al		; and to 8259A-2
.word	0x00eb,0x00eb
mov	al,#0x20		; start of hardware int's (0x20)
out	#0x21,al
.word	0x00eb,0x00eb
mov	al,#0x28		; start of hardware int's 2 (0x28)
out	#0xA1,al
.word	0x00eb,0x00eb
mov	al,#0x04		; 8259-1 is master
out	#0x21,al
.word	0x00eb,0x00eb
mov	al,#0x02		; 8259-2 is slave
out	#0xA1,al
.word	0x00eb,0x00eb
mov	al,#0x01		; 8086 mode for both
out	#0x21,al
.word	0x00eb,0x00eb
out	#0xA1,al
.word	0x00eb,0x00eb
mov	al,#0xFF		; mask off all interrupts for now
out	#0x21,al
.word	0x00eb,0x00eb
out	#0xA1,al

    经过以上代码,现在中断号与用途的对应关系如下所示:

在这里插入图片描述

????总结

    整个setup部分就做了三件事,第一件事是做代码搬运和临时变量存放,第二件事是突破寻址瓶颈,第三件事是进入保护模式。在代码搬运阶段,整个操作系统的代码被放到了内存中0 ~ 0x8000的位置。突破寻址瓶颈可以将寻址空间突破1MB。进入保护模式要注意描述符表以及保护模式下的寻址方式的改变。

????参考资料

[1] linux源码趣读
[2] 一个64位操作系统的设计与实现