时隔很多年,重新学一遍汇编。觉得这个大灰狼学汇编的视频很好。
仔细的听,仔细的做了笔记、实验。感觉很不错!
继续加油!!
-------------------12-----------------------------
;;;;;;;;;;;;;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 ;显示的列号
movbh,0 ;显示的页数
moval,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.binerror:");
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(TableIndicator):用来表示是从全局描述符表中读取描述符还是从局部描述符表中读取描述符。
RPL(Request Priviledge Level):用于特权检查.
形成物理地址
段寄存器-(索引号)-->段描述符表--->段描述符A--->线性地址空间(物理地址) 不考虑分页时,线性地址=物理地址
段描述符结构
既然段描述符包含了段的开始地址和段的界限,那么了解该结构至关重要
段描述符共8个字节,每个字节都具有具体含义
段界限(segmentlimit)20位被分为两个部分,第一部分保存在1,2字节中,第二部分保存在7
段基地址(segmentbase)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 segmentbase 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
数据段描述符
根据以上内容我们可以定义符合数据段描述符的汇编代码
dw07FFh ;段界限
dw0h ;段基地址0~18位
db0h ;段基地址19~23位
db10010010b ;段描述符的第6个字节属性(数据段可读可写)
db11000000b ;段描述符的第7个字节属性
db0 ;段描述符的最后一个字节也就是段基地址的第二部分
代码段描述符
代码段描述符和数据段描述符基本一致,不同在于段基地址和段属性
dw07FFh ;段界限(保持不变)
dw1h ;段基地址0~18位 不同
db80h ;段基地址19~23位 不同
db10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db11000000b ;段描述符的第7个字节属性
db0 ;段基地址的第二部分
----------------------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
dd0h ;Intel规定段描述符表的第一个表项必须为0
gdt_data_addr equ $-gdt_table_start
gdt_data:
dw07FFh ;段界限
dw0h ;段基地址0~18位
db0h ;段基地址19~23位
db10010010b ;段描述符的第6个字节属性(数据段可读可写)
db11000000b ;段描述符的第7个字节属性
db0 ;段描述符的最后一个字节也就是段基地址的第二部分
gdt_code_addr equ $-gdt_table_start
gdt_code:
dw07FFh ;段界限(保持不变)
dw1h ;段基地址0~18位 不同
db80h ;段基地址19~23位 不同
db10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db11000000b ;段描述符的第7个字节属性
db0 ;段基地址的第二部分
gdt_table_end:
gdtr_addr:
dwgdt_table_end-gdt_table_start-1 ;段描述符表长度
ddgdt_table_start ;段描述符表基地址
lgdt [gdtr_addr] ;让CPU读取gdtr_addr所指向内存内容保存到GDT内存当中
;A20地址线问题
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-----------------------16---------------------------------------
A20地址线
由于在当时的8042键盘控制器上恰好有空闲的端口引脚,于是使用了该引脚昨晚与门控制这个地址比特位。
该信号即被称为A20。如果它为0,则比特20及以上地址都被清除。从而实现了兼容性。
由于键盘的控制器速度很慢,因此就不能使用键盘对A20线来进行操作,
为此引进了一个A20快速门选项(Fast GateA20),它使用I/O端口0x92来处理A20信号线,避免了使用慢速的键盘控制器操作方式。
端口概念
在计算机系统中,所有设备都和CPU相连接,尽管相连接但CPU不能直接跟外围设备进行交互数据,
CPU只能和每个设备的寄存器交互数据,然后再由寄存器把数据传给设备。
那么我们给每个设备的寄存器进行编号,这些编号就称之为【【端口号】】。
端口的读
我们用in汇编命令来读取设备寄存器中的内容,格式如下:
in accumport
其中port就是要读取的端口号,accum表示把port端口号的内容放到accum中。accum必须是AL或者AX。
该命令的含义就是读取port端口号内容到AL或者AX寄存器中。
端口的写
我们使用out汇编指令往指定设备中写入数据,格式如下:
out portaccume
该命令含义是把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
dd0h ;Intel规定段描述符表的第一个表项必须为0
gdt_data_addr equ $-gdt_table_start
gdt_data:
dw07FFh ;段界限
dw0h ;段基地址0~18位
db0h ;段基地址19~23位
db10010010b ;段描述符的第6个字节属性(数据段可读可写)
db11000000b ;段描述符的第7个字节属性
db0 ;段描述符的最后一个字节也就是段基地址的第二部分
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:
dw07FFh ;段界限(保持不变)
dw1h ;段基地址0~18位 不同
db80h ;段基地址19~23位 不同
db10011010b ;段描述符的第6个字节属性(代码段可读可执行) 不同
db11000000b ;段描述符的第7个字节属性
db0 ;段基地址的第二部分
gdt_table_end:
gdtr_addr:
dwgdt_table_end-gdt_table_start-1 ;段描述符表长度
ddgdt_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
out92h,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
movcx,11 ;显示的字符串长度
movedi,(80*10+12)*2 ;在屏幕中央显示
mov bx,0
movah,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-($-$$) db0
dw 0aa55h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;