转自边城水手:h t t p ://blog.youkuaiyun.com/zhuichao001/archive/2010/05/23/5618206.aspx
NASM 汇编笔记
段寄存器存放 基地址
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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;