zoukankan      html  css  js  c++  java
  • 存储器的保护(三)——《x86汇编语言:从实模式到保护模式》读书笔记20

    存储器的保护(三)

    修改本章代码清单,使之可以检测1MB以上的内存空间(从地址0x0010_0000开始,不考虑高速缓存的影响)。要求:对内存的读写按双字的长度进行,并在检测的同时显示已检测的内存数量。建议对每个双字单元用两个花码0x55AA55AA和0xAA55AA55进行检测。

    上面的文字选自原书第12章的习题1.
    这篇博文就讨论一下这道题。由于是初学,我不对自己做太高的要求,只要实现功能即可。

    代码清单

            ;文件说明:第12章习题-1
            ;创建日期:2016-3-7
    		
    		
    		;--------- equ some colors
    		
    		GREEN         equ 0x02
    		RED 	      equ 0x04
    		BLUE_LIGHT    equ 0x09
    		YELLOW        equ 0x0e
    		
    		MEMORY_START  equ 0x100000
    		MEMORY_END    equ 0x800000
    		MEMORY_SIZE   equ (MEMORY_END-MEMORY_START)/4  ;以双字为单位
    		
    		LENGTH_OF_BAR equ 6        ; 表示2的6次方
    		BAR_POSITION  equ 10*80+4  ;进度条的位置
    		
            ;设置堆栈段和栈指针 
            mov eax,cs      
            mov ss,eax
            mov sp,0x7c00
    		
    		mov ah,0x00; 清屏
    		mov al,0x03
    		int 0x10
          
            ;计算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,512字节 
            mov dword [ebx+0x14],0x00409a00    ;粒度为1个字节,代码段描述符 
            
    		;创建栈段描述符
            mov dword [ebx+0x18],0x7c00fffe
            mov dword [ebx+0x1c],0x00cf9600
            
            ;初始化描述符表寄存器GDTR
            mov word [cs: pgdt+0x7c00],31      ;描述符表的界限   
     
            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)选择子; ds,es,fs,gs指向了(0..4G)
    		mov ds,eax   
            mov es,eax
            mov fs,eax
            mov gs,eax
          
            mov eax,0x0018                   ;加载栈段选择子
            mov ss,eax
            xor esp,esp                        ;ESP <- 0	
    		
    		; 绘制白色条
    		push (1<<LENGTH_OF_BAR) ;number of blocks
    		push BAR_POSITION
    		push 0x7720 ; white block
    		call put_char
    		
    		push 21*80+25
    		push BLUE_LIGHT
    		push MEMORY_SIZE
    		call show_hex_dword ;显示总共要检测的数量(以双字为单位)
    		
    		; 显示 '/'
    		push 1
    		push 21*80+23
    		push 0x092f ; 蓝色的'/'
    		call put_char
    		
    		xor ecx,ecx          ;计数器清零,记录检测了多少个双字
    		mov ebx,MEMORY_START ;检测的起始地址 
    
    ;-----------------------------------------------------	
    exam:   ;显示正在检测的地址
    	    push 21*80+6
    		push YELLOW
    		push ebx
    		call show_hex_dword
    		
    		mov dword [es:ebx],0x55aa55aa
    		cmp dword [es:ebx],0x55aa55aa
    		jnz err
    		
    		mov dword [es:ebx],0xaa55aa55
    		cmp dword [es:ebx],0xaa55aa55
    		jnz err
    		
    		add ebx,4    ;地址增加4个字节
    		inc ecx
    		
    		push 21*80+15
    		push BLUE_LIGHT
    		push ecx
    		call show_hex_dword ;显示已经检测的数量(以双字为单位)
    			
    		push BAR_POSITION  ;绘制进度条
    		push ecx
    		push MEMORY_SIZE
    		call draw_progress_bar
    			
    		cmp ebx,MEMORY_END  
    		jnz exam
    		
    err:	      
            hlt 
    
    ;--------------------------------------		
    ;功能:在指定位置显示N个字符
    ;输入: push 显示的个数
    ;      push (x*80+y),  表示x行y列
    ;      push 属性和字符  
    ;返回:无
    
    put_char:
    		pushad
    		mov ebp,esp
    		mov ecx,[ebp+11*4]  ; 取得个数
    		mov ebx,[ebp+10*4]  ; 取得位置
    		mov ax,[ebp+9*4]    ;取得属性和字
    		
    put:
    		mov [es:0xb8000+ebx*2],ax
    		inc ebx
    		loop put
    		
    		popad
    		ret 3*4
    		
    ;-----------------------------------------		
    ;功能:根据比例在指定位置绘制进度条
    ;输入: 
    ;      push (x*80+y),  表示x行y列
    ;      push 分子
    ;      push 分母       
    ;返回:无	
    
    draw_progress_bar:
    		pushad
    		mov ebp,esp
    		mov esi,[ebp+11*4]  ; 取得位置
    		mov eax,[ebp+10*4]  ; 取得分子
    		mov ebx,[ebp+9*4]    ;取得分母
    			
    		shr ebx,LENGTH_OF_BAR
    		xor edx,edx
    		div ebx
    		cmp eax,1
    		jb out
    		
    		push eax
    		push esi
    		push 0x2020; 绿色背景,空格
    		call put_char
    		
    out:
    		popad
    		ret 3*4			
    		
    ;-----------------------------------
    ;功能:在指定位置显示16进制的数字
    ;输入: 
    ;      push (x*80+y),  表示x行y列
    ;      push 属性
    ;      push 要显示的值       
    ;返回:无	
    
    show_hex_dword:
    		pushad
    		
    		mov ebp,esp
    		mov esi,[ebp+11*4]  ;取得@1:(x,y)	
    		mov eax,[ebp+9*4]   ;取得@3:value
    		
    		mov ebx,16
    		xor ecx,ecx
    
    remainder:	
    		xor edx,edx
    		div ebx
    		
    		inc ecx
    		push edx
    		cmp eax,0
    		jnz remainder
    		
    		mov ah,[ebp+10*4]   ;取得属性
    print:	
    		pop ebx
    		mov al,[cs:string_hex+ebx]		
    		mov [es:0xb8000+esi*2],ax
    		inc esi
    		loop print
    		
    		popad
    		ret 3*4			
    ;-------------------------------------------------------------------------------
    		pgdt     dw 0
    				 dd 0x00007e00      ;GDT的物理地址
    						
    		string_hex: db'0123456789ABCDEF'
    ;-------------------------------------------------------------------------------                             
    		times 510-($-$$) db 0
            db 0x55,0xaa
     
    

    代码分析

    设计思路

    1. 这个程序实现的主要功能是:检测1MB以上的内存空间,比如检测物理地址为1M~8M的单元。
    2. 检测方法是向每个双字单元写入0x55aa55aa,并读出来和0x55aa55aa做比较,如果相等,则再写入0xaa55aa55,并读出来和0xaa55aa55作比较,如果相等,那么这个双字单元是OK的,把物理地址加上4,继续检测。如果读出的和写入的不相等,那么检测出错,程序停止。
    3. 检测的时候,显示正在检测的内存地址
    4. 显示一个进度条
    5. 显示“已经检测的内存数 / 总共需要检测的内存数”

    下面我们分析具体的实现。不打算逐行讲述所有代码,仅选择重点部分讲解。

    定义一些常量

    		GREEN         equ 0x02 ; 黑底绿字
    		RED 	      equ 0x04 ; 黑底红字
    		BLUE_LIGHT    equ 0x09 ; 黑底蓝色字
    		YELLOW        equ 0x0e ; 黑底黄字
    		
    		MEMORY_START  equ 0x100000
    		MEMORY_END    equ 0x800000
    		MEMORY_SIZE   equ (MEMORY_END-MEMORY_START)/4  ;以双字为单位
    		
    		LENGTH_OF_BAR equ 6        ; 表示2的6次方
    		BAR_POSITION  equ 10*80+4  ;进度条的位置
    

    前四行定义了字符属性;
    中间三行定义了要检测的内存起始地址,结束地址(检测不包含结束地址),还有检测的内存大小(以双字为单位)。之所以用equ定义是因为修改起来方便。
    LENGTH_OF_BAR equ 6 ; 表示2的6次方
    这句话表示进度条的总长度占64(2^6=64)个字符,当然可以根据需要修改。但应该是2的N次方(具体原因下文会说明)。
    BAR_POSITION equ 10*80+4 ;进度条的位置
    这行定义了进度条的位置,如果是x行y列,对应的表示就是(x*80+y);因为一行有80个字符。

    清屏

    		mov ah,0x00; 清屏
    		mov al,0x03
    		int 0x10
    

    这三行代码是为了清屏。具体原理可以参见我的博文《BIOS功能调用之滚屏与清屏》

    http://blog.csdn.net/longintchar/article/details/50806752

    创建GDT

            ;跳过0#描述符
           
    
            ;创建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],0x00409a00    ;粒度为1个字节,代码段描述符 
            
    		;创建栈段描述符
            mov dword [ebx+0x18],0x7c00fffe
            mov dword [ebx+0x1c],0x00cf9600
    

    以上代码用于创建GDT。由于想在引导程序中实现全部功能,所以编译后的文件不能超过512字节。为了节省笔墨,我跳过了0#描述符。
    关于代码段,必须是可读的,因为过程“show_hex_dword”需要访问代码段中的一个表格:
    string_hex: db'0123456789ABCDEF'
    关于栈段描述符的定义,具体讲解参见 存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18 http://blog.csdn.net/longintchar/article/details/50759826

    绘制白色条

    		; 绘制白色条
    		push (1<<LENGTH_OF_BAR) ;number of blocks
    		push BAR_POSITION
    		push 0x7720 ; white block
    		call put_char
    

    这里调用了过程 put_char

    ;--------------------------------------		
    ;功能:在指定位置显示N个字符
    ;输入: push 显示的个数
    ;      push (x*80+y),  表示x行y列
    ;      push 属性和字符   
    ;返回:无
    
    put_char:
    		pushad
    		mov ebp,esp
    		mov ecx,[ebp+11*4]  ; 取得个数
    		mov ebx,[ebp+10*4]  ; 取得位置
    		mov ax,[ebp+9*4]    ;取得属性和字符
    		
    put:
    		mov [es:0xb8000+ebx*2],ax
    		inc ebx
    		loop put
    		
    		popad
    		ret 3*4
    

    以前我们都是用寄存器传递参数,这次我们用栈传递参数。在调用过程之前,先按照要求把参数压入栈中。当进入过程,执行完pushad这条指令后,栈的情况如下图:


    这里用到了pushad和popad指令,如果你不懂的话,可以参考我的另一篇博文:

    《PUSHA/PUSHAD POPA/POPAD 指令详解》

    http://blog.csdn.net/longintchar/article/details/50866801

    所以以下四行就可以取得栈中的参数。

    		mov ebp,esp
    		mov ecx,[ebp+11*4]  ; 取得个数
    		mov ebx,[ebp+10*4]  ; 取得位置
    		mov ax,[ebp+9*4]    ;取得属性和字符
    

    还有一点需要说明,
    ret 3*4 这句话使用了带操作数的过程返回指令。这种用法在原书P278页讲解了。
    如果希望在过程返回的同时,顺便弹出调用者压入的参数(使栈平衡),那么可以用带操作数的过程返回指令。指令格式是:

    	ret imm16
    	retf imm16
    

    这两条指令都允许用16位的立即数作为参数,不同之处仅在于前者是近返回,后者是远返回。立即数一般总是偶数,原因是栈操作总是以字或者双字进行。立即数的值表示在过程返回时应当从栈中弹出多少字节的数据。
    对于我们的put_char过程,因为调用的时候压入了3个参数(3*4=12字节),所以ret后面的参数是12.

    push 0x7720 这句表示压入白底的空格符,显示出来就是白色的小方块了。

    显示总共要检测的内存数量(以双字为单位)

    		push 21*80+25
    		push BLUE_LIGHT
    		push MEMORY_SIZE
    		call show_hex_dword ;显示总共要检测的数量(以双字为单位)
    

    依然用栈来传递参数,调用了过程show_hex_dword

    ;-----------------------------------
    ;功能:在指定位置显示16进制的数字
    ;输入: 
    ;      push (x*80+y),  表示x行y列
    ;      push 属性
    ;      push 要显示的值       
    ;返回:无	
    
    show_hex_dword:
    		pushad
    		
    		mov ebp,esp
    		mov esi,[ebp+11*4]  ;取得@1:(x,y)	
    		mov eax,[ebp+9*4]   ;取得@3:value
    		
    		mov ebx,16
    		xor ecx,ecx
    
    remainder:	
    		xor edx,edx
    		div ebx
    		
    		inc ecx
    		push edx
    		cmp eax,0
    		jnz remainder
    		
    		mov ah,[ebp+10*4]   ;取得属性
    print:	
    		pop ebx
    		mov al,[cs:string_hex+ebx]		
    		mov [es:0xb8000+esi*2],ax
    		inc esi
    		loop print
    		
    		popad
    		ret 3*4			
    

    这段代码的功能就是在指定的位置(压入第一个参数,比如3行4列就写 push 3*80+4),显示指定属性(压入第二个参数,仅低字节有效,比如绿色0x02)的16进制数字(压入第三个参数,比如想在屏幕上显示16进制的8b9c,那么就push 0x8b9c).
    这段代码的设计思路就是把要显示的数不断除以16(因为是以16进制显示),并且把余数压栈,直到商等于0.之后再从栈依次弹出余数,把余数作为索引值查表,将对应的字符写到屏幕上。查表的关键语句是:

    mov al,[cs:string_hex+ebx]		
    

    表格定义在源文件的倒数第三行

    		string_hex: db'0123456789ABCDEF'
    

    因为查表需要对代码段进行访问,所以在创建代码段描述符的时候,一定要让代码段可读。

    开始内存检测

    		xor ecx,ecx          ;计数器清零,记录检测了多少个双字
    		mov ebx,MEMORY_START ;检测的起始地址 
    

    在检测之前,计数器清零,检测的起始地址传送到EBX寄存器。

    exam:   ;显示正在检测的地址
    	    push 21*80+6
    		push YELLOW
    		push ebx
    		call show_hex_dword
    		
    		mov dword [es:ebx],0x55aa55aa
    		cmp dword [es:ebx],0x55aa55aa
    		jnz err
    		
    		mov dword [es:ebx],0xaa55aa55
    		cmp dword [es:ebx],0xaa55aa55
    		jnz err
    		
    		add ebx,4    ;地址增加4个字节
    		inc ecx
    		
    		push 21*80+15
    		push BLUE_LIGHT
    		push ecx
    		call show_hex_dword ;显示已经检测的数量(以双字为单位)
    			
    		push BAR_POSITION  ;绘制进度条
    		push ecx
    		push MEMORY_SIZE
    		call draw_progress_bar
    			
    		cmp ebx,MEMORY_END  
    		jnz exam
    		
    err:	      
            hlt 
    

    上面的代码就是内存检测的主体部分了。
    首先显示正在检测的地址(要检测的地址在ebx中)。然后向这个地址写入花码,并读出比较,如果不相等,就跳转到

    err:	      
            hlt 
    

    如果相等,则ebx加上4,ecx加上1,并且显示ecx的值,绘制进度条,然后继续检测。

    绘制进度条

    ;-----------------------------------------		
    ;功能:根据比例在指定位置绘制进度条
    ;输入: 
    ;      push (x*80+y),  表示x行y列
    ;      push 分子
    ;      push 分母       
    ;返回:无	
    
    draw_progress_bar:
    		pushad
    		mov ebp,esp
    		mov esi,[ebp+11*4]  ; 取得位置
    		mov eax,[ebp+10*4]  ; 取得分子
    		mov ebx,[ebp+9*4]    ;取得分母
    			
    		shr ebx,LENGTH_OF_BAR
    		xor edx,edx
    		div ebx
    		cmp eax,1
    		jb out
    		
    		push eax
    		push esi
    		push 0x2020; 绿色背景,空格
    		call put_char
    		
    out:
    		popad
    		ret 3*4			
    		
    

    上面的这个过程是在指定的位置绘制进绿色的进度条,要求压入三个参数。第一个是位置,第二个是分子,第三个是分母。
    比如说要检测160个双字,当前已经检测了10个了,那么第二个参数就是10,第三个参数就是160。如果之前的白色条的长度是64,那么就绘制64*(10/160)=4个绿色方块。看上去的效果就是绿色条的长度是总长度的十六分之一。
    在每次检测4个字节后,我们就调用这个过程,这样程序运行后就有一个动画效果了。
    这个过程实现的关键是计算出要绘制多少个绿色空格。
    假设白色空格数可以表示成2的m次方。
    计算公式推导如下图:

    根据公式,我们把ebx右移LENGTH_OF_BAR(=6)位,作为除数,被除数就在eax中,然后edx清零,再然后
    edx:eax / ebx(移位运算后的值) = eax ...edx
    余数舍去,假如计算出来画1.5个方块,那么就绘制1个。
    需要注意的是,计算后eax的值可能为0,如果为0就一定要跳出,一个绿色方块也不绘制。
    如果eax大于等于1,那么调用过程put_char绘制绿色方块。

    好了,整个代码的分析就到这里了,我们赶紧看看结果吧。

    检测结束后:

    【end】

  • 相关阅读:
    pat甲级 1155 Heap Paths (30 分)
    pat甲级 1152 Google Recruitment (20 分)
    蓝桥杯 基础练习 特殊回文数
    蓝桥杯 基础练习 十进制转十六进制
    蓝桥杯 基础练习 十六进制转十进制
    蓝桥杯 基础练习 十六进制转八进制
    51nod 1347 旋转字符串
    蓝桥杯 入门训练 圆的面积
    蓝桥杯 入门训练 Fibonacci数列
    链表相关
  • 原文地址:https://www.cnblogs.com/longintchar/p/5323778.html
Copyright © 2011-2022 走看看