X86汇编语言:从事模式到保护模式读书笔记
----------特权级保护
PS:此文并没有多长,只不过末尾附上了完整源代码占了90%的篇幅
1》只在第一次引用一个段的时候进行特权级检查
(你能进入就说明你用有访问权限,此后无需再检查)
2》不允许直接从特权级高的代码段转移到特权级
的代码段
(特权级比你还低,这样的程序信不过,所以禁止)
3》允许特权级高的代码段引用特权级低的数据段
整个内核程序组成
概念:
PL (Privilege Level) :特权级别
CPL(Current Privilege Level) :当前执行的代码段的特权级别,其
值就是段寄存器中的选择子的PL字段
RPL(Request Privilege Level) :选择子中的PL字段,它代表的是
调用者的特权级别
DPL(Description Privilege Level):段描述符中的DPL字段
特权级检查时刻:第一次引用一个段的时候即将一个选择子送到段寄存器之前。例如上图中在将选择子0x0008送入CS之前进行特权级检查
为什么需要RPL?
答:确保特权代码不会代替应用程序访问一个段,除非应用程序拥有访问那个段的权限。举个例子:特权级为3的user调用OS库函数read从磁盘读取数据到一个数据段中。如果user知道了OS数据段的段选择子并不怀好意的把它当作参数传给read,此时RPL就发挥作用了。具体如下:
1》检查当前特权级是否高于或等于数据段描述符的DPL
2》 检查请求特权级是否高于或等于数据段描述符的DPL
3》一旦上面任意一个条件不满足就引发异常中断。
为更好地理解上面那段话我们需要明白这样一个事实:处理器所做的只是机械的检查RPL,CPL,DPL是否满足条件,并不具备鉴别RPL的有效性的能力。什么意思呢?其实意思就是:处理器说我只负责检查RPL,不能判断RPL是否有效,所以OS你必须给我提供正确的RPL字段,如果OS你提供了错误的RPL给我,我也会按照上面的规则进行特权级检查,如果错误的选择子通过了上面的检查,那么就会代入不该代入的选择子,那么系统就很有可能因此崩溃,但这是你OS的错,不管我的事。接着上面的例子:user的PL为3,传递给read函数的数据段选择子的RPL应该为调用者的PL即(3),如果user想写OS的数据段,那么在把选择子送入DS之前进行特权级检查。因为此时已经转到os内核执行了,故CPL为0,OS数据段的DPL=0,第一个条件通过,接下来RPL为3 低于OS数据段的DPL,于是引发异常中断,如果OS提供了错误的RPL字段即RPL=0,那么就会通过上面的特权级检查,那么磁盘读取的数据就可以写到OS的数据段了,系统数据段被篡改,那么系统就很可能因此崩溃,但这一切都是OS自己的错。
接上面:OS一定能提供正确的选择子吗?又是如何提供正确的选择子呢 ?
答:OS一定能提供正确的选择子。什么意思呢?
来人,上代码!
;输入:EDX:EAX=描述符
; EBX=TCB基地址
;返回:CX=描述符的选择子
;说明:此函数可能为多个不同特权级的
; 程序创建LDT描述符,故RPL字段
; 通通设为0,由OS去修改去
fill_descriptor_in_ldt:
.........
or cx,0x0004 ;选择子返回之前设置TI位表示
;描述符在LDT中并且将RPL设为00
.....
ret
;接下来看OS如何修改RPL
;功能:加载并重定位用户程序
;参数:push逻辑扇区号
; push任务控制块基地址
;返回:无
load_relocate_program:
.....
mov ebx,esi;TCB的基地址
call fill_descriptor_in_ldt;此调用返回的选择子的
;RPL字段为0
or cx,0x0003 ;设置选择子的RPL为3(用户程序)
;***************VeryImportant:************************
;** ;OS和CPU两个协议好了:OS向cpu保证提供的选
;** 择子中一定包含正确的RPL,而CPU负责检查RPL
;** ;OS很清楚当前加载的程序的特权级,于是OS
;** 就一定能填充正确的RPL到选择子中(457行)
;**;OS怎么就知道它的特权级呢?那是因为load_relocate_program
;** 就是专门用来加载特权级别为3的用户程序的,只要调用这个
;** 函数OS就知道RPL字段一定是3 。如果要加载特权级别为2
;** 的程序,那么可以写一个专门用来加载特权级为2的程序的函数
;** 只要调用此函数就知道特权级一定为2,于是OS就能保证提供
;** 选择子的RPL字段是正确的
;***************************************************
我们从上一篇文章知道如果用户程序通过存放在头部的选择子来引用一个段,那么选择子中的RPL字段就会在os加载用户程序时修改为用户程序的特权级,但是如果这样调用呢:还是上面的read调用
;假设read用到的OS的数据段选择子为0xf0,并通过eax传递
mov eax,0xf0
call far[fs:read];
;乍一看:数据段选择子的0xf0的RPL字段不是00吗
;不是说如果请求特权级高于或等于数据段描述符的DPL(00)
;就可以通过检查了吗?这里RPL刚好等于DPL,然后不就可以
;将数据读到OS数据段了吗 ? 别急 ,讲完调用门再解释
发生段间转移时存在特权级检查:
1》jmp非依从段:只能在相同特权级段之间转移,不改变CPL
2》jmp依从段 :从低特权级转移到相同特权级或高特权级,不改变 CPL(这个我也不大懂)
3》call非依从段:在相同特权级段之间转移,不改变CPL
4》call非依从段:低特权级别转移到高特权级,必须通过调用门转移,
会修改CPL为目标代码的特权级
5》call依从段 :同2》
调用门:用户程序的PL字段为3,而内核提供的系统调用的PL字段为0,
而处理器禁止从低特权级转移到高特权级,所以需要特殊的方
法,此即调用门。(注:调用门是cpu硬件支持并且也是cpu要
求的)
调用调用门时的特权级检查:
1》当前特权级(CPL)要高于或等于调用门的DPL字段
请求特权级(RPL)要高于或等于调用门的DPL字段
2》当前特权级(CPL)要低于或等于目标代码段
的描述符的DPL字段
注:条件1规定了当前程序是否有权调用该调用门
条件2规定了当前程序是否允许跳转到目标代码段执行
从内核转移到用户程序并执行这个过程是从高特权级转移到低特权级,但是前面说过不允许直接从特权级高的代码段转移到特权级低的代码段。乍一看,这不自相矛盾吗?仔细分析一下调用门就OK了。从低特权级调用调用门,然后转到高特权级代码执行,然后再返回。咦,返回?不就是从高特权级返回到低特权级吗?是的,也就是说处理器允许从高特权级返回到低特权级但必须是通过调用门的retf指令实现转移,也就是说我们只要模拟调用门的环境然后通过执行retf指令就可以实现把控制权从内核转移到用户程序了
话不多说,上代码:
;模拟从调用门返回将控制权转移到用户程序
;这里需要/模拟/需要改变特权级/的情况/(内核到用户,0到3)
;模仿处理器压入参数
push dword [0x08];调用者的堆栈段选择子
push dword 0 ;调用者的esp=0
push dword [0x14];调用者的代码段选择子
push dword [0x10];调用者的eip
;至此,调用门环境已经模拟出来了,接下来就该返回了
retf ;核心指令:将控制权转移到用户程序
;模拟从调用门返回将控制权转移到用户程序
;类似于《王爽汇编语言》中模拟中断,然后再”调用”中断例程
;相同点:
;跳转前:在程序跳转前由硬件完成一些列操作
; 调用门:如果要改变特权级则硬件实现栈切换同时SS:SP入栈和
; 复制参数到新栈,再cs:ip入栈,跳转
; 中断 :硬件实现取中断号,cs:ip入栈,EFLAGSREG入栈,关IF关TF,跳转
;返回时:
; 调用门:retf返回,当执行retf指令时cpu自动进行各种检查
; 中断 :iret返回
; 方法 :模拟的关键点在于最后的返回指令,
; iret要求栈中必须含有CS:IP,retf则要求3~12行
; 就是说用软件完成与硬件相同的的操作,即可创建出返回指令
; 返回所需的条件,即完成软件模拟
调用门返回时进行特权级检查:
当一个用户程序通过调用门调用系统调用,调用结束后将要返回
到用户程序时”CPU”所做工作的最后一步便是检查DS,ES,FS,GS
段寄存器的内容,通过选择子找到段描述符。如果有任意一个
段描述符的DPL字段高于(没有等于)调用者的特权级,那么就会将00送往
该段寄存器。什么意思呢?举个例子:一个特权级为3的用户
程序调用特权级为0的系统调用read读取一段数据,read函数
需要用到操作系统的某些私有数据,read函数通过ES访问
该私有数据空间,于是乎ES中存放的便是OS私有数据段的
选择子,并且在read执行完成后OS私有数据段的选择子依
旧放在ES中,如果user知道了这个秘密,user就可能
会对OS私有数据段有点想法,便想通过ES访问私有数据段。
于是此时最后一步便派上用场了:作为read调用者的user
的特权级别为3,而OS私有数据段的特权级为0,
“CPU”通过这一步检查便知道ES所指的数据段是OS的,
不能随便给用户程序用,(OS和CPU互相帮助,共同抵御
邪恶的user)于是便把该ES段寄存器内的选择子置0
,因为使用全0的选择子访问内存会引发异常中断
,于是user便不敢再妄想通过ES修改OS数据段了
这正和CPU的意:随你用,只要你敢,你不崩溃
算我输(从这点来说:也许CPU比user更坏)
好了,讲完了调用门,我们现在来回答上面那个问题。
mov eax,0xf0
call far[fs:read];
在调用调用门时尽管你提供的数据段选择子的RPL字段为00,但是OS还是有办法知道你提供的RPL是否是正确的。OS是怎么做的呢?为此CPU专门提供了arpl指令来调整段选择子RPL字段的值(Adjust RPL Field of Segment Selector)。
arpl r/m16,r16 (arpl 目的操作数,源操作数)
该指令比较两个段选择子的RPL字段,如果目的操作数的RPL在数值上小于源操作数的RPL字段,则设置ZF标志,并且调正目的操作数的RPL使之和源操作数的RPL字段的值相同。arpl指令是典型的操作系统指令,它通常用于调整传递给操作系统的段选择子,使其RPL字段和应用程序的特权级相匹配,在这种情况下传递给OS的段选择子是作为目的操作数出现的,而应用程序的代码段选择子是作为源操作数出现的,这样,为了防止恶意的数据访问,操作系统应该从当前栈中取得用户程序的代码段选择子(PL=3),作为源操作数,并把当作参数传进来的数据段选择子0xf0(RPL=0)作为目的操作数来执行arpl指令,如果RPL字段不匹配(很明显3!=0),则把数据段选择子的RPL字段调整为应用程序的特权级别3(0xf0调整为0xf3),那么在read函数在将数据段选择子代入段寄存器时将不会通过特权级检查(前面说过:1》检查CPL是否高于数据段DPL 2》检查RPL是否高于数据段的DPL,很明显第一条可以通过,而第二条则通不过,就会引发异常中断,然后应用程序的阴谋便无法得逞了,不是吗?)
read函数可能是这样的
read:
get_user_cs_from_stack
put_it_at_ebx
arpl ebx,eax
do_read
最后可能有人还有点小疑问:应用程序的CS怎么就会放到栈中呢?再回过头去看下我们是如何模拟调用门环境的:在通过调用门执行read之前,硬件会进行一系列操作以供日后从调用门返回时用,其中就包括将CS段寄存器中的选择子入栈,而这个选择子的PL字段代表的一定是应用程序的特权级,于是OS不就可以得到应用程序的特权级了吗?
V
————第14章读书笔记 · 完———
文章末尾附上“内核”(也许担不起内核这个名字)完整源代码:
四个文件:
引导程序负责从实模式进入保护模式并加载内核程序到内存,写入0号扇区
内核程序负责加载一个用户程序,写入1号扇区
用户程序完成从硬盘读一个文件然后在屏幕上显示,写入50号扇区
数据文件,写入100号扇区
//引导程序
;代码清单13-1
;文件名:c13_mbr.asm
;文件说明:硬盘主引导扇区代码
;创建日期:2011-10-28 22:35 ;设置堆栈段和栈指针
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
//接下来是内核源代码
;程序清单:特权级检查示例
;文件说明:内核程序
;创建日期:2017/4/21 10:59
;作者 :邓朝丰
;。。。此程序有BUG。。无法运行。。
;BUG个头啊,见第352行
user_sector equ 0x32
;以下常量定义部分。内核的大部分内容都应当固定
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
;-------------------------------------------------------------------------------
;构造调用门描述符
;参数:EAX=门代码在段内的偏移地址
; BX=门代码所在断的选择子
; CX=段类型及属性
;返回:EDX:EAX=完整描述符
make_gate_descriptor:
push ebx
push ecx
mov edx,eax
and edx,0xffff0000
or dx,cx
and eax,0x0000ffff
shl ebx,16
or eax,ebx
pop ecx
pop ebx
;pop edx 照着他的敲,纠结了两三天为什么不对,
; 原来是自己一不小心ebx写成了edx。
; 郁闷。2017/4/22
retf ;通过远调用调用
sys_routine_end:
;===============================================================================
section core_data vstart=0
;-------------------------------------------------------------------------------
pgdt dw 00 ;用于设置和修改GDT
dd 00
ram_alloc dd 0x00100000
;符号检索表
salt:
salt_1 db"@PrintString"
times 256-($-salt_1) db 00
dd put_string
dw sys_routine_seg_sel
;此处只是暂时填写段选择子,当内核运行时会先安装调用门
;将调用门的选择子回填到此处,于是前面四字节的偏移地址
;就没用了。
;注意:用户程序调用是通过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链:链表实现
tcb_chain dd 0;链表首指针
core_data_end:
;===============================================================================
section core_code vstart=0
;-------------------------------------------------------------------------------
;功能:在LDT内安装一个新的描述符
;输入:EDX:EAX=描述符
; EBX=TCB基地址//注意:此处的TCB的基地址是对于整个0-4G空间来说的即段地址为0000000
;返回:CX=描述符的选择子
fill_descriptor_in_ldt:
push eax
push edx
push edi
push ds
mov ecx,mem_0_4_gb_seg_sel
mov ds,ecx ;在将ecx装入ds前会进行段描述符有效性和特权级检查
;段保护检查:1》段是否存在2》段用途是否与type字段相同3》段是否在内存中
;特权级检查:目标数据段的DPL要低于或者等于CPL 即数值上DPL》=CPl
; 目标数据段的DPL要低于或等于RPL 即数值上DPL》=RPL
; 这样可以防止特权级为3的user(RPL=3)利用特权级为0的os库函数
; 来读写特权级为0的os数据段因为当RPL低于DPL时,因为当RPL低于
; DPL时,os就知道user想要读写os数据段了,于是阻止并引发异常中断
;笔记:1》执行时进行存储器保护检查。
; 2》第一次引用一个段时进行特权级检查和段描述符有效性检查
mov edi,[ebx+0x0c] ;从TCB中获取LDT的基地址
xor ecx,ecx
mov cx,[ebx+0x0a] ;从TCB中获取LDT的界限
inc cx ;新安装的描述符的偏移地址
mov [edi+ecx+0x00],eax
mov [edi+ecx+0x04],edx
add cx,7
mov [ebx+0x0a],cx ;将LDT界限写回
and cx,0xfff8 ;求新安装的描述符的选择子
or cx,0x0004 ;此行VeryImportant:选择子返回之前设置TI位置1指向LDT并且将RPL设为00
;?????????为什么要将RPL设为00,不是这是用户程序的段描述符吗????
;见此程序法第544行:or cx,0x0003
;因为这个函数可能要加载不同特权级别的程序
;并为之创建段描述符,于是乎os便在这个函数里将RPL通通定为00
;但是OS是清楚该程序的RPL是多少,于是乎调用完此程序后OS就
;修改段选择子的RPL,将它设置成正确的值(即用户程序的特权级)
pop ds
pop edi
pop edx
pop eax
ret ;内核私有函数不提供给用户程序故近调用即可
;-------------------------------------------------------------------------------
;功能:加载并重定位用户程序
;参数:push逻辑扇区号
; push任务控制块基地址
;返回:无
load_relocate_program:
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
call sys_routine_seg_sel:allocate_memory;分配160个字节
mov [es:esi+0x0c],ecx ;登记LDT基地址到TCB中
mov word[es:esi+0x0a],0xffff ;登记LDT初始的界限到TCB中(0xffff+1=0000)
;开始加载用户程序
mov eax,core_data_seg_sel
mov ds,eax
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
add ebx,512
test eax,0x000001ff
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
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];TCB中获取程序加载基地址
;建立头部段描述符 (因为user是通过头部段中的变量内容来访问段和调用系统函数,故头部需要建立一个段)
mov eax,edi
mov ebx,[edi+0x04];段长度
dec ebx;段界限
mov ecx,0x0040f200;字节粒度的数据段描述符,特权级3(由os为user创建段描述符,由os决定用户程序特权级)
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi;TCB的基地址
call fill_descriptor_in_ldt
or cx,0x0003 ;设置选择子的RPL为3(用户程序)
;********************************VeryImportant:***************************************************
;*********** ;OS和CPU两个协议好了:OS向cpu保证提供的选择子中一定包含正确的RPL,而CPU负责检查RPL**
;*********** ;OS很清楚当前加载的程序的特权级,于是OS就一定能填充正确的RPL到选择子中(457行)*****
;*************************************************************************************************
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,0x0003
mov [edi+0x14],cx ;登记代码段选择子到头部
;????????????????????????疑惑:为什么只需登记头部选择子到TCB中???????????????????????
;建立用户程序数据段描述符
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
call fill_descriptor_in_ldt
or cx,0x0003
mov [edi+0x1c],cx
;建立堆栈段描述符
mov ecx,[edx+0x0c]
mov ebx,0x000fffff;4KB的倍率
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
call fill_descriptor_in_ldt
or cx,0x0003
mov [edi+0x08],cx
;重定位SALT
mov eax,mem_0_4_gb_seg_sel ;此处与13章不同,因为此处头部段的段描述符是安装在LDT中,
;而此时LDT未加载到LDTR,故只能用4gb的段选择子来访问头部
mov es,eax
mov eax,core_data_seg_sel
mov ds,eax
mov ecx,[es:edi+0x24];需要重定位的表项数
cld ;正方向匹配
add edi,0x28 ;user-salt在4GB段中的偏移
.b2: ;外层循环
push ecx
push edi
mov ecx,salt_items;esi:内核salt表起始地址
mov esi,salt ;edi:用户salt表起始地址
.b3: ;内层循环
push edi
push esi
push ecx
mov ecx,64
repe cmpsd
jnz .b4
mov eax,[esi] ;若匹配,则esi指向内核salt表中该字符串对应的选择子的地址
mov [es:edi-256],eax ;将函数的偏移地址写到用户表中
mov ax,[esi+4]
or ax,0x0003 ;将调用者(即用户程序)的特权级写进选择子的RPL中(OS向CPU保证一定提供正确的RPL)
mov [es:edi-252],ax ;将调用门的选择子回填到用户程序
.b4:
pop ecx
pop esi
add esi,salt_item_len ;比较内核salt表的下一项
pop edi ;从头比较
loop .b3
pop edi
add edi,256;比较usersalt的下一项
pop ecx
loop .b2
mov esi,[ebp+11*4] ;从堆栈取得TCB基地址
;创建0特权级堆栈
mov ecx,4096
mov eax,ecx
mov [es:esi+0x1a],ecx ;将0级栈大小写回TCB
shr dword [es:esi+0x1a],12 ;倍率为4KB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈的基地址(向下生长)
mov [es:esi+0x1e],eax ;将栈基地址写进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;在LDT中登记0栈段描述符
;or cx,0x0003 设置0选择子的特权级为0
mov [es:esi+0x22],cx ;将0栈段选择子回填到TCB中
mov dword [es:esi+0x24],0;将0特权级堆栈的初始ESP设置为0
;创建1特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x28],ecx
shr dword[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 dword[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中
;创建user的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;前一个任务的TSS的指针值设为0
mov edx,[es:esi+0x24] ;esi:全局空间中TCB的首地址
mov [es:ecx+4],edx ;ecx:全局空间中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位图地址字段设置为TSS大小
mov [es:ecx+102],dx ;表示不存在I/O位图
mov word [es:ecx+100],0 ;T位清零 T=0 不产生中断 T=1:每次切换到此任务将产生一次调试异常中断
;在GDT中登记TSS
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段界限
mov ecx,0x00408900 ;TSS描述符(由TYPE字段区分),特权级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中
;LDT,TSS,调用门均属于系统段
;cpu根据TYPE字段来区LDT段描述符,TSS段描述符,调用门描述符
;LDT,TSS特权级均为0(不知为啥)
pop es
pop ds
popad
ret 8 ;丢弃掉调用本过程前压入的参数
;-------------------------------------------------------------------------------
;功能:在TCB链中添加一个任务控制块
;参数:ECX=TCB线性基地址
;返回:无
append_to_TCB_link: ;头插法入链
push eax
push ds
push es
mov eax,core_data_seg_sel
mov ds,eax ;内核数据段
mov eax,mem_0_4_gb_seg_sel
mov es,eax;全局数据段
mov eax,[ds:tcb_chain]
mov [es:ecx],eax
mov [ds:tcb_chain],ecx
pop es
pop ds
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
;开始安装整个系统服务的调用门。特权级之间转移必须使用门
;1》jmp依从段不改变CPL
;2》jmp非依从段则必有CPL=目标代码段的DPL
;3》jmp调用门不改变CPL。。
;4》call依从段则不改变CPL
;5》call非依从段则必须通过调用门
mov edi,salt ;core_salt表起始位置
mov ecx,salt_items;core_salt条目数
.lopx:
push ecx
mov eax,[edi+256] ;32位偏移地址
mov bx,[edi+260] ;16位短选择子
mov cx,0xec00 ;特权级为3的调用门(3以上的特权级别)才允许访问,
;0个参数(因为用寄存器传递参数而没用栈)
call sys_routine_seg_sel:make_gate_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+260],cx ;将门描述符选择子回填至core—salt表
add edi,salt_item_len
pop ecx
loop .lopx
;对门进行测试
mov ebx,message_2
call far[salt_1+256];调用门:偏移量将被忽略
mov ebx,message_3
call sys_routine_seg_sel:put_string ;在内核中调用例程不需要调用门
;创建任务控制块。这不是处理器的要求,但这是OS为了方便任务管理所必须的
mov ecx,0x46 ;先声明一个大小为70B的TCB
call sys_routine_seg_sel:allocate_memory
call append_to_TCB_link ;将任务控制块插入到TCB链头部
push dword 50 ;用户程序位于逻辑扇区50
push ecx ;压入任务控制块起始线性地址
call load_relocate_program ;创建一个任务并初始化任务的TCB
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] ;加载TSS
lldt [ecx+0x10] ;加载LDT
movzx eax,word[ecx+0x44]
mov ds,eax ;切换到用户程序头部
;模拟从调用门返回将控制权转移到用户程序
;模仿处理器压入参数
push dword [0x08] ;调用者的堆栈段选择子
push dword 0 ;!!调用前的esp=0????????为什么,等下去看书
;答:调用前的esp初始化为0,然后0-1就等于0xffffffff了
;注意:只是在用esp访问存储器时才会进行段界限检查,此处不会检查esp
;顶多检查ss段选择子
push dword [0x14] ;调用者的代码段选择子
push dword [0x10] ;调用者的eip
retf ;模拟从调用门返回将控制权转移到用户程序
;类似于《王爽汇编语言》中模拟中断,然后再"调用"中断例程
;相同点:跳转前:在程序跳转前由硬件完成一些列操作
; 调用门:如果要改变特权级则硬件实现栈切换同时SS:SP入栈和
; 复制参数到新栈,再cs:ip入栈,跳转
; 中断 : 硬件实现取中断号,cs:ip入栈,EFLAGSREG入栈,关IF关TF,跳转
; 返回时:
; 调用门:retf返回,当执行retf指令时cpu自动进行各种检查
; 中断 :iret返回
;方法 :模拟的关键点在于最后的返回指令,
; iret要求栈中必须含有CS:IP,retf则要求866~873
; 就是说用软件实现与硬件相同的的操作,创建相同的返回指令
; 返回所需的条件即可
return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;因为c14.asm是以JMP的方式使用调
mov ds,eax ;用门@TerminateProgram,回到这
;里时,特权级仍然为3,会导致异常(817)。
mov ebx,message_6
call sys_routine_seg_sel:put_string
hlt
core_code_end:
;===============================================================================
section core_trail
core_end:
//接下来是用户程序
;代码清单13-3
;文件名:c13.asm
;文件说明:用户程序
;创建日期:2011-10-30 15:19
;===============================================================================
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:
//下面是数据文件
The Intel386 Processor (1985)
The Intel386 processor was the first 32-bit processor in the IA-32 architecture family. It introduced 32-bit registers for use both to hold operands and for addressing. The lower half of each 32-bit Intel386 register retains the properties of the 16-bit registers of earlier generations, permitting backward compatibility. The processor also provides a virtual-8086 mode that allows for even greater efficiency when executing programs created for 8086/8088 processors. [END]