实模式+8086阶段

实模式内存布局

起始 结束 大小 用途
FFFF0 FFFFF 16B BIOS入口地址,此地址也属于BIOS代码,同样属于顶部的640KB字节。只是为了强调其入口地址才单独i特出来。此处16字节的内容时跳转指令jmp f000:e05b
F0000 FFFEF 64KB-16B 系统BIOS范围是F0000~FFFFF共640KB,为说明入口地址,将最上面的16字节从此处去掉,所以此处终止地址是0xFFFEF
C8000 EFFFF 160KB 映射硬件适配器的ROM或内存映射式I/O
C0000 C7FFF 32KB 显示适配器BIOS
B8000 BFFFF 32KB 用于文本模式显示适配器
B0000 B7FFF 32KB 用于黑白显示适配器
A0000 AFFFF 64KB 用于彩色显示适配器
9FC00 9FFFF 1KB EBDA(Extended BIOS Area)扩展BIOS数据区
07E00 9FBFF 622080B约608KB 可用区域
07C00 07DFF 512B MBR被BIOS加载到此处,共512字节
00500 07BFF 30464B约30KB 可用区域
00400 004FF 256B BIOS Data Area(BIOS数据区)
00000 003FF 1KB Interrupt Vector Table(中断向量表)
  • 逻辑地址和物理地址
    • 00000~FFFFF中最低位字节位0时,就可以作为段基地址
    • 物理地址=段基地址>>4 + 偏移地址(4个字节)
    • 每个段的大小最大位64K,即65536个字节(偏移地址4个字节表示最大值)
    • 实际物理地址最大值:10FFEF(段地址:FFFF0+偏移地址:FFFF),当受限与20根地址线(超过了20个bit),最大只能访问到FFFFF

寄存器

通用寄存器

  • AH&AL=AX(accumulator):累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据.
  • BH&BL=BX(base):基址寄存器,常用于地址索引,用来提供数据访问,使用bx提供偏移地址时,段寄存器默认使用(指定使用需要加段超越前缀)ds提供段地址(bp默认访问栈段,bx默认访问数据段)
  • CH&CL=CX(count):计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.
  • DH&DL=DX(data):数据寄存器,用来和外设之间进行数据传输
  • BP(Base Pointer)基址寄存器,用来提供访问栈段,使用bx提供偏移地址时,段寄存器默认使用ss提供段地址(bp默认访问栈段,bx默认访问数据段)
  • SP(Stack Pointer):堆栈指针,与SS配合使用,可指向目前的堆栈位置;ss+sp=栈地址)【压栈操作:高地址(栈底)->低地址(栈顶)(8086每次压栈和出栈都是16字节)】
  • SI(Source Index):源索引寄存器(或变址寄存器)(用法见movsb),si提供偏移地址时,段寄存器默认使用ds提供段地址
  • DI(Destination Index):目标索引寄存器(或变址寄存器)(用法见movsb),di提供偏移地址时,段寄存器默认使用ds提供段地址

指针寄存器

  • 指令指针IP(Instruction Pointer):指令指针IP是一个16位专用寄存器,它指向当前需要取出的指令字节,当BIU从内存中取出一个指令字节后,IP就自动加1,指向下一个指令字节。注意,IP指向的是指令地址的段内地址偏移量,又称偏移地址(Offset Address)或有效地址(EA,Effective Address)。

段寄存器

两个段寄存器之间不能直接传送数据,需要借助通用寄存器

  • CS: 代码段寄存器(Code Segment Register) ,其值为代码段的段值;
  • DS : 数据段寄存器(Data Segment Register) ,其值为数据段的段值;段内访问地址默认位ds寄存器(不指定的情况下)(段内地址位偏移地址)
  • ES: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值;
  • SS: 堆栈段寄存器(Stack Segment Register) ,其值为堆栈段的段值;

标志寄存器

  • FLAGS寄存器
    • bit0(CF):进位标志,当一个算术在结果最高位产生进位或者借位时,标志为1,否则为0
      • 若al = 1000 0000则 add al, al CF = 1
    • bit2(PF):奇偶标志,当算术操作结果在低8位中有偶数个“1”时,此标志位1,反之则为0
    • bit4(AF):辅助进位标志,当一个算术操作在结果的位3(bit3)产生进位或者借位时,此为1,反则0
    • bit6(ZF):零标志,用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0。在判断运算结果是否为0时,可使用此标志位。
    • bit7(SF):符号位,用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF也就反映运算结果的正负号。运算结果为正数时,SF的值为0,否则其值为1。
    • bit8(TF):跟踪标志。该标志可用于程序调试。TF标志没有专门的指令来设置或清楚。如果TF=1,则CPU处于单步执行指令的工作方式,此时每执行完一条指令,就显示CPU内各个寄存器的当前值及CPU将要执行的下一条指令。如果TF=0,则处于连续工作模式。
    • bit9(IF):中断标志,中断允许标志IF位用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。但不管该标志为何值,CPU都必须响应CPU外部的不可屏蔽中断所发出的中断请求,以及CPU内部产生的中断请求。当IF=1时,CPU可以响应CPU外部的可屏蔽中断发出的中断请求;当IF=0时,CPU不响应CPU外部的可屏蔽中断发出的中断请求。
    • bit10(DF):方向标志,用来决定在串操作指令执行时有关指针寄存器发生调整的方向。
    • bit11(OF):溢出标志,对任何一个算术,假定算术为有符号运算,结果超出目标位置所能容纳最大正数或者最小负数时,此为1,反则0

可以提供偏移地址的寄存器

  • 可用BX,SI,DI,BP,其他都为非法
    • mov [ax], dl非法指令
    • mov [dx], bl非法指令
  • 基址变址组合
    • bx+si, bx+di, bp+si, bp+di
    • 非法例子
      • bx+ax
      • ax+cs

文本模式颜色表(80 X 25)

属性:KRGB IRGB(前4为背景色,后4为前景色)

R G B 背景色(K=0不闪烁,K=1闪烁) 前景色I=0 前景色I=1
0 0 0
0 0 1 浅蓝
0 1 0 绿 绿 浅绿
0 1 1 浅青
1 0 0 浅红
1 0 1 品(洋)红 品(洋)红 浅品(洋)红
1 1 0
1 1 1 亮白

指令

指令 使用
$和$$ nasm特有
$:当前行地址
$$:当前所在段起始地址
div(无符号除法) 8位(寄存器或者8位操作数的内存地址)
除数在寄存器AX中
相除后:商在AL中,余数在AH中
示例:
  div bh
  div byte [0x2002]
16位(寄存器或者16位操作数的内存地址)
除数是32位的,低16位在AX中,高16位在DX中
相除后:商在AX中,余数在DX中
示例:
  div bx
  div word [0x2002]
32位(寄存器或者32位操作数的内存地址)
除数是64位的,低32位在EAX中,高32位在EDX中
相除后:商在EAX中,余数在EDX中示例:
  div ebx
  div dword [0x2002]
64位(寄存器或者64位操作数的内存地址)
除数是128位的,低64位在EAX中,高64位在RDX中
相除后:商在RAX中,余数在RDX中
示例:
  div rbx
  div dword [0x2002]
idiv(有符号除法) 寄存器规则同div
正负:如果被除数和除数符号相同,商为正数,否则商为负则为负数
余数的符号始终和被除数相同
mul(无符号乘法) 8位寄存器或者8位操作数的内存地址)
被乘数是8位的,在寄存器AL中
相乘后,乘积是16位的,在寄存器AX中
示例:
  mul bh
  mul byte [0x2002]
16位(寄存器或者16位操作数的内存地址)
被乘数是16位的,在寄存器AX中
相除后:乘积是32位的,低16位在寄存器AX中,高16位在寄存器DX中
示例:
  mul bx
  mul word [0x2002]
32位(寄存器或者32位操作数的内存地址)
被乘数是32位的,在寄存器AX中
相除后:乘积是64位的,低32位在寄存器EAX中,高32位在寄存器EDX中
示例:
  mul ebx
  mul dword [0x2002]
64位(寄存器或者64位操作数的内存地址)被乘数是64位的,在寄存器RAX中
相除后:乘积是128位的,低64位在寄存器RAX中,高64位在寄存器RDX中
示例:
  mul rbx
  mul dword [0x2002]
imul(无符号乘法) 类似mul
add, adc add:相加
adc:进位相加,受进位标志CF影响
or, xor or:或操作
xor:异或指令
可用于寄存器清`xor ax, ax,等价于mov ax 0,用异或操作的都是寄存器,速度会更快
neg, not neg:求补码(取反)
neg dx=> dx * -1
等同-2=>2, 3=>-3
not:逐位求反
shr,ror,shl,rol shr:右移
ror:循环右移
shl:左移
rol:循环左移
near,for near:绝对间近跳转,寻址范围为64K
jmp near table 近距离跳转
dec, inc dec:递减,i--
inc:递增,i++
rep,loop重复指令 rep次数由cx寄存器决定(为0则停止)
rep movsw(cx会自己递减)
loop机器码E2,8位相对偏移量,也就意味着,跳转位置不能太远,循环次数也是由cx确定,为0则中断循环(cx会自己递减)
movsb, movsw 数据传输指令,只会传送一次,一般辅助循环指令使用
使用规则:
DS:SI:原始数据串的段地址:偏移地址,si会自动改变值(根据方向递增或递减)
ES:DI:目标位置的段地址:偏移地址,di会自动改变值(根据方向递增或递减)
设置传送方向:cld,std
cld,std cld:FLAGS寄存器方向位(bit10)清零,此时传送方向,低地址向高地址传送
sld:FLAGS寄存器方向位(bit10)置一,此时传送方向,高地址向低地址传送
cli,sti cli:FLAGS寄存器方向位(bit9:IF)清零
sti:FLAGS寄存器方向位(bit9:IF)置一
SECTION,ALIGN,VSTART section给程序分段,align执行段的内存对齐方式,在同一个程序中,由多个段,理论每个段中的第一个数据地址是0,但是实际上是相对程序开头的地址,所以需要实现某个段的首个数据地址是0时,需要使用VSTART=0(0为指定起始地址,如果为2则每个段其实数据地址为2)
示例:
SECTION data1 ALIGN=16 VSTART=0
  mydata dw 0xFACE
SECTION data2 ALIGN=16 VSTART=0
  string db 'hello'
section code ALIGN=16 VSTART=0
  mov bx,mydata
  mov si,string
section.段名字.start 计算一个段相对整个程序的起始位置,相当于这个段位于程序开头的字节数,也可以是理解为相对程序开头的偏移量(示例见用户程序头部信息)
equ 等同于。常量的声明,类似于#define。常量的声明不会占用汇编地址
resb,resw,resd 在编译过程中保留指定大小的空间,不做初始化
  resb 256 == resw 128 == resd 64 == times 256 db 0
in,out 端口的输入输出不能直接使用mov指令,在intel中需要使用in,out;它们都不影响标志位
in:源操作数通常是dx(存放端口号),目标操作数根据数据大小使用al或ax,当端口号小于256时,源操作数可以使用立即数
out:目标操作数通常为dx或小于256的立即数,源操作数为al或ax(和in相反)
call指令 调用程序(16位相对近调用,所以跳转时cs不用压栈保存数据)。被调用的子程序可以ret或retf指令结尾,过程返回,以此返回主程序继续执行。ip指针寄存器指向下一条指令的偏移地址,调用前ip压栈,返回时出栈。
  call 标号或者目标处的汇编地址(16位相对近调用,前后两个字节的大小-32768到32767,可搭配ret)
  call 寄存器/偏移地址(段内,16位间接绝对近调用,可搭配ret)
  call cx:转移前先将ip压栈,然后用cx中的值填充ip,再进行跳转
  call [0x3000]:转移前将ip压栈,然后ds值左移4位加上偏移地址0x3000取到物理地址,在地址中取出一个字(两个字节),填充到ip,再进行跳转
  call [bx]:转移前将ip压栈,然后ds+bx得到物理地址,在地址中取出一个字(两个字节),填充到ip,再进行跳转
call 16位段地址:16位偏移地址(16位直接绝对远调用,可搭配reft)
  call 0x052e:0x003c:转移前将ip压栈,0x052e替换cs内容,0x003c替换ip内容
  call far 目标地址(16位间接绝对远调用,可搭配reft)
  call far [0x2000]:转移前将cs,ip压栈,然后ds值左移4位加上偏移地址0x2000取到物理地址,在地址中取出两个个字(四个字节),前一个字填充到ip,后一个字填充到cs,再进行跳转
  call far [bx]:转移前将cs,ip压栈,然后ds+bx得到物理地址,在地址中取出两个个字(四个字节),前一个字填充到ip,后一个字填充到cs,再进行跳转
ret,retf ret 用栈中的数据修改IP(近转移)
retf 用栈中数据修改cs:ip(远转移)
他们不一定要搭配call执行使用,也可搭配push使用,push先压缩段地址,在压缩偏移地址,然后调用retf,将从栈中pop出数据填充到cs和ip当中
iret 所有的中断程序结尾必须是以iret指令结尾
iret 所有的中断程序结尾必须是以iret指令结尾
test test 寄存器/地址, 寄存器/立即数
与and类似,不同于目标操作数的值不会被改变,但是FLAGS的值会改变
hlt 停止指令,使CPU进入低功耗状态,直到用中断唤醒
int, int3,into int:软中断 int 中断号
int3:陷阱中断(调式程序使用,断点的原理) int3 中断号
into:溢出中段(OF是1时发生into中断)
int 0x70:这里的0x70中断等同于实时时钟发出的0x70中断
int3 != int(space) 3
条件跳转 js:符号标志SF为1则转移,jns,符号标志为0则转移;
jz:零标志ZF为1则转移,jnz:零标志ZF为0则转移;
jo:溢出标志OF为1则转移,jno:溢出标志OF为0则转移;
jc:进位标志CF为1则转移,jnc:进位标志CF为0则转移;
jp:奇偶标志PF为1则转移,jnp:奇偶标志PF为0则转移;
jcxz:当CX寄存器内容为0时则转移;
cmp,比较两个数后,条件转移(大于等于小于等等)
  助记:
    a:above高于
    b:below低于
    e:equal相等
    n:not不
    g:greater大于
    l:小于
      例子:jnge:不大于等于
无条件转移(jmp) jmp 之后的指令如果跟了 ret,那就直接退出到上一个 call 对应指令的下一行;jmp跳转后会影响cs,ip的值,但不会影响ds,es的值
jmp short 标号/目标处的汇编地址(段内,相对短转移,前后一个字节的大小-128到127)
jmp near 标号/目标处的汇编地址(段内,相对近转移,前后两个字节的大小-32768到32767)
jmp 标号/目标处的汇编地址(编译器决定时short还是near)
jmp 寄存器/内存地址(段内,16位间接绝对近转移)
jmp 16位段地址:16位偏移地址 (段之间转移,16位直接绝对远转移)
示例:
  jmp 0x052e:0x003c:0x052e替换cs内容,0x003c替换ip内容
  jmp far 内存地址(16位间接绝对远转移)
示例:
  jmp far [0x2002]:2002~2003替换ip内容,2004~2005替换cs内容
  jmp far [bx]:ds+bx拿到物理地址中数据中的两个字,第一个字替换ip内容,第二个字替换cs内容

指令对flags寄存器的影响

  • cbw / cwde / cdqe / cwd / cdq/ cqo: 不影响任何标志位。
  • cld : DF=0,对CF、OF、ZF、SF、AF和PF的影响未定义。
  • std : DF=1,不影响其他标志位。
  • inc/dec: CF标志不受影响;对OF、SF、ZF、AF和PF的影响依计算结果而定。
  • add / sub:OF、SF、ZF、AF、CF和PF的状态依计算结果而定。
  • div / idiv:对CF、OF、SF、ZF、AF和PF的影响未定义。
  • mov / movs:这类指令不影响任何标志位。
  • neg:如果操作数为0,则CF=0,否则CF=1;对OF、SF、ZF、AF和PF的影响,计算结果而定。
  • xor:OF=0,CF=O;对SF、ZF和PF依计算结果而定;对AF的影响未定义。

寻址方式

寄存器寻址

操作数时由寄存器引起的

  • mov ax, cx
  • add bx, 0xf000(目标操作数)
  • inc dx

立即数寻址

操作数时由立即数直接指定的

  • add bx, 0xf000(源操作数)
  • mov dx, mydata

内存寻址

数据在内存当中(8086:段地址左移4位加上偏移地址形成20位的物理地)

段地址:由4个段寄存器之一提供(cs, ds,es,ss,默认ds),偏移地址:由指令来提供(内存寻址,实际即使寻找偏移地址)

  • 直接内存寻址:
    • mov ax, [0x5c0f]
    • add word [0x0230], 0x5000(目标操作数)
    • xor byte [es:mydata], 0x55(目标操作数)
  • 变址寻址:内存寻址的一种
    • 类似基址寻址,不同之处主要在于变址寻址是使用si和di索引寄存器
; 使用变址寻址的例子

mov [si],dx
add ax,[di]
xor word [si], 0x8000 ; si提供偏移地址时,默认使用ds提供段地址

mov [si+0x100], al; 8086允许变址寄存器和基址寄存器加上偏移量使用
and byte [di+mydata], 0x80 ; and 位相与

基址寻址

在8086中由基址寄存器bx,bp提供,基址地址也称有效地址(effective address)

	buffer dw 0x20, 0x100, 0x0f,0x300, 0xff00(buffer是标号)
; 以下使用直接寻址方式将所有数据加一

inc word [buffer]
inc word [buffer+2]
inc word [buffer+4]
inc word [buffer+6]
inc word [buffer+8]

; 以下使用基址寻址方式将所有数据加一
mov bx, buffer
mov cx, 5
lpinc:
inc word, [bx]
add bx, 2
loop ipinc

; 越过出栈操作,把栈当作普通段访问
mov ax, 0x5000
mov bx, 0x7000
mov cx, 0x8000
push ax
push bx
push cx

mov bx, sp ; bx指向栈底
mov dx, [ss:bx+2] ; bx+2:8086允许基址寄存器加一个偏移量,不会影响bx和sp的值,只是形成一个和
pop ax ; 出栈用哪个寄存器来接受没有限制
pop bx
pop cx ; 维持栈平衡(压多少,出多少)

; bp基址寄存器的使用
mov ax, 0x5000
mov bx, 0x7000
mov cx, 0x8000
push ax
push bx
push cx

mov bp, sp
mov dx, [bp+2] ; bp提供偏移,默认使用ss段寄存器提供段地址,所以可以省略段超越前缀ss:([ss:bp+2])
pop ax
pop bx
pop cx

基址变址寻址:

基址变址寻址中使用了bx时,默认使用ds提供段地址,访问数据段;使用了bp时,默认使用ss提供段地址,访问栈段(使用示例见示例代码基址变址寻址的使用项中)

[bx + si]
[bx + di]
[bx + si + 偏移量]
[bx + di + 偏移量]

[bp + si]
[bp + di]
[bp + si + 偏移量]
[bp + di + 偏移量]

mov ax, [bx + si + 0x03]

用户程序

  • 用户程序头部必须包含:程序的长度,入口点,段重定位表项数,段重定位表
  • 用户程序起始逻辑扇区号从100开始存放(协定)

设备交互

每个外部设备都有自己的ROM,里面有设备提供的调用例程代码。

ROM映射到C0000~E0000之间,bios把他们的中断程序写入到中断向量表中(IVT)。

外部设备接口(板卡)设计:

字节一 字节二 字节三
55 AA ROM中代码长度(以512为长度) 功能调用例程代码

硬盘(块设备)

  • 扇区时硬盘的基本单位,和cpu交互数据时是按一个扇区一个扇区的交互
  • 个人计算机上硬盘分配了8个端口0x1f0~0x1f7
7 6 5 4 3 2 1 0
固定值1 0:CHS模式;
1:LBA模式
固定值1 0:主硬盘;
1:从硬盘
逻辑扇区号位27 逻辑扇区号位26 逻辑扇区号位25 逻辑扇区号位24
7 6 5 4 3 2 1 0
BSY:为1则表示硬盘忙 DRQ:为1表明硬盘已准备好与主机交换数据 ERR:为1则表明前一个命令执行错误。具体原因可访问端口0x1f1
; LBA28读数据
;第一步:告诉硬盘要操作的扇区数
mov dx, 0x1f2
mov al, 0x01;需要操作一个扇区,如果这个数字是0,非常特殊,它代表要读写256个扇区
out dx, al

;第二步:设置起始LBA号
;扇区号=0000 00000000 00000000 00000010(28位)
mov dx, 0x1f3
mov al, 0x02
out dx, al ;LBA地址7~0
inc dx ;0x1f4
mov al, 0x00
out dx,al ;LBA地址15~8
inc dx ;0x1f5
out dx,al ;LBA地址23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA模式,主硬盘,LBA27~24
out dx,al

;第三步:告诉硬盘读命令
mov dx,0x1f7 ;0x1f7命令端口和断开端口
mov al, 0x20;读命令
out dx,al

;第四步:等待读写操作完成
mov dx,0x1f7
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已经准备好数据传输

;第五步:连续读出数据
;假定DS已指向存放扇区数据的段,BX里是段内偏移地址
mov cx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw

显卡

显卡提供两个端口 0x3d4(索引端口), 0x3d5(数据端口),显卡内部寄存器都有自己的编号,其中两个8位光标寄存器为0x0e(高8位),0x0f(低8位).

当我们把0x0f给索引端口时,数据端口会连接0x0f寄存器,我们就可以通过数据端口去读写数据。

put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es

;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al

mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数

cmp cl,0x0d ;回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor

.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,80
jmp .roll_screen

.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1
mov [es:bx],cl

;以下将光标位置推进一个字符
shr bx,1
add bx,1

.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor

push bx
mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840 ;清除屏幕最底一行
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls

pop bx
sub bx,80

.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al

pop es
pop ds
pop dx
pop cx
pop bx
pop ax

ret

8259芯片

含有:中断屏蔽寄存器,中断服务寄存器

  • 8259主片:

    • INC:与处理器的INTR相连
    • 引脚:0x08~0x0F
    • 端口:0x20,0x21
    • 0x08(bit0):与系统定时器/计数器相连
  • 8259从片:

    • INC:与主片的0x0A引脚相连
    • 引脚:0x70~0x77
    • 端口:0xA0,0xA1
    • 0x70(bit0):与RTC相连,bit0是RTC的中断信号(0:不屏蔽;1:屏蔽中断)
  • 中断程序结束前需要发送中断结束命令(EOI),由服务寄存器处理

实时时钟(Real Time Clock:RTC)

它结合CMOS RAM使用;

RTC与系统总线相连的端口有索引端口:0x70/0x74;数据端口:0x71/0x75

示例:示例代码中RTC周期中断,显示时间

  • CMOS RAM内容分布
偏移地址 内容
0x00
0x01 闹钟秒
0x02
0x03 闹钟分
0x04 时(bit7为0:12小时制;bit7为1:24小时制)
0x05 闹钟时
0x06 星期
0x07
0x08
0x09
0x0A 寄存器A
0x0B 寄存器B
0x0C 寄存器C
0x0D 寄存器D
  • BCD

CMOS RAM内容数值由二进制形式的十进制编码(Binary Coded Decimal)填写

示例:

25:二进制编码:0001 1001

  	BCD编码:0010 0101(拆解位2和5,拆解的数字不能大于9,否则视为无效)

中断信号

  • 寄存器C是只读寄存器
寄存器 比特位 功能
寄存器A bit7 UIP(Update In Process):更新过程指示。(0:更新周期至少在488us内不会启动,此时访问cmos中内容的时间时安全的,1:488us内启动,周期不会超过1984us)
寄存器A 6~4 RTC时基选择。这3位控制外部输入频率的功能。系统将其初始化到010,为RTC选择一个32.768kHz的时钟频率。
寄存器A 3~0 RTC速率选择(Rate Select,RS)。选择分频电路的分节点。此处的选择决定了周期性中断信号发生的时间间隔。若选择0000,表示不产生周期性中断信号。注意:这是时基为32.768kHz的情况,使用其他时基的速率请参考相关资料MC146818.pdf
0000:从不触发中断
0001:3.90625ms
0010:7.8125ms
0011:122.070us
0100:244.141us
0101:488.281us
0110:976.5625us
0111:1.953125ms
1000:3.90625ms
1001:7.8125ms
1010:5.625ms
1011:1.25ms
1100:62.5ms
1101:125ms
1110:250ms
1111:5000ms(最长一秒2次)
寄存器B bit0 DSLSWS(Daylight Savings Legacy Software Support):老软件的夏令时支持(兼容老设备,现已不适用)
寄存器B bit1 HF(Hour Format):小时模式(0:12小时制;1:24小时制)
寄存器B bit2 DM(Data Mode):数据模式(0:BCD编码;1:二进制编码)
寄存器B bit3 SQWE(Square Wave Enable):方波输出允许(兼容老芯片,现已不适用)
寄存器B bit4 UIE(Update-ended Interrupt Enabele):更新周期结束中断允许(0:不产生更新周期中断信号,1反之)
寄存器B bit5 AIE(Alarm Interrupt Enable):闹钟中断是否允许(0:不允许,1:允许)
寄存器B bit6 PIE(Periodic Interrupt Enable):周期性中断是否允许发生(0:不允许;1:允许)
寄存器A选择0000时,PIE自动置0
寄存器B bit7 SET:更新周期禁止(0:更新周期,每秒都发生;1:终止更新周期,并且此后不再产生更新周期。)
寄存器C 0~3 保留位:始终为0
寄存器C bit4 UF(Update-enable Flag):更新结束标志(1:发生;0:没有发生)
寄存器C bit5 AF(Alarm Flag):闹钟中断标志(1:发生;0:没有发生)
寄存器C bit6 PF(Periodic Interrupt Flag):周期性中断标志(1:发生;0:没有发生)
寄存器C bit7 IRQF(Interrupt Request Flag):中断请求标志(1:有中断发生,0:没有中断发生)
当为1时去检查bit4~6是那种中断发生,为IRQF都取会使IRQF清0

中断

NMI:不可屏蔽中断

INTR:可屏蔽中断

中断向量表(Interrupt Vector Table:IVT)

实模式下,cpu可以识别256个中断,共1KB大小(00000~003FF)

表示由基础输入输出系统系统(BIOS)启动时创建的,xp /512xh 0(0x7c00后执行可查看IVT)

格式:(中断号*4便可获得入口地址)

00000~00001:中断0的入口地址的偏移地址

00002~00003:中断0的入口地址的段地址

00004~00005:中断0的入口地址的偏移地址

00006~00007:中断0的入口地址的段地址

无法发生中断的时期

当有命令修改ss栈段寄存器时,这条指令执行及下条指令(所以此处一般后面应该修改sp寄存器)执行时不允许发生中断

中断类别

硬件中断

来自处理器外部的中断(键盘,实时时钟)

内部中断

来自处理器内部的中断,不受IF的影响,出现会立即执行

软中断

编写程序时用指令(int,int3,into)指定的中断,不收IF的影响

中断处理过程

  • 第一步:保护断点的现场

将标志寄存器,断码段寄存器cs,指针寄存器ip及相关寄存器压栈

  • 第二部:执行中断处理程序

此时把拿到的中断号*4拿到程序地址,继而执行

  • 第三步:在遇到iret指令时,相关寄存器出栈,返回断点接着执行
外部中断-->8259A---->IF(可中断,发送中断响应,要求8259A发送一个中断号)-->8259A-->中断处理过程

内部中断----------------------->中断处理过程

软中断------------------------->中断处理过程

BIOS中断调用

call调用指令必要要知道段地址和偏移地址,但是先调用偏移地址的功能时将拿不到地址(因为版本更新,地址也会一直变化),所以此时的设计就是用中断代替call去提供系统服务

  • 示例一
mov ah,0	;每个中段有很多功能,发送软中断前,将0号功能写入ah
int 0x16 ;需要参数的功能得参考bois功能调用表获取参数存放的寄存器

示例代码

1+2+3…+100

jmp start

message:
db '1+2+3+...+100=' ;等同于db '1','+','2','+',.....'='

start:
mov ax, 0x7c0
mov ds, ax

mov ax, 0xb800
mov es, ax

mov si, message
xor di, di
mov cx, start - message

showmsg:
mov al, [si]
mov [es:di], al
inc di
mov byte [es:di], 0x07
inc di
inc si
loop showmsg

xor ax, ax
mov cx, 1

summate:
add ax, cx
inc cx
cmp cx, 100
jle summate

xor cx, cx
mov ss, cx
mov sp, cx

mov bx, 10
decompo:
inc cx
xor dx, dx
div bx
add dl, 0x30
push dx
cmp ax, 0
jne decompo

shownum:
pop dx
mov [es:di], dl
inc di
mov byte [es:di], 0x07
inc di
loop shownum

jmp $

times 510-($-$$) db 0
db 0x55, 0xaa

基址变址寻址的使用

; 使用基址变址寻址就地反转字母,而不适用栈
jmp start
string db 'abcdefghijklmnopqrstuvwxyz'
start:
mov ax, 0x7c0
mov ds, ax

mov bx,string ;数据区首地址
mov si,0 ;正向索引
mov di,start-string-1 ;方向索引

rever:
mov ah,[bx+si]
mov al,[bx+di]
mov [bx+di],al
mov [bx+si],ah ;首尾交换
inc si
dec di
cmp si,di
jl rever

用户程序头部信息

; 包含代码段、数据段和栈段的用户程序

;=======================================================================
section header vstart=0 ;用户程序头部段
;程序总长度[0x00]
program_length:
dd program_end

;用户程序入口点
code_entry:
dw start ;偏移地址[0x40]
dd section.code.start ;段地址[0x06]

;段重定位表项个数[0x0A]
realloc_tbl_len:
dw (segtbl_end-segtbl_begin)/4

;段重定位表
segtbl_begin:
code_segment dd section.code.start ;[0x0c]
data_segment dd section.data.start ;[0x19]
stack_segment dd section.stack.start ;[0x14]
segtbl_end:


;=======================================================================
section code align=16 vstart=0 ;代码段
start:
;初始化执行时,DS和ES指向用户程序头部段
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_pointer ;设置初始的栈顶指针

mov ax,[data_segment] ;设置到用户程序自己的数据段
mov ds,ax

mov ax,0xb800
mov es,ax

mov si,message
mov di,0

next:
mov al,[si]
cmp al,0
je exit

mov byte [es:di],al
mov byte [es:di+1],0x07
inc si
add di,2
jmp next

exit:
jmp $


;=======================================================================
section data align=16 vstart=0 ;数据段
message:
db 'hello world.',0


;=======================================================================
section stack align=16 vstart=0
resb 256
stack_pointer:


;=======================================================================
section trail align=16
program_end:

;bochsrc
;###############################################################
;# Configuration file for Bochs
;###############################################################
;
;# how much memory the emulated machine will have
;megs: 32
;
;# filename of ROM images
;romimage: file=/usr/share/bochs/BIOS-bochs-latest
;vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
;
;# what disk images will be used
;# floppya: 1_44=app.img, status=inserted
;
;# choose the boot disk.
;boot:disk
;
;# where do we send log messages?
;# log: bochsout.txt
;
;# disable the mouse
;mouse: enabled=1
;
;# enable key mapping, using US layout as default.
;# keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map
;
;# 硬盘设置
;ata0-master: type=disk, path="app.img", mode=flat, cylinders=2, heads=16, spt=63

硬盘主引导扇区代码(加载程序)

;文件说明:硬盘主引导扇区代码(加载程序)

app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址

SECTION mbr align=16 vstart=0x7c00
;设置堆栈段和栈指针
mov ax,0
mov ss,ax
mov sp,ax

mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址
mov dx,[cs:phy_base+0x02]
mov bx,16
div bx
mov ds,ax ;令DS和ES指向该段以进行操作
mov es,ax

;以下读取程序的起始部分
xor di,di
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
xor bx,bx ;加载到DS:0x0000处
call read_hard_disk_0

;以下判断整个程序有多大
mov dx,[2]
mov ax,[0]
mov bx,512 ;512字节每扇区
div bx
cmp dx,0
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec ax ;已经读了一个扇区,扇区总数减1
@1:
cmp ax,0 ;考虑实际长度小于等于512个字节的情况
jz direct

;读取剩余的扇区
push ds ;以下要用到并改变DS寄存器

mov cx,ax ;循环次数(剩余扇区数)

@2:
mov ax,ds
add ax,0x20 ;得到下一个以512字节为边界的段地址
mov ds,ax

xor bx,bx ;每次读时,偏移地址始终为0x0000
inc si ;下一个逻辑扇区
call read_hard_disk_0
loop @2 ;循环读,直到读完整个功能程序
pop ds ;恢复数据段基址到用户程序头部段


;计算入口点代码段基址
direct:
mov dx,[0x08]
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址

;开始处理段重定位表
mov cx,[0x0a] ;需要重定位的项目数量
mov bx,0x0c ;重定位表首地址

realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx]
call calc_segment_base
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc

jmp far [0x04] ;转移到用户程序

;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号
; DS:BX=目标缓冲区地址
push ax ;子程序,把本子程序的寄存器数据压栈,子程序结束前恢复他们
push bx
push cx
push dx

mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数

inc dx ;0x1f3
mov ax,si
out dx,al ;LBA地址7~0

inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8

inc dx ;0x1f5
mov ax,di
out dx,al ;LBA地址23~16

inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al

inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al

.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输

mov cx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw

pop dx
pop cx
pop bx
pop ax

ret

;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx

add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
shr ax,4
ror dx,4
and dx,0xf000
or ax,dx

pop dx

ret

;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址

times 510-($-$$) db 0
db 0x55,0xaa

RTC周期中断,显示时间

;===============================================================================
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code.start ;段地址[0x06]

realloc_tbl_len dw (header_end-realloc_begin)/4
;段重定位表项个数[0x0a]

realloc_begin:
;段重定位表
code_segment dd section.code.start ;[0x0c]
data_segment dd section.data.start ;[0x14]
stack_segment dd section.stack.start ;[0x1c]

header_end:

;===============================================================================
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
new_int_0x70:
push ax
push bx
push cx
push dx
push es

.w0:
mov al,0x0a ;阻断NMI。当然,通常是不必要的
or al,0x80
out 0x70,al
in al,0x71 ;读寄存器A
test al,0x80 ;测试第7位UIP
jnz .w0 ;以上代码对于更新周期结束中断来说
;是不必要的
xor al,al
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(秒)
push ax

mov al,2
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(分)
push ax

mov al,4
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(时)
push ax

mov al,0x0c ;寄存器C的索引。且开放NMI
out 0x70,al
in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断
;此处不考虑闹钟和周期性中断的情况
mov ax,0xb800
mov es,ax

pop ax
call bcd_to_ascii
mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示

mov [es:bx],ah
mov [es:bx+2],al ;显示两位小时数字

mov byte [es:bx+4], ':';显示分隔符':'
not byte [es:bx+5] ;反转显示属性

pop ax
call bcd_to_ascii
mov [es:bx+6],ah
mov [es:bx+8],al ;显示两位分钟数字

mov byte [es:bx+10], ':';显示分隔符':'
not byte [es:bx+11] ;反转显示属性

pop ax
call bcd_to_ascii
mov [es:bx+12],ah
mov [es:bx+14],al ;显示两位小时数字

mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向从片发送
out 0x20,al ;向主片发送

pop es
pop dx
pop cx
pop bx
pop ax

iret

;-------------------------------------------------------------------------------
bcd_to_ascii: ;BCD码转ASCII
;输入:AL=bcd码
;输出:AX=ascii
mov ah,al ;分拆成两个数字
and al,0x0f ;仅保留低4位
add al,0x30 ;转换成ASCII

shr ah,4 ;逻辑右移4位
and ah,0x0f
add ah,0x30

ret

;-------------------------------------------------------------------------------
start:
mov ax,[stack_segment]
mov ss,ax
mov sp,ss_pointer
mov ax,[data_segment]
mov ds,ax

mov bx,init_msg ;显示初始信息
call put_string

mov bx,inst_msg ;显示安装信息
call put_string

mov al,0x70
mov bl,4
mul bl ;计算0x70号中断在IVT中的偏移
mov bx,ax

cli ;防止改动期间发生新的0x70号中断

push es
mov ax,0x0000
mov es,ax
mov word [es:bx],new_int_0x70 ;偏移地址。
mov word [es:bx+2],cs ;段地址
pop es

mov al,0x0b ;RTC寄存器B
or al,0x80 ;阻断NMI
out 0x70,al
mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更新
out 0x71,al ;新结束后中断,BCD码,24小时制

mov al,0x0c
out 0x70,al
in al,0x71 ;读RTC寄存器C,复位未决的中断状态

in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,al ;写回此寄存器

sti ;重新开放中断

mov bx,done_msg ;显示安装完成信息
call put_string

mov bx,tips_msg ;显示提示信息
call put_string

mov cx,0xb800
mov ds,cx
mov byte [12*160 + 33*2],'@' ;屏幕第12行,35列

.idle:
hlt ;使CPU进入低功耗状态,直到用中断唤醒
not byte [12*160 + 33*2+1] ;反转显示属性
jmp .idle

;-------------------------------------------------------------------------------
put_string: ;显示串(0结尾)。
;输入:DS:BX=串地址
mov cl,[bx]
or cl,cl ;cl=0 ?
jz .exit ;是的,返回主程序
call put_char
inc bx ;下一个字符
jmp put_string

.exit:
ret

;-------------------------------------------------------------------------------
put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es

;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al

mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数

cmp cl,0x0d ;回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov ax,bx ;
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor

.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,80
jmp .roll_screen

.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1
mov [es:bx],cl

;以下将光标位置推进一个字符
shr bx,1
add bx,1

.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor

mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840 ;清除屏幕最底一行
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls

mov bx,1920

.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al

pop es
pop ds
pop dx
pop cx
pop bx
pop ax

ret

;===============================================================================
SECTION data align=16 vstart=0

init_msg db 'Starting...',0x0d,0x0a,0

inst_msg db 'Installing a new interrupt 70H...',0

done_msg db 'Done.',0x0d,0x0a,0

tips_msg db 'Clock is now working.',0

;===============================================================================
SECTION stack align=16 vstart=0

resb 256
ss_pointer:

;===============================================================================
SECTION program_trail
program_end:

;bochsrc
;设置时钟同步添加以下指令
;clock: sync=realtime,time0=1,rtc_sync=1

BIOS功能调用

;===============================================================================
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code.start ;段地址[0x06]

realloc_tbl_len dw (header_end-realloc_begin)/4
;段重定位表项个数[0x0a]

realloc_begin:
;段重定位表
code_segment dd section.code.start ;[0x0c]
data_segment dd section.data.start ;[0x14]
stack_segment dd section.stack.start ;[0x1c]

header_end:

;===============================================================================
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
start:
mov ax,[stack_segment]
mov ss,ax
mov sp,ss_pointer
mov ax,[data_segment]
mov ds,ax

mov cx,msg_end-message
mov bx,message

.putc:
mov ah,0x0e
mov al,[bx]
int 0x10
inc bx
loop .putc

.reps:
mov ah,0x00
int 0x16

mov ah,0x0e
mov bl,0x07
int 0x10

jmp .reps

;===============================================================================
SECTION data align=16 vstart=0

message db 'Hello, friend!',0x0d,0x0a
db 'This simple procedure used to demonstrate '
db 'the BIOS interrupt.',0x0d,0x0a
db 'Please press the keys on the keyboard ->'
msg_end:

;===============================================================================
SECTION stack align=16 vstart=0
resb 256
ss_pointer:

;===============================================================================
SECTION program_trail
program_end:

保护模式+x86阶段

寄存器

通用寄存器

EAX,EBX,ECX,EDX,EDI,ESI,EBP,ESP

段选择器(还是16位寄存器,但作用发生了改变)

SS,DS,CS,ES,FS,GS保护模式下段寄存器用来保存描述符的选择子,不再用来保存逻辑地址。

他们都拥有一个只有处理器能使用的描述符高速缓存器,用来存放段的线性基地址、界限和属性。段选择器的内容是段描述符的段选择子。段选择子的内容传递个段选择器后,段选择器立即根据段选择子去找到描述符表中的描述符,并取出它。然后将内容传奇个段选择器的描述符高速缓存器中,从此程序用描述符高速缓存器中的段基地址加上段内偏移地址访问数据

32位系统,实模式同样是使用描述符高速缓存器的,他将寄存器中的值左移4位,变为32的基地址(低20有效),高12位全为0。

  • FS: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值;
  • GS: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值。

段选择子(16位)

简称选择子(Segment Select:SS)。用来在描述符表中选择一个描述符。

bit15~bit3:描述符索引。最大是8192(2的13次方),索引从0开始。

bit2:TI(Table Indicator)表指示器。为0时,代表描述符在全局描述符表(GDT)中;为1时,代表在描述符表LDT中。

bit1~bit0:RPL,请求特权级。本程序的特权级。

全局描述符表寄存器GDTR

48位寄存器。用于跟踪GDT表。保存了GDT的起始线性地址和GDT界限值。

起始地址:通常情况下等于物理地址,但不总是等于

控制寄存器

  • CR0

处理器内部寄存器,还有CR1,CR2等等寄存器。CR0是32位寄存器。CR0的bit0:PE,代表着实模式与保护模式的分解线。为0时是实模式,为1时时保护模式。

标志寄存器

  • bit21(ID):判断CPU是否支持cpuid(为1支持,为0不支持)。

任务寄存器TR

用来指向当前任务的tss.寄存器结构类似于段寄存器(段选择器,也有描述符高速缓存器。)

ltr执行:加载ltr选择子到tss选择器,一旦ltr选择器部分发生改变,处理器立刻用ltr选择器中的选择子到gdt中查找对应的ltr描述符,将它传送到ltr的描述符高速缓存器中。

局部描述符表寄存器LDTR

用来指向当前任务的ldt.寄存器结构类似于段寄存器(段选择器,也有描述符高速缓存器。)

lldt执行:加载ldt选择子到ldt选择器,一旦ldt选择器部分发生改变,处理器立刻用ldt选择器中的选择子到gdt中查找对应的ldt描述符,将它传送到ldt的描述符高速缓存器中。

内存布局

高位内存区

实模式下,32处理器地址线不止20根了,可以访问到FFFFF~10FFEF的内存了。这段内存区就叫做高位内存区(High Memory Area:HMA)

寻址方式

内存寻址

  • 32位处理器特有的内存寻址方式
寄存器  +   寄存器          X    比例因子+ 内存偏移
EAX EAX 1
EBX EBX
ECX ECX 2
EDX EDX
ESP + 不能使用ESP X 4 + 8位或32位偏移量(没有16位)
EBP EBP
ESI ESI 8
EDI EDI

示例
add eax,[0x2008]
sub eax,[eax+0x08]
mov ecx,[eax+ebx*8+0x0s]
mov ax,sp;非法,16位寄存器不能使用sp做寻址
mov eax,esp

指令

指令集

指令 使用
lgdt 内存地址指向一个48为的内存地址(16为界限值+32位段基地址),它不影响任何标志位
jmp 实模式:jmp 逻辑段地址:段内偏移地址
保护模式:jmp 描述符选择子:段内偏移地址
xchg 数据交换。[xchg r/m,r/m]
bits bits 16/32:指定本条命令后面的代码的操作尺寸。使用bits时,可能会影响到处理器的流水线中的指令,会清除。实模式和保护模式默认都是16位尺寸的,进入保护模式后需要32位的话需要切换。
    	bits 16 ;或[bits 16] 
    	mov ds,ax
		bits 32 ;或[bits 32]
		mov ds,ax;
		此时编译器会给机器码多一个指令前缀66,会影响工作效率;正确的做法是:mov ds,eax。但nasm编译器无论16位还是32位都不会出现前缀66
bswap 操作数只能时寄存器[bswap r].它用来反转操作数的字节序。若是32位的操作数:
原来的0~7变为了24~31.
原来的8~15变为了16~23.
原来的16~23变为了8~15.
原来的24~31变为了0~7.
cpuid CPU Indentification.获取CPU品牌信息。使用前使用eax(功能号)指定要返回的信息。
指令族vmov..
cmov.. r,r/m(r只能是16位,32位,或者64位的,不能是8位)
        cmovnz al,bl;非法
        cmovg ax,[0x2008]
        cmovge [0x2008],ecx
        cmp eax,edx
        cmovne eax,edx;如果不相等则,把edx值赋给eax
sgdt [sgdt m].获取gdt的信息保存到指定地址中,这个地址必须要有6个字节大小。
movzx 带0扩展指令。扩充小的数到大的数中。(左边补零)
movzx r16,r8/m8
        movzx r32,r8/m8
        movzx r64,r8/m8
        movzx r32,r16/m16
        movzx r64,r16/m16
串数据比较 [cmpsb,cmpsw,cmpsd,cmpsq],他有两个比较串,都必须位于内存中,都有自己的长度。比较时是采用默认尺寸决定使用什么寄存器的。详细见【串比较】
pushad/popad 压入/弹出所有的双字的寄存器到栈中。
压栈顺序:eax-ecx-edx-ebx-esp-ebp-esi-edi
出栈顺序:edi-esi-ebp-废弃-ebx-edx-ecx-eax
xlat 指令执行时,处理器访问指定的表格,用AL中的数字作为偏移量,从表中取出一个字节,传回AL寄存器。隐含操作数:段寄存器DS指向转换表所在段的选择子【类似于数组】。默认尺寸是16位时,使用BX指定转换表的段内偏移。默认尺寸是32位时,使用EBX指定转换表的段内偏移。AL=数字。执行执行后,AL=从表中取出的字节数据。
push 32位处理器支持push直接压入立即数。压入寄存器的实际数据大小需要根据实际判断。
ltr/lldt ltr:加载tss到TR寄存器。
lldt:加载ldt到LDTR寄存器。
他们操作数或者操作基址都是选择子或者存储着选择子。(tss选择子,ldt选择子)

串比较

默认操作尺寸 目的串 源串 REP前缀使用的计数器
16 ES:DI DS:SI CX
32 ES:EDI DS:ESI ECX
64 ES:RDI DS:RSI RCX
重复前缀 终止条件
rep CX/ECX/RCX=0
repe/repz CX/ECX/RCX=0并且ZF=0
repne/repnz CX/ECX/RCX=0并且ZF=1

他们只会从指定内存中拿出对应大小数据去比较一次。所以需要搭配rep执行使用。而我们又需要知道比较多少次,所以rep使用时需要一个前缀计数器。也因为如此,我们需要在比较前设定好指定的计数器大小(串总大小除于每次比较的大小)。因为串比较指令会影响标志寄存器,所以循环可使用其他衍生指令。比较方向由标志寄存器DF位决定(DF=0,从开头开始,DI和SI递增;DF=1,从结尾开始,DI和SI递减)。

  • 具体使用细节
    • 1.用CLD或者STD清除或设置方向位标志DF;
    • 2.根据处理器的默认操作尺寸和方向标志,设置DS和SI/ESI/RSI指向源串;
    • 3.根据处理器的默认操作尺寸和方向标志,设置ES和DI/EDI/RDI指向源串;
    • 4.根据处理器的默认操作尺寸及选择串的比较指令,将比较次数传送到CX/ECX/RCX;
    • 5.根据实际情况,选择以串比较指令冲的一个;
    • 6.根据实际情况,为串比较指令指定一个前缀,可以是repz/repe/repnz/repne;
    • 7.重复比较结束后,根据零标志ZF最后的状态是0还是1,判断两个串是否相同。

操作尺寸

指令的操作长度是指指令中操作数的长度及有效地址(偏移地址、偏移量)的长度。为了设计和扩展指令系统,32为处理器将操作尺寸定义为两种:16位操作尺寸和32位操作尺寸。

16位操作尺寸允许8位或者16位操作数,以及16位有效地址。

32位操作尺寸允许8位或者32位操作数(没有16位),以及32位有效地址。

默认操作尺寸

32位处理器虽然有两种尺寸,但是,在同一时刻,处理器只能按一种操作尺寸工作,要么选择16位操作尺寸,要么选择32位操作尺寸,着叫做处理器的默认操作尺寸。可从描述符中的属性D/B知道是多少位的。当指令有66/67前缀时,指令执行会多一个时钟周期,所以应该尽量减少前缀的添加。

  • 当默认操作尺寸为16位时的指令前缀

66:反转操作数为32位;67:反转有效地址为32位。

汇编指令 机器指令 操作尺寸
mov ad,dl 0x88D0 8位数据
mov ax,dx 0x89D0 16位数据
mov eax,edx 0x6689D0 32位数据
mov [bx+di],dh 0x8831 8位数据,16位数据
mov [bx+di],si 0x8931 16位数据,16位数据
mov [bx+di],esi 0x668931 32位数据,16位数据
mov [ecx],esi 0x66678931 32位数据,32位数据
mov [ecx],si 0x678931 16位数据,32位数据
movsb 0xA4 8位数据
movsw 0xA5 16位数据
movsd 0x66A6 32位数据
  • 当默认操作尺寸为32位时的指令前缀

66:反转操作数为16位;67:反转有效地址为16位。

汇编指令 机器指令 操作尺寸
mov ad,dl 0x88D0 8位数据
mov ax,dx 0x6689D0 16位数据
mov eax,edx 0x89D0 32位数据
mov [bx+di],dh 0x8831 8位数据,16位数据
mov [bx+di],si 0x66678931 16位数据,16位数据
mov [bx+di],esi 0x678931 32位数据,16位数据
mov [ecx],esi 0x8931 32位数据,32位数据
mov [ecx],si 0x668931 16位数据,32位数据
movsb 0xA4 8位数据
movsw 0x66A5 16位数据
movsd 0xA6 32位数据

push压入立即数

实际数据压栈尺寸决定着出栈的接受尺寸。

立即数大小 操作尺寸大小 实际压入的数据尺寸 栈指针的变化
8 16 16:高8位的内容是操作数的符号扩展 SP/ESP减2:由栈段描述符中的B位决定是SP还是ESP
32 32:高24位的内容是操作数的符号扩展 SP/ESP减4:由栈段描述符中的B位决定是SP还是ESP
16 16 16 SP/ESP减2:由栈段描述符中的B位决定是SP还是ESP
32 16 SP/ESP减2:由栈段描述符中的B位决定是SP还是ESP
32 16 32 SP/ESP减4:由栈段描述符中的B位决定是SP还是ESP
32 32 SP/ESP减4:由栈段描述符中的B位决定是SP还是ESP

push压入寄存器

push cs/push ss/push ds/push es/push fs/push gs
操作尺寸大小 实际压入的数据尺寸 栈指针的变化
16 16 SP/ESP减2:由栈段描述符中的B位决定是SP还是ESP
32 32:用比特0扩展到高16位 SP/ESP减4:由栈段描述符中的B位决定是SP还是ESP
### 带参数的ret/retf
指令 执行的操作 说明
ret imm 从栈中弹出数据到指令指针寄存器
栈指针寄存器=栈指针寄存器+imm
指令执行时,如果处理器的默认操作尺寸是16位的,指令指针寄存器是IP,如果默认尺寸是32位的,指令指针寄存器是EIP;否则是RIP
栈指针寄存器是SP还是ESP,取决于SS描述符高数缓存器的B位。
retf imm 从栈中弹出数据到指令指针寄存器
从栈中弹出数据到段寄存器CS;
栈指针寄存器=栈指针寄存器+imm

指令格式

ModR/M和SIB查表:见intel相关开发手册

指令前缀 操作码 ModR/M SIB 偏移量 立即数
如果存在为1个字节 1到3个字节 如果存在为1个字节 如果存在为1个字节 如果存在:1/2/4个字节 如果存在:1/2/4个字节
机器码 汇编语言指令 操作码 ModR/M SIB 偏移量 立即数
mod reg r/m ss index Base
0x031458 add edx,[eax+ebx*2] 0x03 00b 010b 100b 00b 011b 000b
0x14 0x58
0x8184D8003C0000AA55 add word [eax+ebx*8+0x3c00],0x55AA 0x81 10b 000b 100b 11b 011b 000b 0x003C0000 0xAA55
0x84 0xD8
机器码 汇编指令 指令前缀
0xA5 movsw
0xF3A5 req movsw req

设备交互

端口0x92

在32位系统中处理器有一个名为A20M(address 20 mask)的引脚,它连接着一个或门,或门连接着ICH(输入输出控制中心芯片,南桥芯片)的0x92端口的bit1和老式键盘的0x60端口(用来控制A20线的有效与否);

0x91-bit0:连接处理器的INIT引脚。复位处理器,从0变为1时,处理器将复位,计算机重启。

CPUID

功能号 返回
0 返回CPU所支持的最大功能号。
返回信息:
eax=最大支持的功能号
ebx=0x756E6547,是“Genu"的字符编码
ecx=0x49656E69,是”ineI“的字符编码
edx=0x6C65746E,是"ntel"的字符编码
以上三个寄存器的合成是”GenuineIntel",是供应商的信息。
1 返回cpu是否支持cmovcc指令族(edx的bit15:为0不支持,为1支持)。

段描述符

段描述符和描述符表

每个段有关的信息需要8个字节(64位)来描述,我们称之为段描述符。当段描述符集中存放时,集中存放的地方就是描述符表,主要的表就是GDT表。描述符索引从0开始,索引号乘于8就能拿到在描述符表中的偏移地址,偏移地址加上描述符表的基地址,就能拿到线性地址

数据段寄存器只能加载数据段描述符,代码段寄存器只能加载代码段描述符,在加载描述符时会判断类型。但是,平时取指令或读写数据时,段的类型不做检查。

全局描述符表

Global Descriptor Table:GDT。它必须在进入保护模式之前定义,也就是定义于实模式下。

处理器规定:GDT的第一个描述符必须为空描述符(或亚描述符)。

描述符属性

  • 描述符分类

    • 存储器的段描述符:代码段,数据段

    • 系统描述符

      • 系统的段描述符:GDT,LDT,TSS。

      • 门描述符

描述符的属性

描述符中的bit44位,也称S位(位于高双字中)。s=0:系统描述符,且TYPE字段用来指定系统段的类型或门的类型。s=1:存储器的段描述符,且TYPE字段用来区分代码段和数据段。

系统拿到描述符后根据S位去确定处bit44~bit40(bit43~bit40:TYPE)之外的内存是什么数据。因为存储器的段描述符和系统的段描述符结构是不同的

  • 存储器段描述符格式(拆为高低双字节展示)
字节 比特位 属性
高双字 bit31~bit24:段基地址 段基地址的31~24位
bit23:G 段界限粒度(Granularity)位。
G=0:表示界限粒度为字节;
G=1:表示界限粒度为4K字节。
注意:界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
G=0, 实际使用的段界限=描述符中的界限值
G=1,实际使用的段界限=描述符中的界限值x0x1000+0xFFF
bit22:D/B 对于代码段来说,它是D位。操作尺寸/栈上部边界。
值为0时,段使用按16操作;值为1时,段使用32操作。也就决定段界限的大小(向下模式下,段大小是FFFF还是FFFFFFFF)
对于数据段来说,它是B位。如果B=0,并且这个段是栈段时,使用的是sp寄存器。如果B=1,并且这个段是栈段时,使用的是esp寄存器。
bit21:L 长(Long)模式。64位代码段标志。32位系统下为0。
bit20:AVL 软件可利用(Available)位。通常由操作系统使用,处理器不使用。
bit19~bit16:段界限 段界限的bit16~bit19位
bit15:P 存在(Present)位。
P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;
P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
bit14~bit13:DPL 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。
bit12:S s=0:系统描述符,且TYPE字段用来指定系统段的类型或门的类型。
s=1:存储器的段描述符,且TYPE字段用来区分代码段和数据段
bit11~bit8:TYPE bit11:X=0(executable)时,该描述符段不可执行,通常为数据段。
bit11:X=1时,该描述符段可执行,通常为代码段。
X=0时;bit10为E(expand),E=0时,段向上扩展(低地址->高地址:普通段);E=1时,段向下扩展(高地址->低地址:通常为栈段) X=0时;bit9为W,W=0,段不可写入,W=1,段可以正常写入。 bit8:A(Accessed):该段近期访问过或者使用过。段初始化时值为0,被访问过后置1,清0由用户软件和操作系统执行。因此通过检测A就可以知道该段的使用频率。
X=1时;bit10为C,C=0时,为非亦从代码段,表示特权级相同的程序才能转移到这个段执行;C=1时,为亦从代码段,特权级低的程序可以直接转移到这个段来执行。 X=1时;bit9为R,R=0,代码段不可读出,R=1,代码段可读出。
bit7~bit0:段基地址 段基地址的23~15位
低双字 bit31~bit16:段基地址 段基地址的15~0位
bit15~bit0:段界限 段界限的bit0~bit15位。向上扩展的段,段界限值位0时,段的大小位1(0是最小偏移量);向下扩展的段,段界限值位0时,段的大小位sp或esp的能表示的最大值(FFFF或FFFFFFFF)。
### 描述符类型检查

存储器的描述符

  • 第一步:通过选择子,用索引号*8+7<=边界,检查是否存在于gdt中,如果不存在,处理器中止处理,产生异常中断13
  • 第二部:检查描述符类型是否有效,检查描述符与要赋值的段寄存器是否匹配。
段寄存器 数据段(X=0) 代码段(X=1)
只读(W=0) 读写(W=1) 只执行(R=0) 执行、读(R=1)
CS N N Y Y
DS Y Y N Y
ES Y Y N Y
FS Y Y N Y
GS Y Y N Y
SS N Y N N
  • 第三步:检查属性P位,查看描述符所对应的段是否在物理内存中,如果P=0,则不存在物理内存中,它将发出中断11,把对应段调入内存中,把P置1。注意:这里中断后将返回到引起中断的指令,这样将可重新加载描述符。

对于DS,ES,FS和GS的选择器,可以向其加载数值为0的选择子。对于cs和ss的选择器来说,不允许向其传送为0的选择子。

尽管在加载时不会有任何问题,但是访问内存时,会抛出异常。

mov eax,0
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax

mov [ebx],ax;抛出异常

代码段执行的保护

G=0, 实际使用的段界限=描述符中的界限值

G=1,实际使用的段界限=描述符中的界限值x0x1000+0xFFF

  • 但jmp 跳转到一个代码段时,cs,ip的值会被刷新。处理器执行下条指令时,将检查0<=EIP+指令长度-1<=实际使用的段界限,否则发出异常。
  • **段与段之间是可以重叠的。但一个数据段为00000000~FFFFFFFF时,代码段在0x7c00~0x7c00+512KB时,很明显,两个段重叠了,虽然代码段的描述符不能写入,但是可以通过重叠段的数据段去修改代码段中的值。**如果某个段的内存已经存在,又位这段内存添加一个新的段,那么这个新段就可以是旧段的别名。(实际使用将示例代码:冒泡排序)

向上扩展的栈段的保护

向上的数据段也是可以作为栈段的,扩展方向并不影响段界限检查,栈的栈底栈顶方向和向下扩展的段时相同的。偏移量的变化范围也是sp/esp(使用esp还是sp,由描述符的B位来决定)的变化范围。(隐式操作push、pop、call、ret、iret都会用到esp)

向上扩展的栈段检查:操作数的有效地址+操作数的大小-1<=实际使用的段界限

;段基地址0x6c00,段界限0x7ff,粒度为1,向上扩展,数据段
mov eax,0018 ;选择子
mov ss,eax ;ss设置到这个段
mov esp,0x800 ;向上扩展的栈段,应该将栈指针设置为段的大小(段大小:界限值+1)
;向下扩展的栈段,则是mov esp,0

push dword 0x072e074d ;32位系统支持直接压入立即数到栈
push dword 0x072e0750

pop dword [0xb8000]
pop dword [0xb8004]

GDT

为了让程序在内存中能自由的浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移地址。在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行登记。用来存放这些信息的数据结构叫做描述符表。

等级信息有每个段的起始地址,段的界限等各种访问属性。这样,当你访问的偏移地址超出了段的界限时,处理器就会阻止这种访问,并产生一个叫做内部异常的中断

GDT理论上可以位于内存中的任意位置,但是因为它需要在进入保护模式前定义,所以它通常位于00000~FFFFF之间。可在进入保护模式后重定位位置。gdt也有一个自己的描述符,他位于处理器当中。

  • gdt的格式:(64K大小)

一个一个读描述符,包括不仅限于存储器段描述符,ldt描述符,tss描述符。

  • GDTR

bit47~bit16:全局描述符表段基地址;bit15~bit0:全局描述符边界(表大小减一)。

因为边界值是16位的,所以表最大65536字节,又因一个描述符占8个字节,所以最大在表中定义8192个描述符(段)。

LDT

段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。GDT包含系统使用的代码段、数据段、堆栈段和特殊数据段描述符,以及所有任务局部描述符表LDT的描述符。

ldt也是一块内存区,最大也是64K,最多8192个描述符。ldt也有自己的一个描述符,每个描述符对应一个ldt,它安装在GDT表中。

当段描述符S位为0时,Type类型是2时(0010),这个描述符就是ldt描述符。此时D(bit22)位和L(bit21)位都为0.

  • ldt:的格式:(64K大小)

包括不仅限于用户程序头不断的描述符,用户程序代码段的描述符,用户程序数据段的描述符,用户程序栈段的描述符等等的存储器段描述符。

TSS

任务状态段。在一个多任务环境中,当发生了任务切换,需保护现场,因此每个任务的应当用一个额外的内存区域保存相关信息,即任务状态段(TSS);TSS格式固定,104个字节,处理器固件能识别TSS中元素,并在任务切换时读取其中信息。tss也有自己的一个描述符,每个描述符对应一个tss,它安装在GDT表中。

当段描述符S位为0时,Type类型是10B1:B位为0时代表不忙,为1时代表忙,这一位由处理器固件管理(可以有效防止任务切换重加载,或者切换到自己),这个描述符就是ldt描述符。此时D(bit22)位和L(bit21)位都为0.

  • tss的格式:(104字节大小)

    图片

示例代码

GDT使用示例

    ;设置堆栈段和栈指针
mov ax,cs
mov ss,ax
mov sp,0x7c00

;计算GDT所在的逻辑段地址
mov ax,[cs:gdt_base+0x7c00] ;低16位
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx
mov ds,ax ;令DS指向该段以进行操作
mov bx,dx ;段内起始偏移地址

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00

;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800

;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [bx+0x10],0x8000ffff ;描述符低双字
mov dword [bx+0x14],0x0040920b ;描述符高双字,他们代表着数据段,向上扩展,可写
;基地址=FFFF,G=0,P=1,特权级0

;创建#3描述符,保护模式下的堆栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600

;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一)

lgdt [cs: gdt_size+0x7c00]

in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20

cli ;保护模式下中断机制尚未建立,应
;禁止中断
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]

flush:
mov cx,00000000000_10_000B ;加载数据段选择子(0x10),索引从0开始
mov ds,cx

;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'

;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作
mov cx,00000000000_11_000B ;加载堆栈段选择子
mov ss,cx
mov esp,0x7c00

mov ebp,esp ;保存堆栈指针
push byte '.' ;压入立即数(字节)

sub ebp,4
cmp ebp,esp ;判断压入立即数时,ESP是否减4
jnz ghalt
pop eax
mov [0x1e],al ;显示句点

ghalt:
hlt ;已经禁止中断,将不会被唤醒

;-------------------------------------------------------------------------------

gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址

times 510-($-$$) db 0
db 0x55,0xaa

冒泡排序

;冒泡排序
;===================================================================
;设置堆栈段和栈指针
mov eax,cs
mov ss,eax
mov sp,0x7c00

;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位线性基地址
xor edx,edx
mov ebx,16
div ebx ;分解成16位逻辑地址

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

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [ebx+0x00],0x00000000
mov dword [ebx+0x04],0x00000000

;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符

;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,512字节
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符

;创建以上代码段的别名描述符
mov dword [ebx+0x18],0x7c0001ff ;基地址为0x00007c00,512字节
mov dword [ebx+0x1c],0x00409200 ;粒度为1个字节,数据段描述符

mov dword [ebx+0x20],0x7c00fffe
mov dword [ebx+0x24],0x00cf9600

;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限

lgdt [cs: pgdt+0x7c00]

in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20

cli ;中断机制尚未工作

mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...
jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移

[bits 32]
flush:
mov eax,0x0018
mov ds,eax

mov eax,0x0008 ;加载数据段(0..4GB)选择子
mov es,eax
mov fs,eax
mov gs,eax

mov eax,0x0020 ;0000 0000 0010 0000
mov ss,eax
xor esp,esp ;ESP <- 0

mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性

;开始冒泡排序
mov ecx,pgdt-string-1 ;遍历次数=串长度-1
@@1:
push ecx ;32位模式下的loop使用ecx
xor bx,bx ;32位模式下,偏移量可以是16位,也可以
@@2: ;是后面的32位
mov ax,[string+bx]
cmp ah,al ;ah中存放的是源字的高字节
jge @@3
xchg al,ah
mov [string+bx],ax
@@3:
inc bx
loop @@2
pop ecx
loop @@1

mov ecx,pgdt-string
xor ebx,ebx ;偏移地址是32位的情况
@@4: ;32位的偏移具有更大的灵活性
mov ah,0x07
mov al,[string+ebx]
mov [es:0xb80a0+ebx*2],ax ;演示0~4GB寻址。
inc ebx
loop @@4

hlt

;-------------------------------------------------------------------------------
string db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'
;-------------------------------------------------------------------------------
pgdt dw 0
dd 0x00007e00 ;GDT的物理地址
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa

保护模式程序的动态加载和执行

主引导程序

    core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址
core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号

mov ax,cs
mov ss,ax
mov sp,0x7c00

;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址
xor edx,edx
mov ebx,16
div ebx ;分解成16位逻辑地址

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

;跳过0#号描述符的槽位
;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符

;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符

;建立保护模式下的堆栈段描述符 ;基地址为0x00007C00,界限0xFFFFE
mov dword [ebx+0x18],0x7c00fffe ;粒度为4KB
mov dword [ebx+0x1c],0x00cf9600

;建立保护模式下的显示缓冲区描述符
mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
mov dword [ebx+0x24],0x0040920b ;粒度为字节

;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限

lgdt [cs: pgdt+0x7c00]

in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20

cli ;中断机制尚未工作

mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...
jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]
flush:
mov eax,0x0008 ;加载数据段(0..4GB)选择子
mov ds,eax

mov eax,0x0018 ;加载堆栈段选择子
mov ss,eax
xor esp,esp ;堆栈指针 <- 0

;以下加载系统核心程序
mov edi,core_base_address

mov eax,core_start_sector
mov ebx,edi ;起始地址
call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区)

;以下判断整个程序有多大
mov eax,[edi] ;核心程序尺寸
xor edx,edx
mov ecx,512 ;512字节每扇区
div ecx

or edx,edx
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec eax ;已经读了一个扇区,扇区总数减1
@1:
or eax,eax ;考虑实际长度≤512个字节的情况
jz setup ;EAX=0 ?

;读取剩余的扇区
mov ecx,eax ;32位模式下的LOOP使用ECX
mov eax,core_start_sector
inc eax ;从下一个逻辑扇区接着读
@2:
call read_hard_disk_0
inc eax
loop @2 ;循环读,直到读完整个内核

setup:
mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以
;通过4GB的段来访问
;建立公用例程段描述符
mov eax,[edi+0x04] ;公用例程代码段起始汇编地址
mov ebx,[edi+0x08] ;核心数据段汇编地址
sub ebx,eax
dec ebx ;公用例程段界限
add eax,edi ;公用例程段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor
mov [esi+0x28],eax
mov [esi+0x2c],edx

;建立核心数据段描述符
mov eax,[edi+0x08] ;核心数据段起始汇编地址
mov ebx,[edi+0x0c] ;核心代码段汇编地址
sub ebx,eax
dec ebx ;核心数据段界限
add eax,edi ;核心数据段基地址
mov ecx,0x00409200 ;字节粒度的数据段描述符
call make_gdt_descriptor
mov [esi+0x30],eax
mov [esi+0x34],edx

;建立核心代码段描述符
mov eax,[edi+0x0c] ;核心代码段起始汇编地址
mov ebx,[edi+0x00] ;程序总长度
sub ebx,eax
dec ebx ;核心代码段界限
add eax,edi ;核心代码段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor
mov [esi+0x38],eax
mov [esi+0x3c],edx

mov word [0x7c00+pgdt],63 ;描述符表的界限

lgdt [0x7c00+pgdt]

jmp far [edi+0x10]

;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
push eax
push ecx
push edx

push eax

mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数

inc dx ;0x1f3
pop eax
out dx,al ;LBA地址7~0

inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA地址15~8

inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA地址23~16

inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;第一硬盘 LBA地址27~24
out dx,al

inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al

.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输

mov ecx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw

pop edx
pop ecx
pop eax

ret

;-------------------------------------------------------------------------------
make_gdt_descriptor: ;构造描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性(各属性位都在原始
; 位置,其它没用到的位置0)
;返回:EDX:EAX=完整的描述符
mov edx,eax
shl eax,16
or ax,bx ;描述符前32位(EAX)构造完毕

and edx,0xffff0000 ;清除基地址中无关的位
rol edx,8
bswap edx ;装配基址的31~24和23~16 (80486+)

xor bx,bx
or edx,ebx ;装配段界限的高4位

or edx,ecx ;装配属性

ret

;-------------------------------------------------------------------------------
pgdt dw 0
dd 0x00007e00 ;GDT的物理地址
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa

内核程序

    ;以下常量定义部分。内核的大部分内容都应当固定
core_code_seg_sel equ 0x38 ;内核代码段选择子
core_data_seg_sel equ 0x30 ;内核数据段选择子
sys_routine_seg_sel equ 0x28 ;系统公共例程代码段的选择子
video_ram_seg_sel equ 0x20 ;视频显示缓冲区的段选择子
core_stack_seg_sel equ 0x18 ;内核堆栈段选择子
mem_0_4_gb_seg_sel equ 0x08 ;整个0-4GB内存的段的选择子

;-------------------------------------------------------------------------------
;以下是系统核心的头部,用于加载核心程序
core_length dd core_end ;核心程序总长度#00

sys_routine_seg dd section.sys_routine.start
;系统公用例程段位置#04

core_data_seg dd section.core_data.start
;核心数据段位置#08

core_code_seg dd section.core_code.start
;核心代码段位置#0c


core_entry dd start ;核心代码段入口点#10
dw core_code_seg_sel

;===============================================================================
[bits 32]
;===============================================================================
SECTION sys_routine vstart=0 ;系统公共例程代码段
;-------------------------------------------------------------------------------
;字符串显示例程
put_string: ;显示0终止的字符串并移动光标
;输入:DS:EBX=串地址
push ecx
.getc:
mov cl,[ebx]
or cl,cl
jz .exit
call put_char
inc ebx
jmp .getc

.exit:
pop ecx
retf ;段间返回

;-------------------------------------------------------------------------------
put_char: ;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
pushad

;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
in al,dx ;高字
mov ah,al

dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
in al,dx ;低字
mov bx,ax ;BX=代表光标位置的16位数

cmp cl,0x0d ;回车符?
jnz .put_0a
mov ax,bx
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor

.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other
add bx,80
jmp .roll_screen

.put_other: ;正常显示字符
push es
mov eax,video_ram_seg_sel ;0xb8000段的选择子
mov es,eax
shl bx,1
mov [es:bx],cl
pop es

;以下将光标位置推进一个字符
shr bx,1
inc bx
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor

push ds
push es
mov eax,video_ram_seg_sel
mov ds,eax
mov es,eax
cld
mov esi,0xa0 ;小心!32位模式下movsb/w/d
mov edi,0x00 ;使用的是esi/edi/ecx
mov ecx,1920
rep movsd
mov bx,3840 ;清除屏幕最底一行
mov ecx,80 ;32位程序应该使用ECX
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls

pop es
pop ds

mov bx,1920

.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
mov al,bh
out dx,al
dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
mov al,bl
out dx,al

popad
ret

;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
push eax
push ecx
push edx

push eax

mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数

inc dx ;0x1f3
pop eax
out dx,al ;LBA地址7~0

inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA地址15~8

inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA地址23~16

inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;第一硬盘 LBA地址27~24
out dx,al

inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al

.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输

mov ecx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw

pop edx
pop ecx
pop eax

retf ;段间返回

;-------------------------------------------------------------------------------
;汇编语言程序是极难一次成功,而且调试非常困难。这个例程可以提供帮助
put_hex_dword: ;在当前光标处以十六进制形式显示
;一个双字并推进光标
;输入:EDX=要转换并显示的数字
;输出:无
pushad
push ds

mov ax,core_data_seg_sel ;切换到核心数据段
mov ds,ax

mov ebx,bin_hex ;指向核心数据段内的转换表
mov ecx,8
.xlt:
rol edx,4
mov eax,edx
and eax,0x0000000f
xlat

push ecx
mov cl,al
call put_char
pop ecx

loop .xlt

pop ds
popad
retf

;-------------------------------------------------------------------------------
allocate_memory: ;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
push ds
push eax
push ebx

mov eax,core_data_seg_sel
mov ds,eax

mov eax,[ram_alloc]
add eax,ecx ;下一次分配时的起始地址

;这里应当有检测可用内存数量的指令

mov ecx,[ram_alloc] ;返回分配的起始地址

mov ebx,eax
and ebx,0xfffffffc
add ebx,4 ;强制对齐
test eax,0x00000003 ;下次分配的起始地址最好是4字节对齐
cmovnz eax,ebx ;如果没有对齐,则强制对齐
mov [ram_alloc],eax ;下次从该地址分配内存
;cmovcc指令可以避免控制转移
pop ebx
pop eax
pop ds

retf

;-------------------------------------------------------------------------------
set_up_gdt_descriptor: ;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子
push eax
push ebx
push edx

push ds
push es

mov ebx,core_data_seg_sel ;切换到核心数据段
mov ds,ebx

sgdt [pgdt] ;以便开始处理GDT

mov ebx,mem_0_4_gb_seg_sel
mov es,ebx

movzx ebx,word [pgdt] ;GDT界限
inc bx ;GDT总字节数,也是下一个描述符偏移
add ebx,[pgdt+2] ;下一个描述符的线性地址

mov [es:ebx],eax
mov [es:ebx+4],edx

add word [pgdt],8 ;增加一个描述符的大小

lgdt [pgdt] ;对GDT的更改生效

mov ax,[pgdt] ;得到GDT界限值
xor dx,dx
mov bx,8
div bx ;除以8,去掉余数
mov cx,ax
shl cx,3 ;将索引号移到正确位置

pop es
pop ds

pop edx
pop ebx
pop eax

retf
;-------------------------------------------------------------------------------
make_seg_descriptor: ;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
mov edx,eax
shl eax,16
or ax,bx ;描述符前32位(EAX)构造完毕

and edx,0xffff0000 ;清除基地址中无关的位
rol edx,8
bswap edx ;装配基址的31~24和23~16 (80486+)

xor bx,bx
or edx,ebx ;装配段界限的高4位

or edx,ecx ;装配属性

retf

;===============================================================================
SECTION core_data vstart=0 ;系统核心的数据段
;-------------------------------------------------------------------------------
pgdt dw 0 ;用于设置和修改GDT
dd 0

ram_alloc dd 0x00100000 ;下次分配内存时的起始地址

;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel

salt_2 db '@ReadDiskData'
times 256-($-salt_2) db 0
dd read_hard_disk_0
dw sys_routine_seg_sel

salt_3 db '@PrintDwordAsHexString'
times 256-($-salt_3) db 0
dd put_hex_dword
dw sys_routine_seg_sel

salt_4 db '@TerminateProgram'
times 256-($-salt_4) db 0
dd return_point
dw core_code_seg_sel

salt_item_len equ $-salt_4
salt_items equ ($-salt)/salt_item_len

message_1 db ' If you seen this message,that means we '
db 'are now in protect mode,and the system '
db 'core is loaded,and the video display '
db 'routine works perfectly.',0x0d,0x0a,0

message_5 db ' Loading user program...',0

do_status db 'Done.',0x0d,0x0a,0

message_6 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
db ' User program terminated,control returned.',0

bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表
core_buf times 2048 db 0 ;内核用的缓冲区

esp_pointer dd 0 ;内核用来临时保存自己的栈指针

cpu_brnd0 db 0x0d,0x0a,' ',0
cpu_brand times 52 db 0
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0

;===============================================================================
SECTION core_code vstart=0
;-------------------------------------------------------------------------------
load_relocate_program: ;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
push ebx
push ecx
push edx
push esi
push edi

push ds
push es

mov eax,core_data_seg_sel
mov ds,eax ;切换DS到内核数据段

mov eax,esi ;读取程序头部数据
mov ebx,core_buf
call sys_routine_seg_sel:read_hard_disk_0

;以下判断整个程序有多大
mov eax,[core_buf] ;程序尺寸
mov ebx,eax
and ebx,0xfffffe00 ;使之512字节对齐(能被512整除的数,
add ebx,512 ;低9位都为0
test eax,0x000001ff ;程序的大小正好是512的倍数吗?
cmovnz eax,ebx ;不是。使用凑整的结果

mov ecx,eax ;实际需要申请的内存数量
call sys_routine_seg_sel:allocate_memory
mov ebx,ecx ;ebx -> 申请到的内存首地址
push ebx ;保存该首地址
xor edx,edx
mov ecx,512
div ecx
mov ecx,eax ;总扇区数

mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段
mov ds,eax

mov eax,esi ;起始扇区号
.b1:
call sys_routine_seg_sel:read_hard_disk_0
inc eax
loop .b1 ;循环读,直到读完整个用户程序

;建立程序头部段描述符
pop edi ;恢复程序装载的首地址
mov eax,edi ;程序头部起始线性地址
mov ebx,[edi+0x04] ;段长度
dec ebx ;段界限
mov ecx,0x00409200 ;字节粒度的数据段描述符
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+0x04],cx

;建立程序代码段描述符
mov eax,edi
add eax,[edi+0x14] ;代码起始线性地址
mov ebx,[edi+0x18] ;段长度
dec ebx ;段界限
mov ecx,0x00409800 ;字节粒度的代码段描述符
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+0x14],cx

;建立程序数据段描述符
mov eax,edi
add eax,[edi+0x1c] ;数据段起始线性地址
mov ebx,[edi+0x20] ;段长度
dec ebx ;段界限
mov ecx,0x00409200 ;字节粒度的数据段描述符
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+0x1c],cx

;建立程序堆栈段描述符
mov ecx,[edi+0x0c] ;4KB的倍率
mov ebx,0x000fffff
sub ebx,ecx ;得到段界限
mov eax,4096
mul dword [edi+0x0c]
mov ecx,eax ;准备为堆栈分配内存
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;得到堆栈的高端物理地址
mov ecx,0x00c09600 ;4KB粒度的堆栈段描述符
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+0x08],cx

;重定位SALT
mov eax,[edi+0x04]
mov es,eax ;es -> 用户程序头部
mov eax,core_data_seg_sel
mov ds,eax

cld

mov ecx,[es:0x24] ;用户程序的SALT条目数
mov edi,0x28 ;用户程序内的SALT位于头部内0x2c处
.b2:
push ecx
push edi

mov ecx,salt_items
mov esi,salt
.b3:
push edi
push esi
push ecx

mov ecx,64 ;检索表中,每条目的比较次数
repe cmpsd ;每次比较4字节
jnz .b4
mov eax,[esi] ;若匹配,esi恰好指向其后的地址数据
mov [es:edi-256],eax ;将字符串改写成偏移地址
mov ax,[esi+4]
mov [es:edi-252],ax ;以及段选择子
.b4:

pop ecx
pop esi
add esi,salt_item_len
pop edi ;从头比较
loop .b3

pop edi
add edi,256
pop ecx
loop .b2

mov ax,[es:0x04]

pop es ;恢复到调用此过程前的es段
pop ds ;恢复到调用此过程前的ds段

pop edi
pop esi
pop edx
pop ecx
pop ebx

ret

;-------------------------------------------------------------------------------
start:
mov ecx,core_data_seg_sel ;使ds指向核心数据段
mov ds,ecx

mov ebx,message_1
call sys_routine_seg_sel:put_string

;显示处理器品牌信息
mov eax,0x80000002
cpuid
mov [cpu_brand + 0x00],eax
mov [cpu_brand + 0x04],ebx
mov [cpu_brand + 0x08],ecx
mov [cpu_brand + 0x0c],edx

mov eax,0x80000003
cpuid
mov [cpu_brand + 0x10],eax
mov [cpu_brand + 0x14],ebx
mov [cpu_brand + 0x18],ecx
mov [cpu_brand + 0x1c],edx

mov eax,0x80000004
cpuid
mov [cpu_brand + 0x20],eax
mov [cpu_brand + 0x24],ebx
mov [cpu_brand + 0x28],ecx
mov [cpu_brand + 0x2c],edx

mov ebx,cpu_brnd0
call sys_routine_seg_sel:put_string
mov ebx,cpu_brand
call sys_routine_seg_sel:put_string
mov ebx,cpu_brnd1
call sys_routine_seg_sel:put_string

mov ebx,message_5
call sys_routine_seg_sel:put_string
mov esi,50 ;用户程序位于逻辑50扇区
call load_relocate_program

mov ebx,do_status
call sys_routine_seg_sel:put_string

mov [esp_pointer],esp ;临时保存堆栈指针

mov ds,ax

jmp far [0x10] ;控制权交给用户程序(入口点)
;堆栈可能切换

return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;使ds指向核心数据段
mov ds,eax

mov eax,core_stack_seg_sel ;切换回内核自己的堆栈
mov ss,eax
mov esp,[esp_pointer]

mov ebx,message_6
call sys_routine_seg_sel:put_string

;这里可以放置清除用户程序各种描述符的指令
;也可以加载并启动其它程序

hlt

;===============================================================================
SECTION core_trail
;-------------------------------------------------------------------------------
core_end:

用户程序

;===============================================================================
SECTION header vstart=0

program_length dd program_end ;程序总长度#0x00

head_len dd header_end ;程序头部的长度#0x04

stack_seg dd 0 ;用于接收堆栈段选择子#0x08
stack_len dd 1 ;程序建议的堆栈大小#0x0c
;以4KB为单位

prgentry dd start ;程序入口#0x10
code_seg dd section.code.start ;代码段位置#0x14
code_len dd code_end ;代码段长度#0x18

data_seg dd section.data.start ;数据段位置#0x1c
data_len dd data_end ;数据段长度#0x20

;-------------------------------------------------------------------------------
;符号地址检索表
salt_items dd (header_end-salt)/256 ;#0x24

salt: ;#0x28
PrintString db '@PrintString'
times 256-($-PrintString) db 0

TerminateProgram db '@TerminateProgram'
times 256-($-TerminateProgram) db 0

ReadDiskData db '@ReadDiskData'
times 256-($-ReadDiskData) db 0

header_end:

;===============================================================================
SECTION data vstart=0

buffer times 1024 db 0 ;缓冲区

message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,0
message_2 db ' Disk data:',0x0d,0x0a,0

data_end:

;===============================================================================
[bits 32]
;===============================================================================
SECTION code vstart=0
start:
mov eax,ds
mov fs,eax

mov eax,[stack_seg]
mov ss,eax
mov esp,0

mov eax,[data_seg]
mov ds,eax

mov ebx,message_1
call far [fs:PrintString]

mov eax,100 ;逻辑扇区号100
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadDiskData] ;段间调用

mov ebx,message_2
call far [fs:PrintString]

mov ebx,buffer
call far [fs:PrintString] ;too.

jmp far [fs:TerminateProgram] ;将控制权返回到系统

code_end:

;===============================================================================
SECTION trail
;-------------------------------------------------------------------------------
program_end:

ldt tss的使用

;以下常量定义部分。内核的大部分内容都应当固定
core_code_seg_sel equ 0x38 ;内核代码段选择子
core_data_seg_sel equ 0x30 ;内核数据段选择子
sys_routine_seg_sel equ 0x28 ;系统公共例程代码段的选择子
video_ram_seg_sel equ 0x20 ;视频显示缓冲区的段选择子
core_stack_seg_sel equ 0x18 ;内核堆栈段选择子
mem_0_4_gb_seg_sel equ 0x08 ;整个0-4GB内存的段的选择子
;-------------------------------------------------------------------------------
;以下是系统核心的头部,用于加载核心程序
core_length dd core_end ;核心程序总长度#00
sys_routine_seg dd section.sys_routine.start
;系统公用例程段位置#04
core_data_seg dd section.core_data.start
;核心数据段位置#08
core_code_seg dd section.core_code.start
;核心代码段位置#0c
core_entry dd start ;核心代码段入口点#10
dw core_code_seg_sel
;===============================================================================
[bits 32]
;===============================================================================
SECTION sys_routine vstart=0 ;系统公共例程代码段
;-------------------------------------------------------------------------------
;字符串显示例程
put_string: ;显示0终止的字符串并移动光标
;输入:DS:EBX=串地址
push ecx
.getc:
mov cl,[ebx]
or cl,cl
jz .exit
call put_char
inc ebx
jmp .getc
.exit:
pop ecx
retf ;段间返回
;-------------------------------------------------------------------------------
put_char: ;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
pushad
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
in al,dx ;高字
mov ah,al
dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
in al,dx ;低字
mov bx,ax ;BX=代表光标位置的16位数
cmp cl,0x0d ;回车符?
jnz .put_0a
mov ax,bx
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor
.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other
add bx,80
jmp .roll_screen
.put_other: ;正常显示字符
push es
mov eax,video_ram_seg_sel ;0xb8000段的选择子
mov es,eax
shl bx,1
mov [es:bx],cl
pop es
;以下将光标位置推进一个字符
shr bx,1
inc bx
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor

push ds
push es
mov eax,video_ram_seg_sel
mov ds,eax
mov es,eax
cld
mov esi,0xa0 ;小心!32位模式下movsb/w/d
mov edi,0x00 ;使用的是esi/edi/ecx
mov ecx,1920
rep movsd
mov bx,3840 ;清除屏幕最底一行
mov ecx,80 ;32位程序应该使用ECX
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls

pop es
pop ds

mov bx,1920

.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
mov al,bh
out dx,al
dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
mov al,bl
out dx,al

popad

ret

;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
push eax
push ecx
push edx

push eax

mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数

inc dx ;0x1f3
pop eax
out dx,al ;LBA地址7~0

inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA地址15~8

inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA地址23~16

inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;第一硬盘 LBA地址27~24
out dx,al

inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al

.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输

mov ecx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw

pop edx
pop ecx
pop eax

retf ;段间返回

;-------------------------------------------------------------------------------
;汇编语言程序是极难一次成功,而且调试非常困难。这个例程可以提供帮助
put_hex_dword: ;在当前光标处以十六进制形式显示
;一个双字并推进光标
;输入:EDX=要转换并显示的数字
;输出:无
pushad
push ds

mov ax,core_data_seg_sel ;切换到核心数据段
mov ds,ax

mov ebx,bin_hex ;指向核心数据段内的转换表
mov ecx,8
.xlt:
rol edx,4
mov eax,edx
and eax,0x0000000f
xlat

push ecx
mov cl,al
call put_char
pop ecx

loop .xlt

pop ds
popad
retf

;-------------------------------------------------------------------------------
allocate_memory: ;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
push ds
push eax
push ebx

mov eax,core_data_seg_sel
mov ds,eax

mov eax,[ram_alloc]
add eax,ecx ;下一次分配时的起始地址

;这里应当有检测可用内存数量的指令

mov ecx,[ram_alloc] ;返回分配的起始地址

mov ebx,eax
and ebx,0xfffffffc
add ebx,4 ;强制对齐
test eax,0x00000003 ;下次分配的起始地址最好是4字节对齐
cmovnz eax,ebx ;如果没有对齐,则强制对齐
mov [ram_alloc],eax ;下次从该地址分配内存
;cmovcc指令可以避免控制转移
pop ebx
pop eax
pop ds

retf

;-------------------------------------------------------------------------------
set_up_gdt_descriptor: ;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子
push eax
push ebx
push edx

push ds
push es

mov ebx,core_data_seg_sel ;切换到核心数据段
mov ds,ebx

sgdt [pgdt] ;以便开始处理GDT

mov ebx,mem_0_4_gb_seg_sel
mov es,ebx

movzx ebx,word [pgdt] ;GDT界限
inc bx ;GDT总字节数,也是下一个描述符偏移
add ebx,[pgdt+2] ;下一个描述符的线性地址

mov [es:ebx],eax
mov [es:ebx+4],edx

add word [pgdt],8 ;增加一个描述符的大小

lgdt [pgdt] ;对GDT的更改生效

mov ax,[pgdt] ;得到GDT界限值
xor dx,dx
mov bx,8
div bx ;除以8,去掉余数
mov cx,ax
shl cx,3 ;将索引号移到正确位置

pop es
pop ds

pop edx
pop ebx
pop eax

retf
;-------------------------------------------------------------------------------
make_seg_descriptor: ;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
mov edx,eax
shl eax,16
or ax,bx ;描述符前32位(EAX)构造完毕

and edx,0xffff0000 ;清除基地址中无关的位
rol edx,8
bswap edx ;装配基址的31~24和23~16 (80486+)

xor bx,bx
or edx,ebx ;装配段界限的高4位

or edx,ecx ;装配属性

retf

;-------------------------------------------------------------------------------
make_gate_descriptor: ;构造门的描述符(调用门等)
;输入:EAX=门代码在段内偏移地址
; BX=门代码所在段的选择子
; CX=段类型及属性等(各属
; 性位都在原始位置)
;返回:EDX:EAX=完整的描述符
push ebx
push ecx

mov edx,eax
and edx,0xffff0000 ;得到偏移地址高16位
or dx,cx ;组装属性部分到EDX

and eax,0x0000ffff ;得到偏移地址低16位
shl ebx,16
or eax,ebx ;组装段选择子部分

pop ecx
pop ebx

retf

sys_routine_end:

;===============================================================================
SECTION core_data vstart=0 ;系统核心的数据段
;-------------------------------------------------------------------------------
pgdt dw 0 ;用于设置和修改GDT
dd 0

ram_alloc dd 0x00100000 ;下次分配内存时的起始地址

;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel

salt_2 db '@ReadDiskData'
times 256-($-salt_2) db 0
dd read_hard_disk_0
dw sys_routine_seg_sel

salt_3 db '@PrintDwordAsHexString'
times 256-($-salt_3) db 0
dd put_hex_dword
dw sys_routine_seg_sel

salt_4 db '@TerminateProgram'
times 256-($-salt_4) db 0
dd return_point
dw core_code_seg_sel

salt_item_len equ $-salt_4
salt_items equ ($-salt)/salt_item_len

message_1 db ' If you seen this message,that means we '
db 'are now in protect mode,and the system '
db 'core is loaded,and the video display '
db 'routine works perfectly.',0x0d,0x0a,0

message_2 db ' System wide CALL-GATE mounted.',0x0d,0x0a,0

message_3 db 0x0d,0x0a,' Loading user program...',0

do_status db 'Done.',0x0d,0x0a,0

message_6 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
db ' User program terminated,control returned.',0

bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表

core_buf times 2048 db 0 ;内核用的缓冲区

esp_pointer dd 0 ;内核用来临时保存自己的栈指针

cpu_brnd0 db 0x0d,0x0a,' ',0
cpu_brand times 52 db 0
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0

;任务控制块链
tcb_chain dd 0

core_data_end:

;===============================================================================
SECTION core_code vstart=0
;-------------------------------------------------------------------------------
fill_descriptor_in_ldt: ;在LDT内安装一个新的描述符
;输入:EDX:EAX=描述符
; EBX=TCB基地址
;输出:CX=描述符的选择子
push eax
push edx
push edi
push ds

mov ecx,mem_0_4_gb_seg_sel
mov ds,ecx

mov edi,[ebx+0x0c] ;获得LDT基地址

xor ecx,ecx
mov cx,[ebx+0x0a] ;获得LDT界限
inc cx ;LDT的总字节数,即新描述符偏移地址

mov [edi+ecx+0x00],eax
mov [edi+ecx+0x04],edx ;安装描述符

add cx,8
dec cx ;得到新的LDT界限值

mov [ebx+0x0a],cx ;更新LDT界限值到TCB

mov ax,cx
xor dx,dx
mov cx,8
div cx

mov cx,ax
shl cx,3 ;左移3位,并且
or cx,0000_0000_0000_0100B ;使TI位=1,指向LDT,最后使RPL=00

pop ds
pop edi
pop edx
pop eax

ret

;-------------------------------------------------------------------------------
load_relocate_program: ;加载并重定位用户程序
;输入: PUSH 逻辑扇区号
; PUSH 任务控制块基地址
;输出:无
pushad

push ds
push es

mov ebp,esp ;为访问通过堆栈传递的参数做准备

mov ecx,mem_0_4_gb_seg_sel
mov es,ecx

mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址

;以下申请创建LDT所需要的内存
mov ecx,160 ;允许安装20个LDT描述符
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x0c],ecx ;登记LDT基地址到TCB中
mov word [es:esi+0x0a],0xffff ;登记LDT初始的界限到TCB中

;以下开始加载用户程序
mov eax,core_data_seg_sel
mov ds,eax ;切换DS到内核数据段

mov eax,[ebp+12*4] ;从堆栈中取出用户程序起始扇区号
mov ebx,core_buf ;读取程序头部数据
call sys_routine_seg_sel:read_hard_disk_0

;以下判断整个程序有多大
mov eax,[core_buf] ;程序尺寸
mov ebx,eax
and ebx,0xfffffe00 ;使之512字节对齐(能被512整除的数低
add ebx,512 ;9位都为0
test eax,0x000001ff ;程序的大小正好是512的倍数吗?
cmovnz eax,ebx ;不是。使用凑整的结果

mov ecx,eax ;实际需要申请的内存数量
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x06],ecx ;登记程序加载基地址到TCB中

mov ebx,ecx ;ebx -> 申请到的内存首地址
xor edx,edx
mov ecx,512
div ecx
mov ecx,eax ;总扇区数

mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段
mov ds,eax

mov eax,[ebp+12*4] ;起始扇区号
.b1:
call sys_routine_seg_sel:read_hard_disk_0
inc eax
loop .b1 ;循环读,直到读完整个用户程序

mov edi,[es:esi+0x06] ;获得程序加载基地址

;建立程序头部段描述符
mov eax,edi ;程序头部起始线性地址
mov ebx,[edi+0x04] ;段长度
dec ebx ;段界限
mov ecx,0x0040f200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor

;安装头部段描述符到LDT中
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt

or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [es:esi+0x44],cx ;登记程序头部段选择子到TCB
mov [edi+0x04],cx ;和头部内

;建立程序代码段描述符
mov eax,edi
add eax,[edi+0x14] ;代码起始线性地址
mov ebx,[edi+0x18] ;段长度
dec ebx ;段界限
mov ecx,0x0040f800 ;字节粒度的代码段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x14],cx ;登记代码段选择子到头部

;建立程序数据段描述符
mov eax,edi
add eax,[edi+0x1c] ;数据段起始线性地址
mov ebx,[edi+0x20] ;段长度
dec ebx ;段界限
mov ecx,0x0040f200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x1c],cx ;登记数据段选择子到头部

;建立程序堆栈段描述符
mov ecx,[edi+0x0c] ;4KB的倍率
mov ebx,0x000fffff
sub ebx,ecx ;得到段界限
mov eax,4096
mul ecx
mov ecx,eax ;准备为堆栈分配内存
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;得到堆栈的高端物理地址
mov ecx,0x00c0f600 ;字节粒度的堆栈段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x08],cx ;登记堆栈段选择子到头部

;重定位SALT
mov eax,mem_0_4_gb_seg_sel ;这里和前一章不同,头部段描述符
mov es,eax ;已安装,但还没有生效,故只能通
;过4GB段访问用户程序头部
mov eax,core_data_seg_sel
mov ds,eax

cld

mov ecx,[es:edi+0x24] ;U-SALT条目数(通过访问4GB段取得)
add edi,0x28 ;U-SALT在4GB段内的偏移
.b2:
push ecx
push edi

mov ecx,salt_items
mov esi,salt
.b3:
push edi
push esi
push ecx

mov ecx,64 ;检索表中,每条目的比较次数
repe cmpsd ;每次比较4字节
jnz .b4
mov eax,[esi] ;若匹配,则esi恰好指向其后的地址
mov [es:edi-256],eax ;将字符串改写成偏移地址
mov ax,[esi+4]
or ax,0000000000000011B ;以用户程序自己的特权级使用调用门
;故RPL=3
mov [es:edi-252],ax ;回填调用门选择子
.b4:

pop ecx
pop esi
add esi,salt_item_len
pop edi ;从头比较
loop .b3

pop edi
add edi,256
pop ecx
loop .b2

mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址

;创建0特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x1a],ecx
shr dword [es:esi+0x1a],12 ;登记0特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x1e],eax ;登记0特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c09600 ;4KB粒度,读写,特权级0
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
;or cx,0000_0000_0000_0000 ;设置选择子的特权级为0
mov [es:esi+0x22],cx ;登记0特权级堆栈选择子到TCB
mov dword [es:esi+0x24],0 ;登记0特权级堆栈初始ESP到TCB

;创建1特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x28],ecx
shr [es:esi+0x28],12 ;登记1特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x2c],eax ;登记1特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c0b600 ;4KB粒度,读写,特权级1
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0001 ;设置选择子的特权级为1
mov [es:esi+0x30],cx ;登记1特权级堆栈选择子到TCB
mov dword [es:esi+0x32],0 ;登记1特权级堆栈初始ESP到TCB

;创建2特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x36],ecx
shr [es:esi+0x36],12 ;登记2特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x3a],ecx ;登记2特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c0d600 ;4KB粒度,读写,特权级2
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0010 ;设置选择子的特权级为2
mov [es:esi+0x3e],cx ;登记2特权级堆栈选择子到TCB
mov dword [es:esi+0x40],0 ;登记2特权级堆栈初始ESP到TCB

;在GDT中登记LDT描述符
mov eax,[es:esi+0x0c] ;LDT的起始线性地址
movzx ebx,word [es:esi+0x0a] ;LDT段界限
mov ecx,0x00408200 ;LDT描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x10],cx ;登记LDT选择子到TCB中

;创建用户程序的TSS
mov ecx,104 ;tss的基本尺寸
mov [es:esi+0x12],cx
dec word [es:esi+0x12] ;登记TSS界限值到TCB
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;登记TSS基地址到TCB

;登记基本的TSS表格内容
mov word [es:ecx+0],0 ;反向链=0

mov edx,[es:esi+0x24] ;登记0特权级堆栈初始ESP
mov [es:ecx+4],edx ;到TSS中

mov dx,[es:esi+0x22] ;登记0特权级堆栈段选择子
mov [es:ecx+8],dx ;到TSS中

mov edx,[es:esi+0x32] ;登记1特权级堆栈初始ESP
mov [es:ecx+12],edx ;到TSS中

mov dx,[es:esi+0x30] ;登记1特权级堆栈段选择子
mov [es:ecx+16],dx ;到TSS中

mov edx,[es:esi+0x40] ;登记2特权级堆栈初始ESP
mov [es:ecx+20],edx ;到TSS中

mov dx,[es:esi+0x3e] ;登记2特权级堆栈段选择子
mov [es:ecx+24],dx ;到TSS中

mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+96],dx ;到TSS中

mov dx,[es:esi+0x12] ;登记任务的I/O位图偏移
mov [es:ecx+102],dx ;到TSS中

mov word [es:ecx+100],0 ;T=0

;在GDT中登记TSS描述符
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx ;登记TSS选择子到TCB

pop es ;恢复到调用此过程前的es段
pop ds ;恢复到调用此过程前的ds段

popad

ret 8 ;丢弃调用本过程前压入的参数

;-------------------------------------------------------------------------------
append_to_tcb_link: ;在TCB链上追加任务控制块
;输入:ECX=TCB线性基地址
push eax
push edx
push ds
push es

mov eax,core_data_seg_sel ;令DS指向内核数据段
mov ds,eax
mov eax,mem_0_4_gb_seg_sel ;令ES指向0..4GB段
mov es,eax

mov dword [es: ecx+0x00],0 ;当前TCB指针域清零,以指示这是最
;后一个TCB

mov eax,[tcb_chain] ;TCB表头指针
or eax,eax ;链表为空?
jz .notcb

.searc:
mov edx,eax
mov eax,[es: edx+0x00]
or eax,eax
jnz .searc

mov [es: edx+0x00],ecx
jmp .retpc

.notcb:
mov [tcb_chain],ecx ;若为空表,直接令表头指针指向TCB

.retpc:
pop es
pop ds
pop edx
pop eax

ret

;-------------------------------------------------------------------------------
start:
mov ecx,core_data_seg_sel ;使ds指向核心数据段
mov ds,ecx

mov ebx,message_1
call sys_routine_seg_sel:put_string

;显示处理器品牌信息
mov eax,0x80000002
cpuid
mov [cpu_brand + 0x00],eax
mov [cpu_brand + 0x04],ebx
mov [cpu_brand + 0x08],ecx
mov [cpu_brand + 0x0c],edx

mov eax,0x80000003
cpuid
mov [cpu_brand + 0x10],eax
mov [cpu_brand + 0x14],ebx
mov [cpu_brand + 0x18],ecx
mov [cpu_brand + 0x1c],edx

mov eax,0x80000004
cpuid
mov [cpu_brand + 0x20],eax
mov [cpu_brand + 0x24],ebx
mov [cpu_brand + 0x28],ecx
mov [cpu_brand + 0x2c],edx

mov ebx,cpu_brnd0 ;显示处理器品牌信息
call sys_routine_seg_sel:put_string
mov ebx,cpu_brand
call sys_routine_seg_sel:put_string
mov ebx,cpu_brnd1
call sys_routine_seg_sel:put_string

;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
mov edi,salt ;C-SALT表的起始位置
mov ecx,salt_items ;C-SALT表的条目数量
.b3:
push ecx
mov eax,[edi+256] ;该条目入口点的32位偏移地址
mov bx,[edi+260] ;该条目入口点的段选择子
mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才
;允许访问),0个参数(因为用寄存器
;传递参数,而没有用栈)
call sys_routine_seg_sel:make_gate_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+260],cx ;将返回的门描述符选择子回填
add edi,salt_item_len ;指向下一个C-SALT条目
pop ecx
loop .b3

;对门进行测试
mov ebx,message_2
call far [salt_1+256] ;通过门显示信息(偏移量将被忽略)

mov ebx,message_3
call sys_routine_seg_sel:put_string ;在内核中调用例程不需要通过门

;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将任务控制块追加到TCB链表

push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址

call load_relocate_program

mov ebx,do_status
call sys_routine_seg_sel:put_string

mov eax,mem_0_4_gb_seg_sel
mov ds,eax

ltr [ecx+0x18] ;加载任务状态段
lldt [ecx+0x10] ;加载LDT

mov eax,[ecx+0x44]
mov ds,eax ;切换到用户程序头部段

;以下假装是从调用门返回。摹仿处理器压入返回参数
push dword [0x08] ;调用前的堆栈段选择子
push dword 0 ;调用前的esp

push dword [0x14] ;调用前的代码段选择子
push dword [0x10] ;调用前的eip

retf

return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;因为c14.asm是以JMP的方式使用调
mov ds,eax ;用门@TerminateProgram,回到这
;里时,特权级为3,会导致异常。
mov ebx,message_6
call sys_routine_seg_sel:put_string

hlt

core_code_end:

;-------------------------------------------------------------------------------
SECTION core_trail
;-------------------------------------------------------------------------------
core_end: