260×260

科学搜查官yuchanns

理想的生活是纯粹地热爱技术
  • Shenzhen, China
  • 后端开发工程师
Posted a month ago

编写主引导扇区

说明

x86系列文章为书籍《x86汇编语言:从实模式到保护模式》的学习笔记,内容属于笔者学习总结性质。

笔者将之发表到博客上,一方面是作为笔记存档,另一方面希望对同在阅读本书的小伙伴起到理解帮助作用。

计算机的启动过程

前置概念

  • 内存:动态随机访问存储器(DRAM),访问任何一个内存单元的速度和地址无关。
  • BIOS:只读存储器(ROM),固化了开机时要执行的指令(主要是硬件的诊断、检测和初始化)。
  • 硬盘:外存储器之一。

    • 磁头:在同一个轴上拥有许多盘片,每个盘片拥有上下两个磁头,编号为0、1、2、3……以此类推。
    • 磁道:磁头距离圆心每步进一次,都可以形成一个圆圈,就是磁道,依次编号为0、1、2、3……。
    • 柱面:寻道过程是机械动作,需要尽量减少对磁头的移动,所以访问数据优先按照磁头0读第一个盘上面的磁道0、磁头1读第一个盘下面磁道0、磁头2读第二个盘上面的磁道0……读完全部盘面的磁道0后再返回磁头0读第一个盘的磁道1——这一访问过程中形成的圆柱就是柱面,依次编号为0、1、2、3……。
    • 扇区:磁道上可以进行分段,呈现扇形,就是扇区。通常为63个。依次编号为1、2、3、4……。
    • 主引导扇区:0面0道1扇区。

注:需要特别注意,扇区是从1开始编号的。

8086启动过程

执行复位(RESET),使代码段寄存器(CS)内容为0xFFFF,其他寄存器内容为0x0000

地址线分配内存空间给设备,其中顶部64KB分配给ROM,范围0xF0000~0xFFFFF;较低端的640KB分配给DRAM,范围0x00000~0x9FFFF;中间的一部分分配给其他设备。

复位时,CS(0xFFFF)和IP(0x0000)形成物理地址0xFFFF0,就是计算机取的第一条指令的物理地址,正好是ROM-BIOS的范围内。

ROM-BIOS执行完本身的指令后,将硬盘主引导扇区内容加载到0x0000:0x7C00,执行跳转指令到达该位置执行。

主引导扇区继续引导计算机从硬盘的其他部分读取更多的内容加以执行。

编写主引导扇区代码

根据上面所述的启动过程,我们只需要在硬盘的主引导扇区注入我们写的汇编代码编译成的二进制机器码,就可以在计算机启动后引导计算机执行自定义的指令。

使用工具说明

由于笔者使用的操作系统为OSX,与原书的Windows存在差别,部分工具使用替代品,现进行说明:

goland

# 编译源文件到二进制 nasm -f bin /path/to/xxx.asm -o /path/to/xxx.bin # 将二进制文件写入虚拟硬盘 ./main vhd /path/to/xxx.vhd -n=0 -w=/path/to/xxx.bin

一些指令、概念和要求说明

  • 概念:

    • 汇编地址:指令在内存段内的偏移地址,由编译期间计算生成。
    • 标号:在NASM汇编中,每条指令前面都可以拥有一个标号,代表该指令的汇编地址,在编译过程中会被替换成汇编地址。

      ; 这里的infi就是标号 infi: jmp near infi ; 可以不加冒号 infi jmp near infi ; 也可以独占一行 infi: jmp near infi
  • 要求:

    • Intel处理器不允许将一个立即数传送到段寄存器:必须先将立即数传到通用寄存器,然后从通用寄存器传到段寄存器
    • 相同数据宽度:前文提过,通用寄存器可以当做一个16位寄存器或两个8位寄存器来使用,其中16位或8位即不同的数据宽度。
    • 修饰关键字:包括byteword。在数据宽度不明确的情况下用于指定目的操作数的宽度。

      ; 源操作数字面值会被编译器转为ASCII码0x4C ; 可以解释为8位的0x4C也可以解释为16位的0x004C,宽度不明确 ; 而目的操作数是内存地址[0x00],它的宽度也不明确,可以是字单元或字节单元 ; 因此需要使用byte关键字来修饰指定以8位的宽度 mov byte [0x00], `L` ; 因为源操作数为寄存器bh,即16位bx寄存器拆分为两个8位寄存器bh和bl的其中之一 ; 所以数据宽度明确为8位,不需要修饰关键字 mov [0x02], bh ; 目的操作数为寄存器ax,数据宽度明确为16位 mov ax, [0x06]
  • 指令:

    • mov指令:用于数据传送,格式为mov 目的操作数 源操作数。目的操作数必须是通用寄存器或内存单元;源操作数可以是和目的操作数具有相同数据宽度的通用寄存器和内存单元,或者立即数。mov不允许目的操作数和源操作数同时为内存单元。mov不影响源操作数内容。
    • db指令:伪指令,声明字节(Declare Byte),跟在后面的操作数占一个字节长度;如果声明多个数据,各个操作数需要以逗号隔开。
    • dw指令:伪指令,声明字(Declare Word),其余同上。
    • dd指令:伪指令,声明双字(Declare Double Word),其余同上。
    • dq指令:伪指令,声明四字(Declare Quad Word),其余同上。
    • div指令:除法指令,有两种类型。

      • 16位二进制除8位二进制:被除数必须事先传送到AX寄存器,除数可以由8位通用寄存器或者内存单元提供。执行后商在寄存器AL中,余数在寄存器AH中

        ; 使用标号dividnd声明一个字数据,被除数0x3f0即十进制1008 dividnd dw 0x3f0 ; 使用标号divisor声明一个字节数据,除数0x3f即十进制63 divisor db 0x3f ; 事先把被除数从内存单元传送到ax寄存器 mov ax, [dividnd] ; 执行除法指令,除数由内存单元提供,指定数据宽度为字节 div byte [divisor]
      • 32位二进制除16位二进制:因为16位处理器无法直接提供32位被除数,所以要求被除数的高16位在DX中,低16位在AX中
    • xor指令:异或指令,常用于将寄存器清零。对比“把立即数0传送到寄存器”,xor机器码较短,且两个操作数都是通用寄存器,执行速度最快。

两张表

  • ASCII码:阅读方式为水平高3位比特加上垂直低4位比特,例如数字5的ASCII码为 011 0101。

    二进制 000 001 010 011 100 101 110 111
    0000 NUL DLE SPACE 0 @ P ` p
    0001 SOH DC1 ! 1 A Q a q
    0010 STX DC2 " 2 B R b r
    0011 ETX DC3 # 3 C S c s
    0100 EOT DC4 $ 4 D T d t
    0101 ENQ NAK % 5 E U e u
    0110 ACK SYN & 6 F V f v
    0111 BEL ETB ' 7 G W g w
    1000 BS CAN ( 8 H X h x
    1001 HT EM ) 9 I Y i y
    1010 LF SUB * : J Z j z
    1011 VT ESC + ; K [ k {
    1100 FF FS , < L \ l |
    1101 CR GS - = M ] m }
    1110 SO RS . > N ^ n ~
    1111 SI US / ? O _ o DEL
  • 80x25文本模式颜色表:由KRGBIRGB组成,其中前四位为背景色,后四位为前景色。

    R G B 背景色 前景色
    K=0时不闪烁,K=1时闪烁 I=0时正常亮度|I=1时高亮
    0 0 0 黑|灰
    0 0 1 蓝|浅蓝
    0 1 0 绿 绿|浅绿
    0 1 1 青|浅青
    1 0 0 红|浅红
    1 0 1 品红 品红|浅品红
    1 1 0 棕|黄
    1 1 1 白|亮白

    在显示器上显示白底黑字,即背景黑,前景白,二进制为0000 0111,十六进制为0x07;当显示器一片漆黑时,显示的是黑底白字的空白字符,即0x07 0x20

文本模式

地址线将0xB8000~0xBFFFF分配给显卡设备的内存(即显存),用于文本模式。

在该模式下,屏幕上可以显示25行,每行80个字符,共2000个字符。

使用逻辑地址访问显存

由于显存的起始物理地址是0xB8000,所以它的段地址可以看成0xB800,它的起始逻辑地址就是0xB800:0x0000

前文提过,数据寄存器分为DS和ES,我们将显存所在的内存段地址(0xB800)存到ES中。这样访问显存可以通过段超越前缀"es:",例如[es:0x00],的方式进行。

在屏幕上显示字符

  • 显示器上的字符为ASCII编码,所以显示时需要使用ASCII的二进制或十六进制表示。比如数字5,不应该在显存里存储0x05,而是ASCII表上的011 01010x35
  • 屏幕上的每个字符对应着显存中的两个连续字节,前一个是字符在ASCII的代码,后一个是字符的显示属性,即颜色表中的8位二进制。(这就是“一个字符占两个字节”的原因)

文本模式的屏幕右下角偏移地址是多少?

由于每个字符对应着显存中的两个连续字节,也就是1个字符占用了2byte,而文本模式一共有80x25=2000个字符,所以占用了4000byte。屏幕右下角即最后一个字符,它的偏移地址为初始偏移地址加3998byte长度后的结果,也就是3998D,转换为十六进制就是0xF9EH

案例:在屏幕第一个位置显示一个黑底白字的H字符

; Intel处理器不允许将一个立即数传送到段寄存器 ; 必须先将立即数传到通用寄存器 mov ax, 0xb800 ; 然后从通用寄存器传到段寄存器 mov es, ax ; 指定数据宽度为字节,把字面值H的8位ASCII码100 1000B ; 即0x48H传送到es寄存器的段内偏移地址0x00上 ; 这里使用了段超越前缀指定使用的寄存器 mov byte [es:0x00] 'H' ; 然后在相邻的下个字节处传送颜色信息指令 mov byte [es:0x01] 0x07