存储器的保护(三)
改动本章代码清单,使之能够检測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 ;南桥芯片内的port 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<
代码分析
设计思路
- 这个程序实现的主要功能是:检測1MB以上的内存空间。比方检測物理地址为1M~8M的单元。
- 检測方法是向每一个双字单元写入0x55aa55aa。并读出来和0x55aa55aa做比較,假设相等。则再写入0xaa55aa55,并读出来和0xaa55aa55作比較,假设相等。那么这个双字单元是OK的。把物理地址加上4,继续检測。假设读出的和写入的不相等,那么检測出错,程序停止。
- 检測的时候,显示正在检測的内存地址
- 显示一个进度条
- 显示“已经检測的内存数 / 总共须要检測的内存数”
下面我们分析详细的实现。不打算逐行讲述所有代码,仅选择重点部分解说。
定义一些常量
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功能调用之滚屏与清屏》
创建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 绘制白色条
; 绘制白色条 push (1<
这里调用了过程 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 指令详细解释》
所以下面四行就能够取得栈中的參数。
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,ecxremainder: 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
).
查表的关键语句是:
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 examerr: 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_charout: 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】