段寄存器存放 基地址
AX 通用寄存器
CS 存放要被cpu执行的代码的基地址 code segment
IP 别名为指令指针寄存器,存放段地址的偏移地址
CS*16+IP 就是cpu要执行的指令
debug是dos、windows都提供的实模式程序调试工具,可以查看cpu各种寄存器中的内容和机器码级跟踪程序的运行
r命令用来查看和改变各个寄存器内容,
d命令查看内存中的内容,
u命令将内存机器码转为汇编指令,
a命令以汇编指令格式在内存写入指令
t命令但不跟踪
数据段:全局变量
代码段:代码
堆栈段:局部变量
CPU根据DS(Data Segment)这个寄存器和任意一个通用寄存器的值或其他数值组成数据段的物理地址如:
DS:[0] DS:[BX] (内存寻址) (内存访问)
mov ds:[13ABH],1234H 内存地址的内容进行赋值 内存地址=1234
mov [13ABH],1234H CPU默认指向ds
---------------------4-----------------------------
CPU如何知道一段内存空间被当作栈使用?执行入栈出栈时,如何知道哪个单元式栈顶单元?
cpu通过ss这个寄存器和sp通用寄存器来感知堆栈段的存在。
ss存放基地址,sp存放栈顶的偏移地址,任何时候ss:sp都指向栈顶元素
---------------------5,6-----------------------------
汇编语言中变量如何定义
如何屏幕显示
如何进行调试
assume关键字
如何让汇编语言“知道”,我们编写的应用程序有多少个段组成。
assume表示用来假设某一段寄存器和程序中的某一个用segment...ends定义的段相关联
db指令
define byte
label db initializer,initializer,initializer
label 表示任选标号,相当于c语言变量名
msg db "hello world"
vga B800F
字体属性格式
7 6 5 4 3 2 1 0
BL R G B I R G B
闪烁 背景色 高亮 前景色
红底绿字: 01000010B
vga显存地址空间
在80*25列彩色模式下显示器可以显示25行80列
每个字符可以有256种属性(背景色前景色闪烁等)
一个字符在显存中占两个字节,分别存放ascii码值和属性
显示缓冲总共分为8页,每页4kb,显卡可以显示任意页内容一般情况下显示第0页内容即B8000H~B8F9FH
es,扩展段寄存器
不能直接给段寄存器赋值,应该先给通用寄存器赋值,然后再传给段寄存器
loop关键字
语法:
标号: 指令
指令2
look 标号
cx内部规定为loop的循环因子
内存访问
helloworld属于数据段内容,那么其中每个字符都可以通过数据段地址存在ds寄存器中,那么要获得数据段第一个字节内容就要如下表示
ds:[0] ds:[si]
si寄存器相当于通用寄存器
代码段地址自动获取 偏移地址不知道
;;;;;;;;;;;;;;;;;;;;;hello.asm;;;;;;;;;;;;;;;;;;;;;;;;;;;;
assume cs:code,ds:data
data segment
db "hello world"
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0b800h
mov es,bx
mov cx,11
mov si,0
mov bx,0
mov ah,01000010b
s:mov al,ds:[si]
mov es:[bx],ax
mov es:[bx+1],ah
inc si
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-------------------7-----------------------------
什么是中断?
任何一个通用CPU,都具有一种能力,可以在执行完成当前正在执行的指令之后,检测到从CPU外部或内部产生的一种特殊信息,
并立即对接受的信息进程处理。这种信息称之为中断信息。
中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
关于中断的疑问
中断发生时CPU如何找到中断处理程序?
中断处理程序有很多种,那么每个中断处理程序放在哪里?
中断处理程序完成后CPU如何继续运行之前被中断的程序?
中断向量表
在内存中保存,其中存放着256个中断源所对应的中断处理程序入口
中断向量表一般都保存在内存0000:0000到0000:03FE
一个表项存放一个中断向量,也就是一个中断程序入口地址,这个地址包括段地址和偏移地址,每个表占两个字(4个字节),高地址存放段地址,低地址存放偏移地址。
遇到中断时CPU把cs和ip内存入栈暂时保存起来。等中断程序执行结束后通过出栈指令重新获得原来的cs和ip的值(这就是c语言函数调用具体实现)
中断过程这些操作,cpu自动完成
div指令
除法指令:div 寄存器
除数:有8位和16位两种,在一个寄存器或内存单元中。
被除数:默认放在ax或dx中,如果除数为8位,被除数位16位默认放在ax中。如果除数为16位被除数为32位分别放在ax和dx中,dx存放高位,ax存放地位。
结果:如果除数位8位,则al存储除法的商,ah存储除法的余数,如果除数是16位,则ax存储商,dx存储余数。
修改中断表:
如何让CPU不去执行原来的中断处理程序,而去执行我们自己编写的处理程序?
修改中断向量表的入口地址就可以实现。
-------------------8----------------------
如何修改中断向量表?
系统默认在0000:0000到0000:03FE专门存放中断向量表。并且每个表占用两个字。
那么我们就知道0号中断表项所在的内存地址是0000:0000开始的4个字节中。汇编代码就是要对这4个字节赋予我们自己编写的中断处理程序入口地址。
汇编伪代码如下:
mov ds:[0],我们自己中断处理程序偏移地址
mov ds:[2],我们自己中断处理程序段地址
中断随时都可以产生,那么当中断产生时必须马上执行中断处理程序,那么中断处理程序必须放在内存何处?
要保证任何时候中断处理程序存放位置不能被其他程序覆盖。
因此我们必须在内存中找出一段空间是任何程序不适用的。
在正常情况下内存地址0000:0200到0000:0300这段内存是没有其他程序使用的。
中断处理程序内存分布
当中断被触发时程序将被执行,但是程序最开始是数据定义指令,而不是代码执行指令,如何解决?
我们希望一开始执行中断处理程序时马上跳转到显示字符串的汇编代码中执行,这时我们就需要使用汇编指令:jump
跳转指令jump分为三种:
段间跳转 jump far 标号 把cs和ip寄存器的值变为标号所在的内存地址
段内跳转指令 jump near 标号 只修改ip寄存器值为标号的偏移地址
段内短跳转指令 jump short 标号 不修改cs和ip的值,编译器自动计算跳转的位置,不超过256
-------------------9--------------------------
如何把一段汇编代码拷贝到指定的内存位置?
可以用loop指令语句来实现,但是该语句比较繁琐,不太合适。
汇编语言提供rep和movsb指令实现相同的功能。
movsb指令:
字节传送指令:指令在存储单元之间传送字符串
使用movsb指令时ds:si指向了要拷贝字符串的首地址,es:di指向了要拷贝的目的地址。
cld指令拷贝数据的方向是从低字节往高字节拷贝,也就是说每拷贝一个字节si和di加1。
std指令和cld相反。
;;;;;;;;;;;;;;;;;;;;;;;;;;ins.asm;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
assume cs:code
code segment
;第一步:把中断向量表中0号表项内容进行修改,使之指向我们自己编写的中断处理程序的入口地址
start:
mov ax,0
mov ds,ax
mov word ptr ds:[0],0200h
mov word ptr ds:[2],0
;第三步:把我们刚才编写好的0号中断处理程序拷贝到中断向量表中0号表项所指向的内存地址中
;0000:0200
mov ax,cs
mov ds,ax
mov si,offset int0;ds:si 可得拷贝源地址
mov ax,0
mov es,ax
mov di,200h;目的地址设置完毕es:di
mov cx,offset int0end - offset int0;计算出程序总共占多少内存
cld
rep movsb;自动利用es:di,ds:si,cx
;第四步:利用代码自动引发0号中断处理程序
mov ax,1000h
mov bh,1
div bh
mov ax,4c00h
int 21h
;第二步:编写自己的中断处理程序,实现在屏幕中央显示字符串的功能
int0:jmp short int0start
db "i am student"
int0start:mov ax,0b800h
mov es,ax ;配置显存首地址
;要把字符串一个个拷贝到显存地址空间中
mov ax,cs
mov ds,ax
mov si,202h
mov di,12*160+36*2
mov cx,12
s:mov al,ds:[si]
mov es:[di],al
inc si
add di,2
loop s
mov ax,4c00h
int 21h
int0end:nop
code ends
end start
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-------------------10--------------------------
主要内容
开发环境搭建
认识引导程序
nasm汇编
编写第一个启动程序
Visual PC2004 用来启动我们编写的启动程序
VMware运行linux,利用nasm汇编器在linux上进行启动程序的编译工作
知识预备
理解计算机加电过程
BIOS对系统内存的分配
认识引导程序的概念
使用nasm进行编译
制作软盘镜像
当我们按下电源按钮后,计算机是如何从无到有将操作系统运行起来的?
按下开机按钮后,将发送电信号给BIOS。
BIOS获得电信号后将启动自检查程序,检查周边设备是否通电完毕。
检查完毕后自检程序把控制权交还给BIOS,BIOS将读取引导驱动器中的启动程序。
在系统加电时,最初的1MB内存是BIOS为我们准备好的,如下:
0x00000~0x003FF 中断向量表
0x00400~0x004FF BIOS数据区
0x00500~0x07BFF 自由内存区
0x07C00~0x07DFF 引导程序加载区 正好512字节
ox07E00~0x9FFFF 自由内存区
0xA0000~0xBFFFF 显示内存区
0xC0000~0xFFFFF 中断处理程序
认识引导程序
什么样程序才能称为引导程序?
BIOS将所检查启动磁盘的第一个扇区512字节载入内存,放于内存0x0000:0x07c00处。
如果第一扇区最后两个字节是55AA,那么它就是一个引导程序。
引导程序特点
大小是512字节,不能多也不能少,因为BIOS只读取512B到内存中。
它结尾必须是55AA,这是引导扇区标志。
它总是放在磁盘第一个扇区上(0磁头0此道1扇区)因为BIOS只读取第一个扇区。
NASM汇编
是一个为可移植性与模块化而设计的一个80*86的汇编器。它支持相当多的目标文件格式包括linux和windows
引导程序编写
如何使用nasm编写一个引导程序?
什么是nasm,它和masm有什么区别?
如何在linux下安装nasm?
如何用nasm编译自己编写的汇编代码?
nasm和masm区别
nasm拥有一个相当简单的内存引用规则,是任何对内存中内容的存取操作必须要在地址上加方括号。但任何地址值的操作不需要。
比如mov ax,bar的指令表示把bar的地址赋给ax寄存器,这相当于masm中 mov ax,offset bar。
要获得bar变量的值则:mov ax,[bar]。
masm mov ax,es:di nasm mov ax,[es:di]
linux 下安装nasm,下载nasm的rpm包,安装 rpm -ivh nasm***.rpm
使用:nasm hello.asm -o hello ,反汇编:ndisasm hello
-------------------11-----------------------------
BIOS中断程序
系统BIOS为我们提供了众多的中断处理程序给我们调用,其中中断编号为10h的中断处理程序专门实现显示功能。
特别注意:编号为10h的中断并不只提供一个程序,而是提供了很多子程序供我们调用。
$和$$关键字
在nasm中$表示当前指令的偏移地址
在nasm中$$表示指令所在的开始地址
因此我们就可以推算出剩余字节数公式
剩余字节数=510-($-$$)
;;;;;;;;;;;;boot.asm;;;;;;;;;;;;;;;;
;我们的启动程序实现很简单的功能,在屏幕中央打印一行字符串即可
org 07c00h ;org指令明确告诉编译器我程序的段地址是7C00h,而不是原来的0000
;int汇编指令 “int 10h”调用bois里的中断程序:显示字符串
mov ax,cs
mov es,ax
mov bp,msgstr ;es:bp 指向的内容就是我们要显示的字符串地址了
mov cx,12 ;显示的字符串长度
mov dh,12 ;显示的行号
mov dl,36 ;显示的列号
mov bh,0 ;显示的页数
mov al,1 ;显示的是串结构
mov bl,0ch ;显示的字符属性
mov ah,13h ;明确调用13h子程序
msgstr: db "hello my os!"
int 10h
times 510-($-$$) db 0 ;重复n次每次填充值为0
dw 55aah
jmp $ ;不断跳转到当前位置,是个死循环
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;boot.asm;;;;;;;;;;;;;;;;;;
;我们的启动程序实现很简单的功能,在屏幕中央打印一行字符串即可
org 07c00h ;org指令明确告诉编译器我程序的段地址是7C00h,而不是原来的0000
;int汇编指令 “int 10h”调用bois里的中断程序:显示字符串
mov ax,cs
mov es,ax
mov bp,msgstr ;es:bp 指向的内容就是我们要显示的字符串地址了
mov cx,12 ;显示的字符串长度
mov dh,12 ;显示的行号
mov dl,36 ;显示的列号
mov bh,0 ;显示的页数
mov al,1 ;显示的是串结构
mov bl,0ch ;显示的字符属性
mov ah,13h ;明确调用13h子程序
msgstr: db "hello my os!"
int 10h
times 510-($-$$) db 0 ;重复n次每次填充值为0
dw 55aah
jmp $ ;不断跳转到当前位置,是个死循环
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
/write_image.c//
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc,char *argv[])
{
int fd_source;
int fd_dest;
int read_count=0;
char buffer[512]={0};
fd_source=open("boot.bin",O_RDONLY);
if(fd_source<0)
{
perror("open boot.bin error:");
return 0;
}
fd_dest=open("v1.vfd",O_WRONLY);
while((read_count=read(fd_source,buffer,512))>0)
{
write(fd_dest,buffer,read_count);
memset(buffer,0,512);
}
printf("write image ok!");
return 0;
}
-----------------13--------------------------------
主要内容
实模式概念
保护模式概念
选择子
段描述符
系统地址寄存器
实模式概念
计算机加电后,cpu就默认属于real-model(实模式)下
实模式只能访问地址在1M以下的内存称为常规内存,我们把地址在1M以上的内存称为扩展内存。
seg:offset 20位地址,只能访问1M空间
通过这种组合指向的内存地址就是实际的物理内存地址
32位cpu
intel退出32位cpu时完全兼容了16位cpu。
所谓兼容其中很重要的一点就是可以继续使用16位的内存寻址方式。
大家都知道16cpu内存寻址是通过段寄存器:通用寄存器来表示实际的内存地址。
32位cpu地址线32根,最大内存为4GB,如何利用原来的seg:offset(实现20根地址线)表示方式,来表示32根地址线?
保护模式
在保护模式下cpu依然使用段寄存器和通用寄存器来表示内存地址,但如何用20位地址来实现32位地址线寻址能力?
有张表,纪录段地址 开始地址 大小(段界限) 属性
16位段寄存器纪录表的【【索引】】
新的内容访问思路
在实模式下,我们把内存分成一个个内存段来表示,那么在保护模式下内存也被分为一个个内存段表示。
那么我们就把实现分好的内存段信息存入一张表格中,然后段寄存器中保存你要访问内存段所在的这张表格的索引。
保存表中索引的段寄存器,我们称为段选择子。
表中每个表示32位内存段信息我们称之为段描述符。
整张表称之为【【段描述符表】】。
段选择子
段选择子16位(段寄存器16位),其中高13位存放描述符表中的索引,其低3位用来表示段描述符表中所指向的段描述符的属性。
因此表中段描述符最大个数为2^13=8096个。
TI(Table Indicator):用来表示是从全局描述符表中读取描述符还是从局部描述符表中读取描述符。
RPL(Request Priviledge Level):用于特权检查.
形成物理地址
段寄存器-(索引号)-->段描述符表--->段描述符A--->线性地址空间(物理地址) 不考虑分页时,线性地址=物理地址
段描述符结构
既然段描述符包含了段的开始地址和段的界限,那么了解该结构至关重要
段描述符共8个字节,每个字节都具有具体含义
段界限(segment limit)20位被分为两个部分,第一部分保存在1,2字节中,第二部分保存在7
段基地址(segment base)32位被分成两个部分,第一部分23个字节被存放在3,4,5字节中,第二部分放在8
段属性(attributes)包含了该段属性和段界限的第二部分
段基地址剩余部分(base)包含了段界限剩余的8位
内存分配
由于我们现在编写的在裸机上编写程序,因此内存必须我们自己在4GB内存中进行分配
-----------------14--------------------------
段属性
段属性位于段描述符的第6和第7个字节,用来描述该段是数据段还是代码段或者堆栈段,对于数据段或者堆栈段来说是否可读是否可写,
对于代码段来说是否可执行以及段描述符所指定的内存段在物理内存中是否存在。
从左往右
0~3 TYPE :说明存储段描述符所描述的存储段的具体属性。是属于代码段还是数据段,可读可写还是可执行。
4 DT :说明了该描述符所指定的系统端描述符海华丝存储段描述符。
5~6 DPL :表示描述符特权级别。
7 P :表示描述符对地址的转换是否有效。
第二个字节
0~3 Limit :段界限第二部分剩余的4位。
4 AVL :软件可利用完,80386对该位未做规定
5 :0
6 D :表示如果该段是代码段,是否是16位还是32位代码段,如果该段是数据段是否是16还是32位,1表示32位
7 G :段界限粒度位G=0表示段边界64k,G=1表示段边界4GB
段界限
我们既然分了8M的内存段,那么段界限就是8M,那么8M占用多少字节,怎样用16进制表示并争取填充到段描述符中呢?
8M=2^23=800000H
不能直接把800000这个16进制直接写段描述符的相应位置中,并且20位的段界限 23位二进制数如何解决?
段界限公式
段界限=limit*4k+0FFFH
800000=limit*4k+0FFFH
limit就是要填写到段描述符中的段界限位置
limit=(800000-0FFFH)/4k=7FFH
段描述符的填写
我们的偏移地址都是通过8M通过公式得出段界限为7FF
我们第一个内存的段从内存00000处开始,所以段基地址全为0
那么我们创建的段位数据段并且是可读可写的,那么就必须在attributes字段中填写相应的数据。
base attributes segment base segment limit
0000 000000000000 07FF
TYPE :我们定义的是数据段并且我们要求该段可读可写那么tpye值填为0010,如果我们创建的是代码段可读可执行,那么为1010
DT :DT用来区别系统段还是存储段,我们这边都是存储段。
DPL :表示内存段的权限,这里为00表示
P :表示描述符对地址转换是否有效,1表示有效
Limit:表示剩余4位段界限描述符 0000
AVL :保留为0
D :1 我们编写的是保护模式,32位
G :1
数据段描述符
根据以上内容我们可以定义符合数据段描述符的汇编代码
dw 07FFh ;段界限
dw 0h ;段基地址0~18位
db 0h ;段基地址19~23位
db 10010010b ;段描述符的第6个字节属性(数据段可读可写)
db 11000000b ;段描述符的第7个字节属性
db 0 ;段描述符的最后一个字节也就是段基地址的第二部分
代码段描述符
代码段描述符和数据段描述符基本一致,不同在于段基地址和段属性
dw 07FFh ;段界限(保持不变)
dw 1h ;段基地址0~18位 不同
db 80h ;段基地址19~23位 不同
db 10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db 11000000b ;段描述符的第7个字节属性
db 0 ;段基地址的第二部分
----------------------15---------------------------------
Intel规定描述符表的第一个描述符必须是空描述符,也就是第一个描述符全部填充为0
DTR寄存器
全部定义好数据段和代码段描述符后,我们知道这个描述符表是存在了内存的某个位置,
那么CPU如何取得这描述符表所在的位置以及大小?
我们必须把刚刚创建好的描述符表所在地址和长度保存起来供CPU使用
80386CPU有个专门保存描述符表的48位寄存器称之为GDTR寄存器
GDTR寄存器共48位:32位描述符表基地址 16位描述符表界限
gdtr汇编指令
通过lgdt汇编指令可以把GDTR描述符表的大小和起始位置存入gdtr寄存器中,指令格式如下:
lgdt [描述段描述符表的地址]
A20地址线
早期的8086只有20根地址线,只能访问1M的地址空间。CPU寻址则按段+偏移的方式进行。
在32位CPU情况下,如果内存访问到1M内存尾部时再向下访问将会出现什么情况?
16位段+16位偏移的可能范围是0-0x10FFEF(即0xFFFF0+0xFFFF),即1M+65520自字节的范围。
由于只有20根地址线,所以在对1M~1M+65520范围进行访问时会发生“地址回绕”的现象,
就是说实际会访问到0~65520的地方。
在32位CPU下不会产生地址回绕,但有些16位程序正是利用地址回绕特性来编写的,
那么如何兼容这些程序呢?
让32位德二进制数据高12位清空为0,剩下的低20二进制数,如何实现呢。
我们只让32位数据和高全为0低全为1的数相与操作。
;;;;;;;;;;;;;;;;;;;boot.asm,增加了gdt_data等;;;;;;;;;;;;;;;;;;;;;;;;;;
;我们的启动程序实现很简单的功能,在屏幕中央打印一行字符串即可
org 07c00h ;org指令明确告诉编译器我程序的段地址是7C00h,而不是原来的0000
;int汇编指令 “int 10h”调用bois里的中断程序:显示字符串
gdt_table_start:
gdt_null:
dd 0h
dd 0h ;Intel规定段描述符表的第一个表项必须为0
gdt_data_addr equ $-gdt_table_start
gdt_data:
dw 07FFh ;段界限
dw 0h ;段基地址0~18位
db 0h ;段基地址19~23位
db 10010010b ;段描述符的第6个字节属性(数据段可读可写)
db 11000000b ;段描述符的第7个字节属性
db 0 ;段描述符的最后一个字节也就是段基地址的第二部分
gdt_code_addr equ $-gdt_table_start
gdt_code:
dw 07FFh ;段界限(保持不变)
dw 1h ;段基地址0~18位 不同
db 80h ;段基地址19~23位 不同
db 10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db 11000000b ;段描述符的第7个字节属性
db 0 ;段基地址的第二部分
gdt_table_end:
gdtr_addr:
dw gdt_table_end-gdt_table_start-1 ; 段描述符表长度
dd gdt_table_start ; 段描述符表基地址
lgdt [gdtr_addr] ;让CPU读取gdtr_addr所指向内存内容保存到GDT内存当中
;A20地址线问题
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-----------------------16---------------------------------------
A20地址线
由于在当时的8042键盘控制器上恰好有空闲的端口引脚,于是使用了该引脚昨晚与门控制这个地址比特位。
该信号即被称为A20。如果它为0,则比特20及以上地址都被清除。从而实现了兼容性。
由于键盘的控制器速度很慢,因此就不能使用键盘对A20线来进行操作,
为此引进了一个A20快速门选项(Fast Gate A20),它使用I/O端口0x92来处理A20信号线,避免了使用慢速的键盘控制器操作方式。
端口概念
在计算机系统中,所有设备都和CPU相连接,尽管相连接但CPU不能直接跟外围设备进行交互数据,
CPU只能和每个设备的寄存器交互数据,然后再由寄存器把数据传给设备。
那么我们给每个设备的寄存器进行编号,这些编号就称之为【【端口号】】。
端口的读
我们用in汇编命令来读取设备寄存器中的内容,格式如下:
in accum port
其中port就是要读取的端口号,accum表示把port端口号的内容放到accum中。accum必须是AL或者AX。
该命令的含义就是读取port端口号内容到AL或者AX寄存器中。
端口的写
我们使用out汇编指令往指定设备中写入数据,格式如下:
out port accume
该命令含义是把accume中的值写入port端口中。
开启A20地址线
32位计算机加电时默认情况是关闭A20地址线的,CPU要转入保护模式都必须开启A20地址线,如何开启呢?
A20地址线代码如下:
in al,92h
or al,00000010b
out 92h,al
cli汇编指令
在转入保护模式之前,我们必须废除原来的中断向量表。
在汇编语言中使用cli汇编指令来废除实模式下的中断向量表。
这就意味着在保护模式下必须重新建立32位的中断向量表和中断处理程序。
转入保护模式
当我们一切准备好之后,如何明确的告诉CPU我们要进入保护模式?
80386提供了4个32位的控制寄存器CR0~CR3。
其中控制寄存器CR0中某些位时用来标识是否要进入保护模式。
CR1寄存器保留没有被使用。
CR2和CR3用于分页机制(不属于讨论范围)
CR0寄存器
31 PG 控制分页管理机制。PG=0,禁用分页管理机制,此时分段管理机制产生的线性地址直接作为物理地址使用。
PG=1,启用分页管理机制,此时线性地址经分页管理机制转换物理地址。
30~5
4 ET
3 TS
2 EM
1 MP
0 PE 控制分段管理机制,PE=0,处理器运行于实模式;PE=1,处理器处于保护模式
设置CR0寄存器
只要对CR0寄存器的第一位置设为1,就表示要转入保护模式,那么在汇编代码中如何实现呢?
mov eax,cr0
or eax,1
mov cr0,eax
-----------------------17----------------- ---
保护模式下段寄存器
在386保护模式下,CPU的物理内存依然是段寄存器内容加偏移地址形成线性地址。
段寄存器内容表示段描述符表中的索引(或者说段描述符所在的段描述符表的位置)。
-----------------------18---------------------
bochs是c++编写的开源跨平台的虚拟机,具有良好的可移植性。可以对操作系统进行调试时它最大的特色。
continue(c) 程序继续运行知道遇到断点为止。
step(s) 单步跟踪。
vbreak(vb) 在虚拟地址上设置一个断点。 vb 段地址:偏移地址
pbreak(b) 在物理地址上设置一个断点。
lbreak(lb) 在线性地址上设置一个断点。
disassemble 反汇编指令。
info b 显示断点
-----------------------19---------------------
bug
我们实际的代码段和数据段的基地址是由我们代码中的data_32和code_32来表示的。
我们要修改代码段描述符和数据段描述符跟段基地址有关的字节。
根据描述结构我们只要修改3,4,5,8这几个字节的内容,填上我们新的及地址就可以了。
;;;;;;;;;;;;;;;;;;;boot.asm;;;;;;;;;;;;;;;;;;;;;;;;
;我们的启动程序实现很简单的功能,在屏幕中央打印一行字符串即可
[BITS 16]
org 07c00h ;org指令明确告诉编译器我程序的段地址是7C00h,而不是原来的0000
;int汇编指令 “int 10h”调用bois里的中断程序:显示字符串
jmp main
gdt_table_start:
gdt_null:
dd 0h
dd 0h ;Intel规定段描述符表的第一个表项必须为0
gdt_data_addr equ $-gdt_table_start
gdt_data:
dw 07FFh ;段界限
dw 0h ;段基地址0~18位
db 0h ;段基地址19~23位
db 10010010b ;段描述符的第6个字节属性(数据段可读可写)
db 11000000b ;段描述符的第7个字节属性
db 0 ;段描述符的最后一个字节也就是段基地址的第二部分
gdt_video_addr equ $-gdt_table_start
gdt_video: ;用来描述显存地址空间的段描述符
dw 0FFh ;显存段界限就是1M
dw 8000h
db 0Bh
db 10010010b
db 11000000b
db 0
gdt_code_addr equ $-gdt_table_start
gdt_code:
dw 07FFh ;段界限(保持不变)
dw 1h ;段基地址0~18位 不同
db 80h ;段基地址19~23位 不同
db 10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db 11000000b ;段描述符的第7个字节属性
db 0 ;段基地址的第二部分
gdt_table_end:
gdtr_addr:
dw gdt_table_end-gdt_table_start-1 ; 段描述符表长度
dd gdt_table_start ; 段描述符表基地址
;A20地址线问题
main:
xor eax,eax
add eax,data_32
mov word [gdt_data+2],ax
shr eax,16
mov byte [gdt_data+4],al
mov byte [gdt_data+7],ah
xor eax,eax
add eax,code_32
mov word [gdt_code+2],ax
shr eax,16
mov byte [gdt_code+4],al
mov byte [gdt_code+7],ah
;初始化代码段描述符的基地址
cli
lgdt [gdtr_addr] ;让CPU读取gdtr_addr所指向内存内容保存到GDT内存当中
enable_a20:
in al,92h
or al,00000010b
out 92h,al
;设置cr0寄存器第一位为1
mov eax,cr0
or eax,1
mov cr0,eax
;跳转到保护模式中
jmp gdt_code_addr:0
[BITS 32]
;保护模式的功能就是屏幕中央打印hello world
data_32:
db "hello world"
code_32:
mov ax,gdt_data_addr
mov ds,ax
mov ax,gdt_video_addr
mov gs,ax
mov cx,11 ;显示的字符串长度
mov edi,(80*10+12)*2 ;在屏幕中央显示
mov bx,0
mov ah,0ch
s:mov al,[ds:bx]
mov [gs:edi],al
mov [gs:edi+1],ah
inc bx
add edi,2
loop s
jmp $
times 510-($-$$) db 0
dw 0aa55h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;