1.地址、section、vstart
什么是地址
- 地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离。由于指令和变量所占内存大小不同,故它们相对于文件开头的偏移量参差不齐。
什么是section
- 伪指令section对程序中的地址产生任何影响,即在默认情况下,有没有section都一个样,section中数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用
什么是vstart
- section用vstart=来修饰后,可以被赋予一个虚拟起始地址
- vstart=xxxx他并不是告诉编译器加载到地址xxxx,“加载”不是它的工作,这是加载器的工作,编译器只会规划代码,编译器只负责编址
1.4为什么mbr能够正常运行?
- mbr用vstart=0x7c00来修饰的原因,是因为开发人员知道mbr要被加载器(BIOS)加载到物理地址0x7c00,
- 所以说用vstart的时机是:我预先知道我的程序将来被加载到某地址处。程序只有被加载到非0地址时vstart才是有用的。
2.CPU的实模式
CPU的工作原理
CPU大体上可以分为三个部分,它们是控制单元、运算单元、存储单元。
- 控制单元:控制单元是cpu的控制中心,cpu需要它的帮忙才知道自己下一步要做什么。而控制单元大致是:指令寄存器IR、指令译码器ID、指令控制器OC组成。程序被加载到内存后,指令指针寄存器IP指向内存中下一条待执行指令的地址,控制单元根据IP寄存器的指向,将位于内存中的指令逐个装在到指令寄存器中,但它还是不知道这些指令是干什么的,然后指令译码器将位于指令寄存器中的指令按照指令格式来解码,分析出操作码是什么,操作数在哪里之类
- 存储单元:存储单元是指CPU内部的L1、L2缓存及寄存器,待处理的数据就存放在这些存储单元中,这里的数据是指操作数。
- 运算单元:运算单元负责算术运算(加减乘除)和逻辑运算(比较、位移),它从控制单元那接收命令(信号),并执行。
- 控制单元读取下一条待运行的指令,于是读取ip寄存器后,将此地址送上地址总线,CPU根据此地址便得到了指令,并将其存入指令寄存器IR中
- 译码器根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用了,免了去操作数这一过程
- 操作码有了,操作数也齐了,操作控制器给运算单元下令开工,于是运算单元便真正开始执行指令了。ip寄存器的值被加上当前指令的大小,于是ip又指向了下一条指令,接着控制单元又要取下一条指令了,cpu便开始循环执行
实模式下的寄存器
寄存器是一种物理存储元件,只不过它比一般的存储介质要快,能跟上CPU的步伐,所以在CPU内部有好多这样的寄存器来给CPU存储数据
CPU中的寄存器大致分为两大类
- 一类是其内部使用的,对程序员不可见。“是否可见”不是说寄存器能否看得见,是指程序员是否能使用。
- 另一类是对程序员可见的寄存器,我们进行汇编语言程序设计时,能够直接操作的就是这些寄存器,如段寄存器,通用寄存器
寄 存 器 | 助 记 名 称 | 功 能 描 述 |
---|---|---|
ax | 累加器(accumulator) | 使用频度最高,常用于算术运算、逻辑运算、保存与外设输入输出的数据 |
bx | 基址寄存器(base) | 常用来存储内存地址,用此地址作为基址,用来遍历一片内存区域 |
cx | 计数器(counter) | 顾名思义,计数器的作用就是计数,所以常用于循环指令中的循环次数 |
dx | 数据寄存器(data) | 可用于存放数据,通常情况下只用于保存外设控制器的端口号地址 |
si | 源变址寄存器(source index) | 常用于字符串操作中的数据源地址,即被传送的数据在哪里。通常需要与其他指令配合使用,如批量数据传送指令族 movs[bwd] |
di | 目的变址寄存器(destination index) | 和 si 一样,常用于字符串操作。但 di 是用于数据的目的地址,即数据被传送到哪里 |
sp | 栈指针寄存器(stack pointer) | 其段基址是 SS,用来指向栈顶。随着栈中数据的进出,push 和 pop 这两个对栈操作的指令会修改 sp 的值 |
bp | 基址指针(base pointer) | 访问栈有两种方式,一种是用 push 和 pop 指令操作栈,sp 指针的值会自动更新,但我们只能获取栈顶 sp 指针指向的数据。很多时候,我们需要读写在栈底和栈顶之间的数据,处理器为了让开发人员方便控制栈中数据,还提供了把栈当成数据段来访问的方式,即提供了寄存器 bp,所以 bp 默认的段寄存器就是 SS,可通过 SS:bp的方式把栈当成普通的数据段来访问,只不过 bp 不像 sp 那样随 push、pop 自动改变 |
实模式下CPU内存寻址方式
寻址方式,从大方向来看可以分为三大类
- 寄存器寻址:最直接的寻址方式就是寄存器寻址,它是指操作数在寄存器中,直接从寄存器中拿数据就行了。寄存器寻址也属于立即数寻址
mov ax,0x10
mov dx,0x9
- 立即数寻址:立即数就是常数。宏和标号在编译阶段会转化为数字,最终可执行文件中的依然是立即数
mov ax,0x18
mov ax,macro_selector
mov ax,label_start
- 内存寻址 :以上两种方式,操作数一个是在寄存器中,一个是在指令中直接给出,它们都不在内存中。操作数在内存中的寻址方式成为内存寻址。内存寻址又分为:
-
直接寻址:直接寻址就是直接在操作数中给出的数字作为内存地址
mov ax,[0x1234] mov ax,[fs:0x5678]
-
基址寻址:基址寻址就是在操作数中用bx寄存器或bp寄存器作为偏移地址,bx寄存器的默认段寄存器是ds,bp默认段寄存器是ss。
mov ax,[bx] ;这条指令将ds:bx的值送入ax
-
变址寻址:变址寻址起始和基址寻址类似,只是寄存器由bx、bp换成si和di。
mov [di],ax ;将寄存器ax的值存入ds:di指向的内存 mov [si+0x1234],ax ;变址也可以加个偏移量
-
基址变址寻址:基址寻址和变址寻址的结合,即基址寄存器bx或bp加一个变址寄存器si或di
mov [bx+di],ax mov [bx+si],ax
实模式下的ret
- ret指令的功能是,在栈顶弹出2字节的内容来替换ip寄存器
- reft指令的功能是,在栈顶弹出4字节的内容,2字节用来替换ip寄存器,另外2字节用来替换cs寄存器
- call和ret是一对配合,用于近调用和近返回。call far 和retf是一对配合,用于远调用和远返回。
3.让我们直接对显示器说点什么吧
1. CPU如何与外设通信——IO接口
- IO接口是连接CPU与外部设备的逻辑控制部件,分为硬件和软件,硬件协调cpu和外设之间的种种不匹配,软件控制接口电路工作的驱动程序以及完成内部数据传输所需要的程序
- IO接口芯片又可以按照是否可编程来分类,可分为可编程接口芯片和不可编程接口芯片
- 不可编程接口芯片:接口的作用是连接处理器和外部设备,如果外部设备很简单,傻瓜型的,不需要设定就直接能用,就可以用不可编程接口芯片与处理器连接,不可编程接口芯片是种非常简单的IO接口。
- 可编程接口芯片:计算机与IO接口的通信是通过计算机指令实现的,当我们需要定制某些功能时,我们也必须用计算机指令告诉IO接口,哪些设备连接在此IO接口上,此IO接口的工作模式等。这种通过软件指令选择IO接口的功能,工作模式的做法,称为“IO接口控制编程”,这通常是用端口读写指令int/out来实现的
- 一个IO接口包含多个端口,即IO接口上的寄存器,来存储这些信息内容,但同一时刻,只能有一个端口和CPU数据交换。
- IA32体系系统中,因为用于存储端口号的寄存器是16位的,所以最大有65536个端口,即0~65535
- in指令用于从端口读取数据:
in al,dx ;in指令中,端口号只能用dx寄存器
in ax,dx
- out指令用于往端口写数据:
out dx,al ;out指令中,可以选用dx寄存器或立即数充当端口号
out dx,ax
out 立即数,al
out 立即数,ax
2.显存、显卡、显示器
显存地址分布
起始 | 结束 | 大小 | 用途 |
---|---|---|---|
C0000 | C7FFF | 32KB | 显示适配器BIOS |
B8000 | BFFFF | 32KB | 用于文本模式显示适配器 |
B0000 | B7FFF | 32KB | 用于黑白显示适配器 |
A0000 | AFFFF | 64KB | 用于彩色适配器 |
显卡的文本模式也是分为多种模式的,用“列数*行数”来表示,显卡加电后,默认就置为模式80*25,也就是一屏可以打印2000个字符
- 即使在文本模式下,也可以打印出彩色字符。可是ASCLL码都是1字节大小,即使标准ASCLL码也要用7位来为一个字符编码,所以要用另一个字节来表示字符的属性
3.改进MBR,直接操作显卡
;主引导程序
;
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,0
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax ;将ax,dx,es,ss,fs初始化为0
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏 利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 0x10 ; int 0x10
;输出背景色绿色,前景色红色,并且跳动的字符串”1 MBR“
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
jmp $ ; 使程序悬停在此
times 510-($-$$) db 0
db 0x55,0xaa
nasm -o mbr.bin mbr.s 回车
dd if=/your_path/mbr.bin of=/your_path/bochs/hd60m.img bs=512 count=1 conv=notrunc 回车
4.硬盘
1.硬盘的介绍
- 针对硬盘的IO接口是硬盘控制器,硬盘控制器同硬盘的关系,如同显卡和显示器一样,他们都是专门驱动外部设备的模块电路。
- 硬盘和硬盘控制器是整合在一起的,这种接口就成为IDE,主板提供了两个IDE插槽,IDE0 和 IDE1,IDE0叫做primary通道,IDE1叫做Secondary通道,每个通道上分别有主盘master 和 从盘slave
让硬盘工作,我们需要通过读写硬盘控制器的端口,端口就是位于IO控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器
- 端口是通道给出的,端口不是针对某块硬盘的,一个通道上的主、从两块硬盘都用这些端口号。要想操作某通道上的某块硬盘,需要单独指定(devie第4位指定主从)
- date寄存器,16位寄存器,其作用是是读取或写入数据。
- 端口0x171或0x1F1,读硬盘时叫Error寄存器,只有在读取硬盘失败时才有用,里面才会记录失败的信息。在写硬盘时叫Feature,有些命令需要指定额外参数,这些参数就写在feature寄存器中
- sector count 寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值-1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区,这是8位寄存器,最大值为255,若指定为0,则表示要操作256个扇区
- 硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称CHS,但每次都要先算出在哪个盘面,哪个柱面太麻烦了。我们希望磁盘中扇区从0开始依次递增编号,不用考虑扇区的物理结构,这就是寻址方法就是LBA,全称为逻辑块地址(Logical Block Address)
- LBA有两种,一种是LBA28,用28位来描述一个扇区地址,最大寻址范围2的28次方,每个扇区512字节,最大支持128g。另一种是LBA48,最大支持131072TB。
- LBA low寄存器用来存储28位地址的第0~7位,LBA mid用来存储第8~15位,LBA high用来存储第16~23位,还剩4位是由device寄存的低4位来存储
- device寄存器是一个杂项,8位,此寄存器低4位用来存储LBA地址的第24~27位。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘,第6位用来设置是否启用LBA方式,1代表启用LBA模式,0代表启用CHS模式,另外第5位和第7位固定为1,称为MBS位
- 端口0x1f7或0x177,8位,在读硬盘时叫status,用来给出硬盘的状态信息,第0位是ERR位,如果位为1,表示命令出错了,具体原因课件error寄存器。第3位是data request位,如果此位为1,表示硬盘已经把数据准备好了可以读数据了。第6位是DRDY表示硬盘就绪,此位是在对硬盘诊断时用得,表示硬盘检测正常。第7位是BSY位,表示硬盘是否繁忙,1为繁忙。其他四位不关注。在写硬盘时叫command,此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作,主要试用了三个命令:
1)identify:0xEC,即硬盘识别
2)read sector:0x20,即读扇区
3)write sector:0x30,即写扇区
2.常用硬盘操作方法
1)先选择通道,往该通道的sector count寄存器中写入待操作的扇区数。
2)往该通道上的LBA寄存器写入扇区起始地址的低24位。
3)往device寄存器中写入LBA地址的24~27位,并置6位为1,使其为LBA模式。设置第4位,选择操作的硬盘(master硬盘或slave硬盘)
4)往该通道上的command寄存器写入操作命令
5)读取该通道上的status寄存器,判断硬盘工作是否完成。
6)如果以上步骤是读硬盘,进入下一个步骤,否则,完工。
7)将硬盘数据读出。
硬盘工作完成后它已经准备好了数据,一般常用的数据传送方式如下:
1)无条件传送方式
2)查询传送方式
3)中断传送方式
4)直接存储存取方式(DMA)
5)I/O处理机传送方式
5.改善MBR
- 由于MBR受限于512字节大小,所以要在另一个程序中完成初始化环境及加载内核任务,这个程序我们称之为loader,即加载器。
- loader在哪?如何跳过去执行?MBR负责从硬盘上把loader加载到内存,并把接力棒交给loader。
;mbr.s
;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,0
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax ;将ax,dx,es,ss,fs初始化为0
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏 利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 0x10 ; int 0x10
;输出背景色绿色,前景色红色,并且跳动的字符串”1 MBR“
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax, LOADER_START_SECTOR ;loader加载到的扇区
mov bx , LOADER_BASE_ADDR ;lodaer写入的地址
mov cx , 1 ;待读入的扇区数
call rd_disk_m_16
jmp LOADER_BASE_ADDR
;-----------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-----------------------------------------------
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读取硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
mov eax,esi ;恢复ax
;第2步:将LBA地址存入0x1f3~0x1f6
;LBA地址7~0位写入0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入0x1f5
;mov cl,8
shr eax,cl
mov dx,0x1f5
out dx,al
;LBA地址24~27位写入端口0x1f6低4位,高四位设置主盘和LBA模式
shr eax,cl
;and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读扇区命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检查硬盘状态
.not_ready:
;0X17F同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
;mov dx,0x1f7
in al,dx
and al,0x88 ;和0x88进行与操作,保留第3位和第7位
cmp al,0x08 ;0x08表示硬盘准备就绪
jnz .not_ready
;第5步:从0x1f0端口读取数据
mov ax,di
mov dx,256
mul dx
mov cx,ax
;di为读取的扇区数,一个扇区512个字节,每次读入的两个字节共需 di*512/2次,所以是di*256
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
boot.inc
;-------------------loader和kernel--------------------
LOADER_BASE_ADDR equ 0x900 ;LOADER加载地址
LOADER_START_SECTOR equ 0x2 ;LOADER开始的扇区
;loader.s
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
jmp $
nasm -I 路径/include/ -o loader.bin loader.s
dd if=路径/loader.bin of=路径/hd60m.img bs=512 count=1 seek=2 conv=notrunc