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】

  • 相关阅读:
    java 运算符优先级(sum operator priority level)
    how to improve your programming ablity?
    window.returnValue = 00000000; window.close();
    list
    select card 双层
    double color ball
    Linq 左连接 内连接
    DDD 详细 介绍 摘自网络
    没有老板的公司,你适应吗?摘自网络
    Using Java Classes in your .NET Application 摘自网络
  • 原文地址:https://www.cnblogs.com/longintchar/p/5323778.html
Copyright © 2011-2022 走看看