题目: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 = 0x9000mov ds,ax        ! ds = 0x9000mov  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,#0x88int 0x15mov [2],ax ! ax = 从1M处开始的扩展内存大小

获取显示模式

    ! 获取显示卡当前的显示模式! 调用 BIOS 中断 0x10,功能号 ah = 0x0f! 返回: ah=字符列数; al=显示模式;bh=当前显示页。! 0x90004(l个字)存放当前页;0x90006(1字节)存放显示模式;0x90007(1字节)存放字符列数。mov ah,#0x0fint 0x10mov [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  0x10mov [8],ax      ! 我也不知道这个是什么(╯︵╰)mov    [10],bx     ! bh=显示状态(单色模式/彩色模式),bl=已安装的显存大小mov   [12],cx     ! ch=特性连接器比特位信息,cl=视频开关设置信息

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

BIOS 视频中断 0x10

复制硬盘参数表

复制 HD0 的硬盘参数表

! 复制 hd0 的硬盘参数表,参数表地址是中断向量0x41的值,表长度16B
! 中断向量在中断向量表中的位置 = 中断类型号N × 4
! (N*4)的字单元存放偏移地址;
! (N*4+2)的字单元存放段基址。mov ax,#0x0000mov   ds,ax      ! ds=0
! 将内存[4*0x41]处的低2字节(偏移地址)传给si,高2字节(段地址)传给dslds  si,[4*0x41]mov  ax,#INITSEGmov  es,ax          !es = 0x9000mov di,#0x0080mov   cx,#0x10       !重复16次
! ds:si --> es:di(0x9000:0x0080),共传送16Brepmovsb

复制 HD1 的硬盘参数表

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

检查系统是否有第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,#INITSEGmov  es,axmov    di,#0x0090  ! es:di = 0x9000:0x0090mov cx,#0x10mov ax,#0x00    ! AL=0repstosb           ! 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,#0x0000cld               ! 'direction'=0, movs moves forward
do_move:mov es,ax       ! es是目的段地址add   ax,#0x1000cmp   ax,#0x9000   ! 当 ax==0x9000 时结束移动jz   end_movemov ds,ax       ! ds是源段地址,ds比es大0x1000sub    di,di        ! di = 0sub   si,si       ! si = 0mov cx,#0x8000  ! 重复 0x8000次rep             ! ds:si --> es:dimovsw           ! 每次移动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;}
}

搬运示意图如下:

加载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 基地址=0Llidt   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+gdtlgdt    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(输入缓冲器) 写命令,命令码是 0xD1call   empty_8042   ! 等待输入缓冲器为空,即命令被接受mov   al,#0xDF         ! A20 onout    #0x60,al         !向端口 0x60 写参数,参数是 0xDFcall   empty_8042   ! 等待输入缓冲器为空,即参数被接受

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

802x 的各个端口如下图

 mov al,#0xD1        out #0x64,al         !向端口 0x64(输入缓冲器) 写命令,命令码是 0xD1...mov al,#0xDF         ! A20 onout    #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 porttest al,#2        ! is input buffer full?jnz  empty_8042  ! yes - loopret

第 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 sequenceout    #0x20,al        ! send ICW1 to Master.word  0x00eb,0x00eb       ! jmp $+2, jmp $+2out #0xA0,al        ! send ICW1 to Slave.word   0x00eb,0x00eb;------------------------------------------------------; ICW2mov   al,#0x20        ! 送主芯片ICW2命令字,设置起始中断号,要送奇端口 out   #0x21,al.word   0x00eb,0x00ebmov    al,#0x28        ! 送从芯片ICW2命令字,设置起始中断号,要送奇端口out    #0xA1,al.word   0x00eb,0x00eb;-------------------------------------------------------; ICW3mov  al,#0x04        ! 8259-1 is masterout   #0x21,al.word   0x00eb,0x00ebmov    al,#0x02        ! 8259-2 is slaveout    #0xA1,al.word   0x00eb,0x00eb;------------------------------------------------------; ICW4mov   al,#0x01        out #0x21,al.word   0x00eb,0x00ebout    #0xA1,al.word   0x00eb,0x00eb;------------------------------------------------------; OCW1mov   al,#0xFF        ! mask off all interrupts for nowout    #0x21,al.word   0x00eb,0x00ebout    #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。

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

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

  1. LINUX 0.11内核完全剖析学习笔记-第三章内核编程语言和环境

    一.编译器 linux 0.11 集成了两种汇编器.一种是能产生16位代码的as86汇编器,使用配套的ld86链接器:另一种是GUN汇编器gas,使用GNU ld链接器俩链接产生的目标文件. 1.1 ...

  2. setup.s 总结——Linux-0.11 剖析笔记(四)

    上一篇文章 setup.s 解读--Linux-0.11 剖析笔记(三) 详细地解释了 setup.s 的代码,整个代码完成的任务有: 获取一些参数保存在 0x90000 处 保存光标的位置 获取从 ...

  3. bootsect.s 解读——Linux-0.11 剖析笔记(二)

    题目:bootsect.s 解读--Linux-0.11 剖析笔记(二) 文章目录 一些符号常量 老式Linux设备号的命名规则 bootsect 把自己搬运到 0x90000,并跳转 加载 setu ...

  4. main() 函数解析(一)——Linux-0.11 剖析笔记(六)

    文章目录 1. 宏定义`_syscall0` 2. `setup.s`读取的参数 3. 读取CMOS实时时钟信息 3.1 `outb_p(value,port)` 3.2 `inb_p(port)` ...

  5. head.s 剖析——Linux-0.11 剖析笔记(五)

    文章目录 一.加载段寄存器 LSS指令 二.设置中断描述符表(IDT) 中断处理过程 `ignore_int` 三.设置全局描述符表(GDT),加载 GDTR 四.重新加载段寄存器 五.检测A20是否 ...

  6. bootsect.s 预备——Linux-0.11 剖析笔记(一)

    文章目录 boot 目录下文件介绍 16 位代码是什么意思 计算机启动过程 Linux 0.11 启动过程 boot 目录下文件介绍 boot 目录中一共有三个文件,都是用汇编语言写的,如下图(图来自 ...

  7. Linux 0.11内核分析02:系统启动

    目录 1. 内核镜像的构建 1.1 内核源码结构 1.1.1 boot 1.1.2 fs 1.1.3 include 1.1.4 init 1.1.5 kernel 1.1.6 lib 1.1.7 m ...

  8. Linux 0.11 实验环境搭建与调试

    缘起 之前我写过一篇博文:Linux 0.11 实验环境搭建 本以为有了这个环境(gcc-3.4 & gdb-6.8),就可以调试无忧了.谁知遇到了以下问题: (1)用 gdb 调试 main ...

  9. Linux 0.11 实验环境搭建

    如果想学习Linux 0.11,实验环境是必不可少的.最好是能编译,能运行,能调试. 如今大家都是用64位的Linux操作系统,而且是高版本的GCC(4.0以上),所以环境搭建是个难题.要么就是编译后 ...

最新文章

  1. wpsppt怎样让图片模糊_图片优化的14个技巧!
  2. Android移动开发之【Android实战项目】渐变的TextView小漂亮代码
  3. Makefile文件生成
  4. 测试用例的书写方式及测试模板大全
  5. Problem B: C语言习题 矩阵元素变换
  6. 15 Process State and O.S. Scheduling
  7. 留个坑,不知道为什么sqlite3要求组权限才能执行db:migrate,而可以直接执行db:......
  8. 如何学好游戏编程 一
  9. TCP协议如何保证可靠传输
  10. DWZ 富文本编辑器 IE下失去焦点
  11. 超平面(hyperplane)的定义
  12. 迅雷离线下载分享网站
  13. 9.5 用算法和数学奠定专业基础——《逆袭大学》连载
  14. ndis拨号软件 linux,[4G模块]华为ME909S-821 NDIS拨号指令流程
  15. 史上最全数据库笔记(上)
  16. 【数据结构】- 教你一步完美应对面试官让你10分钟内实现带头双向循环链表(下)
  17. Nginx + ModSecurity 报错
  18. java 商城 商品查询_Javaweb网上商城项目实战(17)实现商品详情查询
  19. cl.fe3.xyz index.php,2_FE_Diabetes.ipynb
  20. Oracle Namespace 说明

热门文章

  1. nyoj-491--幸运三角形--简单深搜枚举(TLE)
  2. NYOJ 888 取石子(九)
  3. mysql常用的分组函数
  4. 在.NET Core中三种实现“可插拔”AOP编程方式(附源码)
  5. js操作DOM对象(节点的增删改)
  6. L1-013. 计算阶乘和
  7. URL、Session、Cookies、Server.Transfer、Application和跨页面传送。
  8. tensorflow学习之常用函数总结:tensorflow官方例子中的诸如tf.reduce_mean()这类函数
  9. Python学习笔记:I/O编程
  10. 在Linux下通过Wake On LAN实现网络唤醒远程开机