???? 前言
继上篇博客分享了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
对这段代码而言,将si
和di
设置为了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
标签又在哪里呢,代码先放一放,先来介绍一下段描述符,其结构如下所示:
还记得上文提到的保护模式吗,保护模式是会做区分的,因此此时cs
,ds
这些不同的段终于可以有各自的意义了。段描述符可以给代码段,数据段,其区分的方式是看其中的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
,对应上图的段限长,0x7ff
是2047
,每个段至少占用一个字节,因此是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位操作系统的设计与实现