汇编入门
DB指令:"data byte"的缩写,意思是往文件里直接写入一字节的指令
RESB指令:"reserve byte"的缩写,如果写成RESB 10意思是我们预约了10个字节。
DD、DW、DB的区别
;符号的作用是注释,相当于c++的//符号,DB指令的功能也变得更广,可以直接编写字符串。$符号是一个变量,可以告诉我们这一行现在的字符数。
在启动程序中,我们必须要保证第510字节(即第0x1fe)开始的地方是0x55 0xaa,使用$符号的话就可以直接计算出需要多少个字节来放0x00。
上述源代码中的一些专业的表示的含义如下
ORG指令:这个指令告诉编译器,在开始执行的时候,把这些机器语言指令装载到内存中的哪个地址。有了这个指令的话,美元符$的含义也会有一些变化,它将代表将要读入的内存地址。
ORG指令来源于英文“origin”,意思是“源头、起点”。它会告诉编译器,程序要从指定的这个地址开始,也就是把程序装载到内存中的指定地址。这里指定地址是0x7c00, 对于很多人来讲0x7C00这个地址是很神秘的,不知道这是干什么的。但是对于了解过x86平台下BIOS启动过程的人,对这个地址再熟悉不过了。 BIOS就是将MBR读入0x7C00地址 ,然后进行后续的引导的。操作系统或是bootloader开发者必须假设 他们的汇编代码被加载并从0x7C00处开始执行。
entry指令:这个指令用于指定JMP指令的跳转目的地等。
MOV指令:这个指令的功能很简单,就是一个赋值的功能,虽然简单,但是MOV指令在不同的场景下有不同的意义,掌握它对于掌握汇编和操作系统开发至关重要。
在CPU中有一种名为寄存器的电路,相当于机器语言中的变量。具有代表性的寄存器有以下8个,简单在这里说明一下:
- AX:accumulator,累加寄存器
- CX:counter,计数寄存器
- DX:data,数据寄存器
- BX:base,基址寄存器
- SP:stack pointer,栈指针寄存器
- BP:base pointer,基址指针寄存器
- SI:source index,源变址寄存器
- DI:destination index,目的变址寄存器
这些寄存器都是16位的寄存器,可以存储16位的二进制数。
这8个寄存器全部合起来只有16个字节。换句话说,就算我们把这8个寄存器都用上,CPU也只能存储区区16个字节。
另一方面,CPU中还有8个8位寄存器。
- AL:累加寄存器低位
- CL:计数寄存器低位
- DL:数据寄存器低位
- BL:基址寄存器低位
- AH:累加寄存器高位
- CH:计数寄存器高位
- DH:数据寄存器高位
- BH:基址寄存器高位
需要注意的是BP、SP、DI这几个寄存器是没有高位和低位之分的,如果非要取这些寄存器的低位和高位的话,就必须先MOV,AX,SI,将SI的值赋值到AX中,然后再用AL、AH来取值。
在16位之上的就是32位的CPU和寄存器,对应的寄存器的名字是EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
但是,对于32位寄存器,我们想要使用它的高16位可不像16位寄存器一样能够轻易的取它的低8位和高8位那样方便,我们必须将高32位的数据进行移位,将高16位的数据移到低16位上才可以取到对应的高16位数据。
关于寄存器,还有几个寄存器就是段寄存器,这些段寄存器都是16位的寄存器。
- ES:附加段寄存器
- CS:代码段寄存器
- SS:栈段寄存器
- DS:数据段寄存器
- FS:没有名称
- GS:没有名称
MOV SI,msg 这段代码的含义,就是将msg标签所对应的代码段的起始地址放入SI源变址寄存器中。
这里简单的说明一下CPU和内存的关系,如果说CPU是一个带有实权的掌权者,那么内存就是它的一个外部存储室,也就是说,CPU要通过自己的一部分管脚(引线)想内存发送电信号,告诉内存说,把对应的一个地址上的数据通过我的管脚(引线)传送过来。CPU向内存读写数据时,就是这样进行信息交换的。
内存离CPU相当远,就算是只有10cm左右的距离,可是这和CPU内部的寄存器与CPU自身的距离相比还是相当相当远的,因此,CPU访问内存的速度是要远远小于访问寄存器的速度的。
接下来我们来分析上面代码中出现的一个指令MOV AL,[SI],在我们之前的说明中,MOV指令的数据传送源和传送目的地不仅可以是寄存器或常数,也可以是内存地址。这个时候就可以用方括号[]来表示内存地址,上面的汇编代码的含义就是把SI寄存器中保存的一个内存地址里面保存的数据放到AL寄存器中。
虽然我们可以用寄存器来指定内存地址,但可作为此用途的寄存器非常有限,只有BX、BP、SI、DI这几个。剩下的AX、CX、DX、SP不能用来指定内存地址,这是因为CPU没有处理这种指令的电路,或者说没有表示这种处理的机器语言。没有对应的机器语言当然就不能进行这样的处理了。
MOV指令有一个规则,就是源数据和目的数据必须位数相同。也就是说,能向AL里代入的就只有BYTE,这样一来,就可以省略BYTE。
INT是软件中断指令。电脑中有个名为BIOS的程序,出厂时就组装在电脑主板的ROM单元里。电脑厂家在BIOS中预先写入了操作系统开发人员经常会用到的一些程序,非常方便。BIOS是英文basic input output system的缩写,直译过来就是基本输入输出程序。
最近的BIOS功能非常多,甚至包括了电脑的设定画面,不过它的本质正如其名,就是为操作系统开发人员准备的各种函数的集合,而INT指令就是用来调用这些函数的指令,INT的后面是个数字,相当于这些函数的一个编号,通过不同的编号可以调用不同的BIOS函数。上面的代码调用的是0x10号函数,它的功能是控制显卡。
那么就是按照BIOS的要求去实现寄存器中的内容就可以实现对应的功能,下面是显示一个字符对应的寄存器的要求
这里有一个HLT指令,这个指令的作用是让CPU处于一个休眠的状态,直到外部出现一个变化,比如移动鼠标,或者按下键盘,CPU才会苏醒过来,防止CPU做无谓的运转。
这样,对于上面的那个程序,我们就可以把msg里写的数据一个字符一个字符地显示出来,并且数据变成0之后,HLT指令就会让程序进入无限循环。一个hello world就这样显示出来了。
关于为什么ORG的地址要指向0x7c00的原因:
简单来说就是厂商在写BIOS的时候,需要占用内存的一部分地址,因此这些地址我们操作系统开发人员是不能够去用的,不然会发生很多冲突。同理,内存中还有一些地方是我们不能够使用的,因此就会有我们的地址必须从某个地方开始的规定。
程序源代码
; boot.s引导程序
; hello-os
; TAB=4
ORG 0x7c00
;以下是FAT12的描述
jmp entry
db x90
DB“HELLOIPL” ;可以自由地写引导扇区的名称(8字节)
DW 512 ;一个扇区的大小(必须是512)
DB 1 ;集群的大小(必须是一个扇区)
DW 1 ;FAT从哪里开始(通常是从第一扇区开始)
DB 2 ;FAT的个数(必须是2)
DW 224 ;根目录区域的大小(通常是224个条目)
DW 2880 ;这个驱动器的大小(必须是2880扇区)
DB 0xf0 ;媒体类型(必须是0xf0)
DW 9 ;FAT区域的长度(必须是9扇区)
DW 18 ;1条轨道上有多少扇区(必须是18)
DW 2 ;头部的数量(必须是2)
DD 0 ;因为没有使用分区,所以这里肯定是0。
DD 2880 ;再写一次这个驱动器的大小
DB 0,0,0x29 ;虽然不太清楚,但应该是这个值。
DD 0xffffffffff ;可能是音量序列号
DB“HELLO-OS” ;磁盘名称(11字节)
DB“FAT12” ;格式名称(8字节)
RESB 18 ;姑且留出18个字节
entry:
MOV AX,0 ;寄存器初始化
MOV SS,AX ;SS:栈段寄存器
mov sp,0x7c00 ;栈指针赋为0x7c00,既引导程序初始地址,SP:栈指针寄存器
MOV DS,AX ;DS:数据段寄存器
MOV ES,AX ;ES:附加段寄存器
MOV SI,msg ;SI:source index,源变址寄存器
putloop:
MOV AL,[SI] ;将第一个字符->AL
ADD SI,1 ;SI+1
cmp AL,0 ;比较0寻找最后一个字符,msg之后的byte是0
JE fin ;如果=0,则跳转到fin的地址位置
mov AH,0x0e ;显示字符(字符显示功能);指定文字
MOV BX,15 ;指定颜色(黑色)
INT 0x10 ;调用video bios中断
JMP putloop ;循环跳回
fin:
HLT ;cpu休眠
JMP fin ;无限循环
msg:
DB 0x0a, 0x0a ; 换行*2
DB "hello, world"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 用0x00填充到0x7dfe的命令
DB 0x55, 0xaa