【读书笔记】从实模式到保护模式

计算机语言 x86汇编语言:从实模式到保护模式(操作系统引导课) 原书作者李忠制作

用电表示数据

寄存器的作用:具有记忆功能的器件。锁存可以通过下面的开关控制,平时开关为空,按下开关之后,将输入锁存起来。锁存之后右面灯泡就就不会变化了,除非再次按下锁存开关。下面的开关是关着的,有间隙,是上下按的。

image-20230714005945617

image-20230714011357933

带有寄存器的加法机如何工作:每次输入/数字后存入寄存器,而不是多组线进行输入。

image-20230714011527299

再入加减乘除就是多了其他控制开关,

image-20230714011711790

image-20230714011759337

上面的加减乘除是每个开关,其他控制的地方也可以用一组01值来表示各类操作。

image-20230714011900628

内存

下面每个方块是一个内存单元,用右面地址线01值组合后来代表要查找的哪个内存单元。主流计算机里每个内存单元为8bit。

image-20230714012518396

内存上还有另外一排数据线,即内存的数据,8bit。读写通过控制线来表示。

image-20230714012554469

经过改进之后的计算机器,变为了下图。

指令指针寄存器,用来表示指令的地址,刚开始的时候是下一条要执行的地址。

image-20230714012733838

那么内存中的指令可以看为

image-20230714012911180

然后就可以一条一条自动取出计算了。

image-20230714012947359

处理器

CPU发展史:4004–8008(8位)–8086(16位)–80286–80386(i386)

16位代表有16位的寄存器和16位的算术逻辑部件。

image-20230714015154503

  • 指令集:可以执行的指令,在出产的时候就已经决定了。

image-20230714015453095

8086内部的通用寄存器

有8个通用的寄存器,分别是下面,他们都是16bit的,每个可以分为H/L

image-20230714015538891

1字=2字节。

image-20230714015634239

数据线的宽度和寄存器的宽度一致,是读取内存单元的。

image-20230714015725640

程序在内存中如何分段存放。

image-20230714015925389

image-20230714020015525

程序重定位困境

上图中,比如将程序挪到位置了,那么原来程序中的表示地址的内容将不再使用,解决方法是让表示地址从绝对地址变为相对于程序段开始的offset。

image-20230714020310756

IPR是地址寄存器,DSR是数据段寄存器。

8086有20根地址线,但是寄存器都是16位的,IP寄存器不够用,所以需要另外一个寄存器CS,所以我们一遍表示地址是CS:IP,CS是高位,所以是需要乘上10倍(实际上是16倍,2的4次方,4个偏移)与IP相加的。

image-20230714020540822

image-20230714020751060

上面所说的是8086的策略,

8086的段地址和偏移地址的访问过程就变成了:

image-20230714021025929

执行完1个指令后,IP就会变为03。CS代表是当前代码段的地址表示,IP代表段内的offset。

image-20230714021139933

因为IP是16位的,就代表段的最大大小为64k,即2的16次方,

image-20230714021304002

汇编语言基础指令

  • 大小写不敏感
  • 3FH和3fh是一样的,H代表是16位。63代表是十进制的,0011B代表是二进制的
 mov a,b  ; 将b的内存放到a中
 add a,b ; a=a+b

安装notepad++,nasm,

https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/

NASM的作用是将mov等指令编译成二进制文件。

test.asm

mov bx,0x3f; 16进制,将立即数放到bx中,立即数代表的是内存地址里存放的内容
add ax,bx; 注释
;nasm test.asm -f bin -o test.bin ; -f输出文件的格式,只有二进制内容  -o 输出的文件名

image-20230714023516282

下面显示案例:每一行是16B

image-20230714023527254

如何将NASM编译二进制功能继承到notepad++:

视频用户注意:在刚开始录制配套视频时,尚未编写nasmide2.exe,所以视频中推荐使用Notepad++,现在已经不推荐使用。不排除某些读者朋友坚持使用这个软件,在这种情况下,您可以按如下方法进行配置,配置后将可以提供自动编译过程:

        1,启动NotePad++,在菜单上选择“运行(R)”->“运行(R)”。

        2,在弹出的窗口内,输入:cmd /k pushd "$(CURRENT_DIRECTORY)" & D:\ACERFILES\OLDE\PROGFILES\NASM\nasm.exe -f bin  "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT。其中,“D:\ACERFILES\OLDE\PROGFILES\NASM\”应该改成你自己机器上实际的NASM安装路径。
        
cmd /k pushd "$(CURRENT_DIRECTORY)" & E:\NASM\nasm.exe -f bin  "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT

        3,点击“保存”,然后为这个运行命令起一个名字并分配一个快捷键,这样你下次就可以直接快速执行编译过程。

计算机启动,8086寄存器的状态

计算机启动后,寄存器被强制变为下面的图

image-20230714024336968

可以看到CS:IP=FFFF:0000=FFFF0,可以看到距离最上面只有16B,这个是bios,所以还要继续跳转。

8086的地址范围:

image-20230714024535785

从上到下

  • 内存
    • DRAM动态随机访问存储器
  • 外接设备
    • 显卡:用来转换成视频信号给显示器
  • ROM BIOS,read only memory,只读的,厂家烧制好的程序
bios

image-20230714024904674

image-20230714024931665

但是FFFF0是怎么调到别的地方呢,是使用JMP命令

image-20230714025055912

也就是又调到了0xF000:E05B

image-20230714025142232

硬盘

  • USE
  • HDD:有机器转盘
  • SSD:集成电路硬盘

image-20230714025250593

image-20230714025301558

可能会有多个盘

image-20230714025320780

每一个圆圈是一个磁道

image-20230714025358843

磁道还可以分为扇区,每个扇区大小相等,扇区从1开始编号,其他内容是从0开始编号

image-20230714025523138

image-20230714025410246

编号从柱面外面开始,先将一个柱面填满后再一定到另一个柱面。

主引导扇区

主引导扇区代表是0柱面,0磁道,第一个扇区就是主引导扇区。

image-20230714025612606

如果是从硬盘启动,那么就会跳转到07c00处开始,读取硬盘主引导扇区的内容加载到07c00处,然后调到这里接着执行。JMP 07c00也是bios执行的最后一条指令

image-20230714025815227

虚拟硬盘

每个厂商的虚拟硬盘格式不同,

  • VMDK:vmware虚拟机
  • VDI:virtualBox虚拟机
  • VHD:virtual-PC/Hyper-V虚拟机
VHD的组件组织形式

工具包有个LIZHONG.vhd,他也是一个文件,规范里写明了vhd的内容应该这么组织才算是一个正确的vhd:

  • 文件末尾必须是。。。
  • 512字节
  • 55aa

debug功能,可以使用bochs虚拟机

image-20230714031328738

bochs是一台虚拟机,对应的配置都是通过配置文件来配置的,

如何创建固定尺寸的VHD虚拟硬盘,放到bochs上,可以去virtualBox中利用一下,选择固定大小,20M就够

image-20230714031500695

image-20230714032110182

image-20230714032141394

双击

然后找到这个地方填充,还需要填充磁道之类的,这个信息需要使用工具生成,

image-20230714033047269

image-20230714033254672

image-20230714033327443

填充磁盘内容

mov ax,0x30;
moc dx,0xc0
add ax,bx
times 502 db 0; 502+2+
db 0x55 ; 结尾
db 0xAA
  • 我们当前的任务是需要把一个程序放到主引导扇区,让计算机在启动后执行它。
    为此这一课将演示如何将编译好的主引导扇区程岸写入虚拟硬盘的主引导扇区。

image-20230714034751207

LBA,

image-20230714034714492

image-20230714034805475

image-20230714034823770

image-20230714034838040

image-20230714211340870

00000000000i[PLUGIN] reset of 'serial' plugin device by virtual method
00000000000i[PLUGIN] reset of 'gameport' plugin device by virtual method
00000000000i[PLUGIN] reset of 'iodebug' plugin device by virtual method
00000000000i[PLUGIN] reset of 'usb_uhci' plugin device by virtual method
00000000000i[      ] set SIGINT handler to bx_debug_ctrlc_handler
Next at t=0
#  要跳转了
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0 
<bochs:1> sreg   ;;;;;;;;;
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
        Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000000000, limit=0xffff
idtr:base=0x0000000000000000, limit=0xffff
<bochs:2> r
rax: 00000000_00000000
rbx: 00000000_00000000
rcx: 00000000_00000000
rdx: 00000000_00000000
rsp: 00000000_00000000
rbp: 00000000_00000000
rsi: 00000000_00000000
rdi: 00000000_00000000
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_0000fff0
eflags 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf

; 单步执行为s   b为打断点   c为继续执行到下一断点

每个位数的处理器上表示通用寄存器的方式不同

image-20230714213049519

image-20230714213137555

image-20230714213153743

image-20230714213228013

00000000000i[PLUGIN] reset of 'gameport' plugin device by virtual method
00000000000i[PLUGIN] reset of 'iodebug' plugin device by virtual method
00000000000i[PLUGIN] reset of 'usb_uhci' plugin device by virtual method
00000000000i[      ] set SIGINT handler to bx_debug_ctrlc_handler
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:3> s
Next at t=1
(0) [0x0000000fe05b] f000:e05b (unk. ctxt): xor ax, ax                ; 31c0
<bochs:4> s
Next at t=2
(0) [0x0000000fe05d] f000:e05d (unk. ctxt): out 0x0d, al              ; e60d
<bochs:5> b 0x7c00
# 
<bochs:6> c
....
00001771813i[BIOS  ] ata0-0: PCHS=301/4/17 translation=none LCHS=301/4/17
00005205777i[BIOS  ] IDE time out
00017178813i[BIOS  ] Booting from 0000:7c00
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=17178868
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0030            ; b83000

选择CD-ROM继续,

image-20230715123111369

Next at t=17178868
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0030            ; b83000
<bochs:7> r
rax: 00000000_0000aa55 ; 执行吓一跳指令后变为0030
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0000
rdi: 00000000_0000ffac
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_00007c00
eflags 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov dx, 0x00c0            ; bac000
<bochs:9> r
rax: 00000000_00000030
rbx: 00000000_00000000
。。。

<bochs:12> s
Next at t=17178870
(0) [0x000000007c06] 0000:7c06 (unk. ctxt): add ax, bx                ; 01d8
<bochs:13> s
Next at t=17178871
(0) [0x000000007c08] 0000:7c08 (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000

显卡和显存

为了显示文件,需要显卡,

把显存映射到计算机可以访问的地址空间内,B8000-BFFF可以映射。

MOV ax, 0xb800

image-20230715152744302

mov语法
mov 目的操作数/寄存器/内存地址, 源操作数/寄存器/立即数

mov 段寄存器, 通用寄存器
mov 段寄存, [内存地址] ; 合法

mov ds,0xb800;不合法,不支持立即数直接传送到段寄存器

mov [0x00],al ; 按字节操作
mov ax, [0x02]; 按字操作,将0x02里存储的一个字放到ax寄存器中
moc ax, 0x02
mov byte [0x02], 0xe9
mov word [0x06], 0x3c
mov 0x07,al;错误的
mov [0x01],[0x02];错误的,不支持,其他操作也不支持这种类似的操作

mov ip,0x01;指令存储寄存器不能直接访问,不可以出现在任何指令中

mov ds,ax
mov es,[0x01]

标号

标识离那条最近指令的地址

start: ;; 只是一个表示,也可以写作其他字母
  mov ax,0x41
  ....
  ; 在debug时,不应该执行下面的无效指令,所以后面教程会改成jmp到别的地方运行 ;
  ; jmp 
  current:
  times 510-(current-start) db 0
  db 0x55,0xaa
AS         ;代码清单6-1 
         ;文件名:c06_mbr.asm ; nasm test.asm -l test.lst
         ;文件说明:硬盘主引导扇区代码
         ;创建日期:2011-3-31 21:15,修订于2021-09-06 23:05 
         
         mov ax,0xb800                 ;指向文本模式的显示缓冲区
         mov es,ax

         ;以下显示字符串"Label offset:"
         mov byte [es:0x00],'L'
         mov byte [es:0x01],0x07
         mov byte [es:0x02],'a'
         mov byte [es:0x03],0x07
         mov byte [es:0x04],'b'
         mov byte [es:0x05],0x07
         mov byte [es:0x06],'e'
         mov byte [es:0x07],0x07
         mov byte [es:0x08],'l'
         mov byte [es:0x09],0x07
         mov byte [es:0x0a],' '
         mov byte [es:0x0b],0x07
         mov byte [es:0x0c],"o"
         mov byte [es:0x0d],0x07
         mov byte [es:0x0e],'f'
         mov byte [es:0x0f],0x07
         mov byte [es:0x10],'f'
         mov byte [es:0x11],0x07
         mov byte [es:0x12],'s'
         mov byte [es:0x13],0x07
         mov byte [es:0x14],'e'
         mov byte [es:0x15],0x07
         mov byte [es:0x16],'t'
         mov byte [es:0x17],0x07
         mov byte [es:0x18],':'
         mov byte [es:0x19],0x07

;number表示的分配的空间地址
         mov ax,number                 ;取得标号number的偏移地址
         mov bx,10

         ;设置数据段的基地址
         mov cx,cs
         mov ds,cx

         ;求个位上的数字
         mov dx,0
         div bx
         ;
         mov [0x7c00+number+0x00],dl   ;保存个位上的数字

         ;求十位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x01],dl   ;保存十位上的数字

         ;求百位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x02],dl   ;保存百位上的数字

         ;求千位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x03],dl   ;保存千位上的数字

         ;求万位上的数字 
         xor dx,dx
         div bx
         mov [0x7c00+number+0x04],dl   ;保存万位上的数字

         ;以下用十进制显示标号的偏移地址
         mov al,[0x7c00+number+0x04]
         add al,0x30
         mov [es:0x1a],al
         mov byte [es:0x1b],0x04
         
         mov al,[0x7c00+number+0x03]
         add al,0x30
         mov [es:0x1c],al
         mov byte [es:0x1d],0x04
         
         mov al,[0x7c00+number+0x02]
         add al,0x30
         mov [es:0x1e],al
         mov byte [es:0x1f],0x04

         mov al,[0x7c00+number+0x01]
         add al,0x30
         mov [es:0x20],al
         mov byte [es:0x21],0x04

         mov al,[0x7c00+number+0x00]
         add al,0x30
         mov [es:0x22],al
         mov byte [es:0x23],0x04
         
         mov byte [es:0x24],'D'
         mov byte [es:0x25],0x07
          
   infi: jmp near infi                 ;无限循环
      
  number db 0,0,0,0,0
  
  times 203 db 0 ;
  db 0x55,0xaa
视频用户注意:在刚开始录制配套视频时,尚未编写nasmide2.exe,所以视频中推荐使用Notepad++,现在已经不推荐使用。不排除某些读者朋友坚持使用这个软件,在这种情况下,您可以按如下方法进行配置,配置后将可以提供自动编译过程:

        1,启动NotePad++,在菜单上选择“运行(R)”->“运行(R)”。

        2,在弹出的窗口内,输入:cmd /k pushd "$(CURRENT_DIRECTORY)" & D:\ACERFILES\OLDE\PROGFILES\NASM\nasm.exe -f bin  "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT。其中,“D:\ACERFILES\OLDE\PROGFILES\NASM\”应该改成你自己机器上实际的NASM安装路径。
        
cmd /k pushd "$(CURRENT_DIRECTORY)" & F:\LEEZHONG\NASM\nasm.exe -f bin  "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).lst" & PAUSE & EXIT

        3,点击“保存”,然后为这个运行命令起一个名字并分配一个快捷键,这样你下次就可以直接快速执行编译过程。
cmd /k pushd "$(CURRENT_DIRECTORY)" & F:\LEEZHONG\NASM\nasm.exe -f bin  "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" -l "$(NAME_PART).lst" & PAUSE & EXIT

上述命令不用配置了,直接打开这个文件后点击编译文件,就在该目录里得到bin和lst了。

image-20230715160931783

xp /512xb 0x7c00
b 0x7c00
u/32

把显存映射到计算机可以访问的地址空间内,B8000-BFFF可以映射。

MOV ax, 0xb800
mov ds,ax

在virtualBox中移除原来的存储disk,挂载我们自己创建的。

image-20230715180057975

div语法

简言之就是为了存储商和余数,有时候需要使用2个寄存器进行操作。

div的除数分为8位和16位

除数是8位,被除数必须是16,被除数保存在AX中,AH保存高8位,AL保存低8位,计算结果的商保存在AL中,余数保存在AH中。

除数是16位,被除数必须是32,被除数保存在AX和DX中,DX保存高16位,AX保存低16位,计算结果的商保存在AX中,余数保存在DX中。
add语法
ADD r/m, r/m/imm;

add bh,al;
add cs,dx;
add ax,3;
add word [0x2002],67;
add si,[0x2002]
显示数字

戏码div bx,因为bx是16位,所以商在AX中,余数在DX中

image-20230716164517400
基础寄存器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

段超越前缀

段超越前缀用于2个数据段之间协同工作。段超越前缀用来改变默认段寻址,通常内址寻址是数据段或者堆栈段,但你可以在指令前面加上段超越前缀,就可以访问到其它段内的数据。

段地址:8086/8088指令系统中的段地址有四个:

  • ES Extra segment 附加段寄存器

    DS Data segment 数据段寄存器

    CS Code segment 代码段寄存器

    SS Stack segment 堆栈段寄存器

我们通常用到的寄存器间接寻址方式会用到下边几个

DI, SI, BX, BP

其中前三个对应的段默认位DS,就是数据段寄存器

而最后一个BP默认对应的是SS, 就是堆栈段寄存器

  • 有效地址EA存放在BX、SI、DI或BP中,EA为BX、SI、DI时,默认是DS,BP默认是SS,可以使用段超越前缀改变。

所以当我们要用到代码段寄存器或者附加段寄存器的时候就会用到段超越前缀

例如:

mov ax, [si] = mov ax, ds:[si]

mov ax, [bp] = mov ax, ss:[bp]

而段超越的则必须在前边加上[段地址]

mov ax, cs:[si]
; 如果不加 [es:0x00],则段地址默认是在ds中,使用了段超越前缀后,段地址是在指定的段寄存器里

image-20230716203044671

虚拟机挂载的硬盘变为我们的硬盘。

汇编地址可以理解为在段内的偏移地址。

为什么要+7c00,跳过非指令的数据区
构造自己的数据段
.txt

下面图解释了为什么要加7c00,因为段内偏移地址不是从0开始的。

由于硬盘的主引导扇区加载到内存中后,开始的段地址为0,而段内偏移地址为7c00,程序加载时没有从0开始,
image-20230722204103314

Movsb movsw flags

8086使用DS:SI和ES:DI,分别标注源地址和目标地址的起始位置,此外还有个方向位在flag中被设置着,使用clr可以清除方向,重复的次数由cx指定,其实是字数。

其中方向位在flags寄存器中放着

分别是 byte word

$$$

$代表当前行汇编地址,$$段起始的汇编地址,以后再说不是零的段

info;查看flag
Info  eflags
输出的信息大小写表示的是01

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

loop循环

循环指令loop,cx保存循环次数,先执行-1再判断是否为零

Inc,dec
inc r/m;

inc al;
inc di;
inc type [0x2002]

Mytext是一个字为一组,

条件转移指令

Jns
如果标志位不是1,则跳转


看逻辑运算的结果最高位

Sub,neg
sub r/m, r/m/imm ;

sub al,35;
sub dx,ax;
sub dx,[0x2002];
sub byte [0x2002],23;
sub byte [0x2002],AX;
NEG r/m;
neg al;
neg di;
neg byte [0x2002]
补码

结果都是正确的,这也是补码的特点

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

idiv

Div只能用于 只能用于无符号的除法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

12阶段性总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

程序运行时需要先加载到内存段。如果是从断的起始位置加载,那么汇编地址等于偏移地址。
但但是在前面的学习中,计算机的主引导扇区程序并不是从断开始开始加载的,而是从段内偏移7c00处开始加载。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Flag

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

条件转移指令

这代表他们必须放到上面影响flag的命令后面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

CMP

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

只影响标记位,不影响其他。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

print-stack

基址寻址

8086

  • 寄存器存址

    • add ax,0x2002
    • mov cx,ax
  • 立即数寻址

    • add bx,0x2002
    • mov dx,mydata
  • 内存寻址

    • mov ax,[0x2002]

第八章 硬盘和显卡的访问与控制

离开主引导扇区。不要再用主引导扇区显示内容了。
每种段也可能在一个源程序中有多个,

section和segment定义段

NASM 编译器不关心段的用途,可能也根本不知道段的用途,不知道它是数据段,还是代码段,或是栈段。事实上,这都不重要,段只用来分隔程序中的不同内容。
第一个段的名字是“header”,表明它是整个程序的开头部分;第二个 段的名字是“code”,表明这是代码段;第三个段的名字是“data”,表明这 是数据段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
Intel 处理器要求段在内存中的起始物理地址起码是16 字节对齐的。 这句话的意思是,必须是16 的倍数,或者说该物理地址必须能被16 整 除。
相应地,汇编语言源程序中定义的各个段,也有对齐方面的要求。
具体做法是,在段定义中使用“align=”子句,用于指定某个SECTION 的 汇编地址对齐方式。比如说,“align=16”就表示段是16 字节对齐的, “align=32”就表示段是32 字节对齐的。
如果没有指定align,那么在32位和16位上默认是按照四字节对齐的。

间隔00,必须要填空成4b的个数,
段物理位置必须是46的个数,所以定义对齐,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

段相对于程序的偏移就是段的汇编地址,

每个段里的汇编地址也是按照源程序开始的,但是可以通过 vstart重新指定为相对段的。段内的汇编地址都从指定的数值开始计算。

因为段code的定义中有“vstart=0”子句,所以,标号“putch”的汇编地址要从它所在段的开头计算,而且从0开始计算。
如图8-1 所示,同样的情形也出现在段data 中。段data 的定义中也 有“vstart=0”子句,因此,当我们在段code 中引用段data 中的标号 “string”时(mov ax,string),尽管在图中没有标明,标号“string”所代表 的汇编地址是相对于其所在段data 的。也就是说,传送到寄存器AX 中的 数值是标号string 相对于段data 起始处的长度。
但是,图中最后一个段trail 的定义中没有包含“vstart=0”子句。那就 对不起了,该段内有一个标号“program_end”,它的汇编地址就要从整个 程序开头计算。因为它是整个程序中的最后一行,从这个意义上来说, 它所代表的汇编地址就是整个程序的大小(以字节计)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到be后变成00000了。

加载器和用户程序头

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
code start表示程序的入口点

计算段地址的方式:section.段名字.start,只有中间能改变,用于计算程序段地址相对于程序开头的偏移量B。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
一个程序可能有多个代码段和数据段,具体有多少个段必须在代码的开头指明每个段的地址。
段重定位表的作用就是这个,段重定位表必须以segtbl_segment开始,…end结束。这里是用32位的双字来存储每个段的便宜地址的。
段重定位标的前面是重定位表项个数。每个段重定位表项是四个字节,所以要÷4。
RESB先不用在意。

加载器

用户程序头部段是给加载器用的,接下来介绍加载器的工作流程。

  • 加载器的工作流程:
  • 读取用户程序的起始扇区。
  • 把整个用户程序都读入内存
  • 计算段的物理地址和逻辑段地址(段的重定位)
  • 转移到用户程序执行(将处理器的控制权交给用户程序)
    Userapp.asm是加载起demo
常数

用户程序在硬盘上必须以扇区100开始,
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

常数是使用equ声明的.代表equals
常数的生命不占用会变地址

用户程序在逻辑扇区100开始存放,一个扇区64k
可以把这个地址定义为常量,0x10000,但并没有这样做,而是使用了标号来表示。
课里讲主引导扇区定义为了一个段,只是有了vstart,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个段里的汇编地址也是按照源程序开始的,但是可以通过 vstart重新指定为相对段的。段内的汇编地址都从指定的数值开始计算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

汇编地址需要加7c00

8.2.2 用 户 程 序 头 部

一般来说,加载器和用户程序是在不同的时间、不同的地方,由不 同的人或公司开发的。这就意味着,它们彼此并不了解对方的结构和功 能。事实上,也不需要了解。

16913117710903

头部需要在源程序以一个段的形式出现

用户程序头部起码要包含以下信息。
① 用户程序的尺寸,即以字节为单位的大小。这对加载器来说是很 重要的,加载器需要根据这一信息来决定读取多少个逻辑扇区(在本书 中,所有程序在硬盘上所占用的逻辑扇区都是连续的)。
② 应用程序的入口点,包括段地址和偏移地址。加载器并不清楚用 户程序的分段情况,更不知道第一条要执行的指令在用户程序中的位 置。因此,必须在头部给出第一条指令的段地址和偏移地址,这就是所 谓的应用程序入口点(Entry Point)。
还需要段定位item个数

③ 段重定位表。用户程序可能包含不止一个段,比较大的程序可能 会包含多个代码段和多个数据段。这些段如何使用,是用户程序自己的 事,但前提是程序加载到内存后,每个段的地址必须重新确定一下。

image-20231014134153280

8.3 加载程序的工作流程

8.3.1 初始化和决定加载位置
如图8-7 所示,
围;物理地址A0000 以上,是BIOS 和外围设备的势力范围,有很多传统 的老式设备将自己的存储器和只读存储器映射到这个空间。
如此一来,可用的空间就位于0x10000~9FFFF,差不多500 多 KB。事实上,如果将低端的内存空间合理安排一下,还可以腾出更多空 间,但是没有必要,我们用不了多少。

8.3.2 准备加载用户程序
8.3.3 io设备介绍

外围设备(Peripheral Equipment)。

每一种设备都有自己的怪脾气,都有和别的设备不一样的工作方
式。比如,扬声器需要的是模拟信号,每个扬声器需要两根线,用的插
头也是无线电行业里的标准,话筒也是如此;老式键盘只用一根线向主
机传送按键的ASCII 码,而且一直采用PS/2 标准;新式的USB 键盘尽管也使用串行方式工作,但信号却和老式键盘完全不同。至于网络设施,现在流行的是里面有8 根线芯的五类双绞线,里面的信号也有专门的标准。
一句话,不同的设备,有不同的连线数量,线里面传送的信号也不一样,而且各自的插头和插孔也千差万别,这该如何让处理器跟它打交
道?
话虽这么说,但这些东西不让处理器访问和控制却不行。很明显,
这里需要一些信号转换器和变速齿轮,这就是I/O 接口。
举几个例子,麦克风和扬声器需要一个I/O 接口,即声卡,才能与处理器沟通;显示器也需要一个I/O 接口,即显卡,才能与处理器沟通;USB 键盘同样需要一个I/O 接口,即USB 接口,才能与处理器沟通。很显然,不同的外围设备,都有各自不同的I/O 接口。

这还没完,后面还有两个麻烦的问题。

  • ① 不可能将所有的I/O 接口直接和处理器相连,设备那么多,还有
    些设备现在没有发明出来,将来一定会有。你怎么办?
  • ② 每个设备的I/O 接口都抢着和处理器说话,不发生冲突都难。你
    怎么办?
    对第1 个问题的解答是采用总线技术。总线可以认为是一排电线,所
    有的外围设备,包括处理器,都连接到这排电线上。但是,每个连接到
    这排电线上的器件都必须有拥有电子开关,以使它们随时能够同这排电
    线连接,或者从这排电线上断开(脱离)。这就好比是公共车道,当路
    面上有车时,你就必须退避一下,不能硬冲上去。因此,这排公共电线
    就称为总线(Bus)。
    对第2 个问题的解答是使用输入输出控制设备集中器(I/O Controller
    Hub,ICH)芯片,该芯片的作用是连接不同的总线,并协调各个I/O 接口对处理器的访问。在个人计算机上,这块芯片就是所谓的南桥。

在ICH 内部,集成了一些常规的外围设备接口,如USB、 PATA(IDE)、SATA、老式总线接口(LPC)、时钟等,这些东西对计 算机来说必不可少,故直接集成在ICH 内,我们后面还会详细介绍它们 的功能。

除了这些常用的、必不可少的设备之外,有些设备你可能暂时用不
上,也有些设备还没有发明出来,但迟早有可能连在计算机上。不管是
什么设备,都必须通过它自己的I/O 接口电路同ICH 相连。为了方便,最好是在主板上做一些插槽,同时,每个设备的I/O 接口电路都设计成插卡。这样,想接上该设备时,就把它的I/O 接口卡插上,不需要时,随时
拔下。
为了实现这个目的,或者说为了支持更多的设备,ICH 还提供了对
PCI 或者PCI Express 总线的支持,该总线向外延伸,连接着主板上的若
干个扩展槽,就是刚才说的插槽。举个实例,如果你想连接显示器,那
么就要先插入显卡,然后再把显示器接到显卡上。
除了局部总线和PCI Express 总线,每个I/O 接口卡可能连接不止一
个设备。比如USB 接口,就有可能连接一大堆东西:键盘、鼠标、U 盘
等。因为同类型的设备较多,也涉及线路复用和仲裁的问题,故它们也
有自己的总线体系,称为通信总线或者设备总线。比如图8-9 所示的USB
总线和SATA 总线。
当处理器想同某个设备说话时,ICH 会接到通知。然后,它负责提
供相应的传输通道和其他辅助支持,并命令所有其他无关设备闭嘴。同
样,当某个设备要跟处理器说话,情况也是一样。

8.3.4端口

处理器是通过端口(Port)来和外围设备打交道的。本质上,端口就是一些寄存器,类似于处理器内部的寄存器。不同之处仅仅在于,这些叫做端口的寄存器位于I/O 接口电路中。
端口是处理器和外围设备通过I/O 接口交流的窗口,每一个I/O 接口都可能拥有好几个端口,分别用于不同的目的。
比如,连接硬盘的PATA/SATA 接口就有几个端口,分别是命令端口(当向该端口写入0x20时,表明是从硬盘读数据;写入0x30 时,表明是向硬盘写数据)、状态端口(处理器根据这个端口的数据来判断硬盘工作是否正常,操作是否成功,发生了哪种错误)、参数端口(处理器通过这些端口告诉硬盘读写的扇区数量,以及起始的逻辑扇区号)和数据端口(通过这个端口连续地取得要读出的数据,或者通过这个端口连续地发送要写入硬盘的数据)。
端口只不过是位于I/O 接口上的寄存器,所以,每个端口有自己的数据宽度。在早期的系统中,端口可以是8 位的,也可以是16 位的,现在有些端口会是32 位的。到底是8 位还是16 位,这是设备和I/O 接口制造者的自由。比如,PATA/STAT 接口中的数据端口就是16 位的,这有助于加快数据传输速率,提高传输效率。
端口在不同的计算机系统中有着不同的实现方式。在一些计算机系统中,端口号是映射到内存地址空间的。比如,0x00000~0xE0000 是真实的物理内存地址,而0xE0001~0xFFFFF 是从很多I/O 接口那里映射过来的,当访问这部分地址时,实际上是在访问I/O 接口。

而在另一些计算机系统中,端口是独立编址的,不和内存发生关系。如图8-10 所示,在这种计算机中,处理器的地址线既连接内存,也连接每一个I/O 接口。但是,处理器还有一个特殊的引脚M/IO#,在这里,“#”表示低电平有效。也就是说,当处理器访问内存时,它会让
M/IO#引脚呈高电平,这里,和内存相关的电路就会打开;相反,如果处理器访问I/O 端口,那么M/IO#引脚呈低平,内存电路被禁止。与此同时,处理器发出的地址和M/IO#信号一起用于打个某个I/O 接口,如果该I/O 接口分配的端口号与处理器地址相吻合的话。
Intel 处理器,早期是独立编址的,现在既有内存映射的,也有独立编址的。

在本章中,我们只讲独立编址的端口。
所有端口都是统一编号的,比如0x0001、0x0002、0x0003、…。每个I/O 接口电路都分配了若干个端口,比如,I/O 接口A 有3 个端口,端口号分别是0x0021~0x0023;I/O 接口B 需要5 个端口,端口号分别是0x0303~0x0307。
一个现实的例子是个人计算机中的PATA/SATA 接口(图8-9),每个PATA 和SATA 接口分配了8个端口。但是,ICH 芯片内部通常集成了两个PATA/SATA 接口,分别是主硬盘接口和副硬盘接口。这样一来,主硬盘接口分配的端口号是0x1f0~0x1f7,副硬盘接口分配的端口号是0x170~0x177。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在Intel 的系统中,只允许65536(十进制数)个端口存在,端口号从0 到65535(0x0000~0xffff)。因为是独立编址,所以,端口的访问不能使用类似于mov 这样的指令,取而代之的是in 和out 指令。
in 指令是从端口读,它的一般形式是
in al,dx
或者
In ax,dx
这就是说,in 指令的目的操作数必须是寄存器AL 或者AX,当访问8位的端口时,使用寄存器AL;访问16 位的端口时,使用AX。in 指令的源操作数应当是寄存器DX。
in al,dx 的机器指令码是0xEC,in ax,dx 的机器指令码是0xED,都是一字节的。之所以如此简短,是因为in 指令不允许使用别的通用寄存器,也不允许使用内存单元作为操作数。
也许是为了方便,in 指令还有两字节的形式。此时,前一字节是操作码0xE4 或者0xE5,分别用于指示8 位或者16 位端口访问;后一字节是立即数,指示端口号。
因此,机器指令 E4 F0 就相当于汇编语言指令
In ax,0x03
很显然,因为这种指令形式的操作数部分只允许一字节,故只能访 问0255(0x000xff)号端口,不允许访问大于255 的端口.

in指令不影响任何标志位。
如果要通过端口向外围设备发送数据,则必须通过out 指令。
out 指令正好和in 指令相反,目的操作数可以是8 位立即数或者寄存器DX,源操作数必须是寄存器AL 或者AX。下面是一些例子:
Out 0x37,al
在本章中,我们将采用LBA28 来访问硬盘。
前面说过,个人计算机上的主硬盘控制器被分配了8 位端口,端口号从0x1f0 到0x1f7。假设现在要从硬盘上读逻辑扇区,那么,整个过程如下。
第1 步,设置要读取的扇区数量。这个数值要写入0x1f2 端口。这是

I/O接口内部集成了端口,端口实际上是寄存器,只不过是I/O设备的寄存器,而不是C P U的寄存器,。端口是I/O设备与CPU处理器交互的窗口,每一个I/O接口都可能有多个端口,分别用于处理不同的目的。比如命令端口、数据端口、状态端口,。
因为是寄存器,所以他有自己的位数。当前32位居多。每个端口都有一个数字号,端口号是按需分配的,而且常用的设备都有固定的端口号。
Intel处理器只允许65535个端口。端口的命令不是mov,而是in和out。
in是从端口读数据到处理器,
in al,do
In ax,dx
一般端口号要放在dx中。,
例子

mov dx,0x3c0
In ax,dx

In指令不影响任何标记位。
对于out
out dx/imm8,al/ax

8.3.5 硬盘的读写

硬盘读写的基本单位是扇区。就是说,要读就至少读一个扇区,要 写就至少写一个扇区,不可能仅读写一个扇区中的几个字节。这样一 来,就使得主机和硬盘之间的数据交换是成块的,所以硬盘是典型的块 设备。

从硬盘读写数据,最经典的方式是向硬盘控制器分别发送磁头号、 柱面号和扇区号(扇区在某个柱面上的编号),这称为CHS 模式。这种 方法最原始,最自然,也最容易理解。
实际上,在很多时候,我们并不关心扇区的物理位置,所以希望所 有的扇区都能统一编址。这就是逻辑扇区,它把硬盘上所有可用的扇区都一一从0 编号,而不管它位于哪个盘面,也不管它属于哪个柱面。
关于硬盘和逻辑扇区的知识前面已经有所介绍,这里不再赘述。最 早的逻辑扇区编址方法是LBA28,使用28 个比特来表示逻辑扇区号,从 逻辑扇区0x0000000 到0xFFFFFFF,共可以表示228=268435456 个扇 区。每个扇区有512 字节,所以LBA28 可以管理128 GB 的硬盘。
硬盘技术发展得非常快,最新的硬盘已经达到几百个吉字节的容 量,LBA28 已经落后了。在这种情况下,业界又共同推出了LBA48,采 用48 个比特来表示逻辑扇区号。如此一来,就可以管理131072 TB 的硬 盘容量了。

在本章中,我们将采用LBA28 来访问硬盘。
前面说过,个人计算机上的主硬盘控制器被分配了8 位端口,端口号 从0x1f0 到0x1f7。假设现在要从硬盘上读逻辑扇区,那么,整个过程如 下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
1)设置要读取的扇区数量,要写入1f2端口,。这是 个8 位端口,因此每次只能读写255 个扇区:

mov dx,0x1f2
Mov al,0x01; 要读取的扇区数量
Out dx,al; 如果al是0,那么要读取256个扇区

2)接下来设置起始的扇区号。
假定我们要读写的起始逻辑扇区号为0x02,

一个扇号28位,所以需要存储四个字节。分别写入端口1f3-6。
最后一个端口只用4位,那4位看图左面4个。
28 位的扇区号太长,需要将其分成4段,分别写入端口0x1f3、0x1f4、0x1f5 和0x1f6 号端口。其中,0x1f3号端口存放的是0~7 位;0x1f4 号端口存放的是8~15 位;0x1f5 号端口存放的是16~23 位,最后4 位在0x1f6 号端口。
注意以上代码的最后4 行,在现行的体系下,每个PATA/SATA 接口允许挂接两块硬盘,分别是主盘(Master)和从盘(Slave)。如图8-11
所示,0x1f6 端口的低4 位用于存放逻辑扇区号的24~27位,第4 位用于指示硬盘号,0 表示主盘,1 表示从盘。高3 位是“111”,表示LBA 模式。
1110 000就是0xe0。

3)向1f7写入读写命令,代表in,out。向端口0x1f7 写入0x20,请求硬盘读。这也是一个8 位端口:

Mov dx,0x1f7
Mov al,0x20;20代表读命令
Out dx,al;8位

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4)第四步,等待端口读写完成。
等待读写操作完成。端口0x1f7 既是命令端口,又是状态端口。在通过这个端口发送读写命令之后,硬盘就忙乎开了。如图8-12 所示,在它内部操作期间,它将0x1f7 端口的第7位置“1”,表明自己很忙。
一旦硬盘系统准备就绪,它再将此位清零,说明自己已经忙完了,同时将第3 位置“1”,意思是准备好了,请求主机发送或者接收数据(图8- 12)。完成这一步的典型代码如下:

下面的图是al寄存器分布。
我们读取7和3,然后看是否只有位3位1

5)开始读写

5 步,连续取出数据。0x1f0 是硬盘接口的数据端口,而且还是一 个16 位端口。一旦硬盘控制器空闲,且准备就绪,就可以连续从这个端 口写入或者读取数据。下面的代码假定是从硬盘读一个扇区(512 字 节,或者256 字节),读取的数据存放到由段寄存器DS 指定的数据段, 偏移地址由寄存器BX 指定:
下面的代码假定是从硬盘读一个扇区(512 字 节,或者256 字节),读取的数据存放到由段寄存器DS 指定的数据段, 偏移地址由寄存器BX 指定:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

处理器读取时硬盘控制器是有感知的。会知道有没有取走程序。

最后,0x1f1 端口是错误寄存器,包含硬盘驱动器最后一次执行命令 后的状态(错误原因)。

8.3.6过程调用。

其他语言中的函数,就是过程调用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,主程序把起始逻辑扇区号的高16 位存放在寄存器DI 中(只有低12 位是有效的,高4 位必须保证为“0”),低16 位存放在寄存器SI 中(没办法,16 位的处理器无法直接处理28 位的数据);并约定将读出来的数据存放到由段寄存器DS指向的数据段中,起始偏移地址在寄存器BX 中。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Push
进去之前ip先压栈
Cs 是不变的,

调用过程的指令是“call”。8086 处理器支持四种调用方式。

相对近调用

第一种是16 位相对近调用。
近调用的意思是被调用的目标过程位于当前代码段内,而非另一个不同的代码段,所以只需要得到偏移地址即可。
故该操作数是当前call 指令相对于目标过程的偏移
量。计算过程如下:用目标过程的汇编地址减去当前call 指令的汇编地址,再减去当前call 指令以字节为单位的长度(3),保留16 位的结果。

举个例子:

关键字“near”不是必需的,如果call 指令中没有提供任何关键字,则
编译器认为该指令是近调用。“proc_1”是程序中的一 个标号。在编译阶段,编译器用标号proc_1 处的汇编地址减去本指令的 汇编地址,再减去3,作为机器指令的操作数。
因为16 位相对近调用的操作数是两个汇编地址相减的相对量,所 以,如果被调用过程在当前指令的前方,也就是说,论汇编地址,它比 call 指令的要大,那么该相对量是一个正数;反之,就是一个负数。所 以,它的机器指令操作数是一个16 位的有符号数。换句话说,被调用过 程的首地址必须位于距离当前call 指令-32768~32767 字节的地方。

指令执行阶段,处理器看到操作码0xE8,就知道它应当调用一个 过程。于是,它用指令指针寄存器IP 的当前内容加上指令中的操作数, 再加上3,得到一个新的偏移地址。接着,将IP 的原有内容压入栈。最 后,用刚才计算出的偏移地址取代IP 原有的内容。这直接导致处理器的 执行流转移到目标位置处。
Call 0x0500
很多人认为0x0500 会原封不动地出现在该指令编译后的机器码中, 我相信这只是他们一时糊涂。在call 指令后跟一个标号,和跟一个数值没 有什么不同。标号是数值的等价形式,是代表标号处的汇编地址。在指 令编译阶段,它首先会被转化成数值。
所以,你在call 指令后跟一个数值,只是帮了编译器的忙,帮它省了 一个转化步骤,它依然会用这个数值减去当前指令的汇编地址,来得到 一个偏移量。

image-20230722162255388

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

16位间接绝对近调用

这种调用也是近调用,只能调用 当前代码段内的过程,指令中的操作数不是偏移量,而是被调用过程的 真实偏移地址,故称为绝对地址。不过,这个偏移地址不是直接出现在 指令中,而是由16 位的通用寄存器或者16 位的内存单元间接给出。比如:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
以上,第一条指令的机器码为FF D1,被调用过程的偏移地址位于寄 存器CX 内,在指令执行的时候由处理器从该寄存器取得,并直接取代指 令指针寄存器IP 原有的内容。
第二条指令的机器码为FF 16 00 30。当这条指令执行时,处理器访 问数据段(使用段寄存器DS),从偏移地址0x3000 处取得一个字,作 为目标过程的真实偏移地址,并用它取代指令指针寄存器IP 原有的内 容。
间接绝对近调用指令在执行时,处理器首先按以上的方法计算被调 用过程的偏移地址,然后将指令指针寄存器IP 的当前值压栈,最后用计 算出来的偏移地址取代寄存器IP 原有的内容。
由于间接绝对近调用的机器指令操作数是16 位的绝对地址,因此, 它可以调用当前代码段任何位置处的过程。

16位直接绝对远调用

这种调用属于段间调用,即调用 另一个代码段内的过程,所以称为远调用(far call)。很容易想到,远 调用既需要被调用过程所在的段地址,也需要该过程在段内的偏移地 址。
16 位”是针对偏移地址来说的,而不是限定段地址,尽管段地址事 实上也是16 位的;“直接”的意思是,段地址和偏移地址 在call 指令 中给出了。当然,这里的地址也是绝对地址。比如:
Call 0x2000:0x0030
这条指令编译后的机器码为9A 30 00 00 20,0x9A 是操作码,后面 跟着的两个字分别是偏移地址和段地址,按规定,偏移地址在前,段地 址在后。
处理器在执行时,首先将代码段寄存器CS 的当前内容压栈,接着再 把指令指针寄存器IP 的当前内容压栈。紧接着,用指令中给出的段地址代替CS 原有的内容,用指令中给出的偏移地址代替IP 原有的内容。这 直接导致处理器从新的位置开始执行。

16位间接绝对远调用

这也属于段间调用,被调用过程 位于另一个代码段内,而且,被调用过程所在的段地址和偏移地址是间接
给出的。还有,这里的“16 位”同样是用来限定偏移地址的。

因为是远调用,也就是段间调用,所以,必须给出被调用过程的段 地址和偏移地址。但是,段地址和偏移地址在内存中的其他位置,指令 中仅仅给出的是该位置的偏移地址,需要处理器在执行指令的时候自行 按图索骥,找到它们。
以上,前两条指令是等效的,不同之处仅仅在于,第一条指令直接 给出的是数值,而第二条指令用的是标号。但这无关紧要,在编译后, 标号也会变成数值。
假如在数据段内声明了标号proc_1 并初始化了两个字:

这两个字分别是某个过程的段地址和偏移地址。按处理器的要求, 偏移地址在前,段地址在后。也就是说,0x0102 是偏移地址; 0x2000 是段地址。
那么,为了调用该过程,可以在代码段内使用这条指令:
Call far [proc_1]

当这条指令执行时,处理器访问由段寄存器DS 指向的数据段,从指 令中指定的偏移地址(由标号proc_1 提供)处取得两个字(分别是段地 址0x2000 和偏移地址0x0102);接着,将代码段寄存器CS 和指令指针 寄存器IP 的当前内容分别压栈;最后,用刚才取得的段地址和偏移地址 分别取代CS 和IP 的原值。

ret 和retf 经常用做call 和call far 的配对指令。ret 是近返回指令,当 它执行时,处理器只做一件事,那就是从栈中弹出一个字到指令指针寄 存器IP 中。
retf 是远返回指令(return far),它的工作稍微复杂一点点。当它执 行时,处理器分别从栈中弹出两个字到指令指针寄存器IP 和代码段寄存 器CS 中。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

需要说明的是,尽管call 指令通常需要ret/retf 和它配对,遥相呼
应,但ret/retf 指令却并不依赖于call 指令,这一点你马上就会看到。
call 指令在执行过程调用时不影响任何标志位,ret/retf 指令对标志
位也没有任何影响。

8.3.7 加载用户程序

第一次读硬盘将得到用户程序最开 始的512 字节,这512 字节包括最开始 的用户程序头部,以及一部分实际的指 令和数据。
为了将用户程序全部读入内存,需 要知道它的大小,然后再进一步转换成 它所用的扇区数。如图8-15 所示,用 户程序最开始的双字,就是整个程序的 大小。
为 此 , 代 码 清 单 8-1 第 30 、 31 行,分别将该数值的高16 位和低16 位 传送到寄存器DX 和AX。第32 行,因 为每扇区有512 字节,故将512 传送到 BX 寄存器,并在第33 行用它来做除法 运算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014133440587

image-20231014133427552

8.3.8用户程序重定

用户程序在编写的时候是分段的。因此,加载器下一步的工作是计 算和确定每个段的段地址。

过程calc_segment_base(计算段基址)是在代码清单8-1 的第134 行定义的。它接受一个32位的汇编地址(位于寄存器DX:AX 中),并在 计算完成后向主程序返回一个 16 位的逻辑段地址(位于寄存器 AX 中)。
,再将该起始地址的高16 位加到寄存器DX 中。adc 是带进位加法,它将目的操作数和源操作数相加,然后再加上标志寄存 器CF 位的值(0 或者1)。这样,分两步就可以完成32 位数的加法运 算。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图8-17 所示,逻辑右移指令执行时,会将操作数连续地向右移动 指定的次数,每移动一次,“挤”出来的比特被移到标志寄存器的CF 位, 左边空出来的位置用比特“0”填充。


尽管DX:AX 中是32 位的用户程序起始物理内存地址,理论上,它只 有20 位是有效的,低16位在寄存器AX 中,高4 位在寄存器DX 的低4 位。寄存器AX 经右移后,高4 位已经空出,只要将DX 的最低4 位挪到 这里,就可以得到我们所需要的逻辑段地址。为此,可以使用以下指 令:

ror 的配对指令是循环左移指令rol(ROtate Left)。ror、rol、shl、 shr 的指令格式都是相同的
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
现在仅仅是处理了入口点代码段的重定位,下面开始正式处理用户
程序的所有段,它们位于用户程序头部的段重定位表中。
重定位表的表项数存放在用户程序头部偏移0x0a 处,如图8-5 所 示。代码清单8-1 第65 行,用于将它从该内存地址处传送到寄存器CX, 供后面的循环指令使用。
段重定位表的首地址存放在用户程序头部偏移0x0c 处,因此,第66 行,将0x0c 传送到基址寄存器BX 中。以后,每次只要将BX 的内容加上 4,就指向下一个重定位表项。
第68~74 行是循环体,每次循环开始后,BX 总是指向需要重定位 的段的汇编地址,而且都是双字,需要分别传送到寄存器DX 和AX。然 后调用过程calc_segment_base 计算相应的逻辑段地址,并覆盖到原来 的位置(低字),最后将基址寄存器的内容加上4 ,以指向下一个表项。 当寄存器CX 的内容为0 时,循环结束,所有的段都处理完毕。

8.3.9控制器交给用户程序

现在,用户程序已经在内存中准备就绪,剩下的工作就是把处理器 的控制权交给它。交接工作很简单,代码清单8-1 第76 行,加载器通过 一个16 位的间接绝对远转移指令,跳转到用户程序入口点。
如图8-15 所示,入口点是两个连续的字,低字是偏移地址,位于用 户程序头部内偏移为0x04的地方;高字是段地址,位于用户程序头部内 偏移为0x06 的地方。而且,因为加载器的辛勤工作,该段地址是已经重 定位过的。
处理器执行指令jmp far [0x04]时,会访问段寄存器DS 所指向的数据段,从偏移地址为0x04 的地方取 出两个字,并分别传送到代码段寄存器CS 和指令指针寄存器IP,以替代 它们原先的内容。于是,处理器就像被洗脑了一样,自行转移到指定的 位置处开始执行。
处理器已经跑到用户程序内部去执行了,所以接下来的工作是跟踪 用户程序的工作流程。不过,在此之前,还是先总结一下无条件转移指 令jmp 的用法。

8.3.10 8086处理器的无条件转移指令
相对短转移

操作码为0xEB,操作数是相对于目标位置的偏移量, 仅1 字节,是个有符号数。由于这个原因,该指令属于段内转移指令,而 且只允许转移到距离当前指令-128~127 字节的地方。相对短转移指令 必须使用关键字“short”。例如:
在源程序编译阶段,编译器会检查标号infinite 所代表的值,如果数 值超过了一字节所能允许的数值范围,则无法通过编译。否则,编译器用目标位置的汇编地址减去当前指令的汇编地址,再减去当前指令的长
度(2),保留1 字节的结果,作为机器指令的操作数。
相对短转移指令的汇编语言操作数只能是标号和数值。下面是直接 使用数值的情况:
但数值和标号是等价的。在编译阶段,都被用来计算一个8 位的偏移 量。

2 16
和相对短转移不同,16 位相对近转移指令的转移范围稍大一些。它 的机器指令操作码为0xE9,而且,该指令的长度为3 字节,操作码0xE9 后面还有一个16 位(2 字节)的操作数。
因为是近转移,故其属于段内转移。“相对”的意思同样是指它的操作 数是一个相对量,是相对于目标位置处的偏移量。在源程序编译阶段,
。由于这是 一个16 位的有符号数,故可以转移到距离当前指令-32768~32767 字节
的地方。
16 位相对近转移指令应当使用关键字“near”,比如
在早先的NASM 版本中,关键字near 是可以省略的。若没有指定 short 或者near,那么,编译器自动默认是“near”的。但是最近的版本改 变了这一规则。如果没有指定关键字short 或者near,那么,如果目标位 置距离当前指令-128~127 字节,则自动采用short;否则,采用near。
3 16
这种转移方式也是近转移,即只在段内转移。但是,转移到的目标 偏移地址不是在指令中直接给出的,而是用一个16 位的通用寄存器或者 内存地址来间接给出的。比如:

8.4用户程序的工作流程

伪指令resb(REServe Byte)的意思是从当前位置开始,保留指定 数量的字节,但不初始化它们的值。在源程序编译时,编译器会保留一 段内存区域,用来存放编译后的内容。当它看到这条伪指令时,它仅仅 是跳过指定数量的字节,而不管里面的原始内容是什么。内存是反复使 用的,谁也无法知道以前的使用者在这里留下了什么。也就是说,跳过 的这段空间,每个字节的值是不确定的。
Resw resd

回车和换行的概念最早起源于老式打字机。那种打字机上有滚筒, 用于使纸张上下卷动,每敲击一个按键,字车往右移动一格,位于下一 个可打印的位置。在这种古老而不失先进性的设备上,将字车推到最左
边,也就是一行的开始,叫做回车( Carriage Return);而拧一下滚 筒,将纸上卷一行,叫做换行(Line Feed)。如果既回车,又换行,那 么,字车将位于下一行的行首。这个过程通常叫做回车换行(CRLF)。

8.4.4光标

光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄 存器是8 位的,合起来形成一个16 位的数值。比如,0 表示光标在屏幕 上第0 行第0 列,80 表示它在第1 行第0 列,因为标准VGA 文本模式是 25 行,每行80 个字符。这样算来,当光标在屏幕右下角时,该值为25× 80-1=1999。
光标寄存器是可读可写的。你可以从中读出光标的位置,也可以显卡的操作非常复杂,内部的寄存器也不是一般地多。为了不过多 占用主机的I/O 空间,很多寄存器只能通过索引寄存器间接访问。
索引寄存器的端口号是0x3d4,可以向它写入一个值,用来指定内部 的 某 个 寄 存 器 。 比 如 , 两 个 8 位 的 光 标 寄 存 器 , 其 索 引 值 分 别 是 14(0x0e)和15(0x0f),分别用于提供光标位置的高8 位和低8 位。
指定了寄存器之后,要对它进行读写,这可以通过数据端口0x3d5 来进行。

8.5

第 9章 中 断 和 动 态 时 钟显 示

如何把多个程序调入内存,是操作系统的事情,这个可以先放一 放。现在的问题是,当一个程序执行时,它是不会知道还有别的程序正 眼巴巴地等着执行。在这种情况下,中断(Interrupt)这种工作机制就应 运而生了。
中断就是打断处理器当前的执行流程,去执行另外一些和当前工作 不相干的指令,执行完之后,还可以返回到原来的程序流程继续执行。

9.1 外部硬件中断

外部硬件中断,就是从处理器外面来的中断信号。当外 部设备发生错误,或者有数据要传送(比如,从网络中接收到一个针对 当前主机的数据包),或者处理器交给它的事情处理完了(比如,打印 已经完成),它们都会拍一下处理器的肩膀,告诉它应当先把手头上的 事情放一放,来临时处理一下。

外部硬件中断是通过两个信号线引入处理器内部的。从很早的时候起,也就是8086 处理器的时代,这两根线的名字就叫NMI和INTR。

非屏蔽中断

在某些具有怀疑精神的人眼里,用两根信号线来接受外部设备中断 可能是多余的,也许只需要一根就可以了。这似乎有此道理,但是,来 自外部设备的中断很多,也不是每一个中断都是必须处理的。有些中 断,在任何时候都必须及时处理,因为事关整个系统的安全性。比如, 在使用不间断电源的系统中,当电池电量很低的时候,不间断电源系统 会发出一个中断,通知处理器快掉电了。再比如,内存访问电路发现了 一个校验错误,这意味着,从内存读取的数据是错误的,处理器再努力 工作也是没有意义的。
在所有这些情况下,处理器必须针对这些中断采 取必要的措施,隐瞒真相必然会对用户造成不可挽回的损失。除此之 外,更多的中断是可以被忽略或者延迟处理的,如果某个程序希望不被 打扰的话。

首先,所有的严重事件都必须无条件地加以处理,这种类型的中断是不会被阻断和屏蔽的,称为非屏蔽中断(Non Maskable Interrupt,NMI)。
非屏蔽中断,nmi,中断源比较少,可以控制外部设备是否可以中断。
中断控制器的作用,仲裁,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当一个中断发生时,处理器将会通过中断引脚NMI 和INTR 得到通 知。除此之外,它还应当知道发生了什么事,以便采取适当的处理措 施。每种类型的中断都被统一编号,这称为中断类型号、中断向量或者 中断号。但是,由于不可屏蔽中断的特殊性——几乎所有触发NMI 的事 件对处理器来说都是致命的,甚至是不可纠正的。在这种情况下,努力 去搞清楚发生了什么,通常没有太大的意义,这样的事最好留到事后, 让专业维修人员来做。
也正是这个原因,在实模式下,NMI 被赋予了统一的中断号2,不再 进行细分。一旦发生2号中断,处理器和软件系统通常会放弃继续正常工 作的“念头”,也不会试图纠正已经发生的问题和错误,很可能只是由软件 系统给出一个提示信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.1.2 可 屏 蔽 中 断

和NMI 不同,更多的时候,发往处理器的中断信号通常不会意味着 灾难。当然,有时候也会非常紧急,比如,在一个由计算机控制的车床 上,当零件快速通过铣具时,处理器应当立即处理中断,并向铣具发送 信号,告诉它应当如何切削。
这类中断有两个特点,第一是数量很多,毕竟有很多外部设备;第 二是它们可以被屏蔽,这样处理器就像是没听见、没看见一样,不会对 它们进行处理。所以,这类硬件中断称为可屏蔽中断。尽管不处理中断 就会把零件铣坏,但是否允许处理器看见该中断,是你自己的事,这是 处理器赋予你的权利。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可屏蔽中断是通过INTR 引脚进入处理器内部的,像NMI 一样,不可 能为每一个中断源都提供一个引脚。而且,处理器每次只能处理一个中 断。在这种情况下,需要一个代理,来接受外部设备发出的中断信号。 还有,多个设备同时发出中断请求的几率也是很高的,所以该代理的任 务还包括对它们进行仲裁,以决定让它们中的哪一个优先向处理器提出 服务请求。

在个人计算机中,用得最多的中断代理就是8259 芯 片,它就是通常所说的中断控制器,从8086 处理器开始,它就一直提供 着这种服务。即使是现在,在绝大多数单处理器的计算机中,也依然有 它的存在。

Intel 处理器允许256 个中断,中断号的范围是0~255,8259 负责提供其中的15 个,但中断号并不固定。之所以不固定,是因为当初设计的时候,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in 和out 指令来改变它的状态,包括各引脚的中断号。正是因为这样,它又叫可编程中断控制器(Programmable Interrupt Controller,PIC)。

第一块8259 芯片的代理 输出INT 直接送到处理器的INTR 引脚,这是主片(Master);第二块 8259 芯片的INT 输出送到第一块的引脚2 上,是从片(Slave),两块芯 片之间形成级联(Cascade)关系。

如此一来,两块 8259 芯片可以向处理器提供 15 个中断信号。当 时,接在8259 上的15 个设备都是相当重要的,如PS/2 键盘和鼠标、串 行口、并行口、软磁盘驱动器、IDE 硬盘等。现在,这些设备很多都已 淘汰或者正在淘汰中,根据需要,这些中断引脚可以被其他设备使用。
如图9-2 所示,8259 的主片引脚0(IR0)接的是系统定时器/计数器 芯片;从片的引脚0 (IR0)接的是实时时钟芯片RTC,该芯片是本章的 主角,很快就会讲到。总之,这两块芯片的固定连接即使是在硬件更新 换代非常频繁的今天,也依然没有改变。

在8259 芯片内部,有中断屏蔽寄存器(Interrupt Mask Register, IMR),这是个8 位寄存器,对应着该芯片的8 个中断输入引脚,对应的
位是0 还是1,决定了从该引脚来的中断信号是否能够通过8259 送往处 理器(0 表示允许,1 表示阻断,这可能出乎你的意料)。当外部设备通 过某个引脚送来一个中断请求信号时,如果它没有被IMR 阻断,那么, 它可以被送往处理器。注意,8259 芯片是可编程的,主片的端口号是 0x20 和0x21,从片的端口号是0xa0 和0xa1,可以通过这些端口访问 8259 芯片,设置它的工作方式,包括IMR 的内容。
中断能否被处理,除了要看8259 芯片的脸色外,最终的决定权在处 理器手中。回到前面第6章,参阅图6-2,你会发现,在处理器内部,标 志寄存器有一个标志位IF,这就是中断标志(Interrupt Flag)。当IF 为0 时,所有从处理器INTR 引脚来的中断信号都被忽略掉;当其为1 时,处 理器可以接受和响应中断。
IF 标志位可以通过两条指令cli 和sti 来改变。这两条指令都没有操作 数,cli(CLear Interrupt flag)用于清除IF 标志位,sti(SeT Interrupt flag)用于置位IF 标志。

image-20231014133302087

image-20231014133242742

在计算机内部,中断发生得非常频繁,当一个中断正在处理时,其 他中断也会陆续到来,甚至会有多个中断同时发生的情况,这都无法预 料。不用担心,8259 芯片会记住它们,并按一定的策略决定先为谁服 务。总体上来说,中断的优先级和引脚是相关的,主片的IR0 引脚优先级 最高,IR7 引脚最低,从片也是如此。当然,还要考虑到从片是级联在主 片的IR2 引脚上。
最后,当一个中断事件正在处理时,如果来了一个优先级更高的中 断事件时,允许暂时中止当前的中断处理,先为优先级较高的中断事件 服务,这称为中断嵌套。

9.1.3 实 模 式 下 的中 断 向 量 表

所谓中断处理,归根结底就是处理器要执行一段与该中断有关的程 序(指令)。处理器可以识别256 个中断,那么理论上就需要256 段程 序。这些程序的位置并不重要,重要的是,在实模式下,处理器要求将 它们的入口点集中存放到内存中从物理地址0x00000 开始,到0x003ff结束,共1KB的空间内,这就是所谓的中断向量表( Interrupt Vector Table,IVT)。

如图9-3 所示,每个中断在中断向量表中占2 个字,分别是中断处理 程序的偏移地址和段地址。中断0 的入口点位于物理地址0x00000 处, 也就是逻辑地址0x0000:0x0000;中断1 的入口点位于物理地址0x00004 处,即逻辑地址0x0000:0x0004;其他中断以此类推,总之是按顺序的。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在8259 芯片那里,每个引脚都赋予了一个中断号。而且,这些中断号是可以改变的,可以对8259 编程来灵活设置,但不能单独进行,只能 以芯片为单位进行。比如,可以指定主片的中断号从0x08 开始,那么它 每个引脚IR0~IR7 所对应的中断号分别是0x08~0x0e。

image-20231014133224671

中断信号来自哪个引脚,8259 芯片是最清楚的,所以它会把对应的 中断号告诉处理器,处理器拿着这个中断号,要顺序做以下几件事。

  1. 1 保护断点的现场。首先要将标志寄存器FLAGS 压栈,然后清除它 的IF 位和TF 位。TF 是陷阱标志,这个以后再讲。接着,再将当前的代 码段寄存器CS 和指令指针寄存器IP 压栈。
  2. 2 执行中断处理程序。由于处理器已经拿到了中断号,它将该号码 乘以4(毕竟每个中断在中断向量表中占4 字节),就得到了该中断入口 点在中断向量表中的偏移地址。接着,从表中依次取出中断程序的偏移 地址和段地址,并分别传送到IP 和CS,自然地,处理器就开始执行中断 处理程序了。
    注意,由于IF 标志被清除,在中断处理过程中,处理器将不再响应硬件中断。如果希望更高优先级的中断嵌套,可以在编写中断处理程序 时,适时用sti 指令开放中断。
  3. 3 返回到断点接着执行。所有中断处理程序的最后一条指令必须是 中断返回指令iret。这将导致处理器依次从栈中弹出(恢复)IP、CS 和 FLAGS 的原始内容,于是转到主程序接着执行。
    iret 同样没有操作数,执行这条指令时,处理器依次从栈中弹出数值 到IP、CS 和标志寄存器。
    顺便􏰀醒一句,由于中断处理过程返回时,已经恢复了FLAGS 的原 始内容,所以IF 标志位也自动恢复。也就是说,可以接受新的中断。
    和可屏蔽中断不同,NMI 发生时,处理器不会从外部获得中断号, 它自动生成中断号码2,其他处理过程和可屏蔽中断相同

中断随时可能发生,中断向量表的建立和初始化工作是由BIOS 在计算机启动时负责完成的。
BIOS 为每个中断号填写入口地址,因为它不知道多数中断处理程序的位置,所以,一律将它们指向一个相同的入口地址,在那里,只有一条指令:iret。也就是说,当这些中断发生时,只做一件事,那就是立即返回。当计算机启动后,操作系统和用户程序再根据自己的需要,来修改某些中断的入口地址,使它指向自己的代码。马上你就会看到,我们在本章也是这样做的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.1.4 实 时时 钟 、CMOS RAM 和BCD 编 码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么计算机能够准确地显示日期和时间? 原因很简单,
在外围设备控制器芯片ICH 内部,集成了

  • 实时时钟电路( Real Time Clock, RTC)和
  • 两小块由互补金属氧化物 (CMOS)材料组成的静态存储器(CMOS RAM) 。

实时时钟电路负责 计时,而日期和时间的数值则存储在这块存储器中。
除了日期和时间的保存功能外,RTC 芯片也可以提供闹钟和周期性的中断功能。
RTC 芯片由一个振荡频率为32.768kHz 的石英晶体振荡器(晶振) 驱动,经分频后,用于对CMOS RAM 进行每秒一次的时间刷新

日期和时间信息是保存在CMOS RAM 中的,通常有128 字节,而日 期和时间信息只占了一小部分容量,其他空间则用于保存整机的配置信 息,比如各种硬件的类型和工作参数、开机密码和辅助存储设备的启动 顺序等。这些参数的修改通常在BIOS SETUP 开机程序中进行。要进入 该程序,一般需要在开机时按DEL、ESC、F1、F2 或者F10 键。具体按 哪个键,视计算机的厂家和品牌而定。
RTC 芯片由一个振荡频率为32.768kHz 的石英晶体振荡器(晶振) 驱动,经分频后,用于对CMOS RAM 进行每秒一次的时间刷新。

如表9-1 所示,常规的日期和时间信息占据了CMOS RAM 开始部分 的10 字节,有年、月、日和时、分、秒,报警的时、分、秒用于产生到 时间报警中断,如果它们的内容为0xC0~0xFF,则表示不使用报警功 能。

对CMOS RAM 的访问,需要通过两个端口来进行。0x70 或者0x74 是 索引端口,用来指定CMOS RAM 内的单元;0x71 或者0x75 是数据端 口,用来读写相应单元里的内容。举个例子,以下代码用于读取今天是 星期几:

不得不说的是,从很早的时候开始,端口0x70 的最高位(bit 7)是 控制 NMI 中断的开关。当它为 0 时,允许 NMI 中断到达处理器,为 1 时,则阻断所有的NMI 信号,其他7 个比特,即0~6位,则实际上用于 指定CMOS RAM 单元的索引号,这种规定直到现在也没有改变。

如图9-4 所示,尽管端口0x70 的位7 不是中断信号,但它能控制与 非门的输出,决定真正的NMI 中断信号是否能到达处理器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通常来说,在往端口0x70 写入索引时,应当先读取0x70 原先的内 容,然后将它用于随后的写索引操作中。但是,该端口是只写的,不能 用于读出。
为了解决这个问题,同时也为了兼容以前的老式硬件,ICH 芯片允 许通过切换访问模式来临时取得那些只写寄存器的内容,但这涉及更高 层次的知识,已经超出了当前的话题范畴。现在,我们只想把问题搞得 简单些,这么说吧,NMI 中断应当始终是允许的,在访问RTC 时,我们 直接关闭NMI,访问结束后,再打开NMI,而不管它以前到底是什么样 子。

CMOS RAM 中保存的日期和时间,通常是以二进制编码的十进制数 (Binary Coded Decimal,BCD),这是默认状态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014133104501

CMOS RAM 中保存的日期和时间,通常是以二进制编码的十进制数 (Binary Coded Decimal,BCD),这是默认状态,如果需要,也可以 设置成按正常的二进制数来表示。要想说明什么是BCD 编码,最好的办 法是举个例子。比如十进制数25,其二进制形式是00011001。但是,如 果采用BCD 编码的话,则一个字节的高4 位和低4 位分别独立地表示一 个0 到9 之间的数字。因此,十进制数25 对应的BCD 编码是 00100101
25表示
0001 1001 十六进制
0010 0101 BCD

单元0x0A~0x0D 不是普通的存储单元,而被定义成4 个寄存器的索 引号,也是通过0x70 和0x71 这两个端口访问的。这4 个寄存器用于设置 实时时钟电路的参数和工作状态。
现在,我们想让RTC 芯 片定期发出一个中断,当这个中断发生的时候,还能执行我们自己编写 的代码,来访问CMOS RAM,在屏幕上显示一个动态走动的时钟。

9.1.6 初 始化8259、RTC 和 中 断向 量 表

image-20231014131355591

当处理器执行任何一条改变栈段寄存器SS 的指令时,它会在 下一条指令执行完期间禁止中断。

。要想改变代码段和数据段,只需 要改变段寄存器就可以了。但栈段不同,因为它除了有段寄存器,还有 栈指针。因此,绝大多数时候,对栈的改变是分两步进行的:先改变段 寄存器SS 的内容,接着又修改栈指针寄存器SP 的内容。

  • RTC芯片的中断信号,通向中断控制器8259 从片的第1 个中断引脚
    IR0。
  • 在计算机启动期间,BIOS 会初始化中断控制器,将主片的中断号
    设为从0x08 开始,将从片的中断号设为从0x70开始。
  • 所以,计算机启动后,RTC 芯片的中断号默认是0x70。
  • 尽管我们可以通过对8259 编程来改变它,但是没有必要。

RTC 到8259 的中断线只有一根,而RTC可以产生多种中断。比如 闹钟中断、更新结束中断和周期性中断(参见表9-3 和表9-4)。RTC 的 计时(更新周期)是独立的,产生中断信号只是它的一个赠品。所以, 如果希望它能产生中断信号,需要额外设置。
以上所说的三种中断,我们只要设置一种就可以了。其实,

  • 最简单 的就是设置更新周期结束中断
  • 每当RTC 更新了CMOS RAM 中的日期 和时间后,将发出此中断。
  • 更新周期每秒进行一次,该中断也每秒 发生一次。
    钩子
    在访问RTC 期间,最好是阻断NMI,因此,第 148、149 行,先用or 指令将AL 的最高位置1,再写端口0x70。

第150、151 行,用于通过数据端口0x71 写寄存器B。写的内容是 0x12,其二进制形式为00010010,对照表9-3,其意义不难理解:

  • 允许 更新周期照常发生,
  • 禁止周期性中断,
  • 禁止闹钟功能,
  • 允许更新周期结束中断,
  • 使用24 小时制,
  • 日期和时间采用BCD 编码。

每次当中断实际发生时,可以在程序(中断处理过程)中读寄存器C 的内容来检查中断的原因。比如,每当更新周期结束中断发生时,RTC 就将它的第4 位置1。该寄存器还有一个特点,就是每次读取它后,所有 内容自动清零。而且,如果不读取它的话(换句话说,相应的位没有清 零),同样的中断将不再产生。

为了修改某中断在中断向量表中的登记项,需要先找到它。第132~ 135 行,将中断号0x70乘以4,就是它在中断向量表内的偏移。
接着,第142~145 行,访问中断向量表内0x70 号中断的表项,分 别写入新中断处理过程的偏移地址和段地址。新的中断处理过程是从标 号new_int_0x70 处开始的,而且位于当前代码段内。所以,该中断处理 过程的偏移地址就是标号new_int_0x70 的汇编地址(注意,段code 的 定义中带有vstart=0 子句),段地址就是当前段寄存器CS 的内容。表项 修改完毕,从栈中恢复段寄存器ES 的原始内容。

接下来,我们要设置RTC 的工作状态,使它能够产生中断信号给 8259 中断控制器。。正常情 况下,8259 是不会允许RTC 中断的,所以,需要修改它内部的中断屏蔽 寄存器IMR。IMR 是一个8 位寄存器,位0 对应着中断输入引脚IR0,位7 对应着引脚IR7,相应的位是0 时,允许中断,为1 时,关掉中断。

test 指令在功能上和and 指令是一样的,都是将两个操作数按位进行 逻辑“与”,并根据结果设置相应的标志位。但是,test 指令执行后,运算 结果被丢弃(不改变或破坏两个操作数的内容)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014133016509

image-20231014133004104

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.1.7使处理器进入低功耗状态

hlt 指令使处理器停止 执行指令,并处于停机状态,这将降低处理器的功耗。处于停机状态的 处理器可以被外部中断唤醒并恢复执行,而且会继续执行hlt 后面的指 令。

9.1.8 实时时钟中断的处理过程

用于读RTC 寄存器A,根据UIP 位的状态来决定是等 待更新周期结束,还是继续往下执行。UIP 位为0 表示现在访问CMOS RAM 中的日期和时间是安全的。注意第36 行,用于把寄存器AL 的最高 位置1,从而阻断NMI。当然,这是不必要的,当NMI 发生时,整个计算 机都应当停止工作,也不在乎中断处理过程能否正常执行。

test 指令用于测 试寄存器AL 的第7 位是否为1。
“test”的意思是“测试”。顾名思义,可以用这条指令来测试某个寄存 器,或者内存单元里的内容是否带有某个特征。
test 指令在功能上和and 指令是一样的,都是将两个操作数按位进行 逻辑“与”,并根据结果设置相应的标志位。但是,test 指令执行后,运算 结果被丢弃(不改变或破坏两个操作数的内容)。

9.2内中断

和硬件中断不同,内部中断发生在处理器内部,是由执行的指令引起的。比如,当处理器检测到div 或者idiv 指令的除数为零时,或者除法 的结果溢出时,将产生中断0(0 号中断),这就是除法错中断。

再比如,当处理器遇到非法指令时,将产生中断6。非法指令是指指 令的操作码没有定义,或者指令超过了规定的长度。操作码没有定义通 常意味着那不是一条指令,而是普通的数。

内部中断不受标志寄存器IF 位的影响,也不需要中断识别总线周 期,它们的中断类型是固定的,可以立即转入相应的处理过程

9.3软中断

软中断是由int 指令引起的中断处理。这类中断也不需要中断识别总 线周期,中断号在指令中给出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Int3是断点中断指令,机器指令码为CC。这条指令在调试程序的时 候很有用,当程序运行不正常时,多数时候希望在某个地方设置一个检 查点,也称断点,来查看寄存器、内存单元或者标志寄存器的内容,这 条指令就是为这个目的而设的。
指令都是连续存放的,因此,所谓的断点,就是某条指令的起始地 址。int3 是单字节指令,这是有意设计的。当需要设置断点时,可以将 断点处那条指令的第1 字节改成0xcc,原字节予以保存。当处理器执行 到int3 时,即发生3 号中断,转去执行相应的中断处理程序。中断处理程 序的执行也要用到各个寄存器,这会破坏它们的内容,但push 指令不 会。我们可以在该程序内先压栈所有相关寄存器和内存单元,然后分别 取出予以显示,它们就是中断前的现场内容。最后,再恢复那条指令的 第1 字节,并修改位于栈中的返回地址,执行iret 指令。
int3 和int 3 不是一回事。前者的机器码为CC,后者则是CD 03,这就是通常所说的int n,其操作码为0xCD,第2 字节的操作数给出 了中断号。举几个例子:

9.3.1 Bios 中断

image-20231014131417044

因为有了软中断,这是个利好条件。每次操作系统加载完自己之 后,以中断处理程序的形式􏰀供硬盘读写功能,并把该例程的地址填写 到中断向量表中。这样,无论在什么时候,用户程序需要该功能时,直 接发出一个软中断即可,不需要知道具体的地址。

最有名的软中断是BIOS 中断,之所以称为BIOS 中断,是因为这些 中断功能是在计算机加电之后,BIOS 程序执行期间建立起来的。换句话 说,这些中断功能在加载和执行主引导扇区之前,就已经可以使用了。

BIOS 中断,又称BIOS 功能调用,主要是为了方便地使用最基本的 硬件访问功能。不同的硬件使用不同的中断号,比如,使用键盘服务 时,中断号是0x16,即
int 0x60
为了区分针对同一硬件的不同功能,使用寄存器AH 来指定具 体的功能编号。举例来说,以下指令用于从键盘读取一个按键:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里,当寄存器AH 的内容是0x00 时,执行int 0x16 后,中断服 务例程会监视键盘动作。
当它返回时,会在寄存器AL 中存放按键的 ASCII 码。
BIOS 可能会为一些简单的外围设备􏰀供初始化代码和功能 调用代码,并填写中断向量表,但也有一些BIOS 中断是由外部设备接口 自己建立的。

首先,每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接 口电路、硬件控制器等,都有自己的只读存储器(Read Only Memory, ROM),类似于BIOS 芯片,这些ROM 中􏰀供了它自己的功能调用例 程,以及本设备的初始化代码。按照规范,前两个单元的内容是0x55 和 0xAA,第三个单元是本ROM 中以512 字节为单位的代码长度;从第四 个单元开始,就是实际的ROM 代码。
其次,我们知道,从内存物理地址A0000 开始,到FFFFF 结束,有 相当一部分空间是留给外围设备的。如果设备存在,那么,它自带的 ROM 会映射到分配给它的地址范围内。
在计算机启动期间,BIOS 程序会以2KB 为单位搜索内存地址C0000 ~E0000 之间的区域。当它发现某个区域的头两个字节是0x55 和0xAA 时,那意味着该区域有ROM 代码存在,是有效的。接着,它对该区域做 累加和检查,看结果是否和第三个单元相符。如果相符,就从第四个单 元进入。这时,处理器执行的是硬件自带的程序指令,这些指令初始化 外部设备的相关寄存器和工作状态,最后,填写相关的中断向量表,使 它们指向自带的中断处理过程

第三部分,32位保护模式

32位x86处理器编程架构

处理器架构,或者处理器编程架构,是指一整套的硬件结构, 以及与之相适应的工作状态,这其中的灵魂部分就是一种设计理念,决 定了处理器的应用环境和工作模式,也决定了软件开发人员如何在这种 模式下解决实际问题。架构内的资源对程序员来说是可见的、可访问 的,受程序的控制以改变处理器的运行状态;非架构的资源取决于具体 的硬件实现。

Intel 32 位处理器架构简称IA-32(Intel Architecture,32-bit),是 以1978 年的8086 处理器为基础发展起来的。在那个时候,他们只是想 造一款特别牛的处理器,也没考虑到架构。尽管那些人是专家,但和我 们一样不是千里眼,这是很正常的。

正如我们已经知道的,8086 有20 根地址线,可以寻址1MB 内存。 但是,它内部的寄存器是16 位的,无法在程序中访问整个1MB 内存。所 以,它也是第一款支持内存分段模型的处理器。还有,8086 处理器只有 一种工作模式,即实模式。当然,在那时,还没有实模式这一说。
由于8086 处理器的成功,推动着Intel 公司不断地研发更新的处理 器,32 位的时代就这样到来了。到目前为止,到底有多少种类型,我也 说不清楚。尽管8086 是16 位的处理器,但它也是32 位架构内的一部 分。原因在于, 32 位的处理器架构是从 8086 那里发展来的,是基于 8086 的,具有延续性和兼容性。

就我们曾经用过的产品而言,32位的处理器有32根地址线,数据线 的数量是 32根或者64根。特别是最近最新的处理器,都是 64 根。因 此,它可以访问2 32,即4GB 的内存,而且每次可以读写连续的4 字节或 者8 字节,这称为双字(Double Word)或者4 字(Quad Word)访问。 当然,如果你要按字节或者字来访问内存,也是允许的。

10.1 IA-32 架 构 的 基本 执 行 环境

10.1.1 寄 存 器的扩展

在16 位处理器内,有8 个通用寄存器AX、BX、CX、DX、SI、DI、 BP 和SP,其中,前4个还可以拆分成两个独立的8 位寄存器来用,即 AH、AL、BH、BL、CH、CL、DH 和DL。如图10-1 所示,32 位处理器 在16 位处理器的基础上,扩展了这8 个通用寄存器的长度,使之达到32 位。

为了在汇编语言程序中使用经过扩展(Extend)的寄存器,需要给 它们命名,它们的名字分别是EAX、EBX、ECX、EDX、ESI、EDI、 ESP 和EBP。可以在程序中使用这些寄存器,即使是在实模式下:

但是,就像以上指令所示的那样,指令的源操作数和目的操作数必 须具有相同的长度,个别特殊用途的指令除外。因此,像这样的搭配是 不允许的,在程序编译时,编译器会报告错误:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
如果目的操作数是32 位寄存器,源操作数是立即数,那么,立即数 被视为32 位的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

32 位通用寄存器的高16 位是不可独立使用的,但低16 位保持同16 位处理器的兼容性。因此,在任何时候它们都可以照往常一样使用

可以在32 位处理器上运行16 位处理器上的软件。但是,它并不是 16 位处理器的简单增强。事实上,32 位处理器有自己的32 位工作模 式,在本书中,32 位模式特指32 位保护模式。在这种模式下,可以完全、充分地发挥处理器的性能。同时,在这种模式下,处理器可以使用 它全部的32 根地址线,能够访问4GB 内存。

图10-2,在32 位模式下,为了生成32 位物理地址,处理器 需要使用32 位的指令指针寄存器。为此,32 位处理器扩展了IP,使之达 到32 位,即EIP。当它工作在16 位模式下时,依然使用16 位的IP;工作 在32 位模式下时,使用的是全部的32 位EIP。和往常一样,即使是在32 位模式下,EIP 寄存器也只由处理器内部使用,程序中是无法直接访问的。对IP 和EIP 的修改通常是用某些指令隐式进行的,这些指令包括 JMP、CALL、RET 和IRET 等等。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

另外,在16 位处理器中,标志寄存器FLAGS 是16 位的,在32 位处 理器中,扩展到了32位,低16 位和原先保持一致。关于EFLAGS 中的各 个标志位,将在后面的章节中逐一介绍。
在32 位模式下,对内存的访问从理论上来说不再需要分段,因为它 有32 根地址线,可以自由访问任何一个内存位置。但是,IA-32 架构的 处理器是基于分段模型的,因此,32 位处理器依然需要以段为单位访问 内存,即使它工作在32 位模式下。
不过,它也提供了一种变通的方案,即,只分一个段,段的基地址 是0x00000000,段的长度(大小)是4GB。在这种情况下,可以视为不 分段,即平坦模型(Flat Mode)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个程序都有属于自己的内存空间。在16 位模式下,一个程序可以 自由地访问不属于它的内存位置,甚至可以对那些地方的内容进行修 改。这当然是不安全的,也不合法,但却没有任何机制来限制这种行 为。在32 位模式下,处理器要求在加载程序时,先定义该程序所拥有的段,然后允许使用这些段。定义段时,除了基地址(起始地址)外,还
附加了段界限、特权级别、类型等属性。当程序访问一个段时,处理器 将用固件实施各种检查工作,以防止对内存的违规访问。

,在32 位模式下,传统的段寄存器,如CS、SS、 DS、ES,保存的不再是16位段基地址,而是段的选择子,即,用于选择 所要访问的段,因此,严格地说,它的新名字叫做段选择器。除了段选 择器之外,每个段寄存器还包括一个不可见部分,称为描述符高速缓存 器,里面有段的基地址和各种访问属性。这部分内容程序不可访问,由 处理器自动使用。

10.1.2基本工作模式

8086 具有16 位的段寄存器、指令指针寄存器和通用寄存器(CS、 SS、DS、ES、IP、AX、BX、CX、DX、SI、DI、BP、SP),因此, 我们称它为16 位的处理器。尽管它可以访问1MB 的内存,但是只能分段 进行,而且由于只能使用16 位的段内偏移量,故段的长度最大只能是 64KB。8086 只有一种工作模式,即实模式。当然,这个名称是后来才 提出来的。

1982 年的时候,Intel 公司推出了80286 处理器。这也是一款16 位 的处理器,大部分的寄存器都和8086 处理器一样。因此,80286 和8086 一样,因为段寄存器是16 位的,而且只能使用16位的偏移地址,在实模 式下只能使用64KB 的段;尽管它有24 根地址线,理论上可以访问224, 即16MB 的内存,但依然只能分成多个段来进行。
但是,80286 和8086 不一样的地方在于,它第一次提出了保护模式 的概念。

在保护模式下,段寄存器中保存的不再是段地址,而是段选择子真正的段地址位于段寄存器的描述符高速缓存中,是24位的。因 此,运行在保护模式下的80286 处理器可以访问全部16MB 内存。

80286 处理器访问内存时,不再需要将段地址左移,因为在段寄存器的描述符高速缓存器中有24 位的段物理基地址。这样一来,段可以位于16MB 内存空间中的任何位置,而不再限于低端1MB 范围内,也不必 非得是位于16 字节对齐的地方。不过,由于80286 的通用寄存器是16位 的,只能提供16 位的偏移地址,因此,和8086 一样,即使是运行在保 护模式下,段的长度依然不能超过64KB。对段长度的限制妨碍了80286 处理器的应用,这就是16 位保护模式很少为人所知的原因。
实模式等同于8086 模式,在本书中,实模式和16 位保护模式统称 16 位模式。在16 位模式下,数据的大小是8 位或者16 位的;控制转移 和内存访问时,偏移量也是16 位的。
1985 年的80386 处理器是Intel 公司的第一款32 位产品,而且获得 了极大成功,是后续所有32 位产品的基础。本书中的绝大多数例子,都 可以在80386 上运行。和8086/80286 不同,80386处理器的寄存器是32 位的,而且拥有32 根地址线,可以访问2 32,即4GB 的内存。
80386,以及所有后续的32位处理器,都兼容实模式,可以运行实模式下的8086 程序。而且,在刚加电时,这些处理器都自动处于实模式下,此时,它相当于一个非常快速的8086 处理器。只有在进行一番设置 之后,才能运行在保护模式下。

10.1.3 线性地址

IA-32 处理器支持多任务。在多任务环境下,任务的创建需要分配内 存空间;当任务终止后,还要回收它所占用的内存空间。在分段模型 下,内存的分配是不定长的,程序大时,就分配一大块内存;程序小 时,就分配一小块。时间长了,内存空间就会碎片化,就有可能出现一 种情况:内存空间是有的,但都是小块,无法分配给某个任务。为了解 决这个问题,IA-32 处理器支持分页功能,分页功能将物理内存空间划分 成逻辑上的页。页的大小是固定的,一般为4KB,通过使用页,可以简化内存管理。
如图10-3 所示,当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(Linear Address),线性地址还要经页部件转换后,才是物理地址。

线性地址的概念用来描述任务的地址空间。如图10-3 所示,IA-32 处理器上的每个任务都拥有4GB 的虚拟内存空间,这是一段长4GB 的平 坦空间,就像一段平直的线段,因此叫线性地址空间。相应地,由段部 件产生的地址,就对应着线性地址空间上的每一个点,这就是线性地 址。

10.2 现代处理器的结构和特点

10.3 32位模式的指令系统

十一章 进入保护模式

11.2 全局描述表

为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移地址。 在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行登记。
一个段有关的信息需要 8 个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述 符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是 挨在一起,集中存放的,这就构成一个描述符表。

最主要的描述符表是全局描述符表(Global Descriptor Table ,GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保 护模式前,必须要定义全局描述符表。

,为了跟踪全局描述符表,处理器内部有一个48 位的 寄存器,称为全局描述符表寄存器(GDTR)。该寄存器分为两部分,分 别是32 位的线性地址和16 位的边界。32 位的处理器具有32 根地址线
gdtr有48位,前32是基地之,后16位是边界。

image-20231014132850663

理论上,全局描述符表可以位于内存中的任何地方。但是,如图11- 2 所示,由于在进入保护模式之后,处理器立即要按新的内存访问模式工 作,所以,必须在进入保护模式之前定义GDT。==但是,由于在实模式下 只能访问1MB 的内存,故GDT 通常都定义在1MB 以下的内存范围中。==当然,允许在进入保护模式之后换个位置重新定义GDT。

,全局描述符表的界限值就是表内最后1 字节的偏移量。第 1 字节的偏移量是0,最后1 字节的偏移量是表大小减一。如果界限值为 0,表示表的大小是1 字节。
因为GDT 的界限是16位的,所以,该表最大是216 字节,也就是 65536 字节(64KB)。又因为一个描述符占8 字节,故最多可以定义 8192 个描述符。实际上,不一定非得这么多,到底有多少,视需要而 定,但最多不能超过8192 个。

image-20231014132829628

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014132812290

11.3 存储器的段描述符

在保护模 式下,内存的访问机制完全不同,即必须通过描述符来进行。所以,这些段 必须重新在GDT 中定义。

在实模式下,主引 导程序的加载位置是0x0000:0x7c00,也 图11-3 进入保护模式前的内存映象
就是物理地址0x07c00。因为现在的地址 是32 位的,所以它现在对应着物理地址0x00007c00。主引导扇区程序共 512(0x200)字节,所以,我们决定把GDT 设在主引导程序之后,也就 是物理地址0x00007e00 处

实模式和保护模式在内存访问上是有区别的,在保护模式下,你不能说访问哪个段就访问哪个段,在访问之前,必须先在GDT 内定义要访问的内存段。

所以,描述符不是由用户程序自己建立的,而是在加载时,由操作系统根据你的程序结构而建立的,而用户程 序通常是无法建立和修改GDT 的,也就只能老老实实地在自己的地盘上工作。

每个描述符在GDT 中占8字节,也就是2个双字, 或者说是64 位。图中,下面是低32 位,上面是高32 位。

很明显,描述符中指定了32 位的段起始地址,以及20 位的段边界。 在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左 移4 位(乘以16)。和实模式不同,在32 位保护模式下,段地址是32 位 的线性地址,如果未开启分页功能,该线性地址就是物理地址。页功能 将在第16 章和第17 章讲解,而且开启页功能需要做很多准备工作。目 前,如果没有特别说明,线性地址就是物理地址。描述符中的段基地址 和段界限不是连续的,把它们分成几段似乎不科学。但这也是没有办法的事,这是从80286 处理器上带来的后遗症。80286 也是16 位的处理 器,也有保护模式,但属于16 位的保护模式。而且,其地址是24 位的, 允许访问最多16MB 的内存。尽管80286 的16 位保护模式从来也没形成 气候,但是,32 位处理器为了保持同80286 的兼容,只能在旧描述符的 格式上进行扩充,这是不得已的做法

描述符的分类
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

s位标识描述符类型。当该位是“0”时, 表示是一个系统段;为“1”时,表示是一个代码段或者数据段(栈段也是 特殊的数据段)。系统段将在以后介绍。

image-20231014132706254

存储器的段描述符

image-20231014132650735

X=0代表是数据段
Extend数据段的扩展方向,0代表向上扩展

W段是否可写。

G 位是粒度(Granularity)位,用于解释段界限的含义。当G 位是 “0”时,段界限以字节为单位。此时,段的扩展范围是从1 字节到1 兆字 节(1B~1MB),因为描述符中的界限值是20 位的。相反,如果该位是 “1”,那么,段界限是以4KB 为单位的。这样,段的扩展范围是从4KB 到 4GB。
S 位用于指定描述符的类型(Descriptor Type)。当该位是“0”时, 表示是一个系统段;为“1”时,表示是一个代码段或者数据段(栈段也是 特殊的数据段)。系统段将在以后介绍。

DPL 表示描述符的特权级(Descriptor Privilege Level,DPL)。这 两位用于指定段的特权级。共有4 种处理器支持的特权级别,分别是0、 1、2、3,其中0 是最高特权级别,3 是最低特权级别。刚进入保护模式 时执行的代码具有最高特权级0(可以看成是从处理器那里继承来的), 这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系 统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3 特权 级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有 些处理器指令(特权指令)只能由0 特权级的程序来执行,为的就是安 全。
在这里,描述符的特权级用于指定要访问该段所必须具有的最低特 权级。如果这里的数值是2,那么,只有特权级别为0、1 和2 的程序才能访问该段,而特权级为3 的程序访问该段时,处理器会予以阻止。特权级 将在以后专门讲解,谁也不希望自己的特权级最低,何况现在有随便决 定段特权级别的自由。那么,好吧,我们现在一律将特权级设定为最高 的0。

P 是段存在位(Segment Present)。P 位用于指示描述符所对应的 段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内 存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在, 这时,就应当把描述符的P 位清零,表示段并不存在。另外,同样是在 内存空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给 当前急需内存的程序使用(当前正在执行的),这时,同样要把段描述 符的P 位清零。当再次轮到它执行时,再装入内存,然后将P位置1。
P 位是由处理器负责检查的。每当通过描述符访问内存中的段时, 如果P 位是“0”,处理器就会产生一个异常中断。通常,该中断处理过程 是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内 存,并将P 位置1。在多用户、多任务的系统中,这是一种常用的虚拟内 存调度策略。当内存很小,运行的程序很多时,如果计算机的运行速度 变慢,并伴随着繁忙的硬盘操作时,说明这种情况正在发生

D/B 位是“默认的操作数大小”(Default Operation Size)或者“默认 的 栈 指 针 大 小 ” ( Default Stack Pointer Size ) , 又 或 者 “ 上 部 边 界 ” (Upper Bound)标志。
设立该标志位,主要是为了能够在32 位处理器上兼容运行16 位保护 模式的程序。尽管这种程序现在已经非常罕见了,但它毕竟存在过,兼 容,这是Intel 公司能够兴旺发达的重要因素。
该标志位对不同的段有不同的效果。对于代码段,此位称做“D”位, 用于指示指令中默认的偏移地址和操作数尺寸。D=0 表示指令中的偏移 地址或者操作数是16 位的;D=1,指示32 位的偏移地址或者操作数。
举个例子来说,如果代码段描述符的D 位是0,那么,当处理器在这 个段上执行时,将使用16 位的指令指针寄存器IP 来取指令,否则使用32 位的EIP。
对于栈段来说,该位被叫做“B”位,用于在进行隐式的栈操作时,是 使用SP 寄存器还是ESP 寄存器。隐式的栈操作指令包括push、pop 和 call 等。如果该位是“0”,在访问那个段时,使用SP 寄存器,否则就是使 用ESP 寄存器。同时,B 位的值也决定了栈的上部边界。如果B=0,那么栈段的上部边界(也就是SP 寄存器的最大值)为0xFFFF;如果B= 1 , 那 么 栈 段 的 上 部 边 界 ( 也 就 是 ESP 寄 存 器 的 最 大 值 ) 为 0xFFFFFFFF。
对于本书来说,它应当为“1”。本书不过多涉及16 位保护模式,它已 经非常罕见了。
L 位是64 位代码段标志(64-bit Code Segment),保留此位给64 位处理器使用。目前,我们将此位置“0”即可。

TYPE 字段共4 位,用于指示描述符的子类型,或者说是类别。如表 11-1 所示,对于数据段来说,这4 位分别是X、E、W、A 位;而对于代 码段来说,这4 位则分别是X、C、R、A 位。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

表11-1 中,X 表示是否可以执行(eXecutable)。数据段总是不可 执行的,X=0;代码段总是可以执行的,因此,X=1。
对于数据段来说,E 位指示段的扩展方向。E=0 是向上扩展的,也 就是向高地址方向扩展的,是普通的数据段;E=1 是向下扩展的,也就 是向低地址方向扩展的,通常是栈段。W 位指示段的读写属性,或者说 段是否可写,W=0 的段是不允许写入的,否则会引发处理器异常中断; W =1 的段是可以正常写入的。
对 于 代 码 段 来 说 , C 位 指 示 段 是 否 为 特 权 级 依 从 的 (Conforming)。C=0 表示非依从的代码段,这样的代码段可以从与它 特权级相同的代码段调用,或者通过门调用;C=1 表示允许从低特权级 的程序转移到该段执行。关于特权级和特权级检查的知识将在第14 章介 绍。R 位指示代码段是否允许读出。代码段总是可以执行的,但是,为 了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由R 位指定。R=0 表示不能读出,如果企图去读一个R=0 的代码段,会引发 处理器异常中断;如果R=1,则代码段是可以读出的,即可以把这个段 的内容当成ROM 一样使用。
也许有人会问,既然代码段是不可读的,那处理器怎么从里面取指 令执行呢?事实上,这里的R 属性并非用来限制处理器,而是用来限制 程序和指令的行为。一个典型的例子是使用段超越前缀“CS:”来访问代码 段中的内容。
数据段和代码段的A 位是已访问(Accessed)位,用于指示它所指 向的段最近是否被访问过。在描述符创建的时候,应该清零。之后,每 当该段被访问时,处理器自动将该位置“1”。对该位的清零是由软件(操 作系统)负责的,通过定期监视该位的状态,就可以统计出该段的使用 频率。当内存空间紧张时,可以把不经常使用的段退避到硬盘上,从而 实现虚拟内存管理。
AVL 是软件可以使用的位(Available),通常由操作系统来用,处 理器并不使用它。如果你把它理解成“好吧,该安排的都安排了,最后多 出这么一位,不知道干什么用好,就给软件用吧”,我也不反对,也许 Intel 公司也不会说些什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于代码段来说,C是否是特权急的段,为0表示只有特权急相通的才能直接转移到这个段执行。
R是否允许读出,限制处理器像访问数据段一样访问代码段的内容。
A是否已访问过

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

dpl决定了特权级。
这里的r只是限制了像访问数据段一样访问代码短的内容,而不是限制是否可以读程序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

x=1是代码短,必然向上扩展。x=0时候是代码短,需要看e,e=0为向上扩展。
如果超过段界限,就会引发异常中断。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

x=0和e=1,通常都是栈段

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

g位指定了段界限的单位,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

g=0,字节,g=1,4k
上面计算的是g=1时

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

p位,代表段描述副代表段内容是否在内存中。p=0就会引发中断读入内存。

image-20231014132433878

l位,是给64位使用的,先不讲

image-20231014132422225

d/b指定是16还是32位,sp还是esp。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

avl可用的,用于操作系统,处理器不用;

image-20231014132357964

11.4 安装存储器的段描述符并加载GDTR

不要忘了,我们现在还处于实模式下。因此,在GDT 中安装描述
符,必须将GDT 的线性地址(物理地址)转换成逻辑段地址和偏移地
址。
GDT 的线性地址是我们直接给出的,放在程序中的标号gdt_base 处。第12 行,将GDT 线性基地址的低16 位传送到寄存器AX 中。和从前 一样,这里使用了段超越前缀“cs:”,表明是访问代码段中的数据;又因 为 主 引 导 程 序 的 实 际 加 载 位 置 是 逻 辑 地 址 0x0000:0x7c00 , 故 标 号 gdt_base处的偏移地址是gdt_base+0x7c00。
同样地,第13 行将GDT 线性基地址的高16 位传送到寄存器DX。

处理器规定,gdt中第一个描述符必须是空描述符dumpy,双字*2。
进入保护模式后,在屏幕上显示文本代表进入32位了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014132311298

image-20231014132256653

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image-20231014132236489

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

lgdt m48;

这就是说,该指令的操作数是一个48 位(6字节)的内存区域。在 16 位模式下,该地址是16 位的;在32 位模式下,该地址是32 位的。该 指令在实模式和保护模式下都可以执行.
在这6 字节的内存区域中,要求前(低)16 位是GDT 的界限值,后 (高) 32 位是 GDT 的基地址。在初始状态下(计算机启动之后), GDTR 的基地址被初始化为0x00000000;界限值为0xFFFF。

该指向不影响任何标志位。

将gdt的基地址加载到gdtr。

image-20231014132116489

lgdt m
这里的m并不是说是得48位地址,而是指必须是一个48位的操作数。

image-20231014132105761

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
lgdt不影响标志位。
这就是说,该指令的操作数是一个48 位(6字节)的内存区域。在 16 位模式下,该地址是16 位的;在32 位模式下,该地址是32 位的。该 指令在实模式和保护模式下都可以执行。
这6 字节的内存区域中,要求前(低)16 位是GDT 的界限值,后 (高) 32 位是 GDT 的基地址。在初始状态下(计算机启动之后), GDTR 的基地址被初始化为0x00000000;界限值为0xFFFF。
该指向不影响任何标志位

11.5 关于第21条地址线A20的问题

在即将进入保护模式之前,这里还涉及一个历史遗留问题,那就是 处理器的第21 根地址线,编号A20。“A”是Address 的首字符,就是地 址,A0 是第一根地址线,A31 是第32 根地址线,所以,A20 就是第21 根地址线。在8086 处理器上运行程序不存在A20 问题,因为它只有20 根地址线。
实模式下的程序只能寻址1MB 内存,那是因为它依赖16 位的段地址 左移4 位,加上16 位的偏移地址来访问内存。当逻辑段地址达到最大值 0xFFFF 时,再加一,就会因进位而绕回到0x0000,因为段寄存器只能 保留16 位的结果。至于段内偏移地址,也是如此。

原来20位地址线的时候可以进位后变成0,但在32位下如何保持原来的这种特性不变?答案为默认都是这种用法。
只需要强制第21 根地址线恒为“0”就可 以了。这样,0x0FFFFF 加1 的进位被强制为“0”,结果是0x000000;再 加1,是0x000001,……,永远和实模式一样。

,IBM 公司使用一个与门来控制第21 根地址线 A20,并把这个与门的控制阀门放在键盘控制器内,端口号是0x60。向 该端口写入数据时,如果第1 位是“1”,那么,键盘控制器通向与门的输 出就为“1”,与门的输出就取决于处理器A20 是“0”还是“1”。

这种做法持续了若干年,直到80486 处理器推出后,才有了更快速 的办法。相信在此期间,Intel 公司和IBM 公司都听到了不少的抱怨,为 什么进入保护模式这么麻烦,一定要改改。从80486 处理器开始,处理 器本身就有了A20M#引脚,意思是A20 屏蔽(A20 Mask),它是低电平 有效的。

如图11-7 所示,输入输出控制器集中芯片ICH 的处理器接口部分, 有一个用于兼容老式设备的端口0x92,第7~2 位保留未用,第0 位叫做 INIT_NOW,意思是“现在初始化”,用于初始化处理器,当它从0 过渡到 1 时,ICH 芯片会使处理器INIT#引脚的电平变低(有效),并保持至少 16 个PCI 时钟周期。通俗地说,向这个端口写1,将会使处理器复位, 导致计算机重新启动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

端口0x92的位1用于控制A20,叫做替代的A20门控制 ( Alternate A20 Gate , ALT_A20_GATE),它和来自键盘控制器的A20 控制线一起,通过或门连接到处理器的A20M#引脚。和使用键盘控制器 的端口不同,通过0x92 端口显得非常迅速,也非常方便快捷,因此称为 Fast A20。
当INIT_NOW 从0 过渡到1 时,ALT_A20_GATE 将被置“1”。这就是 说,计算机启动时,第21 根地址线是自动启用的。A20M#信号仅用于单 处理器系统,多核处理器一般不用。特别是考虑到传统的键盘控制器正 逐渐被USB 键盘代替,这些老式设备也许很快就会消失。

接着来看代码清单11-1。
端口0x92 是可读写的,第40~42 行,先从该端口读出原数据,接 着,将第2 位(位1)置“1”,然后再写入该端口,这样就打开了A20。

11.6 保护模式下的内存访问

控制这两种模式切换的开关原是在一个叫CR0 的寄存 器。
CR0 是处理器内部的控制寄存器(Control Register,CR)。之所 以有个“0”后缀,是因为还有CR1、CR2、CR3 和CR4 控制寄存器,甚至 还有CR8。
CR0 是32 位的寄存器,包含了一系列用于控制处理器操作模式和运 行状态的标志位。如图11-8 所示,它的第1 位(位0)是保护模式允许位 (Protection Enable,PE),是开启保护模式大门的门把手,如果把该 位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
保护模式下的中断机制和实模式不同,因此,原有的中断向量表不再适用,而且,必须要知道的是,在保护模式下,BIOS 中断都不能再用,因为它们是实模式下的代码。在重新设置保护模式下的中断环境之前,必须关中断,这就是第44 行的用意。
pe位置1就进入了保护模式。

8086 处 理 器 的 段 寄 存 器 是 16 位 的 , 共 有 4 个 : CS 、 DS 、 ES 和 SS。而在32 位处理器内,在原先的基础上又增加了两个段寄存器FS 和 GS。
每个段寄存器还包括一个不可见的部分,称为描述符高速缓 存器,用来存放段的线性基地址、段界限和段属性。既然不可见,那就 是处理器不希望我们访问它。事实上,我们也没有任何办法来访问这些 不可见的部分,它是由处理器内部使用的

实模式下的6 个段寄存器CS、DS、ES、FS、GS 和SS,在保护模 式下叫做段选择器。和实模式不同,保护模式的内存访问有它自己的方 式。在保护模式下,尽管访问内存时也需要指定一个段,但传送到段选 择器的内容不是逻辑段地址,而是段描述符在描述符表中的索引号。
在保护模式下访问一个段时,传送到段选择器的是 段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述

GDT 的线性基地址在GDTR 中,又因为每个描述符占8 字节,因此,描述符在表内的偏移地址是索引号乘以8。
。TI 是􏰁述符表指示器(Table Indicator),TI =0 时,表示􏰁述符在GDT 中;TI=1 时,􏰁述符在LDT 中。LDT 的知 识将在后面进行介绍,它也是一个􏰁述符表,和GDT 类似。RPL 是请求 特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求 访问这个内存段。每个程序都有特权级别,也将在后面慢慢介绍,现在 只需要将这两位置成“00”即可。


此后,每当有访问内存的指令时,就不再访问GDT 中的描述符,直 接用当前段寄存器描述符高速缓存器提供线性基地址

,在实模式下,段寄存器􏰁述符高 速缓存器的内容仅低20 位有效,高12 位全部是零。

18.7

image-20231014132015001

image-20231014131959375

位0是复位,可能是导致重启计算机的。

下面这3条指令用于打开a20
就是一个或操作后重新写入

开启保护模式。为1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后cli关中断,原来的中断像量表不可用了,需要重新建立。
然后用上面类似的3条指令将末尾只为1.

11.7 清空流水线并串行化处理器

这里有两个亟待解决的问题。
第一,正如上一节所述,即使是在实模式下,段寄存器的描述符高 速缓存器也被用于访问内存,仅低20 位有效,高12 位是全零。当处理器 进入保护模式后,这些内容依然残留着,但不影响使用,程序可以继续 执行。但是,这些残留的内容在保护模式下是无效的,迟早会在执行某 些指令的时候出问题。因此,比较安全的做法是尽快刷新 CS、 SS、 DS、ES、FS 和GS 的内容,包括它们的段选择器和描述符高速缓存 器。

第二,在进入保护模式前,有很多指令已经进入了流水线。因为处 理器工作在实模式下,所以它们都是按16 位操作数和16 位地址长度进行 译码的,即使是那些用bits 32 编译的指令。进入保护模式后,受CS 段 描述符高速缓存器中实模式残留内容的影响,处理器进入16 位保护模式 工作。如果保护模式下的代码是16 位的,影响可能不大,但如果是用 bits 32 编译的,那么,由于对对操作数和默认地址大小的解释不同,指 令的执行结果可能会不正确,所以必须清空流水线。同时,那些通过乱 序执行得到的中间结果也是无效的,必须清理掉,让处理器串行化执 行,即,重新按指令的自然顺序执行。
这里有一个两全其美的方案,那就是使用远转移指令jmp 或者远过程调用指令call。处理器最怕转移指令,遇到这种指令,一般会 清空流水线,并串行化执行;另一方面,远转移会重新加载段选择器 CS,并刷新描述符高速缓存器中的内容。一个建议的方法是在设置了控 制寄存器CR0 的PE 位之后,立即用jmp 或者call 转移到当前指令流的下 一条指令上
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

creg显示控制寄存器
用大小写代表01

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后在主引导程序中会重新设置位1.

下面的表达方式都变了
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

32位进入保护后,家了一个描述副告诉缓存起,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ti=1时代表在lgt中

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

执行下面2条后,就变了。接下来的段地址都在缓存起中

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传因为已经关中断了,所以不会放行。

image-20231014131155946

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

12章 存储器的保护。

在16 位模式和32 位模式下,一些老式的编译器会生成不同的机 器代码。下面是一个例证:

由于在16 位模式下,默认的操作数大小是字(2 字节),故生成8E D8 也不难理解。在32 位模式下,默认的操作数大小是双字(4 字节)。 由于指令中的源操作数是16 位的AX,故编译后的机器码前面应当添加前 缀0x66 以反转默认的操作数大小,即66 8E D8。

很遗憾,由于这一点点区别,有前缀的和没有前缀的相比,处理器 在执行时会多花一个额外的时钟周期。问题在于,这样的指令用得很频 繁,而且牵扯到内存段的访问,自然也很重要。因此,它们在16 位模式 和32 位模式下的机器指令被设计为相同。即都是8E D8,不需要指令前 缀。

image-20231014131121667

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有效地址
11.5停止

12课 内存选择符

12.2.2 创 建GDT 并 安 装 段 描 述 符

。对于以4KB(十进制数4096 或者十六进制数 0x1000)为粒度的段,描述符中的界限值加1,就是该段有多少个4KB。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

十二章 存储器的保护

利用存储器的保护功能,也可以实现一些有价值的
功能,比如虚拟内存管理。当处理器访问一个实际上不存在的段时,会
引发异常中断。操作系统可以利用这一点,通过接管异常处理过程,并
用硬盘来进行段的换入和换出,从而实现在较小的内存空间运行尽可能
大、尽可能多的程序。

12.2 进入32 位保护模式
12.2.1 话说mov ds,ax 和mov ds,eax

mov ds,ax 在16 位模式和32 位模式下,一些老式的编译器会生成不同的机器代码。
bit 16下; mov ds,ax; 8E D8
bit 32下; mov ds,ax; 66 8E D8
由于在16 位模式下,默认的操作数大小是字(2 字节),故生成8E D8 也不难理解。在32 位模式下,默认的操作数大小是双字(4 字节)。
由于指令中的源操作数是16 位的AX,故编译后的机器码前面应当添加前缀0x66以反转默认的操作数大小,即66 8E D8。
由于这一点点区别,有前缀的和没有前缀的相比,处理器在执行时会多花一个额外的时钟周期。问题在于,这样的指令用得很频繁,而且牵扯到内存段的访问,自然也很重要。因此,它们在16位模式和32位模式下的机器指令被设计为相同。即都是8E D8,不需要指令前缀。
这可难倒了很多编译器,它们固执地认为,在32 位模式下,源操作
数是16 位的寄存器AX时,应当添加指令前缀。好吧,为了照顾它们,很
多程序员习惯使用这种看起来有点别扭的形式: mov ds eax;
你别说,还真有效,果然生成的是不加前缀的8E D8。
说到这里,我觉得NASM 编译器还是非常优秀的,起码它不会有这样的问题。因此,不管处理器模式如何变化,也不管指令形式如何变化,以下代码编译后的结果都一模一样:
bit 16下; mov ds,ax; moc ds,eax
bit 32下;mov ds ax; MOV DS,EAX
上面都是8E D8

12.2.2 创建GDT 并安装段描述符

准备进入保护模式。
首先是创建GDT,并安装刚进入保护模式时就要使用的描述符。第
12~15 行,首先计算GDT 在实模式下的逻辑地址。在上一章里,GDT
的大小和线性基地址分别是用两个标号gdt_size 和gdt_base 声明和初始化的:
GDT_SIZE DW 0;
GTT_BASE DD 0X000007E00;
但是后面已经改成了
gdpt DW 0 ;2B
DD 0X00007E00 ;4B

mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位线性基地址

在32 位处理器上,即使
是在实模式下,也可以使用32 位寄存器。所以,第12 行,直接将GDT
的32 位线性基地址传送到寄存器EAX 中。
32位的除法:

64 位的被除数在EDX:EAX 中,32 位被除数可以在32 位通用
寄存器中,也可以在32位内存单元中。EAX中的商是段地址,仅低16位有效;EDX中的余数是段内偏移地址,仅低16 位有效。
EDX:EAX / 16 == EAX … EDX

         mov ds,eax                         ;令DS指向该段以进行操作
         mov ebx,edx                        ;段内起始偏移地址

安装空描述符。该描述符的槽位号是0,处理器不允许访问这个描述符,任何时候,使用索引字段为0 的选择子来访问该描述
符,都会被处理器阻止,并引发异常中断。在现实中,一个忘了初始化
的指针往往默认值就是0,所以空描述符的用意就是阻止不安全的访问。
很多人喜欢用这个槽位来记载一些私人信息,做一些特殊的用途,认为
反正处理器也不用它。但是,这样做可能是不安全的,还没有证据表明
Intel 公司保证决不会使用这个槽位

         mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
         mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符

基地址0x00000000,段界限0xFFFFF,段粒度4KB。
段界限值:段描述符中的段界限值×0x1000+0xFFF
也就是4gb。

; 线性基地址为0x00007C00;段界限为0x001FF,粒度为字节。对于
向上扩展的段来说,段界限在数值上等于段的长度减去1,因此该段的长
度是0x200,即512 字节。
         mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节
         mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符
;根据上一章的经验,该段实际上就是当前程序所在的段(正在安装
该描述符呢),也就是主引导程序所在的区域。尽管在描述符中把它定
义成32 位的段,但它实际上既包含16 位代码,也包含32 位代码。[bits
32]之前的代码是16 位的,之后的代码是32 位的。不过,在该描述符生
效的时候,处理器的执行流已经位于32 位代码中了。

如果需要访问代码段内的数 据,只能重新为该段安装一个新的描述符,并将其定义为可读可写的数 据段。这样,当需要修改代码段内的数据时,可以通过这个新的描述符 来进行。

当两个以上的描述符都描述和指向同一个段时,把另 外 的 描 述 符 称 为 别 名(alias)。注意,别名技术并非仅仅用于读写代码段,如果两个程序想共享同一个内存区域,可以分别为每个程序都创建一个描述符,而且它们都指向同一个内存段,这也是别名应用的例子。

;3637安装段栈描述符
; 线性基地址是0x00007C00,段界限为0xFFFFE,粒度为4KB
; 该段和代码段使用同一个线性基地址,,代码段是向上(高地址方向)扩展的,而栈段是向下(低地址方向)扩展的
         mov dword [ebx+0x20],0x7c00fffe
         mov dword [ebx+0x24],0x00cf9600
; 设置GDT 的界限值为39,因为这里共有5 个描述符,总大小为40 字节,界限值为39。
         mov word [cs: pgdt+0x7c00],39      ;描述符表的界限

12.3 修改段寄存器时的保护

随着程序的执行,经常要对段寄存器进行修改。此时,处理器在变
更段寄存器以及隐藏的描述符高速缓存器的内容时,要检查其代入值的
合法性.

; 这条指令会隐式地修改段寄存器CS
         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移

58 flush:
59 mov eax,0x0018
60 mov ds,eax
61
62 mov eax,0x0008 ;加载数据段(0…4GB)选择子
63 mov es,eax
64 mov fs,eax
65 mov gs,eax
66
67 mov eax,0x0020 ;0000 0000 0010 0000
68 mov ss,eax
69 xor esp,esp ;ESP <- 0

当这些指令执行时,处理器把指令中给出的选择子传送到段寄存器的选择器部分。但是,处理器的固件在完成传送之前,要确认选择子是正确的,并且该选择子选择的描述符也是正确的。

第55~68 行的指令执行之后,段寄存器CS 指向512 字节的32位代码段,基地址是0x00007C00;DS 指向512字节的32 位数据段,该段是上述代码段的别名,因此基地址也是0x00007C00;ES、FS 和GS 指向同一个段,该段是一个4GB 的32 位数据段,基地址为0x00000000;SS 指向4KB 的32位栈段,基地址为0x00007C00外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在当前程序中,选择子的TI 位都是0,故所有的描述符都在GDT中。如图12-2 所示,GDT的基地址和界限,都在寄存器GDTR 中。描述符在内存中的地址,是用索引号乘以8,再和描述符表的线性基地址相加得到的,而这个地址必须在描述符表的地址范围内。换句话说,索引号乘以8得到的数值,必须位于描述符表的边界范围之内。换句话说,处理器从GDT 中取某个描述符时,就要求描述符的8 个字节都在GDT 边界之内,也就是索引号×8+7 小于等于边界。

如果检查到指定的段描述符,其位置超过表的边界时,处理器中止 处理,产生异常中断13,同时段寄存器中的原值不变。
举个例子来说,若描述 符的类别是只执行的代码段(表11-1),则不允许加载到除CS 之外的其 他段寄存器中。
具体地说,首先,描述符的类别字段必须是有效的值,0000 是无效 值的一个例子。
然后,检查描述符的类别是否和段寄存器的用途匹配。其规则如表 12-1 所示。


如果P=0,表明虽然描述符已被定义,但该段实际上并不存在于物理内存中。此时,处理器中止处理,引发异常中断11。一般来说,应当定义一个中断处理程序,把该描述符所对应的段从硬盘等外部存储器调入内存,然后置P 位。中断返回时,处理器将再次尝试刚才的操作。
如果P=1,则处理器将描述符加载到段寄存器的描述符高速缓存器,同时置A 位(仅限于当前讨论的存储器的段描述符)。

只有可以写入的数据段才能加载到SS 的选择器,CS 寄存器只允许加载代码段描述符。另外,对于DS、ES、FS 和GS 的选择器,可以向其加载数值为0 的选择子,即
XOR EAX,RAX;
MOV DS,EAX;

尽管在加载的时候不会有任何问题,但在,真正要用来访问内存时,就会导致一个异常中断。这是一个特殊的设计,处理器用它来保证系统安全,这在后面会讲到。不过,对于CS 和SS的选择器来说,不允许向其传送为0 的选择子

12.4 地址变换时的保护

12.4.1 代码段执行时的保护

在32 位模式下,尽管段的信息在描述符表中,但是,一旦相应的描述符被加载到段寄存器的描述符高速缓存器,则处理器取指令和执行指令时,将不再访问描述符表,而是直接使用段寄存器的描述符高速缓存器,从中取得线性基地址,同指令指针寄存器EIP 的内容相加,共同形成32位的物理地址从内存中取得下一条指令。不过,在指令实际开始执行之前,处理器必须检验其存放地址的有效性,以防止执行超出允许范围之外的指令。

每个代码段都有自己的段界限,位于其描述符中。实际使用的段界 限,其数值和粒度(G)位有关,如果G=0,实际使用的段界限就是描 述符中记载的段界限;如果G=1,则实际使用的段界限为

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
要执行的那条指令,其长度减1 后,与EIP 寄存器的值相加,结果必须小于等于实际使用的段界限,否则引发处理器异常
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

12.4.2 栈操作时的保护

对栈操作的指令一般是push、pop、ret、iret 等。这些指令在代码段中执行,但实际操作的却是栈段。
现在只讨论32 位的栈段,即,其描述符B 位是1 的栈段。处理器在 这样的段上执行压栈和出栈操作时,默认使用ESP 寄存器。

实际使用的段界限就是段内不允许访问的最低端偏移地址。至于最高端的地址,则没有限制,最大可以是0xFFFFFFFF。
实际使用的段界限+1<=(ESP的内容-操作数的长度)<=0xFFFFFFFF

假设现在ESP 的内容是0x00007A04,执行PUSH EDX后,因为是要压入一个双字(4 字节),故处理器在向栈中写入数据之前,先将ESP 的内容减去4,得到0x7A00,这就是ESP 寄存器在进行压栈 操 作 时 的 新 值 。 因 为 该 值 小 于 实 际 使 用 的 段 界 限 0x7A00 加 一(0x7A01),因此不允许执行该操作。

但是,如果执行的是这条指令:PUSH AX;因为要压入一个字(2 字节),故实际执行压栈操作时,ESP的内容是OX7C04-2=OX7C02 。结果大于实际使用的段界限加一,允许操作。

67 mov eax,0x0020 ;0000 0000 0010 0000
68 mov ss,eax
69 xor esp,esp ;ESP <- 0
这三行设置栈的线性基
地址为0x00007C00,段界限为0xFFFFE,粒度为4KB,并设置栈指针寄存器ESP 的初值为0。
因为段界限的粒度是4KB(G=1),故实际使用的段界限为 0XFFFFE × 0X1000 +0XFFF=OXFFFFEFFF;

12.4.3 数据访问时的保护

因为是向上扩展的,所以代码段的检查规则同样适用于数据段。不同之处仅仅在于,对于取指令来说,是否越界取决于指令的长度;而对于数据段来说,则取决于操作数的尺寸问内存。

在32 位模式下,处理器使用32 位的段基地址加上32 位的偏移量, 共同形成32 位的物理地址来访问内存。段基地址由段描述符指定,而偏 移量由指令直接或者间接给出。很显然,在段最大的时候,可以自由访 问4GB 空间内的任何一个单元。

第13章 程序的动态加载和执行

所有的段在使用之前,都必须以描述符的形式在描述符表中进行定义,那么,像操作系统这样的软件,又怎么能够加载和执行其他各种用户程序呢?毕竟,你并不知道这些程序都定义了哪些段,每个段是什么类型,有多长。

内核不能放到主引导扇区里,毕竟它都很大。所以,计算机首先从 主引导程序开始执行,主引导程序负责加载内核,并转交控制权。然后,内核负责加载用户程序,并提供各种例程给用户程序调用。提供给用户 程 序 调 用 的 例 程 也 叫 应 用 程 序 接 口 ( Application Programming Interface,API),本章用简单的方法来允许用户程序使用API 工作

13.2 内核的结构、功能和加载
13.2.1 内核的结构

内核分为四个部分,分别是初始化代码、内核代码段、内核数据段和公共例程段主引导程序也是初始化代码的组成部分
初始化代码用于从BIOS 那里接管处理器和计算机硬件的控制权,安装最基本的段描述符,初始化最初的执行环境。然后,从硬盘上读取和加载内核的剩余部分,创建组成内核的各个内存段。初始化代码大部分位于代码清单13-1 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内核的代码和数据位于代码清单13-2 中。如图13-1 所示,内核代码段是在第385 行定义的,用于分配内存,读取和加载用户程序,控制用户程序的执行。

第7 行开始,一直到第12 行,用于声明常数。很明显,这是一些 内存段的选择子,它们对应的描述符会在内核初始化的时候创建。这些段是内核的段,供内核代码使用,对内核代码是透明的,内核代码“知道” 每个段选择子的具体数值,就象你知道自己办公室里有哪些人,可以直接喊他的名字让他做某件事一样。但是,段选择子的具体数值是和它们在GDT 中的位置相关的。为了不至于在往后因为调整段的位置而修改程 序代码,将它们声明成常数是最好的。

,不要忘了这个表达式,我们以 前学过的,它用来得到段的起始汇编 地址:

13.2.2 内核的加载

接下来,从第9 行开始,一直到第55 行,是为进入保护模式做准 备。如图13-2 所示,因为主引导程序的加载位置是物理地址 0x00007C00,所以,从这个位置往上是512 字节的初始化代码段,从这 个位置往下是4KB 的内核栈。

全局描述符表(GDT)是不可或缺的,和从前一样,我们将它定义在从物理地址0x00007E00开始的地方,紧挨着初始化代码段。GDT 可大可小,最大能达到64KB,所以,它的空间一定要留够。
和GDT 一样,内核程序的大小也是不定的,但可以规定它的起始位 置。在这里,我们决定将内核程序加载到从物理内存地址0x00040000 开始的地方。从这个地方往上,一直到0x0009FFFF,都是它的地盘,取决于它到 底有多大,想用多少就用多少。从0x000A0000 往上,是ROM BIOS, 硬件专有的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在进入保护模式之前,初始化程序(主引导程序)已经在全局􏰁述 符表(GDT)中安装了几个必要的􏰁述符。如图13-3 所示,

  • 第一个于访问0~4GB 内存的数据段,它很重要,内核只有在具备了访问全部 4GB 内存空间的能力时,才能随心所欲地做任何事情。
  • 第二个是初始化代码段,也就是主引导程序所在的段。进入保护模 式后,要继续执行主引导程序的后半部分代码,必须按处理器的要求, 为它创建描述符。
  • 最后两个分别是初始的栈段和显示缓冲区的描述符。这里定义的栈 在初始化过程中就要使用,而在进入内核之后,它又是内核的栈。

32位模式下的循环指令需要使用 ECX寄存器,而不是CX。
目前我们使用常量定义内核在磁盘的位置。
初始化代码并不知道内核有多大,所以也就不知道应该读多少个扇 区。不过,它可以先读一个扇区,因为那里包含着内核的头部数据,根据这些数据,就可以知道内核的总扇区数。
每次过程返回时,会使EBX 寄存器的值比 原来多512。

13.2.3 安装内核的段描述符

要使内核工作起来,首要的任务是为它的各个段创建描述符。换句话说,还要为GDT 续添新的描述符**。进入保护模式前,

标号pgdt 所指向的内存位置包 含了GDT 的基地址和大小。现在,我们的任务是重新从标号pgdt 处取得 GDT 的基地址,为其添加描述符,并修改它的大小,然后用lgdt 指令重 新加载一遍GDTR 寄存器,使修改生效

而我们当前正在保护模式下执行主引导程序。保护模式下的代码段只是用来执行的,是否能读出,取决于其描述符的类别字段,但无论如何它都不能写入。

现在可以创建与内核相关的其他段􏰁述符。首先是公共例程段。如 图13-5 所示,内核头部偏移0x04 处的一个双字,就是公共例程段的起始 汇编地址。由于内核被加载的物理地址是由EDI 寄存器指向的,所以, 第99 行,直接访问4GB 内存段,从该偏移位置取出公共例程段的起始汇编地址。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13-3程序的组成部分
  • 内核头部数据,功加载时定位内核的各个部分。16行
  • 公共例程段;34行
  • 内核数据段;330行
  • 内核代码段;385行
  • 尾部,用于计算内核长度;599行

从偏移量为0x10 开始的地方用于指示内核入口点,可以通过标号core_entry 引用,
在主引导程序加载了内核之后,从这里把处理器的控制权交给内核代码。

13.3在内核中执行

cpuid 指令(CPU Identification)用于返回处理器的标识和特性信 息。EAX 用于指定要返回什么样的信息,也就是功能。有时候,还要用 到ECX 寄存器。cpuid 指令执行后,处理器将返回的信息放在EAX、 EBX、ECX 或者EDX 中。

13.4 用户程序的加载和重定位

13.-3用户程序解读

所有操作系统的可执行文件都包括文件头,这里也不例外。事实 上,这也是我们熟悉的、一贯的做法。在文件头内的偏移0 处,是一个双 字,指示了用户程序的大小,以字节为单位。

偏移量为0x08 处的双字是为栈保留的,和早先的做法不同,内核不 要求用户程序􏰀供栈空间,而改由内核动态分配,以减轻用户程序编写 的负担。当内核分配了栈空间后,会把栈段的选择子填写到这里,用户 程序开始执行时,可以从这里取得该选择子以初始化自己的栈;

偏移量为0x10 处的双字,是用户程序入口点的32 位偏移地址
偏移量为0x14 处的双字,是用户程序代码段的起始汇编地址。当内核完成对用户程序的加载和重定位后,将把该段的选择子回填到这里 (仅占用低字部分)。这样一来,它和0x10 处的双字一起,共同组成一个6 字节的入口点,内核从这里转移控制到用户程序。

为了使开发人员能够利用它所􏰀供的API,操作系统至少要公开它 们。在早期的系统中,这些API 以中断号的方式公布,因为它们是通过 软中断进入的。不过,另一种常见的办法是使用符号名。比如,操作系 统􏰀供了一个例程,用于显示光标跟随的字符串,那么,它可以公布一 个符号名:

当然,它肯定不会同时公布一个段地址和偏移地址,因为它也不能 保证地址不会变化。在操作系统的开发手册中,会列出所有符号名。符号名在高级语言里就是库函数名。

内核要求,用户程序必须在头部偏移量为0x28 的地方构造一个表格,并在表格中列出所有要用到的符号名。每个符号名的长度是256 字节,不足部分用0x00 填充,这意味着每个符号名的长度最多可以是256 个字符。在用户程序加载后,内核会分析这个表格,并将每一个符号名替换成相应的内存地址,这就是过程的重定位。为了方便起见,我们把该表格叫做“符号-地址检索表”(Symbol-Address Lookup Table, SALT)。不要上网搜索这个词,也不要查别的资料,这不是一个标准, 是我自己随心所欲、特立独行的产物

第29~36 行声明了三个标号,并分别初始化了三个符号名,每一个 256 字节,不足部分是用0 填充的。每个符号名都以“@”开始,这并没有 任何特殊意义,仅仅在概念上用于表示“接口”的意思。为了计算需要填充 多少个0,它们都使用了相似的表达式,比如:

13.4.2 计算用户程序占用的扇区数

用户程序的加载是在例程load_relocate_program 内进行的,该过程 需要用ESI 寄存器传入用户程序的起始逻辑扇区号。当过程返回时,在 AX 寄存器内包含了指向用户头部段的选择子。
调 用读硬盘的过程read_hard_disk_0 来预读用户程序。进入过程前,EAX 寄存器的内容是用户程序的起始逻辑扇区号;数据的存放地点是内核缓冲区core_buf。

条件转移指令和传送指令相结合的产物,既有条件转移指令的多样 性,又执行的是传送操作。但是,和mov 指令不同的是,它的目的操作 数只允许是16 位或者32 位通用寄存器,源操作数只能是相同宽度的通用 寄存器和内存单元,以下是几个常用的例子:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13.4.3 简单的动态内存分配

allocate_memory 例程位于代码清单13-2 的公共例程段中,它仅仅 需要通过ECX 寄存器传入希望分配的字节数。当过程返回时,ECX 寄存 器包含了所分配内存的起始物理地址。

13.4.4 段的重定位和描述符的创建

既然用户程序已经全部读入内存,现在的任务就是根据它的头部信 息来创建段􏰁述符。
读用户程序头部信息,根据这些信息创建头部段􏰁 述符。

从过程make_seg_descriptor返回时,EDX:EAX 中包含了64 位的段􏰁述符。紧接着,第 439 行调用公共例程段内的另一个过程set_up_gdt_descriptor,把该􏰁述 符安装到GDT 中。

set_up_gdt_descriptor 也属于公共例程段,是在第263 行定义的, 它需要通过EDX:EAX 传入􏰁述符作为唯一的参数。该过程返回时,CX 寄存器中包含了那个􏰁述符的选择子。

要在GDT 内安装􏰁述符,必须知道它的物理地址和大小。而要知道 这些信息,可以使用指令sgdt(Store Global Descriptor Table Register),它用于将GDTR 寄存器的基地址和边界信息保存到指定的内存位置。sgdt 指令的格式为
sgdt m
6字节内存

下面的工作是计算􏰁述符的安装地址。这个地址可以这样计算:先 得到􏰁述符表的界限值,将它加一,得到􏰁述符表的总字节数,这实际 上也是新􏰁述符在GDT 内的偏移量。然后,用GDT 的线性地址加上这个 偏移量,就是用于安装新􏰁述符的线性地址。
movzx,其作用是带零扩展的传送(Move with Zero- Extend)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

也就是说,movzx 指令的目的操作数只能是16 位或者32 位的通用 寄存器,源操作数只能是8位或者16 位的通用寄存器,或者指向一个8 位 或16 位内存单元的地址。而且,很有意思的是,目的操作数和源操作数的大小是不同的。这里有几个例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
对于上面的第一个例子,如果指令执行前,AL 寄存器的内容是 0xC0,那么,指令执行后,CX 寄存器的内容为0x00C0;对于第二个例 子,处理器访问段寄存器DS 所指向的段,从偏移地址0x2000 处取得一 字节,左边添加24 个“0”,使之扩展到32 位,然后传送到EAX 寄存器; 对于第三个例子,如果指令执行前,BX 寄存器的内容为0x55AA,那 么,指令执行后,ECX 寄存器的内容为0x000055AA。

movsx,意思是带符号扩展的传送(Move with Sign-Extension),指令格式为

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
和movzx 不同,movsx 在执行扩展时,用于扩展的比特取自源操作 数的符号位。比如

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GDT 的界限是16 位的,允许64KB 的大小,即8192 个􏰁述符,似 乎不需要使用32 位的寄存器EBX。事实上,还是需要的,因为后面要用 它来计算新􏰁述符的32 位线性地址,加法指令add要求的是两个32 位操 作数。

lgdt 指令重新加载GDTR,使新的􏰁述符生效。

除以8,商就是我们所要 得到的􏰁述符索引号。最后,将索引号左移3 次,留出TI 位和RPL 位 (TI=0,指向GDT,RPL=00),这就是要生成的选择子。

继续回到过程load_relocate_program。

440行,将该段的选择子写 回到用户程序头部,供用户程序在接管处理器控制权之后使用。实际 上,在内核向用户程序转交控制权时,也要用到。

用于重定位用户程序代码段和数据段,并创建和安 装相应的􏰁述符,整个过程都是一样的,也很容易理解。
唯一不同的是栈段,栈所用的空间不需要用户程序􏰀供,而是由内 核动态分配。内核分配栈空间时,是以4KB 为单位的。

从用户程序头部偏移为0x0C 的地方获得一个建议的栈大 小。这是一个倍率,至少应当为1,说明用户程序希望分配4KB 栈。如果 为2,说明希望分配8KB;为3 则表明希望分配12KB,依此类推。

用4096(4KB)乘以倍率,得到所需要的栈大小, 然后,用这个值去申请内存。

13.4.5 重定位用户程序内的符号地址

为了使用内核提供的例程,用户程序需要建立一个符号-地址对照表 (SALT)。这样,当用户程序加载后,内核应该根据这些符号名来回填 它们对应的入口地址,这称为符号地址的重定位。显然,重定位的过程 就是字符串匹配和比较的过程。
为了对用户程序内的符号名进行匹配,内核也必须建立一张符号-地 址对照表(SALT)。
内核的SALT 表位于代码清单13-2 的内核数据段中,从第338 行开 始,一直到第357行结束。实际上,这个表是可以根据需要扩展的。
如图13-9 所示,用户程序内的SALT 表,每个条目是256 字节,用 于容纳符号名,不足256字节的,用零填充。内核中的SALT 表,每个条 目则包括两部分,第一部分也是256 字节的符号名;第二部分有6 字节, 用于容纳4 字节的偏移地址和2 字节的段选择子,因为符号名是用来􏰁述 例程的,这6 字节就是例程的入口地址。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
当用户程序终止并返回时,返回点位于标号return_point 所在的位 置。该标号位于第582 行,属于内核代码段。在这一行之前,是内核将 控制权交给用户程序的指令。

内核的SALT 表是静态的,适用于所有要加载的用户程序,理所当然 地要比用户程序的SALT表大,因为它要􏰀供所有可被用户程序调用的过 程列表。至于用户程序,根据需要,它只会列出自己用到的那些。

在用户程序加载时,内核的任务是比对这两张SALT 表,并将用户程序SALT 表中的符号名替换成相应的入口地址。为了便于说明,用户程序 的SALT 表简称U-SALT,内核的SALT 表简称C-SALT。

比对的过程就是两个字符串的比较过程,可以使用cmps 指令 (Compare String Operands)。该指令有3 种基本的形式,分别用于字 节、字和双字的比较:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在16 位模式中,源字符串的首地址由DS:SI 指定,目的字符串的首 地址由ES:DI 指定;在32位模式下,则分别是DS:ESI 和ES:EDI。在处理 器内部,cmps 指令的操作是把两个操作数相减,然后根据结果设置标志 寄存器中相应的标志位。
取决于标志寄存器EFLAGS 中的DF 位,如果DF=0,表明是正向比 较,也就是按地址递增的方向比较,这些指令执行后,SI(ESI)和 DI(EDI)的内容分别加1、加2 和加4;否则,如果DF =1,表明是反向 比较,这些指令执行后,SI(ESI)和DI(EDI)的内容分别减1、减2 和 减4。
单纯的cmps 指令只比较一次,它属于推一下才动一动的那种类型。 所以,需要加指令前缀rep 使比较连续进行。连续比较的次数由 CX(ECX)寄存器控制,在16 位模式下,使用CX 寄存器;在32 位模 式下,使用ECX 寄存器,举个例子:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
该指令执行时,每次比较4 字节,连续比较直至ECX 寄存器的内容 为零。
问题是,用rep 前缀比不出个所以然来,你就是重复比较100000 次,也看不出两个字符串哪里不同。所以,针对cmps 指令,应当使用 repe(repz)和repne(repnz)前缀,前者的意思是“若相等(为零)则 重复”,后者的意思是“若不等(非零)则重复”。但无论是哪种情况,总 的比较次数由CX(ECX)控制,表13-1 显示了这几种控制手段的区别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
可见,repe/repz 用于搜索第一个不匹配的字节、字或者双字, repne/repnz 用于搜索第一个匹配的字节、字或者双字。无论如何,匹配 和不匹配的位置分别由(E)SI 和(E)DI 寄存器指示。

清标志寄存器EFLAGS 中的方向标志,使cmps 指令按正 向进行比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13.5 执行用户程序

使段寄存器FS 指向头部段,因为 后面要调用内核过程,而这些过程都要求使用DS,所以要把DS 解放出 来。

14章,任务和特权级保护

视频24.1

特权指令

首先,当一个程序老老实实地访问只属于它自己的段时,基本的段 保护机制是很有效的。但是,一个失控的程序,或者一个恶意的程序, 依然可以通过追踪和修改􏰁述符表来达到它们访问任何内存位置的目 的。比如说,如果用户程序知道GDT 的位置,它可以通过向段寄存器加 载操作系统的数据段描述符,或者在GDT 中增加一个指向操作系统数据区的描述符,来修改只属于操作系统的私有数据。对于处理器那种和3 岁 小孩相仿的智力,所有这一切都是合法的。

14.1任务的隔离和特权级保护

使用mov cr0,eax设置PE位后,处理器自动处理0特权级,或者说处理器正在处于一个0特权级的代码段。
但是这个特权级无法使用cs来指示,因为依然保存着实模式下的逻辑端地址,而不是端选择子。
使用jmp 0x0010:flush刷新后cs才会刷新,才会指向当前特权级。
引入特权级后,jmp后CPL=DPL,当前CPL=0,代码段DPL原则上也必须是0.
任何时候都不允许从低特权级转移到高特权级代码段。

程序每运行一次就生成了一次task。

一直以来,我们把所有的段􏰁述符都放在GDT 中,而不管它属于内 核还是用户程序。如图14-1 所示,为了有效地在任务之间实施隔离,处 理器建议每个任务都应当具有自己的􏰁述符表,称为局部􏰁述符表 LDT(Local Descriptor Table),并且把专属于自己的那些段放到LDT 中。

为了追踪和访问这些LDT,处理器使用了局部描述符表寄存器(LDT Register:LDTR)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在一个多任务的系统中,会有很多任务在轮流执行,正在执行中的 那个任务,称为当前任务(Current Task)。因为LDTR 寄存器只有一 个,所以,它只用于指向当前任务的LDT。每当发生任务切换时,LDTR 的内容被更新,以指向新任务的LDT。和GDTR 一样,LDTR 包含了32 位线性基地址字段和16 位段界限字段,以指示当前LDT 的位置和大小。
段选择子的位2 是表指示器(Table Indicator:TI),若TI=0,表示从GDT 中加载描述符;TI=1,表示从 当前任务的LDT 中加载描述符。

为了保存任务的状态,并在下次重新执行时恢复它们,
每个任务都应当用一个额外的内存区域保存相关信息,这叫做任务状态段(Task State Segment:TSS)。
如图14-2 所示,任务状态段TSS 具有固定的 格式,最小尺寸是104 字节, 图中所标注的偏移量是十进制的。处理器固件能够识别TSS 中的每个元素,并在任务切换的时候读取其中的信 息,具体的细节将在后面讲述。

和LDT 一样,处理器用TR 寄存器来指向当前任务的TSS。和 GDTR、LDTR 一样,TR 寄存器在处理器中也只有一个。当任务切换发 生的时候,TR 寄存器的内容也会跟着指向新任务的TSS。这个过程是这 样的:首先,处理器将当前任务的现场信息保存到由TR 寄存器指向的 TSS;然后,再使TR 寄存器指向新任务的TSS,并从新任务的TSS 中恢 复现场。
为什么这个寄存器叫TR,而不是TSSR。原因很简 单,TSS 是一个任务存在的标志,用于区别一个任务和其他任务。所 以,这个寄存器叫做任务寄存器(Task Register:TR)。

14.1.2 全局空间和局部空间

每个任务实际上包括两个部分:全局部 分和私有部分。全局部分是所有任务共有的,含有操作系统的软件和库 程序,以及可以调用的系统服务和数据;私有部分则是每个任务各自的 数据和代码,与任务所要解决的具体问题有关,彼此并不相同。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
任务实际上是在内存中运行的,所以,所谓的全局部分和私有部 分,其实是地址空间的划分,即全局地址空间和局部地址空间,简称全 局空间和局部空间。
地址空间的访问是依靠分段机制来进行的。具体地说,需要先在􏰁 述符表中定义各个段的􏰁述符,然后再通过􏰁述符来访问它们。因此, 全局地址空间是用全局􏰁述符表(GDT)来指定的,而局部地址空间则 是由每个任务私有的局部􏰁述符表(LDT)来定义的。

14.1.3 特权级保护概述

中断发生时需要操作系统提供中断处理过程,所以每个任务必须包括两个部分,全局部分和私有部分。
任务是在内存中运行的,所以对应就是全局和局部地址空间。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在实模式下,段寄存器存放的是段地址;而在保护模式下,段寄存 器存放的是段选择子,段地址则位于􏰁述符高速缓存器中。当处理器正在一个代码段中取指令和执行指令时,那个代码段的特权级叫做当前特 权级(Current Privilege Level,CPL)。正在执行的这个代码段,其选 择子位于段寄存器CS 中,其最低两位就是当前特权级的数值。
普通的应用程序则工作在特权级别3 上。没有人愿意将自己的 程序放在特权级3 上,但是,只要你在某个操作系统上面写程序,这就由 不得你。应用程序编写时,不需要考虑GDT、LDT、分段、􏰁述符这些 东西,它们是在程序加载时,由操作系统负责创建的,应用程序的编写 者只负责具体的功能就可以了。应用程序的加载和开始执行,也是由操 作系统所主导的,而操作系统一定会将它放在特权级3 上。当应用程序开 始执行时,当前特权级CPL 自然就会是3。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

任务是由操作系统加载和创建的,与任务相关的信息都在它自己的 任务状态段(TSS)中,其中就包括一个EFLAGS 寄存器的副本,用于 指示与当前任务相关的机器状态,比如它自己的I/O特权级IOPL。在多任 务系统中,随着任务的切换,前一个任务的所有状态被保存到它自己的 TSS中,新任务的各种状态从其TSS 中恢复,包括EFLAGS 寄存器的 值。

。一般来说,控制转移只允许发生 在两个特权级相同的代码段之间。如果当前特权级为2,那么,它可以转移到另一个DPL 为2 的代码段接着执行,但不允许转移到DPL 为0、1 和 3 的代码段执行。不过,为了让特权级低的应用程序可以调用特权级高的 操作系统例程,处理器也提供了相应的解决办法。

方法一 将高特权级的代码段定义为依从的

第一种方法是将高特权级的代码段定义为依从的。回到第11 章,在 那一章里,表11-1 给出了段􏰁述符的TYPE 字段。代码段描述符的TYPE 字段有C 位,如果C=0,这样的代码段只能供同特权级的程序使用;否 则,如果C=1,则这样的代码段称为依从的代码段,可以从特权级比它 低的程序调用并进入。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是,即使是将控制转移到依从的代码段,也是有条件的,要求当前特权级CPL 必须低于,或者和目标代码段描述符的DPL 相同。即,在 数值上,

数据段
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

举例来说,如果一个依从的代码段,其􏰁述符的DPL 为1,则只有特 权级别为1、2、3 的程序可以调用,而特权级为0 的程序则不能。在任何 时候,都不允许将控制从较高的特权级转移到较低的特权级。
依从的代码段不是在它的DPL特权级上运行,而是在调用程序的特权级上运行。就是说,当控制转移到依从的代码段上执行时,不改变当前特权级CPL,段寄存器CS的CPL 字段不发生变化,被调用过程的特权级依从于调用者的特权级,这就是为什么它被称为“依从的”代码段。

方法二 门

除了依从的代码段,另一种在特权级之间转移控制的方法是使用 门。门(Gate)是另一种形式的描述符,称为描述符,简称门。和段描述符不同,段描述符用于􏰁述内存段,门描述符则用于描述可执行的代码,比如一段程序、一个过程(例程)或者一个任务
事际上,根据不同的用途,门的类型有好几种。不同特权级之间的过程调用可以使用调用门;中断门/陷阱门是作为中断处理过程使用的; 任务门对应着单个的任务,用来执行任务切换。在本章里,我们重点介绍的是调用门(Call Gate)。
所有描述符都是64 位的,调用门描述符也不例外。在调用描述符中,定义了目标过程(例程)所在代码段的选择子,以及段内偏移。要想通过调用门进行控制转移,可以使用jmp far 或者call far 指令,并把调 用描述符的选择子作为操作数。
使用jmp far 指令,可以将控制通过门转移到比当前特权级高的代码段,但不改变当前特权级别。但是,如果使用call far指令,则当前特权 级会上升到目标代码段的特权级别。也就是说,处理器是在目标代码段的特权级上执行的。但是,除了从高特权级别的例程(通常是操作系统 例程)返回外,不允许从特权级高的代码段将控制转移到特权级低的代码段,因为操作系统不会引用可靠性比自己低的代码。

特权级变了后调用栈必须切换特权级,还要复制参数,所以调用门必须记录参数的数量。


注意代码中的粗体部分,对照一下段描述符的格式,你会发现,这些段描述符的DPL都是0。也就是说,我们将这些段的特权级定为最高级别。

Cs

特权级保护机制只在保护模式下才能启用,而进入保护模式的方法是设置CR0 寄存器的PE位。而且,处理器建议,在进入保护模式后,执行的第一条指令应当是跳转或者过程调用指令,以清空流水线和乱序执行的结果,并串行化处理器,就像这样:
jump dword 0x0010:flush

请求特权级

RPL 的意思是请求特权级(Requested Privilege Level)。我们知道,要将控制从一个代码段转移到另一个代码段,通常是使用jmp 和call 指令,并在指令中提供目标代码段的选择子,以及段内偏移量(入口 点)。而为了访问内存中的数据,也必须先将段选择子加载到段寄存器 DS、ES、FS 或者GS 中。不管是实施控制转移,还是访问数据段,这都可以看成是一个请求,请求者提供一个段选择子,请求访问指定的段。从这个意义上来说,RPL也就是指请求者的特权级别(Requestor’s Privilege Level)。
段选择子实际上由三部分组成,分别是􏰁 述符的索引号、表指示器TI 和RPL 字段。在以上指令中,段选择子 0x0010 的TI 位是0,意味着目标代码段的􏰁述符在GDT 中。该选择子索 引字段的值是2,指向(GDT 中的)2 号􏰁述符。

在绝大多数时候,请求者都是当前程序自己,因此,CPL=RPL。 要判断请求者是谁,最简单的方法就是看提供了选择子。以下是两个典型的例子:

但是,在一些并不多见的情况下,RPL 和CPL 并不相同。如图14-6 所示,特权级为3 的应用程序希望从硬盘读一个扇区,并传送到自己的数据段,因此,数据段描述符的DPL 同样会是3。

由于I/O 特权级的限制,应用程序无法自己访问硬盘。好在位于0 特权级的操作系统提供了相应的例程,但必须通过调用门才能使用,因为特权级间的控制转移必须通过门。假设,通过调用门使用操作系统例程时,必须传入3个参数,分别是CX 寄存器中的数据段选择子、EBX 寄存 器中的段内偏移,以及EAX 中的逻辑扇区号。

在执行这条指令时,CX 寄存器中的段选择子,其RPL字段的值是3,当前特权级CPL已经变成0,因为通过调用门实施控制转移可以改变当前特权级。显然,请求者并非当前程序,而是特权级为3的应用程序, RPL和CPL并不相同。

如图14-7 所示,人类的可恶之处是无孔不入,总爱钻空子。想象一下,应用程序的编写者通过钻研,知道了操作系统数据段的选择子,而且希望用这个选择子访问操作系统的数据段。当然,他不可能在应用程 序里访问操作系统数据段,因为那个数据段的DPL 为0,而应用程序工作 时的当前特权级为3,处理器会很机警地把来访者拒之门外。


但是,他可以借助于调用门。调用门工作在目标代码段的特权级 上,一旦处理器的执行流离开应用程序,通过调用门进入操作系统例程时,当前特权级从3变为0。当那个不怀好意的程序将一个指向操作系统 数据段的选择子通过CX 寄存器作为参数传入调用门时,因为当前特权级 已经从3 变为0,可以从硬盘读出数据,并且允许向操作系统数据段写入扇区数据,他得逞了!

看得出来,单纯依靠处理器硬件无法解决这个难题,但它可以在原来的基础上多增加一种检查机制,并把如何能够通过这种检查的自由裁量权交给软件(的编写者)。
引入请求特权级(RPL)的原因是处理器在遇到一条将选择子传送到段寄存器的指令时,无法区分真正的请求者是谁。但是,引入RPL 本身并不能完全解决这个问题,这只是处理器和操作系统之间的一种协议,处理器负责检查请求特权级RPL,判断它是否有权访问,但前提是提供了正确的RPL;内核或者操作系统负责鉴别请求者的身份,并有义务保证RPL的值和它的请求者身份相符,因为这是处理器无能为力的。

操作系统的编写者很清楚段选择子的来源,即,真正的请求者是 谁。当它自己读写一个段时,这没有什么好说的;当它提供一个服务例 程时,3特权级别的用户程序给出的选择子在哪里,也是由它定的,它也 知道。在这种情况下,它所要做的,就是将该选择子的RPL 字段设置为 请求者的特权级(可以使用arpl 指令,将在本章的后面介绍)。剩下的工作就看处理器了。每当处理器执行一个将段选择子传送到段寄存器 (DS、ES、FS、GS)的指令,比如:
mov ds,cx 时,会检查以下两个条件是否都能满足。
● 当前特权级CPL 高于或者和数据段描述符的DPL 相同。即,在数值上,CPL≤数据段描述符的DPL;
●请求特权级RPL 高于或者和数据段描述符的DPL 相同。即,在数值上,RPL≤数据段描述符的DPL。
如果以上两个条件不能同时成立,处理器就会阻止这种操作,并引发异常中断。

按照Intel 公司的说法,引入RPL 的意图是“确保特权代码不会代替应 用程序访问一个段,除非应用程序自己拥有访问那个段的权限”。多数读 者都只在字面上理解这句话的意思,而没有意识到,这句话只是如实地描述了处理器自己的工作,并没有保证它可以鉴别RPL 的有效性。

  • 首先,将控制直接转移到非依从的代码段,要求当前特权级CPL 和请求特权级RPL 都等于目标代码段􏰁述符的DPL。即,在数值上,

  • 其次,要将控制直接转移到依从的代码段,要求当前特权级CPL 和 请求特权级RPL 都低于,或者和目标代码段􏰁述符的DPL 相同。即,在 数值上

  • 第三,高特权级别的程序可以访问低特权级别的数据段,但低特权 级别的程序不能访问高特权级别的数据段。访问数据段之前,肯定要对 段寄存器DS、ES、FS 和GS 进行修改,比如
    Mov fs,ax

    1. 最后,处理器要求,在任何时候,栈段的特权级别必须和当前特权 级CPL 相同。因此,随着程序的执行,要对段寄存器SS 的内容进行修改 时,必须进行特权级检查。以下就是一个修改段寄存器SS 的例子:

在对段寄存器SS 进行修改时,要求当前特权级CPL 和请求特权级 RPL 必须等于目标栈段􏰁述符的DPL。即,在数值上,

0 特权级是最高的特权级别,当一个系统的各个部分都位于0 特权级 时,各种特权级检查总能够获得通过,就像这种检查和检验并不存在一 样。所以,处理器的设计者建议,如果不需要使用特权机制的话,可以 将所有程序的特权级别都设置为0,就像我们一直所做的那样。

14.3 内核程序的初始化

image-20231014130452980

image-20231014130439895

image-20231014130412296

image-20231014130358099

image-20231014130344658

第15章 任务切换

image-20231014130316569
从80286 开始的处理器是面向多任务系统而设计的。在一个多任务 的环境中,可以同时存在多个任务,每个任务都有各自的局部􏰁述符表 (LDT)和任务状态段(TSS)。在局部􏰁述符表中存放着专属于任务局 部空间的段的􏰁述符。可以在多个任务之间切换,使它们轮流执行,从 一个任务切换到另一个任务时,具体的切换过程是由处理器固件负责进 行的。
所谓多任务系统,是指能够同时执行两个以上任务的系统。即使前 一个任务没有执行完,下一个任务也可以开始执行。但是,什么时候切 换到另一个任务,以及切换到哪一个任务执行,主要是操作系统的责 任,处理器只负责具体的切换过程,包括保护前一个任务的现场。
有两种基本的任务切换方式,

  • 一种是协同式的,从一个任务切换到 另一个任务,需要当前任务主动地请求暂时放弃执行权,或者在通过调 用门请求操作系统服务时,由操作系统“趁机”将控制转移到另一个任务。 这种方式依赖于每个任务的“自律”性,当一个任务失控时,其他任务可能 得不到执行的机会。
  • 另一种是抢占式的,在这种方式下,可以安装一个定时器中断,并在中断服务程序中实施任务切换。硬件中断信号总会定时出现,不管处理器当时在做什么,中断都会适时地发生,而任务切换也就能够顺利进行。在这种情况下,每个任务都能获得平等的执行机会。而且,即使一 个任务失控,也不会导致其他任务没有机会执行。

抢占式多任务将在第17 章讲解,本章先介绍多任务任务切换的一般 工作原理,掌握任务切换的几种方法,以及它们各自的特点。

15.2 任务切换前的设置


在上一章中,我们已经创建过一个任务,那个任 务的特权级别是3,即最低的特权级别。一开始,处理器是在任务的全局 空间执行的,当前特权级别是0,然后,我们通过一个虚假的调用门返 回,使处理器回到任务的局部空间执行,当前特权级别降为3。
,操作系统 除了为每一个任务􏰀供服务外,也会有一个作为任务而独立存在的部 分,而且是0 特权级别的任务,以完成一些管理和控制功能,比如􏰀供一 个界面和用户进行交互。

本章同样没有主引导程序,还要使用第13 章的主引导程序,内核部 分有一些改动,增加了和任务切换有关的代码。

任务状态段(TSS)是一个任务存在的标志,没有它,就无法执行 任务切换,因为任务切换时需要保存旧任务的各种状态数据。第909~ 911 行用于申请创建TSS 所需的内存。为了追踪程序管理器的TSS,需 要保存它的基地址和选择子,保存的位置是内核数据段。第431 行,声 明并初始化了6 字节的空间,前32 位用于保存TSS 的基地址,后16 位则 是它的选择子。

15.3 任务切换的方法

第一种任务切换的方法是借助于中断,这也是现代抢占式多任务的基础。原因很简单,只要中断没有被屏蔽,它就能随时发生。特别是定时器中断,能够以准确的时间间隔发生,可以用来强制实施任务切换。 毕竟,没有哪个任务愿意交出处理器控制权,也没有哪个任务能精确地把握交出控制权的时机。

我们知道,在实模式下,内存最低地址端的1KB 是中断向量表,保 存着256 个中断处理过程的段地址和偏移地址。当中断发生时,处理器 把中断号乘以4,作为表内索引号访问中断向量表,从相应的位置取出中 断处理过程的段地址和偏移地址,并转移到那里执行。

在保护模式下,中断向量表不再使用,取而代之的,是中断描述符表。不要害怕,它和GDT、LDT 是一样的,用于保存􏰁述符。唯一不同 的地方是,它保存的是门描述符,包括中断门、陷阱门和任务门。如果 你觉得这些术语太过于陌生,那就回忆一下调用门,这些门和调用门是非常类似的。当中断发生时,处理器用中断号乘以8(因为每个描述符占 8 字节),作为索引访问中断描述符表,取出门描述符。门描述符中有中 断处理过程的代码段选择子和段内偏移量,这和调用门是一样的。接着,转移到相应的位置去执行。
一般的中断处理可以使用中断门和陷阱门。回忆一下调用门的工作原理,它只是从任务的局部空间转移到更高特权级的全局空间去执行,本质上是一种任务内的控制转移行为。与此相同,中断门和陷阱门允许在任务内实施中断处理,转到全局空间去执行一些系统级的管理工作,本质上,也是任务内的控制转移行为。
但是,在中断发生时,如果该中断号对应的门是任务门,那么,性质就截然不同了,必须进行任务切换。即,要中断当前任务的执行,保护当前任务的现场,并转换到另一个任务去执行。
如图15-2 所示,这是任务门(Task-Gate)􏰁述符的格式。从图中可 见,相对于其他各种􏰁述符,任务门􏰁述符中的多数区域没有使用,所以显得特别简单。
任务门􏰁述符中的主要成份是任务的TSS 选择子。任务门用于在中断发生时执行任务切换,而执行任务切换时必须找到新任务的任务状态 段(TSS)。所以,任务门应当指向任务的TSS。为了指向任务的TSS, 只需要在任务门描述符中给出任务的TSS 选择子就可以了。
任务门􏰁述符中的P 位指示该门是否有效,当P 位为“0”时,不允许 通过此门实施任务切换;DPL 是任务门描述符的特权级,但是对因中断 而发起的任务切换不起作用,处理器不按特权级施加任何保护。但是,这并不意味着DPL 字段没有用处,当以非中断的方式通过任务门实施任务切换时,它就有用了,关于这一点,你马上就会看到。


这样,当中断发生时,处理器用中断号乘以8 作为索引访问中断􏰁述 符表。当它发现这是一个任务门(􏰁述符)时,就知道应当发起任务切 换。于是,它取出任务门􏰁述符;再从任务门􏰁述符中取出新任务的 TSS 选择子;接着,再用TSS 选择子访问GDT,取出新任务的TSS 􏰁述 符。在转到新任务执行前,处理器要先把当前任务的状态保存起来。当 前任务的TSS 是由任务寄存器TR 的当前内容指向的,所以,处理器把每 个寄存器的“快照”保存到由TR 指向的TSS 中。然后,处理器访问新任务 的TSS,从中恢复各个寄存器的内容,包括通用寄存器、标志寄存器 EFLAGS、段寄存器、指令指针寄存器EIP、栈指针寄存器ESP,以及局 部􏰁述符表寄存器(LDTR)等。最终,任务寄存器TR 指向新任务的 TSS,而处理器旋即开始执行新的任务。一旦新任务开始执行,处理器 固件会自动将其TSS 􏰁述符的B 位置“1”,表示该任务的状态为忙。

当中断发生时,可以执行常规的中断处理过程,也可以进行任务切 换。尽管性质不同,但它们都要使用iret 指令返回。前者是返回到同一任 务内的不同代码段;后者是返回到被中断的那个任务。问题是,处理器 如何区分这两种截然不同的返回类型呢?
如图15-3 所示,32 位处理器的EFLAGS 有NT 位(位14),意思是 嵌套任务标志(Nested Task Flag)。每个任务的TSS 中都有一个任务 链接域(指向前一个任务的指针,参见上一章TSS的结构),可以填写 为前一个任务的TSS 􏰁述符选择子。如果当前任务EFLAGS 寄存器的NT 位是“1”,则表示当前正在执行的任务嵌套于其他任务内,并且能够通过 TSS 任务链接域的指针返回到前一个任务。

图15-3 标志寄存器EFLAGS 的NT 位
因中断而引发任务切换时,取决于当前任务(旧任务)是否嵌套于 其他任务内,其EFLAGS寄存器的NT 位可能是“0”,也可能是“1”。不过 这无关紧要,因为处理器不会改变它,而是和其他寄存器一道,写入 TSS 中保护起来。另外,当前任务(旧任务)肯定处于“忙”的状态,其 TSS 􏰁述符的B 位一定是“1”,在任务切换后同样保持不变。
对新任务的处理是,要把老任务的TSS 选择子填写到新任务TSS 中 的任务链接域,同时,将新任务EFLAGS 寄存器的NT 位置“1”,以允许 返回(转换)到前一个任务(老任务)继续执行。同时,还要把新任务 TSS 􏰁述符的B 位置“1”(忙)。
可以使用iret 指令从当前任务返回(转换)到前一个任务,前􏰀是当 前任务EFLAGS 寄存器的NT 位必须是“1”。无论任何时候处理器碰到iret 指令,它都要检查NT 位,如果此位是0,表明是一般的中断过程,按一 般的中断返回处理,即,中断返回是任务内的(中断处理过程虽然属于 操作系统,但属于任务的全局空间);如果此位是1,则表明当前任务之 所以能够正在执行,是因为中断了别的任务。因此,应当返回原先被中 断的任务继续执行。此时,由处理器固件把当前任务EFLAGS 寄存器的 NT 位改成“0”,并把TSS 􏰁述符的B 位改成“0”(非忙)。在保存了当前 任务的状态之后,接着,用新任务(被中断的任务)的TSS 恢复现场。

除了因中断引发的任务切换之外,还可以用远过程调用指令CALL, 或者远跳转指令JMP 直接发起任务切换。在这两种情况下,CALL 和 JMP 指令的操作数是任务的TSS 􏰁述符选择子或任务门。以下是两个例 子:

当处理器执行这两条指令时,首先用指令中给出的􏰁述符选择子访 问GDT,分析它的􏰁述符类型。如果是一般的代码段􏰁述符,就按普通 的段间转移规则执行;如果是调用门,按调用门的规则执行;如果是 TSS 􏰁述符,或者任务门,则执行任务切换。此时,指令中给出的32 位 偏移量被忽略,原因是执行任务切换时,所有处理器的状态都可以从 TSS 中获得。注意,任务门􏰁述符可以安装在中断􏰁述符表中,也可以 安装在全局􏰁述符表(GDT)或者局部􏰁述符表(LDT)中。
如果是用于发起任务切换,call 指令和jmp 指令也有不同之处。使用 call 指令发起的任务切换类似于因中断发起的任务切换。这就是说,由 call 指令发起的任务切换是嵌套的,当前任务(旧任务)TSS 􏰁述符的B 位保持原来的“1”不变,EFLAGS 寄存器的NT 位也不发生变化;新任务 TSS 􏰁述符的B 位置“1”,EFLAGS 寄存器的NT 位也置“1”,表示此任务 嵌套于其他任务中。同时,TSS 任务链接域的内容改为旧任务的TSS 􏰁 述符选择子。
如图15-4 所示,假设任务1 是整个系统中的第一个任务。当任务1 开始执行时,其TSS 􏰁述符的B 位是“1”,EFLAGS 寄存器的NT 位是 “0”,不嵌套于其他任务。
当从任务1 转换到任务2 后,任务1 仍然为“忙”,EFLAGS 寄存器的 NT 位不变(在其TSS中);任务2 也变为“忙”,EFLAGS 寄存器的NT 位变为“1”,表示嵌套于任务1 中。同时,任务1 的TSS 􏰁述符选择子也 被复制到任务2 的TSS 中(任务链接域)。

最后是从任务2 转换到任务3 执行。和从前一样,任务2 保持“忙”的 状态,EFLAGS 寄存器的NT 不变(在其TSS 中);任务3 成为当前任 务,其TSS 􏰁述符的B 位变成“1”(忙),EFLAGS 寄存器的NT 位也变 成“1”,同时,其TSS 的任务链接域指向任务2。
用CALL 指令发起的任务切换,可以通过iret 指令返回到前一个任 务。此时,旧任务TSS 􏰁述符的B 位,以及EFLAGS 寄存器的NT 位都 恢复到“0”。
和call 指令不同,使用jmp 指令发起的任务切换,不会形成任务之间 的嵌套关系。执行任务切换时,当前任务(旧任务)TSS 􏰁述符的B 位 清零,变为非忙状态,EFLAGS 寄存器的NT 位不变;新任务TSS 􏰁述 符的B 位置“1”,进入忙的状态,EFLAGS 寄存器的NT 位保持从TSS 中 加载时的状态不变。
任务是不可重入的。
任务不可重入的本质是,执行任务切换时,新任务的状态不能为 忙。这里有两个典型的情形:

第一种情形,执行任务切换时,新任务不能是当前任务自己。试想 一下,如果允许这种情况发生,处理器该如何执行现场的保护和恢复操 作?
第二种情形,如图15-4 所示,不允许使用CALL 指令从任务3 切换 到任务2 和任务1 上。如果不禁止这种情况的话,任务之间的嵌套关系将 会因为TSS 任务链接域的破坏而错乱。
处理器是通过TSS 􏰁述符的B 位来检测重入的。因中断、iret、call 和jmp 指令发起任务切换时,处理器固件会检测新任务TSS 􏰁述符的B 位,如果为“1”,则不允许执行这样的切换。

15.4 用call jmp iret指令发起任务切换的实例

15.5 处理器在实施任务切换时的操作

第16章 分页机制和动态页面分配

每个段􏰁述符有A 位,每当访问一个段 时,处理器会将其置位。A 位的清零由操作系统定时进行,它可以借此 机会统计段的访问频度

分页功能从总体上说,是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题。尽管操作系统也可 以用软件来实施固定长度的内存分配,但太过于复杂,由处理器固件来 做这件事,可以使速度和效率最大化。

16.1 分页机制概述

16.1.1简单分页模型

如图16-3 所示,内存的分配涉及段空间的分配和页分配。请仔细看 这幅图,左边是虚幻的,或者说虚拟的4GB 内存空间,称为虚拟内存; 右边呢,是实实在在的内存,被分成1048576 个 4KB 页面。

在分段之后,操作系统的任务是把段拆开,并分别映射到物理页。 注意,段必须是连续的,但不要求所分配的页都是连续的、挨在一起 的。事实上,在开机之后,会运行不同的程序,这都要分配页。然后, 有些程序关闭了,页面要回收。几个回合下来,空闲的页零零散散地分 布在物理内存中,一般不会是连续的。分配页面时,操作系统会搜索那 些空闲的页,并分配给程序使用,所分配页面的总长度要大于等于段长 度。

从段部件输出的是线性地址,或者叫虚拟地址。为了根据线性地址 找到页的物理地址,操作系统必须维护一张表,把线性地址转换成物理 地址,这是一个反过程。


每个任务都有自 己的页映射表,
尽管有很多任务,而且每个任务都有自己的4GB 虚拟内存空间,但 是,很重要的是,在整个系统中,物理页面是统一调配的

另一个会被质疑的问题是,每个任务都有4GB 虚拟内存空间,而物 理内存只有一个,最大也才4GB,根本不够分的。事实上,的确不够分 配。但是,操作系统可以将暂时不用的页退避到磁盘,调入马上就要使 用的页,通过这种手段来实现分页内存管理。这就是为什么内存容量较 小时,程序越来越慢,硬盘工作指示灯不停地闪烁的原因。
以上,就是基本的段页式内存管理机制。

16.1.2 页目录、页表和页

为了完成从虚拟地址(线性地址)到物理地址的转换, 操作系统应当为每个任务准备一张页映射表。因为任务的虚拟地址空间 为4GB,可以分出1048576 个页,所以,映射表需要1048576 个表项, 用于存放页的物理地址。又因为每个表项占4 字节,所以,映射表的总大 小为4MB。

当然,你可能会建议先划出一小块内存给它,然后,根据需要再动 态扩展。的确,这是可行的。但是,因为一个特殊的原因,这张表在实 际使用的时候,它的前半部分和后半部分会被同时用到。具体是什么原 因,马上就要讲到,也正是因为这个尚未说明的原因,这张表从一开始 就必须完全定义,而且不可避免地要占用4MB 内存空间。为了解决这个 问题,同时又不会浪费宝贵的内存空间,处理器设计了层次化的分页结 构。

如图16-6 所示,在将1048576 个页归拢到1024 个页表之后,接 着,再用一个表来指向1024个页表,这就是页目录表(Page Directory Table,PDT),和页表一样,页目录项的长度为4 字节,填写的是页表 的物理地址,共指向1024 个表页,所以页目录表的大小是4KB,正好是 一个标准页的长度。

这样的层次化分页结构是每个任务都拥有的,或者说,每个任务都有自己的页目录和页表。如图16-7 所示,在处理器内部,有一个控制寄存器CR3,存放着当前任务页目录的物理地址,故又叫做页目录基址寄存器(Page Directory Base Register,PDBR)。
每个任务都有自己的任务状态段(TSS),它是任务的标志性结 构,存放了和任务相关的各种数据,其中就包括了CR3 寄存器域,存放 了任务自己的页目录物理地址。当任务切换时,处理器切换到新任务开 始执行,而CR3 寄存器的内容也被更新,以指向新任务的页目录位置。 相应地,页目录又指向一个个的页表,这就使得每个任务都只在自己的 地址空间内运行。
从图16-7 中还可以看出,页目录和页表也是普通的页,混迹于全部 的物理页中。它们和普通页的不同之处仅仅在于功能不一样。当任务撤 销之后,它们和任务所占用的普通页一样会被回收,并分配给其他任 务。

16.1.3 地址变换的具体过程

第17章 中断和异常的处理与抢占式多任务

17.1 中断和异常

1 Interrupt
中断包括硬件中断和软中断。
硬件中断是由外围硬件设备发出的中断信号引发的,以请求处理器 􏰀供服务。当I/O 接口发出中断请求时,会被像8259A 和I/O APIC 这样 的中断控制器收集,并发送到处理器。硬件中断完全是随机产生的,与 处理器的执行并不同步。当中断发生时,处理器要先执行完当前的指 令,然后才对中断进行处理。
软中断是由int n 指令引发的中断处理,n 是中断号或者叫类型码。
2 Exception
异常就是我们在介绍16 位汇编语言时所说的内部中断。它们是处理 器内部产生的中断,表示在指令执行的过程中遇到了错误的状况。当处 理器执行一条非法指令,或者因条件不具备,指令不能正常执行时,将 引发这种类型的中断。以上所列的情况都是异常情况,所以内部中断又 叫异常或者异常中断。比如,在执行除法指令div/idiv 时,遇到了被0 除 的情况(除数是0);再比如,使用jmp 指令发起任务切换时,指令的操 作数不是一个有效的TSS 􏰁述符选择子。

根据异常情况的性质和严重性,异常又分为以下三种,并分别实施 不同的处理。
●故障(Faults)。故障通常是可以纠正的,比如,当处理器执行一 个访问内存的指令时,发现那个段或者页不在内存中(P=0),此时, 可以在异常处理程序中予以纠正(分配内存,或者执行磁盘的换入换出 操作),返回时,程序可以重新启动并不失连续性。为了做到这一点, 当故障发生时,处理器把机器状态恢复到引起故障的那条指令之前的状 态,在进入异常处理程序时,压入栈中的返回地址(CS 和EIP 的内容) 是指向引起故障的那条指令的,而不像通常那样指向下一条指令。如此 一来,当中断返回时,将重新执行引起故障的那条指令,而且不再出错 (如果引起异常的情况已经妥善处置)。这意味着,异常并不总是意味 着坏消息,相反,很多时候,它是有益的,就像益虫。如果没有异常, 虚拟内存管理将无从谈起。
●陷阱(Traps)。陷阱中断通常在执行了截获陷阱条件的指令之后 立即产生,如果陷阱条件成立的话。陷阱通常用于调试目的,比如单步 中断指令int3 和溢出检测指令into。陷阱中断允许程序或者任务在从中断 处理过程返回之后继续进行而不失连续性。因此,当此异常发生时,在 转入异常处理程序之前,处理器在栈中压入陷阱截获指令的下一条指令 的地址。
●终止(Aborts)。终止标志着最严重的错误,诸如硬件错误、系统 表(GDT、LDT 等)中的数据不一致或者无效。这类异常总是无法精确 地报告引起错误的指令的位置,在这种错误发生时,程序或者任务都不 可能重新启动。一个比较典型的终止类异常是“双重故障”(中断号为 8),当发生一次异常后,处理器在转入该中断的处理程序时,又发生另 外的异常(如该中断处理程序所在的段不在内存中,或者栈溢出)。对 于中断处理程序来说,很难从栈中获得有关如何纠正此类错误的明确信 息,往往是发生极为重大的错误时才伴随着这种异常,所以再继续执行

引起此异常的程序或任务已相当困难,操作系统通常只能把该任务从系 统中抹去。
中断和异常发生时,处理器将挂起当前正在执行的过程或者任务, 然后执行中断和异常处理过程。返回时,处理器恢复程序或者任务的执 行,而且被打断的程序或任务的执行不失连续性,除非遇到一个终止类 型的异常。对于某些异常,处理器在转入异常处理程序之前,会在当前 栈中压入一个称为错误代码的数值,帮助程序进一步诊断异常产生的位 置和原因。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值