Linux源码分析:从16位实模式到32位保护模式

编程入门 行业动态 更新时间:2024-10-25 17:25:03

Linux源码分析:从16位实<a href=https://www.elefans.com/category/jswz/34/1771241.html style=模式到32位保护模式"/>

Linux源码分析:从16位实模式到32位保护模式

回顾

从上文得知,在BIOS将Linux的磁盘引导程序bootsect加载到0x07C00之后,bootsect开始执行,其先是将自己移动到了0x90000处,然后设置了段寄存器ds、es、ss,后将setup、system程序加载至了指定位置,并确认了根设备号,最终通过段间跳转指令将CPU控制权交给了setup程序。

至此,操作系统的内核程序已经加载完成,但是计算机依旧运行在16位的实模式下,也就意味着只能利用20根地址总线(即0 ~ 19号地址线),寻址空间仅1MB,也就是寻址范围为0 ~ 0xFFFFF。实模式下的特征是在1MB寻址空间内可以直接软件访问BIOS及周边硬件,但是没有硬件支持的分页机制和实时多任务概念。对于一个现代操作系统来说,这显然是不合适的。因此,从setup开始,做的至关重要的一件事情就是从实模式转变到保护模式下,成为一个真正的现代操作系统。

一、从BIOS中获取系统数据

setup做的第一件事就是从BIOS中获取系统数据,并将其保存到0x90000 - 0x901FF的位置。0x90000是bootsect的始址,并不超过0x90200,但是由于bootsect已经完成了任务,所以这段空间可以直接覆盖掉。

先是读取光标位置,通过BIOS中断0x100x03号功能来实现,代码如下

! ok, the read went well so we get current cursor position and save it for
! posterity.mov ax,#INITSEG ! this is done in bootsect already, but...mov ds,axmov ah,#0x03    ! read cursor posxor bh,bhint 0x10        ! save it in known place, con_init fetchesmov [0],dx      ! it from 0x90000.

该功能号的入口参数为页号码,通过寄存器bx的高八位bh来传递,这里传入的是0,即通过xor运算将bh寄存器清零。

返回的参数包括

  • ch,扫描开始线;
  • cl,扫描结束线;
  • dh,行号(0x00是顶端);
  • dl,列号(0x00是左边)。

dx寄存器总共两个字节,从0x90000开始保存,即占用0x900000x90001两个字节。

接下来获取拓展内存大小(即RAM中高于1MB的部分),调用0x15中断的0x88功能号实现,代码如下

! Get memory size (extended mem, kB)mov ah,#0x88int 0x15mov [2],ax

返回值保存在ax寄存器中,共两个字节,保存在0x900020x90003中。

接下来读取显卡数据,通过0x10中断的0x0f功能实现,代码如下

! Get video-card data:mov ah,#0x0fint 0x10mov [4],bx      ! bh = display pagemov [6],ax      ! al = video mode, ah = window width

返回参数为

  • ah,字符列数;
  • al,显示模式;
  • bh,当前显示页。

然后分别保存在偏移为46的位置。

接下来检查显示方式并取参数,分别通过0x10中断的0x120x10功能来实现,代码如下

! check for EGA/VGA and some config parametersmov ah,#0x12mov bl,#0x10int 0x10mov [8],axmov [10],bxmov [12],cx

返回参数为

  • bh,显示状态(0x00-彩色模式, I/O端口 = 0x3dX0x01-单色模式, I/O端口=0x3bX);
  • bl,安装的显示内存;
  • cx,显示卡特性参数。

然后分别保存在偏移为8, 10, 12的位置。

接着读取第一个硬盘的信息。需要注意的是,第一个硬盘参数表的首地址是中断向量0x41的向量值,该参数表的长度为16字节,保存的地址始址为0x90080,连续16个字节(0x10)。代码如下

! Get hd0 datamov ax,#0x0000mov ds,axlds si,[4*0x41]mov ax,#INITSEGmov es,axmov di,#0x0080mov cx,#0x10repmovsb

和前面不一样,这里使用es:di来指向传输的目的地址,而ds:si则指向参数表的地址,即源地址。

接下来要读取第二个硬盘的信息,代码逻辑和上述一致,其参数表的地址是中断向量0x46的地址值,由于第一个硬盘参数表刚好保存到0x9008F的位置,那么第二个硬盘的参数表就是从0x90090开始,连续16个字节(0x10),代码如下

! Get hd1 datamov ax,#0x0000mov ds,axlds si,[4*0x46]mov ax,#INITSEGmov es,axmov di,#0x0090mov cx,#0x10repmovsb

最后要做的是检查系统是否存在第二个硬盘,如果不存在则将上述保存的第二个硬盘参数表清零,代码如下

! Check that there IS a hd1 :-)mov	ax,#0x01500mov	dl,#0x81int	0x13jc	no_disk1cmp	ah,#3je	is_disk1
no_disk1:mov	ax,#INITSEGmov	es,axmov	di,#0x0090mov	cx,#0x10mov	ax,#0x00repstosb

这个过程通过调用中断0x130x15号功能来实现。入口参数为dl=驱动器号,其中0x8X表示硬盘:0x80表示第一个硬盘、0x81表示第二个硬盘,那么自然这里必然是0x81。其出口参数为ah=类型码00表示不存在此盘,并将CF置位;01表示软驱,没有change-line支持;02表示软驱或其它可移动设备,有change-line支持;03表示硬盘。

通过jc指令检查CF是否置位,如果置位即不存在第二个硬盘,那么就跳转至no_disk1处清零第二个参数表。如果存在第二个硬盘,那么jc指令自然不满足则执行cmp指令判断设备是否为硬盘,如果是则将标志寄存器ZF置位(即ah == 03)。再通过je指令判断ZF是否置位,如果置位那么代表设备为硬盘,那么就跳转至is_disk1处继续执行。即

is_disk1:

二、关中断!并将system移动至0x00000处

is_disk1第一句代码就是关中断,如下

is_disk1:! now we want to move to protected mode ...cli         ! no interrupts allowed !

cli指令将CPU标志寄存器中中断允许标志IF置零,即不允许中断。关中断是16位实模式进入32位保护模式的标志性动作,这意味着接下来就可以废除16位实模式下的中断向量表,并初步打开32位寻址空间、建立保护模式下的中断响应机制等,这些都是与32位保护模式相配套的。

作为转变的开始,已经关闭了中断,那么接下来系统将不会响应中断,以便一心一意向保护模式转变。现在开始将system移动至内存始址处,代码如下

! 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

其中es:di指向目的地址0x0000:0x0处,ds:si指向源地址0x10000:0x0处,由于起初假设system模块不会超过0x80000,即512KB,那么就不会超过0x90000,即system最初不会覆盖bootsect。那么这段程序就是将[0x10000, 0x10000 + 512KB)的内存数据块移动[0x00000, 0x00000 + 512KB)处,移动的数据块长度为0x8000节,即512KB。也就是说将每个源地址字节向内存低端移动0x10000个位置最终到达目标位置,上述汇编代码可以用如下伪码描述

ax = 0x0000
while truees = axax += 0x1000ds = axdi = si = 0x0ds:si to es:di, moving one seg continuously, i.e. 64KBif ax + 0x1000 == 0x9000break

再进一步地说明,上述伪码中各参数变动情况如下所示(注:下述while循环用来解释上述中的ds:si to es:di, moving one seg continuously, i.e. 64KB)

The first cycle is as follows:es = 0x0000, ax = 0x1000, ds = 0x1000, di = 0x0, si = 0x0, cx = 0x8000while cx != 0x0000ds:si to es:di, one word at a time, i.e. movswcx -= 0x0001The second cycle is as follows:es = 0x1000, ax = 0x2000, ds = 0x2000, di = 0x0, si = 0x0, cx = 0x8000while cx != 0x0000ds:si to es:di, one word at a time, i.e. movswcx -= 0x0001
...

这就很容易理解了。那么该段汇编就可以整体上用如下伪码描述

ax = 0x0000
while truees = axax += 0x1000ds = axdi = si = 0x0cx = 0x8000while cx != 0x0000ds:si to es:di, one word at a time, i.e. movswcx -= 0x0001if ax + 0x1000 == 0x9000break

至此,就完成了对system模块的移动。对system模块的移动起到了如下的效果

  • 废除了BIOS中断向量表,等同于废除了BIOS所提供的实模式下的中断服务程序;
  • 回收已经无用的内存空间,因为要向保护模式转变,BIOS中断向量表所占空间自然无用,应当回收;
  • 让system模块占据内存物理地址最天然、有利的位置。

三、设置IDT与GDT

IDT,即中断描述符表(Interrupt Descriptor Table),其保存的是所有中断服务程序的入口地址,就类似于实模式下的中断向量表,这也是构建保护模式下中断机制的开始。实模式下终端向量表的始址在0x00000处,这个位置是固定的,而保护模式下IDT的位置是不固定的,可以在任何位置,那么为了找到IDT就要将IDT的入口地址保存在一个寄存器当中,这个寄存器就是IDTR(Interrupt Descriptor Table Register, IDT基址寄存器),该寄存器共48位,即3个字长,第一个字是限长,剩余的两个字是基地址,结构如下

47----15-----0base | limit 

将IDT入口地址传递给IDTR的过程就是设置IDTR,代码如下

    lidt    idt_48      ! load idt with 0,0

那么这里标号idt_48对应的就是IDT的入口地址,idt_48的内容如下

idt_48:.word	0           ! idt limit=0.word	0,0         ! idt base=0L

按照上面的解释,这就很容易理解了。第一个字为限长,这里为0,剩余两个字为基址,也是0,即基址就是0x00000处。这里基址用两个字描述的初衷在于第三个字是段基址,第二个字就是偏移,那么整个地址0x00000就是0x00000 = 0x0000 * 16 + 0x0000。即这里的IDT依旧是放在内存开始处。下面的GDTR结构的解读也是同理。

与实模式不同的是,保护模式下的段寻址是通过GDT(Global Descriptor Table, 全局描述符表)完成的,GDT中存放的是段寄存器内容,其数据结构为数组。GDT在操作系统进程切换中意义重大,其中存放了每个任务的LDT(Local Descriptor Table, 局部描述符表)地址和TSS(Task Structure Segment, 任务状态段)地址,以完成进程中各段的寻址、现场保护与现场恢复。

GDT的初始内容已经写在了setup程序中,如下

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

可以整理为如下一张表

 index |    GDT
------------------2   | 00C0 9200| 0000 07FF
------------------1   | 00C0 9A00| 0000 07FF
------------------0   | 0000 0000| 0000 0000

其中每个GDT表项共64位,即8字节/4字,结构如下

31------------------------------16-15-----------------------0Base 0:15            |         Limit 0:15
63--------56-55---52-51---------48-47---------40-39--------32Base 24:31 | Flags | Limit 16:19 | Access Byte | Base 16:23

Access Byte的结构如下

8---7-------5---4----3----2----1---0Pr | Privl | 1 | Ex | Dc | RW | Ac

Flags的结构如下

8---7----6---5--4Gr | Sz | 0 | 0

特别说明的Privl为特权级,如果为00表示内核特权级,如果为11,则表明用户特权级;Gr标志位为颗粒度标志,如果为1,那么段限长的单位为4KB,如果为0,那么就是1B。

显而易见,GDT将段基址与段限长拆分保存在不连续的bit位中。这是为了兼容286架构。那么现在GDT表项就很容易理解了。

以第一项GDT为例,其内容为

00C0 9A00
0000 07FF

0-15bit48-51bit构成段限长,内容为

007FF

将其转换为10进制就是2047bit,即2KB;再看一下颗粒度标志,其包含在最后一个字节,即

00C0    // 00000000 11000000

可见,颗粒度标志位的值为1,那么也就意味着段限长实际上为0x007FF * 4KB = 8MB。确定了段限长,我们再确定一下段基址,16-31bit32-39bit56-63bit构成了段基址,那么合起来就是

0x0000000

即内存始址。

现在我们知道了GDT的内容与含义,那么设置GDT就是将GDT表的始址保存在GDTR(Global Descriptor Table Register, GDT基地址寄存器)中,通过下述指令完成

    lgdt    gdt_48      ! load gdt with whatever appropriate

也就是GDT的始址信息保存在gdt_48标号处,其内容为

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

GDTR与IDTR的结构一致,那么也就是说第一个字0x800为限长,即十进制2048bit 或 2KB,由于每8Byte构成一个段描述符,所以GDT中共有256项;第三个字也就是GDT的基地址,即0x9000;第二个字当中,512为一扇区,也就是0x00200gdt为setup程序中GDT表的偏移,那么512+gdt实际上就是指向了0x9000段中偏移为gdt的位置处。所以整个地址就可以计算为0x9000 * 16 + (512 + gdt)

综上,总结一下,IDTR与GDTR分别说明了IDT与GDT的入口地址在哪(base),也说明了IDT和GDT中最多有多少个表项(limit)。

四、打开A20,实现32位寻址

这是进入保护模式的关键,因为保护模式下必须突破16位寻址以实现32位寻址,就是通过打开A20地址线实现的。

IBM公司最初的PC机使用的是Intel 8088处理器。该微机中地址线只有20根(A0-A19),当是RAM只有几百KB不到1MB,这20根地址线是完全够用的,所能寻址的最高地址为0xffff:0xffff,即1MB处(0xfffff = 0xffff * 16 + 0xffff),那么对于超过1MB的寻址地址将环绕到内存始址处(注意这个细节,可以利用这个特点来检测A20地址线是否打开)。当1985年引入AT机时使用的Intel 80286处理器具有24根地址线,最高寻址16MB,并且有一个与8088完全兼容的实模式运行方式,但是,在寻址值超过1MB时,80286却无法像8088那样实现地址寻址环绕。为了完全实现兼容,IBM最终使用了一个被称之为A20的信号,当A20为0时,那么比特20及以上的地址线都会被清除,从而实现兼容。

机器启动时,A20是默认关闭的,所以只能实现实模式下1MB寻址,那么要进入保护模式就需要打开A20,实现32位寻址。代码如下

! that was painless, now we enable A20call  empty_8042mov   al,#0xD1      ! command writeout   #0x64,alcall  empty_8042mov   al,#0xDF      ! A20 onout   #0x60,alcall  empty_8042

选通A20之后,Linux 0.11就可以实现32位寻址,其线性寻址空间就是4GB!

五、对8259A中断控制器进行重编程

由于CPU在保护模式下,int 0x00 - int 0x1F被Intel保留为内部不可屏蔽中断和异常中断。如果不对8259A进行重新编程的话,那么也就意味着int 0x00 - int 0x1F会被保护模式下的Intel保留中断所覆盖掉,因此,必须重新编程,其本质就是重新建立映射关系。代码如下

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.mov al,#0x11        ! initialization sequenceout	#0x20,al        ! send it to 8259A-1.word   0x00eb,0x00eb       ! jmp $+2, jmp $+2out	#0xA0,al        ! and to 8259A-2.word   0x00eb,0x00ebmov	al,#0x20        ! start of hardware int's (0x20)out	#0x21,al.word   0x00eb,0x00ebmov	al,#0x28        ! start of hardware int's 2 (0x28)out	#0xA1,al.word   0x00eb,0x00ebmov	al,#0x04        ! 8259-1 is masterout	#0x21,al.word   0x00eb,0x00ebmov	al,#0x02        ! 8259-2 is slaveout	#0xA1,al.word   0x00eb,0x00ebmov	al,#0x01        ! 8086 mode for bothout	#0x21,al.word   0x00eb,0x00ebout	#0xA1,al.word   0x00eb,0x00ebmov	al,#0xFF        ! mask off all interrupts for nowout	#0x21,al.word   0x00eb,0x00ebout	#0xA1,al

重新编程前后的中断请求号与中断号之间的对应关系如下

    before       |        after
-----------------------------------
IRQ0  -> 0X00    |    IRQ0  -> 0X20
IRQ1  -> 0X01    |    IRQ1  -> 0X21
...
IRQ14 -> 0X0E    |    IRQ14 -> 0X2E
IRQ15 -> 0X0F    |    IRQ15 -> 0X2F

六、进入32位保护模式

setup执行以来,从关中断到对8259A重编程,都是在为进入保护模式做准备,下面两行代码直接设置CPU进入32位保护模式运行。

    mov	ax,#0x0001  ! protected mode (PE) bitlmsw    ax      ! This is it!

lmsw指令的作用是加载机器状态字,即load Machine Status Word,也称之为控制寄存器CR0,共32位,存放系统控制标志,其第0位为PE(Protected Mode),若为1则表明设置处理器工作方式为保护模式。

#0x0001就是总共16位,最低位为1,上述代码加在CR0之后就可以通过ax寄存器将PE位置1,自此,CPU正式进入保护模式。

七、跳转!进入system执行head.s

CPU进入保护模式工作后,最明显的特征就是要根据GDT决定后续执行程序所在段(即,要执行的程序在哪)。setup向head.s跳转也是同理,如下

    jmpi    0,8     ! jmp offset 0 of segment 8 (cs)

通过段间跳转指令jmpi跳转至8:0处执行,这里的8如何理解呢?

如果将8转为二进制就容易了,即1000,这里的每个比特位都有含义,第一第二位表示内核特权级,前面说过,00表示GDT表项为内核特权级,11则为用户特权级;第三位用于区分GDT和LDT,若为0则表示GDT,1表示LDT;第四位1表示GDT的第二项,即索引为1的那一项,我们前面刚好分析过这一项。那么上述代码就是跳转到以第二项GDT的base为段地址,以0位偏移的内存处。上面分析过,第二项GDT的base为0x0000000,即内存始址,那么再加上偏移0还是内存始址,这里是什么呢?就是system模块的head.s程序。

本文完!

更多推荐

Linux源码分析:从16位实模式到32位保护模式

本文发布于:2024-02-08 20:41:15,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1674818.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:模式   源码   Linux

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!