我是靠谱客的博主 干净魔镜,最近开发中收集的这篇文章主要介绍25、保护模式程序的动态加载和执行01、本章目标和内容提要02、内核结构和加载前的准备工作03、创建安装内核中各段的描述符04、段描述符的创建和BSWAP指令05、进入内核执行06、进入内核之后显示文本07、用CPUID指令显示处理器品牌信息08、准备加载用户程序09、预读用户程序并得到它的大小10、条件传送簇CMOVcc11、计算以512字节为单位的用户程序总长度12、内存分配的基本策略和方法13、内存分配的简易实现过程14、加载用户程序15、准备安装用户程序的段描述符16、用SGDT和MOV,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 01、本章目标和内容提要
  • 02、内核结构和加载前的准备工作
  • 03、创建安装内核中各段的描述符
  • 04、段描述符的创建和BSWAP指令
  • 05、进入内核执行
  • 06、进入内核之后显示文本
  • 07、用CPUID指令显示处理器品牌信息
  • 08、准备加载用户程序
  • 09、预读用户程序并得到它的大小
  • 10、条件传送簇CMOVcc
  • 11、计算以512字节为单位的用户程序总长度
  • 12、内存分配的基本策略和方法
  • 13、内存分配的简易实现过程
  • 14、加载用户程序
  • 15、准备安装用户程序的段描述符
  • 16、用SGDT和MOVZX指令确定GDT的位置
  • 17、安装新描述符并生成选择子
  • 18、安装用户程序的段描述符并回填选择子
  • 19、用户程序的执行和退出
  • 20、虚拟机观察内核的加载和用户程序的执行与退出

上一节:24、存储器的保护
下一节:26、用户程序编程接口及其实现

01、本章目标和内容提要

在这里插入图片描述

02、内核结构和加载前的准备工作

内核的加载包含c13_mbr.asm全部和c13_core.asm一小部分。

c13_mbr.asm用于:

  • 从BIOS中接管处理器以及计算机硬件的控制权;
  • 安装最基本的段描述符;
  • 初始化最初的执行环境;
  • 然后从硬盘上读取和加载内核的剩余部分;
  • 创建组成内核的每一个内存段。

c13_core.asm中定义和内核的分段信息:

  • 第一个分段,内核的头部,用于记录内核总长度,每个段的相对位置、以及入口点信息,以上统计信息用于高速初始化代码如何加载内核;
  • 第二个分段,公共例程段,本质上是一个代码段,包括一些可以反复使用的子过程,比如显示字符串例程、硬盘读写例程、内存分配例程、描述符创建和安装例程。这些子过程可以为内核自己使用也可提供给用户程序使用;
  • 第三个分段,数据段,包括系统核心数据,供内核自己使用;
  • 第四个分段,代码段,包含进入内核之后首席那要执行的代码、以及用于内存分配、读取和加载用户程序、控制用户程序的代码。

程序从c13_mbr.asm中一步步执行即可,具体看视频和程序。此小节代码如下:

	;代码清单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 
									   ;加载的是2个字节,此时ESP位置为:
									   ;    0 - 2 = 0xFFFFFFFE
									   ;加载的是4个字节,此时ESP位置为:
									   ;    0 - 4 = 0xFFFFFFFC
	
	;以下加载系统核心程序 
	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                            ;循环读,直到读完整个内核 
。。。
。。。

03、创建安装内核中各段的描述符

上一节中已经将内核全部读入内存,这一节是找到内核的每一个段为它们创建并安装描述符。在保护模式下,内核访问自己的段也需要通过描述符。

内核的段描述符安装在GDT中,之前已经使用lgdt指令加载了GDTR,在这里我们就需要为GDT安装新的描述符。那我们的任务就是从标号pgdt处取得GDT的基地址,为其添加新的描述符,并修改新的界线值,之后使用lgdt指令再次加载GDTR使其生效。

但是现在程序正处于保护模式,对代码段保护的意思就是在代码段中不能通过代码段描述符修改代码段中的内容。但是可以使用哪个指向全部4G字节空间的段进行修改。
在这里插入图片描述
如上图,左边大括号就是4G字节的段、左边小括号就是现在主引导程序所在的段,有重叠。

问:内核代码段的描述符还没有创建和安装,如何知道其选择子是什么?
答:内核是常驻内存的不会改变,内核在加载自己时会提前规划好每个段描述符的类型,以及他们在描述符表中的位置,这样一来就知道每个段选择子的具体数值,因此为了方便可以将每个段的选择子定义为常数。此时内存布局:
在这里插入图片描述

04、段描述符的创建和BSWAP指令

本节代码如下:

...
 setup:
         mov esi,[0x7c00+pgdt+0x02]         ;不可以在代码段内寻址pgdt,但可以
                                            ;通过4GB的段来访问
											;在编译期间计算,处理器默认使用
											;DS描述符高速缓存器中的32位线性基地址0 + 偏移
											;来访问内存
         ;建立公用例程段描述符
         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
...
...
;-------------------------------------------------------------------------------
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
...

内存布局如下,使用的是4G字节的数据段从0开始,那么公共例程段的逻辑地址就是EDI指定的地址。
在这里插入图片描述
使用make_gdt_descriptor子程序构造描述符的低32位:
在这里插入图片描述

高32位的基地址部分:
在这里插入图片描述
数据交换指令:
在这里插入图片描述
以字节为单位进行交换:
在这里插入图片描述
高32位的段界限部分:
在这里插入图片描述

05、进入内核执行

本节代码:

。。。
         ;建立核心数据段描述符
         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]             
。。。

内核加载之后的GDT布局:
在这里插入图片描述
4G字节段内存布局:
在这里插入图片描述
执行jmp far [edi+0x10]指令时:处理器使用DS描述符高速缓存器中的基地址0 加上 指令中的有效偏移地址生成线性地址去访问内存取出32位段内偏移16位段选择子,接着用16位段选择子GDT中取出段描述符,经过检查之后加载到CS描述符高速缓存器,同时将32位段内偏移传送到指令指针寄存器EIP,之后处理器就是使用CS描述符高速缓存器中的基地址 加上 指令指针寄存器EIP的偏移作为逻辑地址去执行指令,即到内核入口点取指令并执行指令。

06、进入内核之后显示文本

进入内核之后显示文本信息,本节代码如下:

	...
	...
SECTION sys_routine vstart=0                ;系统公共例程代码段 
;-------------------------------------------------------------------------------
         ;字符串显示例程
put_string:                                 ;显示0终止的字符串并移动光标 
                                            ;输入:DS:EBX=串地址
                                            ;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                                
	...
	...
	...
start:
         mov ecx,core_data_seg_sel           ;使ds指向核心数据段 
         mov ds,ecx

         mov ebx,message_1
         call sys_routine_seg_sel:put_string	;直接绝对远调用
...

进入strart标号之后,先将内核自己的数据段选择子core_data_seg_sel传送给ds,之后将文本信息的标号message_1传送给ebx,之后使用call指令进行过程调用,处理器将段寄存器CS和指令指针寄存器EIP(下一条指令的有效地址)压栈保存,使用指令中的系统公共例程代码段的选择子sys_routine_seg_sel取得描述符,经过检查之后加载到CS描述符高速缓存器,同时使用指令中的偏移量put_string改变指令指针寄存器EIP,这样就转到目标过程内部执行。

其中movsd指令的操作和当前默认操作尺寸相关:
1、当前操作尺寸是16位,源操作数由DS : SI指定、目的操作数由ES : DI指定;
2、当前操作尺寸是32位,源操作数由DS : ESI指定、目的操作数由ES : EDI指定;
且由于当前是在32位操作尺寸下,所以重复传送的次数由ECX指定。

07、用CPUID指令显示处理器品牌信息

cpuid(CPU identification)指令用于返回处理器的标识和特性信息,需要使用EAX指定要返回什么样的信息,指定功能号。
在这里插入图片描述
cpuid指令是在80486后期版本引入,在执行cpuid指令之前,要检测处理器是否支持该指令:
在这里插入图片描述
EFLAGS位21ID位,为0表示不支持cpuid指令,为1表示支持cpuid指令。

本节代码如下:

。。。
。。。
         cpu_brnd0        db 0x0d,0x0a,'  ',0
         cpu_brand  times 52 db 0
         cpu_brnd1        db 0x0d,0x0a,0x0d,0x0a,0
。。。
。。。
         ;显示处理器品牌信息 
         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
。。。
。。。

08、准备加载用户程序

接上一节,加载用户程序:

。。。
。。。
         message_5        db  '  Loading user program...',0
         
         do_status        db  'Done.',0x0d,0x0a,0
。。。
。。。
        mov ebx,message_5
         call sys_routine_seg_sel:put_string
         mov esi,50                          ;用户程序位于逻辑50扇区 
         call load_relocate_program
。。。
。。。

接下来主要讲解了用户程序的构造,由什么段组成,具体有什么等等,看代码c13.asm即可。

09、预读用户程序并得到它的大小

具体看代码c13.asm即可,接上一节,本节主要加载了用户程序的第一个扇区,代码如下:

。。。
;===============================================================================
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                        ;读取程序头部数据 
											;将ESI中的逻辑扇区号传送给EAX
         mov ebx,core_buf                        
         call sys_routine_seg_sel:read_hard_disk_0
。。。

10、条件传送簇CMOVcc

mov指令传统方式:会影响流水线效率
在这里插入图片描述
使用CMOVcc指令简化传送指令:ne表示Not Equal
在这里插入图片描述
IntelCMOVcc指令相关文档:
在这里插入图片描述
在这里插入图片描述
条件传送指令是从P6家族引入的,使用下列指令查看处理器是否支持:
在这里插入图片描述
信息将返回到EBX、ECX、EDX,在EDX位15为0表示不支持条件传送指令、为1表示支持。

11、计算以512字节为单位的用户程序总长度

在二进制中,512整数倍的数,低9位均为0。
在这里插入图片描述
具体看代码c13.asm即可,接上一节,代码如下:

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

12、内存分配的基本策略和方法

操作系统必须记录所有可以分配的物理内存,当一个程序要求分配内存时,内存管理程序从可分配的内存中切割出一部分将其标记为已使用。已分配的内存在使用之后还需要负责回收它们,将其标记为空闲以便再次分配。

当内存空间紧张时,内存管理程序还需要查找那些很久未使用的程序,将其移出到硬盘上,腾出空间给需要使用内存的程序使用。下次用到这些内存时再将其加载到内存中。这就是虚拟内存管理

现在的内存布局:
在这里插入图片描述
若一次分配512字节内存,从0x00100000处开始,则下一次从0x00100200处开始分配。
在这里插入图片描述

13、内存分配的简易实现过程

具体代码看c13_core0.asm,本节代码如下:

。。。
。。。
;-------------------------------------------------------------------------------
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						;下面这些指令使内存按照4字节对齐
         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
。。。
。。。

14、加载用户程序

具体代码看c13_core0.asm,本节代码如下,主要时申请内存之后使用loop指令循环读取用户程序。

。。。
。。。
         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                           ;循环读,直到读完整个用户程序
。。。
。。。

15、准备安装用户程序的段描述符

本节代码如下:

。。。
        ;建立程序头部段描述符
         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                   
。。。

使用make_seg_descriptor创建描述符、使用set_up_gdt_descriptor安装描述符。

其中头部段描述符:
在这里插入图片描述

  • G位:为0表示段界限以字节为单位
  • P位:为1表示段存在
  • S位:为表示这是一个存储器的段描述符
  • X位:为0表示这是一个数据段
  • E位:为0表示向上扩展
  • W位:为0表示可读可写

16、用SGDT和MOVZX指令确定GDT的位置

sgdt m指令:将GDTR的内容保存到内存地址m
在这里插入图片描述
movzx指令,扩展指令:使用0进行扩展。
在这里插入图片描述
在这里插入图片描述
movsx:使用符号位进行扩展:
在这里插入图片描述
在这里插入图片描述
本节代码如下:

。。。
。。。
;-------------------------------------------------------------------------------
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]                   ;下一个描述符的线性地址 
。。。
。。。

17、安装新描述符并生成选择子

接上一节:

         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 
。。。
。。。

安装了新的描述符之后使用lgdt [pgdt] 指令使得新描述符生效,还需要生成它的选择子。
在这里插入图片描述
使用段的界线值 / 8就是对应的索引号。

18、安装用户程序的段描述符并回填选择子

代码如下:

         ;建立程序头部段描述符
         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    

回填选择子:
在这里插入图片描述
之后用户程序头部的0x04地址处就是选择子,而且只用到了低32位,高32位还是原来的值。
在这里插入图片描述
接着创建代码段描述符:

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

在这里插入图片描述
在这里插入图片描述
之后就进行安装和加载描述符,后面还有数据段和栈段描述符:

。。。
。。。
         ;建立程序数据段描述符
         mov eax,edi
         add eax,[edi+0x14]                 ;数据段起始线性地址
         mov ebx,[edi+0x18]                 ;段长度
         dec ebx                            ;段界限
         mov ecx,0x00409200                 ;字节粒度的数据段描述符
         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
。。。
。。。

19、用户程序的执行和退出

load_relocate_program程序返回之前将用户程序头部段选择子保存在AX中。

;===============================================================================
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 ax,[edi+0x04]

         pop es                             ;恢复到调用此过程前的es段 
         pop ds                             ;恢复到调用此过程前的ds段
      
         pop edi
         pop esi
         pop edx
         pop ecx
         pop ebx
      
         ret

load_relocate_program程序返回内核程序:

...
...
     	 call load_relocate_program
         mov ebx,do_status
         call sys_routine_seg_sel:put_string
      
         mov [esp_pointer],esp               ;临时保存堆栈指针
       
         mov ds,ax							 ;从load_relocate_program程序返回时,用户程序头部段选择子存在AX中

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

这条call far [0x08]指令执行时,处理器用段寄存器DS访问用户程序头部段,从偏移为8的地方取出32位偏移量传送到指令指针EIP,再取出16位段选择子传送到段寄存器K,这样处理器进入用户程序内部执行。下面是用户程序start代码:

。。。
。。。
SECTION code vstart=0

start:
         mov eax,ds		;此时DS指向用户程序头部段
         mov fs,eax		;但是我们不能失去对DS的追踪,所以使用其他FS段寄存器重新设置DS

         ;mov eax,[stack_seg]
         ;mov ss,eax
         ;mov esp,stack_end

         ;mov eax,[data_seg]
         ;mov ds,eax

         ;用户程序要做的事情(省略)

         retf                                     ;将控制权返回到系统

code_end:
。。。
。。。

从用户程序返回到内核代码段:

。。。
。。。
	call far [0x08]                     ;控制权交给用户程序(入口点)
	                                    ;堆栈可能切换 
	
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	;在主引导程序进入保护模式之前使用cli指令清除了中断,
		;所以在是哟个hlt指令停机之后处理器不在会被中断唤醒
。。。
。。。

20、虚拟机观察内核的加载和用户程序的执行与退出

主引导程序:0、内核程序:1、用户程序:50
在这里插入图片描述
Virtual Box虚拟机允运行:
在这里插入图片描述
Bochs虚拟机运行:
在这里插入图片描述
可以观察EFLAGSGDT、内核程序头部和用户程序头部内容、栈的变化。

上一节:24、存储器的保护
下一节:26、用户程序编程接口及其实现

最后

以上就是干净魔镜为你收集整理的25、保护模式程序的动态加载和执行01、本章目标和内容提要02、内核结构和加载前的准备工作03、创建安装内核中各段的描述符04、段描述符的创建和BSWAP指令05、进入内核执行06、进入内核之后显示文本07、用CPUID指令显示处理器品牌信息08、准备加载用户程序09、预读用户程序并得到它的大小10、条件传送簇CMOVcc11、计算以512字节为单位的用户程序总长度12、内存分配的基本策略和方法13、内存分配的简易实现过程14、加载用户程序15、准备安装用户程序的段描述符16、用SGDT和MOV的全部内容,希望文章能够帮你解决25、保护模式程序的动态加载和执行01、本章目标和内容提要02、内核结构和加载前的准备工作03、创建安装内核中各段的描述符04、段描述符的创建和BSWAP指令05、进入内核执行06、进入内核之后显示文本07、用CPUID指令显示处理器品牌信息08、准备加载用户程序09、预读用户程序并得到它的大小10、条件传送簇CMOVcc11、计算以512字节为单位的用户程序总长度12、内存分配的基本策略和方法13、内存分配的简易实现过程14、加载用户程序15、准备安装用户程序的段描述符16、用SGDT和MOV所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部