开始写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中断或者显存显示字符
基础教程推荐
- IOS获取系统相册中照片的示例代码 2023-01-03
- MVVMLight项目Model View结构及全局视图模型注入器 2023-05-07
- iOS开发使用XML解析网络数据 2022-11-12
- Android实现短信验证码输入框 2023-04-29
- Android开发Compose集成高德地图实例 2023-06-15
- iOS中如何判断当前网络环境是2G/3G/4G/5G/WiFi 2023-06-18
- iOS Crash常规跟踪方法及Bugly集成运用详细介绍 2023-01-18
- iOS开发 全机型适配解决方法 2023-01-14
- Android Compose自定义TextField实现自定义的输入框 2023-05-13
- Flutter进阶之实现动画效果(三) 2022-10-28