setup.s 解读——Linux-0.11 剖析笔记(三)

题目:setup.s 解读——Linux-0.11 剖析笔记(三)

更新记录

版本 时间 修订内容
1.0 2018-4-14 增加了“获取显示模式”这一节,AL取值的表格
2.0 2020-6-27 补充了一些内容

本文由 setup.s 分析—— Linux-0.11 学习笔记(二) 修改而来。

文章目录

  • 定义符号常量
  • 获取一些参数保存在 0x90000 处
    • 保存光标的位置
    • 获取从 1M 处开始的扩展内存大小
    • 获取显示模式
    • 检查显示方式(EGA/VGA)并获取参数
    • 复制硬盘参数表
      • 复制 HD0 的硬盘参数表
      • 复制 HD1 的硬盘参数表
      • 检查系统是否有第2个硬盘
  • 关中断
  • 移动 system 模块到 0x00000
  • 加载IDT
  • 加载GDT
  • 开启A20
  • 设置8259
    • ICW1
    • ICW2
    • ICW3
    • ICW4
    • OCW1
  • 进入保护模式
  • 总结

为了节省篇幅,完整的代码就不贴了。

定义符号常量

INITSEG  = 0x9000	! bootsect.s 的段地址
SYSSEG   = 0x1000	! system loaded at 0x10000 
SETUPSEG = 0x9020	! 本程序的段地址

注意:以上这些参数应该和 bootsect.s 中的相同。

获取一些参数保存在 0x90000 处

保存光标的位置

	mov	ax,#INITSEG	 ! INITSEG = 0x9000
	mov	ds,ax        ! ds = 0x9000
	mov	ah,#0x03     ! 功能号=3,获取光标的位置
	xor	bh,bh        ! bh = 页号 = 0(输入)
	int	0x10		 ! 输出: DH=行号,DL=列号
	mov	[0],dx	     ! 保存光标的行号和列号到 0x90000,共占2字节.

获取从 1M 处开始的扩展内存大小

    ! 利用 BIOS 中断 0x15 功能号 ah = 0x88 取系统所含扩展内存大小,并保存在内存 0x90002 处
    ! 返回:ax=从0xl00000(lM)处开始的扩展内存大小(KB).若出错则CF置位,ax=出错码
	mov	ah,#0x88
	int	0x15
	mov	[2],ax ! ax = 从1M处开始的扩展内存大小

获取显示模式

    ! 获取显示卡当前的显示模式
    ! 调用 BIOS 中断 0x10,功能号 ah = 0x0f
    ! 返回: ah=字符列数; al=显示模式;bh=当前显示页。
    ! 0x90004(l个字)存放当前页;0x90006(1字节)存放显示模式;0x90007(1字节)存放字符列数。
	mov	ah,#0x0f
	int	0x10
	mov	[4],bx		! bh = 当前显示页
	mov	[6],ax		! al = 显示模式, ah = 字符列数(窗口宽度)

AL 取值的含义如下表

AL Type Format Cell Colors Adapter Addr Monitor
0 text 40x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
1 text 40x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
2 text 80x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
3 text 80x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
4 graphic 320x200 8x8 4 CGA,EGA b800 Comp,RGB,Enh
5 graphic 320x200 8x8 4 (shades) CGA,EGA b800 Composite
6 graphic 640x200 8x8 2 CGA,EGA b800 Comp,RGB,Enh
7 text 80x25 9x14* 3 (b/w/bold) MDA,EGA b000 TTL Mono
8,9,0aH PCjr modes
0bH,0cH (reserved; internal to EGA BIOS)
0dH graphic 320x200 8x8 16 EGA,VGA a000 Enh,Anlg
0eH graphic 640x200 8x8 16 EGA,VGA a000 Enh,Anlg
0fH graphic 640x350 8x14 3 (b/w/bold) EGA,VGA a000 Enh,Anlg,Mono
10H graphic 640x350 8x14 4 or 16 EGA,VGA a000 Enh,Anlg
11H graphic 640x480 8x16 2 VGA a000 Anlg
12H graphic 640x480 8x16 16 VGA a000 Anlg
13H graphic 640x480 8x16 256 VGA a000 Anlg

Notes: With EGA, VGA, and PCjr you can add 80H to AL to initialize a video mode without clearing the screen.

The character cell size for modes 0-3 and 7 varies, depending on the hardware. On modes 0-3: CGA=8x8, EGA=8x14, and VGA=9x16. For mode 7, MDPA and EGA=9x14, VGA=9x16, LCD=8x8.

检查显示方式(EGA/VGA)并获取参数

	! 检查显示方式(EGA/VGA)并获取参数。
	! 调用 BIOS 中断 0x10,功能号: ah = 0xl2,子功能号: bl = 0xl0
	! 返回:bh=显示状态。 0x00-彩色模式,I/O 端口=0x3dX
	!                  0x01-单色模式,I/O 端口=0x3bX
	! bl = 安装的显示内存。0x00 - 64k
	!                   0x01 - 128k
	!                   0x02 - 192k
	!                   0x03 - 256k
	! cx = 显示卡特性参数。
	!
	mov	ah,#0x12 ! 功能号
	mov	bl,#0x10 ! 子功能号
	int	0x10
	mov	[8],ax 	    ! 我也不知道这个是什么(╯︵╰)
	mov	[10],bx 	! bh=显示状态(单色模式/彩色模式),bl=已安装的显存大小
	mov	[12],cx 	! ch=特性连接器比特位信息,cl=视频开关设置信息

关于返回参数的详细解释,还是看这张图吧,图片来自赵炯博士的《Linux内核完全剖析》(机械工业出版社,2006)。

BIOS 视频中断 0x10

setup.s 解读——Linux-0.11 剖析笔记(三)_第1张图片

复制硬盘参数表

复制 HD0 的硬盘参数表

! 复制 hd0 的硬盘参数表,参数表地址是中断向量0x41的值,表长度16B
! 中断向量在中断向量表中的位置 = 中断类型号N × 4
! (N*4)的字单元存放偏移地址;
! (N*4+2)的字单元存放段基址。

	mov	ax,#0x0000
	mov	ds,ax      ! ds=0
! 将内存[4*0x41]处的低2字节(偏移地址)传给si,高2字节(段地址)传给ds
	lds	si,[4*0x41]
	mov	ax,#INITSEG
	mov	es,ax          !es = 0x9000
	mov	di,#0x0080
	mov	cx,#0x10       !重复16次
! ds:si --> es:di(0x9000:0x0080),共传送16B
	rep
	movsb

复制 HD1 的硬盘参数表

! 复制 hd1 的硬盘参数表,参数表地址是中断向量0x46的值,表长度16B
! 道理同上一小节,此处不赘述
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]
	mov	ax,#INITSEG ! INITSEG = 0x9000
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
! ds:si --> es:di(0x9000:0x0090),共传送16B
	rep
	movsb

检查系统是否有第2个硬盘

! 检查系统是否有第2个硬盘,如果没有就把第2个参数表清零
! 利用 BIOS 中断调用 0x13 的取盘类型功能,功能号 ah = 0xl5;
! 输入: dl=驱动器号(0x8X 是硬盘:0x80 指第 1 个硬盘,0x81 第 2 个硬盘)
! 输出: ah=类型码;00-没有这个盘,CF 置位;
!                  01-是软驱,没有 change-line 支持;
!                  02 -是软驱(或其他可移动设备),有 change-line 支持;
!                  03 -是硬盘。
!
	mov	ax,#0x01500 ! 功能号 ah=0x15,读取盘类型
	mov	dl,#0x81    ! dl=驱动器号,0x81代表第2个硬盘
	int	0x13        
	jc	no_disk1    ! CF置位,表示没有这个盘
	cmp	ah,#3       
	je	is_disk1    ! ah=3表示存在第2个硬盘,跳转到is_disk1
no_disk1:
! 清空第2个表
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090  ! es:di = 0x9000:0x0090
	mov	cx,#0x10
	mov	ax,#0x00    ! AL=0
	rep
	stosb           ! Store AL at address es:di
is_disk1:

关中断

! 为进入保护模式做准备

	cli			! no interrupts allowed !

移动 system 模块到 0x00000

bootsect.s 引导程序将 system 模块读入到 0xl0000 开始的位置。由于当时假设 system 模块最大长度不会超过 0x80000 (512KB),即其末端不会超过内存地址 0x90000,所以 bootsect.s 会把自己移动到0x90000 开始的地方,并把 setup 加载到它的后面。下面这段程序的用途是再把整个 system 模块移动到 0x00000 位置,即把从 0x10000 到 0x8ffff 的内存数据块(共512KB)整块地向内存低端移动了0x10000(64KB)。

! 从代码实现来看,是一小块(0x10000B=64KB)一小块移动的,共移动8小块。
	mov	ax,#0x0000
	cld		    	! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! es是目的段地址
	add	ax,#0x1000
	cmp	ax,#0x9000   ! 当 ax==0x9000 时结束移动
	jz	end_move
	mov	ds,ax		! ds是源段地址,ds比es大0x1000
	sub	di,di        ! di = 0
	sub	si,si       ! si = 0
	mov cx,#0x8000  ! 重复 0x8000次
	rep             ! ds:si --> es:di
	movsw           ! 每次移动2B.
	jmp	do_move    ! 本轮一共移动 0x8000*2B = 0x10000B=64KB. 准备下一轮移动

end_move:

上面的汇编代码写成伪C语言代码如下:

ax = 0;
cld;

while(1){
	es = ax;
	ax += 0x1000;
	if(ax == 0x9000)
		break;	//结束移动
	ds = ax;
	di = si = 0;
	for(int i=0; i<0x8000; ++i){
	   memcpy(es:di, ds:si, 2);
	   di += 2;
	   si += 2;
	}
}

搬运示意图如下:
setup.s 解读——Linux-0.11 剖析笔记(三)_第2张图片

加载IDT

CPU 要求在进入保护模式之前需设置 IDT 表, 这里先设置一个长度为 0 的空表.

end_move:
	mov	ax,#SETUPSEG	
	mov	ds,ax            !ds = 0x9020,指向本程序段,setup.s 被加载到 0x90200
	! idt_48 标号处的内容如下
	! idt_48:
	!        .word	 0			    ! idt 界限值=0
	!		 .word	 0,0		    ! idt 基地址=0L
	
	lidt	idt_48		! load idt with 0,0

加载GDT

	!gdt_48 标号处的内容如下
	!gdt_48:
	!.word	0x800	     ! 0x800 = 2048, 2048/8=256,可容纳256个描述符, 其实0x7ff即可
	!.word	512+gdt,0x9	 ! setup.s被加载到0x90200, gdt base = 0x90200+gdt = 0x90000+512+gdt
	lgdt	gdt_48		

系统开机时,默认进入实模式,这时候如果想进入保护模式,必须先设置 GDT 表,所以首先是在实模式设置GDT 表,为进入保护模式做准备。进入保护模式后,如果不开启分页,那么线性地址是等同于物理地址的,所以,在实模式设置 GDT 的时候,用的是物理地址。

当进入保护模式且启用分页,这时候线性地址不再等同于物理地址,线性地址经过页部件转换后才是物理地址。所以之前设置的物理地址,会被当成线性地址。为了不出问题,这个线性地址就要和物理地址等价,即经过页部件的转换后值不变。

斜体字是我自己的理解,不知道对不对。

开启A20

什么是A20?为什么要开启?可以参考我的博文: 关于A20

PC 机主板上的键盘接口是专用接口,它可以看作是常规串行端口的一个简化版本。该接口被称为键盘控制器,它使用串行通信协议接收键盘发来的扫描码数据。主板上所采用的键盘控制器是 Intel 8042 芯片或其兼容芯片。现今的主板上已经不包括独立的 8042 芯片了,但是主板上其他集成电路会为兼容目的而模拟 8042 芯片的功能。另外,该芯片输出端口 P2 各位被分别用于其他目的。bit 0 (P20 引脚)用于实现 CPU 的复位操作(低电平导致复位),bit 1(P21 引脚)用于控制 A20 信号线的开启与否,为 1 时就开启(选通)A20 信号线,为 0 则禁止 A20 信号线。

P2 各个位的含义如下表:

Pin Name Function
0 P20 System Reset. 1: Normal;0: Reset computer
1 P21 Gate A20
2 P22 Undefined
3 P23 Undefined
4 P24 Input Buffer Full
5 P25 Output Buffer Empty
6 P26 Keyboard Clock. 1: Pull Clock low;0: High-Z
7 P27 Keyboard Data. 1: Pull Data low ,0: High-Z
	call empty_8042      ! 等待输入缓冲器为空
	mov	al,#0xD1		
	out	#0x64,al         !向端口 0x64(输入缓冲器) 写命令,命令码是 0xD1
	call	empty_8042   ! 等待输入缓冲器为空,即命令被接受
	mov	al,#0xDF		 ! A20 on
	out	#0x60,al         !向端口 0x60 写参数,参数是 0xDF
	call	empty_8042   ! 等待输入缓冲器为空,即参数被接受

第三行,0x64,是状态寄存器(读)或者输入缓冲器(写)

802x 的各个端口如下图

setup.s 解读——Linux-0.11 剖析笔记(三)_第3张图片setup.s 解读——Linux-0.11 剖析笔记(三)_第4张图片

	mov	al,#0xD1		
	out	#0x64,al         !向端口 0x64(输入缓冲器) 写命令,命令码是 0xD1
	
	...
	mov	al,#0xDF		 ! A20 on
	out	#0x60,al         !向端口 0x60 写参数,参数是 0xDF
	...

向 0x64 端口写命令,可以带一个参数,参数从端口 0x60 写入。

0xD1 是命令码,表示写芯片 8042 的输出端口 P2,此命令后面带一个字节的参数,这个参数由端口 0x60 写入。要开启 A20,就要使参数的 b1=1,另外还要使 b0=1,否则系统会复位。

mov al,#0xDF

0xDF 是参数,写成二进制是 1101_1111,其他位不理解,总之 P20 和 P21 都配成 1 了。

至于机器是否真正开启了 A20 地址线,我们还需要在进入保护模式之后再测试一下。这个工作放在了head.s 程序中。head.s 的代码咱们以后再分析。

解释一下empty_8042这个过程。

empty_8042:
	.word	0x00eb,0x00eb !机器码,跳转到下一句,为了延时
	in	al,#0x64	! 8042 status port
	test al,#2		! is input buffer full?
	jnz	empty_8042	! yes - loop
	ret

第 3 行:读端口 0x64 到 AL.

读端口 0x64 就是读 8042 的状态寄存器(一个 8bit 的只读寄存器),bit 1为 1 时表示输入缓冲器满,为 0 时表示输入缓冲器空。要向 8042 写命令(通过 0x64 端口写入),必须当输入缓冲器为空时才可以

第 4 行:用于检测 bit 1,如果为 1,则跳转到 empty_8042 标号处继续检测,直到 bit 1为 0 才返回。所以empty_8042这个过程就是为了等待输入缓冲器为空。

这里介绍一下 test 指令。

Operation
TEMP ← SRC1 AND SRC2;
SF ← MSB(TEMP);
IF TEMP = 0
THEN ZF ← 1;
ELSE ZF ← 0;
FI:
PF ← BitwiseXNOR(TEMP[0:7]);
CF ← 0;
OF ← 0;
(AF is Undefined)

第 4 行,把 AL 寄存器的值和 0000_0010b 做“按位与”操作,如果结果是 0,那么 ZF 置位,否则,ZF 是 0;所以,这句代码就是测试 AL 的 bit 1 是否是 1,是 1 则结果不是 0,跳转到 empty_804;是 0 则 ZF 置位,返回。

设置8259

	; ICW1 
	mov	al,#0x11		! initialization sequence
	out	#0x20,al		! send ICW1 to Master
	.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
	out	#0xA0,al		! send ICW1 to Slave
	.word	0x00eb,0x00eb
	;------------------------------------------------------
	; ICW2
	mov	al,#0x20		! 送主芯片ICW2命令字,设置起始中断号,要送奇端口 
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28		! 送从芯片ICW2命令字,设置起始中断号,要送奇端口
	out	#0xA1,al
	.word	0x00eb,0x00eb
	;-------------------------------------------------------
	; ICW3
	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
	;------------------------------------------------------
	; ICW4
	mov	al,#0x01		
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	;------------------------------------------------------
	; OCW1
	mov	al,#0xFF		! mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

字(0x00eb)是直接使用机器码表示的一条相对跳转指令,起延时作用。0xeb是直接近跳转指令的操作码,带1个字节的相对位移值。因此跳转范围是 -128到 +127。CPU 通过把这个相对位移值加到 EIP 寄存器中就形成一个新的有效地址。注意:执行某条指令的时候,EIP会指向它的下一条指令。所以,CPU执行0x00eb的时候,会把EIP的值加上 0 ,其实就是下一条指令的地址,然后跳转到那里去执行。

0x00eb,0x00eb这两条指令共可提供 14~20 个 CPU 时钟周期的延迟时间。在 as86 中没有表示相应指令的助记符,因此 Linus 在 setup.s 等一些汇编程序中就直接使用机器码来表示这种指令。另外,每个空操作指令 N0P 的时钟周期数是 3 个,因此若要达到相同的延迟效果就需要 6 至 7 个 N0P 指令。

关于 8259A 的详细知识可以参考我的博文 : 详解8259A

对于每个命令字的端口,我列了一张速查表。

命令字 A0 主片端口地址 从片端口地址 备注
ICW1 0 0x20 0xA0 D4 = 1
ICW2 1 0x21 0xA1
ICW3 1 0x21 0xA1
ICW4 1 0x21 0xA1
OCW1 1 0x21 0xA1
OCW2 0 0x20 0xA0 D4-D3 = 00
OCW3 0 0x20 0xA0 D4-D3 = 01

ICW1

; ICW1 
mov	al,#0x11		! initialization sequence
out	#0x20,al		! send ICW1 to Master
.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
out	#0xA0,al		! send ICW1 to Slave
.word	0x00eb,0x00eb

2-3 行:向主片写入 0x11 = 0001_0001b, 表示初始化命令开始,它是 ICW1 命令字。 对照表格可以知道(黑体字)——边沿触发、 多片8259级联、最后要发送 ICW4 命令字。

第 5 行,向从片写 0x11

ICW1 含义
D0 1:需要ICW4 0:不需要ICW4
D1 1:单片 0:级联
D2 =0;
D3 1:电平触发 0:边沿触发
D4 =1
D7-D5 =000

ICW2

; ICW2
mov	al,#0x20		! 送主芯片ICW2命令字,设置起始中断号,要送奇端口 
out	#0x21,al
.word	0x00eb,0x00eb
mov	al,#0x28		! 送从芯片ICW2命令字,设置起始中断号,要送奇端口
out	#0xA1,al
.word	0x00eb,0x00eb

3-4 行:送主芯片 ICW2 命令字,设置起始中断号为 0x20,则主片 0~7 级对应的中断号是 0x20~0x27;

6-7 行:送从芯片 ICW2 命令字,设置起始中断号为 0x28,则从片 8~15 级对应的中断号是 0x28~0x2F;

ICW3

; ICW3
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

2-3 行:送主芯片 ICW3 命令字,0x04 = 0000_0100b,表示主芯片的 IR2 连从芯片的 INT。

5-6 行:送从芯片 ICW3 命令字,表示从芯片的 INT 连到主芯片的 IR2 引脚上。

Linux-0.11 内核把 8259A 主片的 ICW3 设置为 0x04,即 S2=1,其余各位为0。表示主芯片的 IR2 引脚连接一个从芯片。从芯片的 ICW3 被设置为 0x02,即其标识号为 2。表示此从片连接到主片的 IR2 引脚。 因此,中断优先级的排列次序为:0 级最高,1 级次之,接下来是从片上的 8~15 级,最后是主片的 3~7 级。

ICW4

; ICW4
mov	al,#0x01		
out	#0x21,al
.word	0x00eb,0x00eb
out	#0xA1,al
.word	0x00eb,0x00eb

送 ICW4 命令字。普通 E0I(需发送指令来复位)、非缓冲方式、非特殊全嵌套。

ICW4 含义
D7-D5 =0
D4 1:特殊全嵌套 ; 0:非特殊全嵌套
D3-D2 0X:非缓冲 ; 10:缓冲-从片; 11:缓冲-主片
D1 1:自动 EOI 0:普通 EOI
D0 =1

8259A 的数据线与系统数据总线的连接有缓冲和非缓冲两种方式,这里配置成后者

普通结束方式是通过在中断服务子程序中编程写入操作命令字 OCW2,向 8259A 传送一个普通中断结束(EOI,end of interrupt)命令(命令中不指定要复位的中断级)来清除 ISR 中优先级别最高的置位。

非特殊全嵌套也叫普通嵌套,也叫做完全嵌套或者普通完全嵌套。此方式是 8259A 在初始化时默认选择的方式。其特点是:IR0 优先级最高,IR7 优先级最低。在 CPU 中断服务期间,若有新的中断请求到来,只允许比当前服务的优先级更的中断请求进入,对于“同级”或“低级”的中断请求则禁止响应。

OCW1

在对 8259A 设置了初始化命令字后,芯片就已准备好接收设备的中断请求信号了。但在 8259A 工作期间,我们也可以利用操作命令字 OCW1~OCW3 来监测 8259A 的工作状况,或者随时改变初始化时设定的 8259A 的工作方式。

需要说明的是,与初始化命令字 ICW1~ICW4 需要按规定的顺序进行设置不同,操作命令字 OCW1~OCW3 的设置没有规定其先后顺序,使用时可根据需要灵活选择不同的操作命令字写入到 8259A 中。

; OCW1
mov	al,#0xFF		
out	#0x21,al           ! 屏蔽主片所有中断请求
.word	0x00eb,0x00eb
out	#0xA1,al           ! 屏蔽从片所有中断请求。

OCW1 用于对 8259 的中断屏蔽寄存器进行读/写操作,写 1 则屏蔽对应级别的中断。
在这里插入图片描述

进入保护模式

下面设置并进入 32 位保护模式运行。

首先加载机器状态字(lmsw,Load Machine Status Word),也称控制寄存器 CR0,其比特位 0 置 1 将使 CPU 切换到保护模式,并且运行在特权级0,即当前特权级 CPL = 0。此时各个段寄存器仍然指向与实地址模式中相同的线性地址处(在实地址模式下线性地址与物理地址相同)。在设置该比特位后,随后一条指令必须是一条段间跳转指令,用于刷新 CPU 当前指令队列。因为 CPU 是在执行一条指令之前就已从内存读取该指令并对其进行译码。然而在进入保护模式以后那些属于实模式的预先取得的指令信息就变得不再有效。而一条段间跳转指令就会刷新 CPU 的当前指令队列,即丢弃这些无效信息。另外,Intel 手册上建议 80386 或以上 CPU 应该使用指令 mov cr0,ax 切换到保护模式。lmsw 指令仅用于兼容以前的 286 CPU。

setup.s 解读——Linux-0.11 剖析笔记(三)_第5张图片

mov	ax,#0x0001	! Protection Enable (bit 0 of CR0).
lmsw ax		    ! 实际上lmsw指令仅仅加载CR0的低4位,由低到高分别是PE,MP,EM,TS
jmpi 0,8		! jmp offset 0 of segment 8 (cs)

实际上lmsw指令仅仅加载CR0的低4位,由低到高分别是PE,MP,EM,TS. 这里我们仅关注 PE,其他的都设为 0.

jmpi 0,8 段间跳转指令。执行后,CS=8,IP=0.

关于这里的段间跳转,要多说几句。
即使是在实模式下,段寄存器的描述符高速缓存器也被用于访问内存,仅低 20 位有效,高 12 位是全零。当处理器进入保护模式后,这些内容依然残留着,但是,这些残留的内容在保护模式下是无效的,迟早会在执 行某些指令的时候出问题。因此,比较安全的做法是尽快刷新 CS、SS、DS 、ES 、FS 和 GS 的内容,包括它们的段选择器和描述符高速缓存器。

在进入保护模式之前,有很多指令已经进入了流水线。因为处理器工作在实模式下,所以它们都是按 16 位操作数和 16 位地址长度进行译码的,即使是那些用 bits 32 编译的指令。进入保护模式后,受 CS 段描述符高速缓存器中实模式残留内容的影响,处理器进入 16 位保护模式工作。如果保护模式下的代码是 16 位的,影响可能不大,但如果是用 bits 32 编译的,那么,由于对操作数和默认地址大小的解释不同,指令的执行结果可能会不正确,所以必须清空流水线。同时,那些通过乱序执行得到的中间结果也是无效的,必须清理掉,让处理器串行化执行,即重新按指令的自然顺序执行。

怎么办呢?这里有一个两全其美的方案,那就是使用段间跳转指 jmpi。处理器最怕转移指令,遇到这种指令,一般会淸空流水线,并串行化执行;另一方面,段间跳转会重新加载段选择器 CS,并刷新描述符高速缓存器中的内容。

jmpi 0,8 中的 “8 ”是保护模式下的段选择子,用于选择描述符表(GDT 或 LDT)和描述符表项以及所要求的特权级。段选择子长度为 16 位(2字节)。

段选择子
b1-b0 请求特权级(RPL)
b2 0:全局描述符表 1:局部描述符表
b15-b3 描述符表项的索引, 指出选择第几项描述符(从 0 开始)

位 0-1 表示请求特权级(RPL),Linux 操作系统只用到两级——0 级(内核级)和 3 级(用户级);位 2 用于选择全局描述符表还是局部描述符表;位 3-15 是描述符表项的索引,指出选择第几项描述符。所以段选择子 8(= 0000_0000_0000_1000b)表示请求特权级0、使用全局描述符表 GDT 中第 1 个段描述符项(GDT 表在后文分析),该项是一个代码段描述符,指出代码段的基地址是 0,又因为偏移值是 0,所以这个跳转指令会跳转到0地址,即运行system模块。

从逻辑地址到线性地址的转换规则如下图:
setup.s 解读——Linux-0.11 剖析笔记(三)_第6张图片

到这里,setup.s 文件就分析完了。不过还剩一个小尾巴,就是文件末尾定义的 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

有了这个小程序,分析段描述符再也不用发愁了,So easy !
80x86描述符总结及解析描述符的小程序

索引号 描述符类型 基地址 段界限 粒度 P DPL 备注 选择子
0 空描述符 - - - - - - -
1 代码段 0 0X7FF 4KB 1 0 代码段,非一致性,可读 0x08
2 数据段 0 0X7FF 4KB 1 0 数据段,向上扩展,可写 0x10

非一致性代码段,这样的代码段可以被同级代码段调用,或者通过门调用;

一致性代码段,可以从低特权级的程序转移到该段执行(但是低特权级的程序仍然保持自身的特权级)。

注意:所有的数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而与代码段不同,数据段可以被更高特权级的程序或过程访问,而无需使用特殊的访问门。

其实描述符的知识点比较多,以后用到再说。

关于数据段和代码段描述符的详细知识,可以参考我的博文:

数据段描述符和代码段描述符

总结

setup.s 做的工作有:

  1. 获取一些参数保存在 0x90000 处
    • 保存光标的位置
    • 获取从 1M 处开始的扩展内存大小
    • 获取显示模式
    • 检查显示方式(EGA/VGA)并获取参数
    • 复制硬盘参数表(包括检查系统是否有第2个硬盘)
  2. 关中断
  3. 移动 system 模块到 0x00000
  4. 加载 IDT 和 GDT
  5. 开启 A20
  6. 设置 8259
  7. 进入保护模式(使 CR0 的 PE 位 = 1)
  8. 跳转到 0 地址执行

参考资料
1《Linux内核完全剖析》(赵炯,机械工业出版社,2006)
2《x86汇编语言:从实模式到保护模式》(李忠,2013)
3 http://webpages.charter.net/danrollins/techhelp/0114.HTM
4 https://www.rpi.edu/dept/cis/software/g77-mingw32/info-html/as.html#Invoking

你可能感兴趣的:(Linux-0.11)