沃梦达 / IT编程 / 移动开发 / 正文

windows下写操作系统---实践(3)开始写MBR1---使用BIOS中断或者显存显示字符

开始写MBR1---使用BIOS中断显示字符初步介绍一下硬盘的知识。 硬盘的最小单位是扇区(一般是512字节),而扇区表示法有两种:1.LBA[逻辑块寻址方式]------------------线性地址,纯以扇区为计算单位。2.CHS[Cyli...

开始写MBR1---使用BIOS中断显示字符

初步介绍一下硬盘的知识。

 

硬盘的最小单位是扇区(一般是512字节),而扇区表示法有两种:

1.LBA[逻辑块寻址方式]------------------线性地址,纯以扇区为计算单位。

2.CHS[Cylinder/Head/Sector]-----------3D参数 . 既磁头数(Heads), 柱面数(Cylinders), 扇区数(Sectors per track)

 

前期仅需要知道,这两种寻址的计算方式不同,LBA是从0扇区开始,CHS是从0柱面0磁头1扇区即可,而大部分的书都是以CHS来讲解,那就暂时不关心LBA。(不了解的东西可以先放一放,碰到了再学)

 

上一节说过了,BIOS的最后一项工作就是检测0柱面0磁头1扇区的最后两个字节是否为0x55和0xaa,如果是就从硬盘加载到0x7c00h这个位置来,然后jmpf 0x0000:0x7c00,至于为什么是0x7c00,这是BIOS规定的,很多大牛做了各种分析,这里就不深究了,只需知道BIOS完事后,就jmp到这里。

 

那我们就开始来写第一个汇编程序MBR.S用BIOS中断来显示QQ:22093636。

1.创建一个CODE文件夹,打算以后把内核的代码都放在这个文件夹里面。

2.用VIM 创建mbr.s(先上代码,再做解释).

section MBR vstart=0x7c00

mov ax,cs

mov ds,ax

mov es,ax

mov fs,ax

mov ss,ax

mov sp,0x7c00

;中断清屏

mov ax,0x0600

mov cx,0x0

mov bx,0x300

mov dx,0x184f

int 0x10

;显示文字

mov ax,message

mov bp,ax

mov cx,11

mov dx,0

mov bx,0x2

mov ax,0x1301

int 0x10

jmp $

message db "QQ:22093636"

times 510-($-$$) db 0

db 0x55,0xaa

 

先将代码做详细解释:

section MBR vstart=0x7c00h 有两个关键字section和vstart

 

说起这个section或者segment,请原谅我的长篇大论,因为很多书上写的比较模糊,喜欢说节就是段,段就是节,其实这是不对的,你想想,如果节和段是相等的,何必出现两个概念,就好像大部分的书会说地址就是指针,指针就是地址,这纯属扯淡,如果地址和指针是一样的,就没必要出现两个概念,不废话,我们开始啰嗦节和段这个概念:

 

C程序大体上分为预处理、编译、汇编和链接4个阶段。

1。预处理阶段是预处理器将高级语言中的宏展开,去掉代码注释,为调试器添加行号等。

2。编译阶段是将预处理后的高级语言进行词法分析、语法分析、语义分析、优化,最后生成汇编代码。

3。汇编阶段是将汇编代码编译成目标文件,也就是转换成了目标机器平台上的机器指令。

4。链接阶段是将目标文件连接成可执行文件。

这里我们只关注汇编和链接这两个阶段。

 

------在汇编源码中,通常用语法关键字section或segment来表示一段区域,它们是编译器提供的伪指令,作用是相同的,都是在程序中"逻辑地"规划一段区域,此区域便是节。

-------注意,此时所说的section或segment都是汇编语法中的关键字,它们在语法中都表示"节",不是段,只是不同编译器的关键字不同而已,关键字segment在语法中也被认为与section意义相同。

-------首先汇编器根据语法规则,会将汇编源码中表示"节"的语法关键字section或segment在目标文件中编译成"节",此"节"便是我们要讨论的section。

-------经过汇编生成目标文件之后,由这些section或segment修饰的程序区域便成为了"节"(section)。但操作系统加载程序时并不关心节的数量和大小,操作系统只关心节的属性,因为程序必然是要加载到内存中才能运行的,而内存的访问会涉及到保护模式的很多知识,先略掉,此时汇编器只生成了目标文件,尚未链接,因此这个将"节"合并的工作是由链接器来完成的,链接器将目标文件中属性相同的节合并成一个大的section集合,此集合便称为segment,也就是段,此段便是我们平时所说的可执行程序内存空间中的代码段和数据段。

 

 

 

这代码的意思是这个节名叫MBR,偏移地址是从0X7c00h开始,什么是节,就是告诉编译器,我要这块区域做点事,给我划出来,最终的时候,还会把相同属性(是相同属性,不是相同名字)的节合并成段,而偏移地址解释起来就有点麻烦,而我又是一个特别怕麻烦的人,简短点打个比方吧:

如果没有section MBR vstart=0x7c00h 这句话,那编译器默认是起始地址是0,而代码是:

00000000 Number1 dw 10

00000002 Number2 dw 20

................. ..........................

................. ..........................

如果下次你mov ax,Number1,那ax会是0,而mov ax,Number2,ax则会是2,记住,这里MOV的是地址,不是地址里的值。

如果你section MBR vstart=0x7c00h,编译后加载到内存后那mov ax,Number1 ,ax则会是0x7c00h,因为我们的MBR是运行在0x7c00这个地址的,所以需要加上偏移量,如果对偏移量不是很懂的话,你就单单理解,我把下面的节里的数据和标号定个位,以后会运行在这个地址上,编译器在编译阶段,会自动给你的标号和数据地址加上具体地址,说得好像有点绕,那就以图做说明。

 

第一副图是设置vstart的,加载进内存的变化:

 

这一副是没设置vstart的,加载进内存的变化:

 

不知道上图有没有解决你的疑惑,如果还是不理解,那就Baidu吧。

接下来:

--------------------------------------------------------------------

mov ax,cs

mov ds,ax

mov es,ax

mov fs,ax

mov ss,ax

mov sp,0x7c00

--------------------------------------------------------------------

简单的汇编代码,但涉及到复杂的CPU机制,前期简单说一下吧,估且听一听,在32位CPU内部有16个寄存器,其中有6个段寄存器:CS,DS,ES,FS,GS,SS,首先要理解,从硬件层面来说,段寄存器是有96位的,分为可见部分和不可见部分,图虚线部分是不可见部分,只不过实模式下,只有低16位才有效,而在保护模式下,16位可见部分变为段选择子,去GDT表(全局描述符表)中查询段描述符,将里面的内容重新组合填充进不可见部分,以后进行操作时,CPU会根据不可见部分来进行段的权限检查,不过如今的操作系统在开户了页保护后,已经弱化了段的功能,这是后话,至于后续的复杂机制,用到哪学到哪,现在了解即可。

 

 

这几行代码的整体意思是:用CS段寄存器的值通过AX(通用寄存器)给别的段寄存器赋值,由于我们开始用到的是jmpf 0x0000:0x7c00,所以CS段的值为0,而别的段寄存器,CPU并不能自己给自己赋值,而且也没有立即数到段寄存器的电路实现,所以需要通过(AX,BX,CX,DX等都行,一般喜欢用AX)通用寄存器来中转。至于初始化栈指令[mov sp,0x7c00],在CPU里,有代码段的程序就要有栈,这是CPU的规则,遵守总没错,如果你不设置也行,只不过你不能call,push,pop等等功能,上一节课了解过0x500--0x7BFF约30K是可用区域,而且栈机制又是从高地址往低地址方向扩展的,所以选在这里咯。

 

我们还是遵循用到哪学到哪的原则,因为马上要用BIOS的中断来清屏,所以接下来需要了解点显存的知识。

1.显卡在加电自检后,会把自己初始成80*25的文本模式,屏幕上可以显示25行,一行显示80个字(这里是文学上的字,不是计算机的计量单位16位组成的字),一屏显示2000个字。

2.0xB8000--0XBFFFF这段物理地址留给显卡的,用来屏幕显示文本。

OK,我们目前的代码仅需了解到这个程度即可,接下来使用BIOS的6号中断。

;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

 

以上的注释应该很详细了,唯独对BH这个行属性,估计不是很明白,这其实就是说我设置清屏后这些行的前景色和背景色,根据下图,我的bh=0x07,背景色为0000:不闪烁黑(你第7位设置1背景也不会闪烁)。前景色为:0111 白色,实际上你清屏后,你的前景色设置了,只有光标会变色,你在写文字的时候,还是要去设置文字属性的(如下图3)。

 

 

 

接下来就是显示字符串的代码了

;;;;;;;;; 打印字符串 ;;;;;;;;;;;

;还是用10h中断,不过这次是调用13号子功能打印字符串

mov ax, message ;message变量的定义在下面实现,这里是将message变量的地址给ax

mov bp, ax ; es:bp 为串首地址, es此时同cs一致,

; 开头时已经为sreg初始化

 

; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略

mov cx, 11 ; cx 为串长度,不包括结束符0的字符个数

mov ax, 0x1301 ; 子功能号13是显示字符及属性,要存入ah寄存器,

; al设置写字符方式 ah=01: 显示字符串,光标跟随移动

mov dx,0 ; DH,DL=起始行,列

mov bx, 0x2 ; bh存储要显示的页号,此处是第0页,

; bl中是字符属性, 属性黑底绿字(bl = 02h)

int 0x10 ; 执行BIOS 0x10 号中断

 

以上应该没难度,看一看自己领悟吧,建议下个“汇编金手指”,不懂的可以在里面看看。

 

接下来:jmp $

$和$$是编译器预留的关键字,并非CPU原生支持的,NASM提供的伪指令,仅仅起到标号的作用,在编译阶段,会将具体的值填进去。

$ :当前行的地址。

$$: 当前section的地址。

 

例如1:

section MBR vstart=0x7c00 ,那$$在编译阶段就会被填 入0x7c00,如果没有这句话,那在编译阶段$$会被置为0。

例如2:

section MBR vstart=0x7c00

mov ax,0

_test:jmp $

我们先找简单的说:

0x7c00这个地址的指令是mov ax,0

0x7c03 jmp -2

0x7c05 mov ax,cs

 

我们要先了解EIP运行的一个基本概念:

CPU根据EIP指令寄存器的值取指令--->修改EIP指令寄存器指向下一条指令--->执行当前指令--->

在取出jmp $这条指令的时候,EIP就已经是下一条的指令地址0x7c05了,所以在编译阶段时,$被替换成了0x7c03-0x7c05=-2,这里的-2是偏移地址,切记是偏移地址。最终执行时,变成:jmp 0x7c05-2【伪代码,方便解释】。

 

其实我对上述的计算抱怀疑态度,以NASM程序员们的那种天才大脑,怎么会用这么笨的方法,直接用0-当前代码的长度,不就可以得出来了,何必还要找两个地址来相减,但国内很多老师是这么教的,就不较真了(主要是水平有限)。

 

至于为什么要执行jmp $ ,是为了不让CPU跑飞了,在我可控的范围内,无限循环。

 

终于要到最后了:

message db "QQ:22093636" ;定义变量

times 510-($-$$) db 0 ;重复定义数据或指令

db 0x55,0xaa

 

times 510-($-$$) db 0

$-$$ 当前行的地址 减去 section开始的地址,得到的是已有代码或者数据的大小。

由于0柱面0磁头1扇区的最后两个字节必须为0x55和0xaa,所以只有前510个字节未放置代码或者数据的地方初始化为0,这样510-($-$$),就可以得到未定义代码或者数据的大小,然后通过times从当前行开始初妈化0

 

OK,终于搞完了,剩下的就是动手做了。

1.用vim写完mbr.s

2.在cmd下,输入nasm -o mbr.bin mbr.s

3.然后用:dd if=\目录\mbr.bin of=\目录\c.img bs=512 count=1 conv=notrunc 写进第一个扇区

bs=一个扇区多少字节。

count=总共写几个扇区

conv=如果要写进的代码超过了这COUNT个扇区,是继续往后面扇区写,还是截断,notrunc是不截断。

4.双击bochsrc.bxrc。

 

(运行结果)

 

接下来那我们再修改MBR.S用直接操控显存来显示Fly...。

老样子,先上代码:

section MBR vstart=0x7c00

mov ax,cs

mov ds,ax

mov es,ax

mov fs,ax

mov ss,ax

mov sp,0x7c00

 

;中断清屏

mov ax,0x0600

mov cx,0x0

mov bx,0x300

mov dx,0x184f

int 0x10

 

;显示文字

mov ax,message

mov bp,ax

mov cx,11

mov dx,0

mov bx,0x2

mov ax,0x1301

int 0x10

 

;操作显存来显示文字

mov ax,0xb800

mov gs,ax

mov byte[gs:0xa0],'F'

mov byte[gs:0xa1],0x2

mov byte[gs:0xa2],'l'

mov byte[gs:0xa3],0x2

mov byte[gs:0xa4],'y'

mov byte[gs:0xa5],0x2

mov byte[gs:0xa6],'.'

mov byte[gs:0xa7],0x2

mov byte[gs:0xa8],'.'

mov byte[gs:0xa9],0x2

mov byte[gs:0xaa],'.'

mov byte[gs:0xab],0x2

 

jmp $

message db "QQ:22093636"

times 510-($-$$) db 0

db 0x55,0xaa

 

上面初步介绍过0xB8000--0XBFFFF这段物理地址留给显卡的,用来文本模式适配器的,总共32K,那文本模式下屏幕显示的起始地址就是0xB8000。

一屏可以显示80列*25行=2000个字(这里是文学上的字,不是计算机的计量单位16位组成的字),一个文字由两个字节组成(低8位ascii码,高8位文字属性),总共可以显示8屏。

mov byte[gs:0xa0],'F'

mov byte[gs:0xa1],0x2

至于偏移地址为什么是0xa0,是因为我们在开始已经显示了一行QQ:22093636,一行是80个字,由160个字节组成,地址是从0开始,所以是0-159,由16进制显示会更方便点,就是0-9F,所以第二行的最开始是0x9f+1=0xa0,以后都是依此类推。

我们知道一个文字是由两个字节组成,0xa0已经mov 了'F',那他的高字节,就是字符属性,上面【颜色表】已经有所说明了,就是背景是黑色,前景色是绿色,所以运行后的结果图为:

注意,这个光标我们并没有设置,所以不会跟随我们的文字,后续会通过修改显存寄存器里的内容来达到修改光标的目的。

终于搞完了!

本文标题为:windows下写操作系统---实践(3)开始写MBR1---使用BIOS中断或者显存显示字符