主引导扇区 (Main Boot Sector, MBR)

  • 什么是主引导扇区:
    处理器加电或者复位之后(简单来说就是启动计算机),如果硬盘是首选的启动设备,那么ROM-BIOS(基本输入输出系统)将试图读取硬盘的0面0道1扇区(简单来说就是第一个扇区),这就是主引导扇区
  • 主引导扇区的特点:
    • 扇区数据仅有512字节
    • MBR应该以0x550xAA这两个字节结尾来确保有效性(可以理解为这个结束标志表示这个文件是MBR,大家有兴趣可以去了解魔数)
    • ROM-BIOS程序首先检测文件最后的两个字节(0x55, 0xAA),若有效则将MBR加载到逻辑地址0x0000:0x7c00处,即物理地址0x07c00

在屏幕上显示文字

  • 文字如何显示在屏幕上

    • 显卡:为显示器提供内容,并控制显示器的显示模式和状态
    • 显示器:将显卡提供的内容以视觉可见的方式呈现在屏幕上
    • 显示存储器(VRAM,显存):显卡控制显示器的最小单位是像素,一个像素对应着屏幕上的一个点,要显示的内容预先写入显存
  • 显示器的工作模式

    • 图形模式:显存里的内容---> 比特0---像素不亮,比特1--像素亮
      图形模式
      显卡周期性地从显存中提取比特,并按顺序显示在屏幕上

    • 文本模式:
      在图形模式下控制像素的明暗以显示字符非常麻烦。所以就有了文本模式。我们只需将字符的ASCII代码存放到显存中,然后显卡提取,显示器打印就行了,控制像素的工作就交给字符发生器
      文本模式

    • 不论是哪种工作模式,要让屏幕显示文字,我们只需关注将要显示的内容写入显存中即可

  • 访问显存
    内存

    这是一张1MB的内存条,物理地址0xF0000-0xFFFFF是属于ROM-BIOS的区域,0xB8000-0xBFFFF是输入显存的区域,也就是说,我们只需往这个地址里写内容,就可以将其显示在屏幕上了

    • 字符属性:在显存中,一个字符是要占用两个字节的,第一个字节存储字符的ASCII码,第二个字节存储字符的显示属性(也就是字符的颜色等等)

    字符

    K位是闪烁位(1-闪烁,0-不闪烁),I位是亮度位(1-高亮,0-正常),前景色就是字符的颜色,背景色就是字符的背景色

    • 显示字符
    ; es指向显存
    mov ax, 0xb800
    mov es, ax
    
    ; 打印字符
    mov byte [es:0x00], 'L'     ; 可以直接用字符,但要注意引号
    mov byte [es:0x01], 0x07    ; 字符的显示属性 00000111,黑底白字
    

    上面的byte用来修饰目的操作数,指定操作的字节数,这里不赘述了

显示标号的汇编地址

  • 汇编地址
    汇编地址是在源程序编译期间,编译器为每条指令确定的汇编地址指示该指令相对于程序或者段起始处的距离,以字节计,当编译后的程序装入物理内存后,它又是该指令在内存段内的偏移地址

    总结起来就是指令相对于程序起始地址的偏移量

  • 标号
    简单来说标号就相当于存储汇编地址的变量,这样可以不用特意去记地址,更人性化
    例如start: mov ax, 0xb800(这里冒号可写可不写),这里标号start就存储着指令mov ax, 0xb800的汇编地址,这样写的好处就方便跳转

显示十进制数字

需要注意的是数字是没法直接显示在屏幕上的,我们需要将其转化成字符写入显存中才可以显示。

例如我们想要显示整数123,需要先将其转化为'1','2','3'并写入显存,其实这就是该整数的个位、十位、百位

  • 在程序中声明并初始化数据
    我们需要再内存中留出一些空间来保存这些数位
    在这里介绍下声明数据所用的伪指令(伪指令没有对应的机器指令,由编译器处理)

    • DB(declare byte): 声明字节数据
    • DW(declare word): 声明数据
    • DD(declare double word): 声明双字数据
    • DQ(declare quad word): 声明四字数据

    例如number db 0, 0, 0,我们就声明了3个字节数据,并将其初始化为0,number代表了这些数据的起始汇编地址,我们就可以将分解的数位保存在这里啦

  • 分解数的各个数位
    其实就是不断地用它的进制数去除得到的余数就是各个数位了。例如十进制数123,禁止数为10,那就用10除3次,依次是3、2、1

程序:

; ------- 分解数位
mov ax, 123
mov bx, 10 ; 存储进制数

mov cx, cs
mov ds, cx ; 因为现在数据和代码都在同一个段内,所以ds和cs相同

mov dx, 0
div bx
mov [0x7c00 + number + 0x00], dl ; ds = 0x0000,number表示的汇编地址是相对于程序加载处0x7c00的,0x00表示第一个数位

xor dx, dx ; 将dx清0
div bx
mov [0x7c00 + number + 0x01], dl
...
...
number db 0, 0, 0

下面这张图就解释了为什么是[0x7c00 + number]而不是[number]

number

div指令的用法大家就自行查阅了哈

  • 显示分解出来的各个数位
    唯一需要注意的就是将内存中的数字转化为字符,0的ASCII码是0x30,所以数字加上0x30就能转化成ASCII码了
 mov al,[0x7c00+number+0x00]   ; 取出数字
 add al,0x30                   ; 转化为ASCII码
 mov [es:0x22],al              ; 将字符写入显存,这个写入地址大家看着改哈
 mov byte [es:0x23],0x04       ; 设置字符显示属性

使程序进入无限循环状态

可以用jmp $表示悬停在此行,或者用infi: jmp near infi就一直循环在这里

  • 直接绝对转移: jmp后是一个绝对地址
    例如jmp 0x5000:0xfc0,这会直接设置cs和ip寄存器

  • 相对转移: jmp后是一个标号,是以相对量进行转移
    例如infi: jmp near infi,这里的near关键字是用于指示相对量是16位的
    具体就是用标号(目标位置)处的汇编地址减去当前指令的汇编地址,在减去当前指令的长度,就得到转移的操作数---相对量

    在举个例子好了,以下的指令都是假设的,只是为了方便理解

    汇编地址     标号    汇编指令的机器码
       0        one         0 0 0    ; 3字节
       3        two         1 1 1    ; 3字节,假设这一行是相对的跳转指令
       6        three       2 2 2    ; 3字节
       9        four        3 3 3    ; 3字节
    
    1. 假设标号为two的那行指令是要跳转到标号为four,首先目标汇编地址是9,当前指令的汇编地址是3,当前指令的长度为3字节,相对量=9-3-3=3,也就是说向前移动3个字节就能成功跳转到标号four
    2. 假设标号为two的那行指令是要跳转到标号为one,首先目标汇编地址是0,当前指令的汇编地址是3,当前指令的长度为3字节,相对量=0-3-3=-6,也就是说向后移动6个字节就能成功跳转到标号one

理解了以上后,我们就明白,两个汇编地址A-B相减得到的是包含的字节数(有点类似于[B开头,A开头),数学上的区间),在减去当前指令的长度就能得到汇编地址相差多少字节(相对量,类似于(B结尾,A开头) )

bochs下的程序调试入门

  • 启动bochs
    bochs在执行它启动后的第一条指令后,会停下来等待调试命令
    启动bochs
    红色方框中,绿色线表示当前所在的地址,前边方括号是物理地址,后边是逻辑地址,蓝色线表示下一条指令,黄色线表示下一条指令的机器码

    我们可以看到程序启动时的地址是f000:fff0即物理地址0xffff0,这就是第一条指令的物理地址,在上图中,物理地址是以64位的宽度显示的,而这个bochs的地址线是32位的,现代处理器在加电启动时,会将剩余的高位地址线强制位高电平,所以显示的物理地址多了4个f,这好像是为了将BIOS放到可寻址内存的最高端以保持连续

    jmpf 0xf000:e05b这条指令是为了跳转到BIOS

  • 单步执行命令s(step)
    s
    输入单步执行命令s,单步执行一次只执行一条指令,可以看到,当前的地址已经发生转变了,说明执行了跳转指令

  • 断点命令b(breakpoint) 和持续(继续)执行c(continue)
    bc
    输入b 0x7c00,表示程序会再0x7c00处停止,输入c表示继续执行,没有断点的话会执行到结束

  • r命令显示通用寄存器的内容(register)
    r

  • sreg命令显示段寄存器的内容(segment register)
    sreg

  • xp命令显示指定内存中的内容
    xp命令每次只显示一个双字,也就是4个字节
    xp/n 0xb8000表示从0xb8000开始,显示n个双字

  • q命令退出调试(quit)

完整源程序

mov ax, 0xb800 			; es指向文本模式的显存缓冲区
mov es, ax

mov byte [es:0x00], 'L'
mov byte [es:0x01], 0x07 ; 黑底白字
mov byte [es:0x02], 'a'
mov byte [es:0x03], 0x07 ; 黑底白字
mov byte [es:0x04], 'b'
mov byte [es:0x05], 0x07 ; 黑底白字
mov byte [es:0x06], 'e'
mov byte [es:0x07], 0x07 ; 黑底白字
mov byte [es:0x08], 'l'
mov byte [es:0x09], 0x07 ; 黑底白字
mov byte [es:0x0a], ' '
mov byte [es:0x0b], 0x07 ; 黑底白字
mov byte [es:0x0c], 'o'
mov byte [es:0x0d], 0x07 ; 黑底白字
mov byte [es:0x0e], 'f'
mov byte [es:0x0f], 0x07 ; 黑底白字
mov byte [es:0x10], 'f'
mov byte [es:0x11], 0x07 ; 黑底白字
mov byte [es:0x12], 's'
mov byte [es:0x13], 0x07 ; 黑底白字
mov byte [es:0x14], 'e'
mov byte [es:0x15], 0x07 ; 黑底白字
mov byte [es:0x16], 't'
mov byte [es:0x17], 0x07 ; 黑底白字
mov byte [es:0x18], ':'
mov byte [es:0x19], 0x07 ; 黑底白字

; ------- 分解数位
mov ax, 12345
mov bx, 10 

mov cx, cs
mov ds, cx ; 因为现在数据和代码都在同一个段内,所以ds和cs相同

mov dx, 0
div bx
mov [0x7c00 + number + 0x00], dl ; ds = 0x0000,number表示的汇编地址是相对于程序加载处0x7c00的,0x00表示第一个数位

xor dx, dx
div bx
mov [0x7c00 + number + 0x01], dl

xor dx, dx
div bx
mov [0x7c00 + number + 0x02], dl

xor dx, dx
div bx
mov [0x7c00 + number + 0x03], dl

xor dx, dx
div bx
mov [0x7c00 + number + 0x04], dl
; ----- 分解数位结束

; ------- 显示数字
 mov al,[0x7c00+number+0x04]
 add al,0x30
 mov [es:0x1a],al
 mov byte [es:0x1b],0x04
 
 mov al,[0x7c00+number+0x03]
 add al,0x30
 mov [es:0x1c],al
 mov byte [es:0x1d],0x04
 
 mov al,[0x7c00+number+0x02]
 add al,0x30
 mov [es:0x1e],al
 mov byte [es:0x1f],0x04

 mov al,[0x7c00+number+0x01]
 add al,0x30
 mov [es:0x20],al
 mov byte [es:0x21],0x04

 mov al,[0x7c00+number+0x00]
 add al,0x30
 mov [es:0x22],al
 mov byte [es:0x23],0x04
 
 mov byte [es:0x24],'D'
 mov byte [es:0x25],0x07
  
infi: jmp near infi
; ------- 显示数字结束

; 数据
number db 0, 0, 0, 0, 0 ; 5字节

times 203 db 0 ; 零填充至510字节
dw 0xaa55 ; 结束标志
 posted on 2024-08-02 11:43  Dylaris  阅读(235)  评论(0)    收藏  举报