试着解读 Win7 的 MBR(NASM 反汇编)
; int 19h 会将引导扇区(MBR)的内容读到 0x7c00 位置,然后开始执行
; 0x7c00 位置的代码就是这里的代码
; 段指针 ss,ds,es 都为 0
; 栈顶 = 0x7c00
00000000 33C0 xor ax,ax
00000002 8ED0 mov ss,ax
00000004 BC007C mov sp,0x7c00
00000007 8EC0 mov es,ax
00000009 8ED8 mov ds,ax
; ==============================
; 将引导扇区复制一份到 0x600 处,然后从副本中继续执行
; 0x500-0x7BFF 是自由空间,可以随意使用
; movsb 源地址 DS:SI = 0:0x7c00
; movsb 目标地址 DS:DI = 0:0x600
; movsb 长度 CX = 0x200 // 即 512
0000000B BE007C mov si,0x7c00
0000000E BF0006 mov di,0x600
00000011 B90002 mov cx,0x200
; 设置 SI、DI 为自增模式(std 为自减模式)
00000014 FC cld
; 复制字节 DS:SI -> DS:DI,重复 CX 次
00000015 F3A4 rep movsb
; 跳转到 0:0x61C 处继续执行
00000017 50 push ax
00000018 681C06 push word 0x61c
; pop IP; pop CS
0000001B CB retf
; ==============================
; 0:0x61C 就是这里
; 允许 CPU 响应可屏蔽中断
0000001C FB sti
; ==============================
; 查找活动分区
; 只有 4 个分区表项,所以循环 4 次
0000001D B90400 mov cx,0x4
; 从 0x600 开始的 0x1BE 位置,即后面的分区表位置
00000020 BDBE07 mov bp,0x7be
; 循环开始
; 分区表项的第一字节为“活动分区标记”:
; 0x80-0xFF 表示活动分区(有符号数为 -128 ~ -1)
; 0x00 表示非活动分区
; 其它值(0x00 - 0x7F)为无效标记(有符号数为 1 ~ 127)
; 判断标记是否小于 0,即判断标记是否在 0x80-0xFF 范围内
; cmp 是有符号数比较
00000023 807E0000 cmp byte [bp+0x0],0x0
; 如果在 0x80-0xFF 范围内,则跳转到 0x34 开始读取“分区引导记录 PBR”的代码(一个扇区)
00000027 7C0B jl 0x34
; 不在 0x80-0xFF 范围内,也不是 0,肯定在 0x01-0x7F 范围内,则跳转到 0x13B,打印错误信息
; short、near、far 在反汇编中体现不出差别,都会跳转到目标所指的地址
00000029 0F850E01 jnz near 0x13b
; 如果是 0,则表示非活动分区,继续分析下一个分区表项
; 每个分区表项的长度为 16 字节,也就是 0x10
0000002D 83C510 add bp,byte +0x10
; 检查下一个分区表项,直到 CX = 0
00000030 E2F1 loop 0x23
; ==============================
; 循环 4 次都没有发现活动分区,无法启动 OS,按照规范调用 Int 18h
; 早期的 BIOS 的 Int 18h 中断服务程序就是启动 ROM-Basic,
; 现在的 BIOS 一般是打印错误信息,然后 hlt(挂起 CPU)。
; ROM BASIC 是一个 ROM 解释器,允许用户运行和创建 BASIC 程序。
; 今天的电脑不再包括 BASIC ROM; 但是,使用 IBM 兼容计算机的用户
; 仍可能会遇到与 ROM BASIC 相关的错误消息。
00000032 CD18 int 0x18
; ==============================
; 找到活动分区,尝试读取活动分区的第一个扇区
; 将启动设备的“设备号”写入 [bp+0x0](分区表项的第一个字节,活动分区标记的位置)
; dl 中的值是 int 19h 留下的“启动设备号”
00000034 885600 mov [bp+0x0],dl
; 保存当前分区表项的地址(int 13h 可能会修改 bp)
00000037 55 push bp
; 分区表项之后的第二个字节(用来存储尝试次数)
; 如果读取扇区失败,会再次尝试读取,总共尝试 5 次
00000038 C6461105 mov byte [bp+0x11],0x5
; 分区表项之后的第一个字节(用来标记是否支持 int 13h 扩展)
0000003C C6461000 mov byte [bp+0x10],0x0
; ==============================
; 检查是否支持 int 13h 扩展
; int 13h 的 41h 号功能:检查是否支持 Int 13H 扩展
00000040 B441 mov ah,0x41
; int 13h 的参数:固定值 55AAh
; 另一个参数 DL:驱动器号
00000042 BBAA55 mov bx,0x55aa
; 检查是否支持 int 13h 扩展
; 返回值:成功 CF = 0,失败 CF = 1
; 成功 BX = 0xaa55,失败 BX = 0x55aa
00000045 CD13 int 0x13
; 恢复当前分区表项的地址
00000047 5D pop bp
; ==============================
; 如果进位标志(即 CF)为 1,则表示不支持 int 13h 的扩展功能,
; 则跳转到 0x59,使用普通 int 13h 读磁盘
00000048 720F jc 0x59
; 如果返回值 BX 不是 AA55h,则表示不支持 int 13h 的扩展功能,
; 则跳转到 0x59,使用普通 int 13h 读磁盘
0000004A 81FB55AA cmp bx,0xaa55
0000004E 7509 jnz 0x59
; 若支持 Int 13h 扩展,则返回值 CX 的第 1 位表示是否支持第一个子集,
; 第 2 位表示是否支持第二个子集。
; test 指令将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。
; 这里检查 int 13h 是否支持第一个子集。
00000050 F7C10100 test cx,0x1
; 若不支持,则跳转到 0x59,使用普通 int 13h 读磁盘
00000054 7403 jz 0x59
; 将 [bp+0x10] 的值设置为非 0,表示支持 int 13h 扩展
00000056 FE4610 inc byte [bp+0x10]
; ==============================
; 开始尝试读取扇区,最多尝试 5 次
; pushad 指令压入 32 位寄存器(保护现场)
; 入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI。
00000059 6660 pushad
; 判断是否支持 int 13h 扩展(非 0 支持,0 不支持)
0000005B 807E1000 cmp byte [bp+0x10],0x0
; 若不支持,则跳转到 0x87,使用普通 int 13h 的 CHS 方式读取数据
0000005F 7426 jz 0x87
; ==============================
; 下面开始使用 int 13h 的扩展功能读取分区引导记录(一个扇区)
; 扩展 Int 13h 通过 LBA(逻辑块地址)读取磁盘数据
; 在栈中填写“磁盘地址数据包”(这里的块大小为扇区大小,即 512 字节)
; struct DiskAddressPacket {
; BYTE PacketSize; // 本结构体大小(值为 16)
; BYTE Reserved; // 保留为 0
; WORD BlockCount; // 要读取多少个块
; DWORD BufferAddr; // 读取后保存到哪里(段:偏移)
; QWORD BlockNum; // 从哪个块开始读取(块号从 0 开始计数)
; }
; 从哪个块开始读取(高 32 位)
00000061 666800000000 push dword 0x0
; 从哪个块开始读取(低 32 位)
; [bp+0x8] 是活动分区的起始扇区位置(相对于磁盘开头的偏移量,从 0 开始计数)
00000067 66FF7608 push dword [bp+0x8]
; 读取后保存到哪里(段地址)
0000006B 680000 push word 0x0
; 读取后保存到哪里(偏移地址)
0000006E 68007C push word 0x7c00
; 读多少个块
00000071 680100 push word 0x1
; 结构体大小:16 字节(同时保留的字节为 0)
00000074 681000 push word 0x10
; ==============================
; int 13h 的 42h 号功能:扩展读
00000077 B442 mov ah,0x42
; int 13h 的参数 DL:驱动器号
00000079 8A5600 mov dl,[bp+0x0]
; int 13h 的参数 DS:SI = 0:SP 表示“磁盘地址数据包”的地址(之前在栈中填写的数据)
0000007C 8BF4 mov si,sp
; 开始扩展读
0000007E CD13 int 0x13
; ==============================
; 将标志寄存器的低 8 位写入 AH 中(保护现场)
00000080 9F lahf
; 扩展读完毕,将 SP 指向下一个分区表项(add 指令会影响标志寄存器)
00000081 83C410 add sp,byte +0x10
; 将 AH 的内容写入标志寄存器的低 8 位(恢复现场)
00000084 9E sahf
; 跳过普通 int 13h 调用,跳转到 0x9b,检查是否读取成功
00000085 EB14 jmp short 0x9b
; ==============================
; 下面开始使用普通 int 13h 读取分区引导记录(一个扇区)
; 普通 int 13h 通过 CHS(柱面、磁头、扇区)读取磁盘数据
; 通过 int 13h 的 02h 号功能读取扇区
; AL = 01 表示要读取的扇区数
00000087 B80102 mov ax,0x201
; ES:BX 表示读取后保存到哪里
0000008A BB007C mov bx,0x7c00
; DL 驱动器号(00H-7FH:表示可移动磁盘,80H-0FFH:表示固定磁盘)
0000008D 8A5600 mov dl,[bp+0x0]
; DH 磁头
00000090 8A7601 mov dh,[bp+0x1]
; CL 扇区(其中低 6 位是扇区,高 2 位是柱面的高 2 位)
00000093 8A4E02 mov cl,[bp+0x2]
; CH 柱面(柱面的低 8 位,加上前面的高 2 位,共 10 位)
00000096 8A6E03 mov ch,[bp+0x3]
; 开始读取
00000099 CD13 int 0x13
; ==============================
; 读取完毕,恢复 32 位寄存器(恢复现场)
; 与入栈顺序 EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 相反
0000009B 6661 popad
; 无论是普通 int 13h 还是扩展 int 13h,成功则 CF = 0,失败则 CF = 1
; 如果读取成功,则跳转到 0xbb,检查读取的扇区是否有启动标记
0000009D 731C jnc 0xbb
; 如果读取失败,则尝试次数 -1
0000009F FE4E11 dec byte [bp+0x11]
; 若尝试次数未达到 5 次,则跳转到 0xb0 处复位磁盘,再次尝试读取
000000A2 750C jnz 0xb0
; ==============================
; 若 5 次尝试都失败,此时 [bp] 中存储的是 int 19h 给出的启动设备号。
; 如果启动设备是 0x80 则跳转到 0x136,打印错误信息。
000000A4 807E0080 cmp byte [bp+0x0],0x80
000000A8 0F848A00 jz near 0x136
; 如果启动设备不是 0x80(而是 0x81-0xFF 之间的设备)
; 则尝试从 0x80 读取数据,而不是从 int 19h 给出的启动设备中读取数据。
000000AC B280 mov dl,0x80
000000AE EB84 jmp short 0x34
; ==============================
; 复位磁盘驱动器,再次尝试读取
; 保存 bp(保护现场),因为 int 13h 可能会改写 bp
000000B0 55 push bp
; int 13h 的第 0 号功能:复位磁盘系统
000000B1 32E4 xor ah,ah
; int 13h 参数:DL = 驱动器号
000000B3 8A5600 mov dl,[bp+0x0]
; 开始复位磁盘
000000B6 CD13 int 0x13
; 恢复 bp(恢复现场)
000000B8 5D pop bp
; 再次尝试读取扇区
000000B9 EB9E jmp short 0x59
; ==============================
; 读取成功
; 检查读取的扇区是否含有启动标记 55 AA
000000BB 813EFE7D55AA cmp word [0x7dfe],0xaa55
; 如果没有,则跳转到 0x131,打印出错信息
000000C1 756E jnz 0x131
; 如果有,则保存启动设备号([bp+0x0] 在 word 的低 8 位)
; push 指令一次至少压入 2 个字节,不允许只压入 1 个字节(也就是 2 字节对齐)
000000C3 FF7600 push word [bp+0x0]
; ==============================
; 打开 A20 地址线(打开后才能访问 1M 以上的内存)
; 调用函数 0x156,等待键盘的“输入缓冲区”为空
; 只有当输入缓冲区为空时才可以向其端口写入命令
000000C6 E88D00 call 0x156
; 如果键盘的“输入缓冲区”始终为满,则跳转到 0xE2
000000C9 7517 jnz 0xe2
; 禁止中断
000000CB FA cli
; 通知键盘,准备向键盘的输出端口写入数据
; 0xD1 命令码:表示要写数据到键盘控制器的 P2 端口
; P2 端口的 1 位用于打开 A20 线
000000CC B0D1 mov al,0xd1
000000CE E664 out 0x64,al
; 调用函数 0x156,等待键盘的输入缓冲区为空
; 缓冲区为空,表示上面的命令被接受
000000D0 E88300 call 0x156
; 打开 A20 地址线
000000D3 B0DF mov al,0xdf
000000D5 E660 out 0x60,al
; 调用函数 0x156,等待键盘的输入缓冲区为空
; 缓冲区为空,表示上面的命令被接受
000000D7 E87C00 call 0x156
; 0xff 不知道什么作用
000000DA B0FF mov al,0xff
000000DC E664 out 0x64,al
; 调用函数 0x156,等待键盘的输入缓冲区为空
; 缓冲区为空,表示上面的命令被接受
000000DE E87500 call 0x156
; 允许中断
000000E1 FB sti
; ==============================
; int 1ah 的 bbh 号功能:
; 不知道做什么用的
000000E2 B800BB mov ax,0xbb00
000000E5 CD1A int 0x1a
; 如果 EAX 不为 0 则跳转到 0x127,开始执行分区引导记录
; 不知道做什么用的
000000E7 6623C0 and eax,eax
000000EA 753B jnz 0x127
; 如果 EBX 不为 0x41504354 则跳转到 0x127,开始执行分区引导记录
; 不知道做什么用的
000000EC 6681FB54435041 cmp ebx,0x41504354
000000F3 7532 jnz 0x127
; 如果 CX 不为 0x102 则跳转到 0x127,开始执行分区引导记录
; 不知道做什么用的
000000F5 81F90201 cmp cx,0x102
000000F9 722C jc 0x127
; ==============================
; 填写 32 位寄存器(EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI)
000000FB 666807BB0000 push dword 0xbb07
00000101 666800020000 push dword 0x200
00000107 666808000000 push dword 0x8
0000010D 6653 push ebx
0000010F 6653 push ebx
00000111 6655 push ebp
00000113 666800000000 push dword 0x0
00000119 6668007C0000 push dword 0x7c00
0000011F 6661 popad
; int 1ah 的 bbh 号功能:
; 不知道做什么用的
00000121 680000 push word 0x0
00000124 07 pop es
00000125 CD1A int 0x1a
; ==============================
; 恢复设备驱动器号到 DL
00000127 5A pop dx
00000128 32F6 xor dh,dh
; 跳转到分区引导记录,继续引导
0000012A EA007C0000 jmp 0x0:0x7c00
; ==============================
; 没有发现活动分区,无法启动 OS,按照规范调用 Int 18h
; 早期的 BIOS 的 Int 18h 中断服务程序就是启动 ROM-Basic,
; 现在的 BIOS 一般是打印错误信息,然后 hlt。
; ROM BASIC 是一个 ROM 解释器,允许用户运行和创建 BASIC 程序。
; 今天的电脑不再包括 BASIC ROM; 但是,使用 IBM 兼容计算机的用户
; 仍可能会遇到与 ROM BASIC 相关的错误消息。
0000012F CD18 int 0x18
; ==============================
; 打印出错信息(索引号 [0x7b7] = 9A)"Missing operating system"
00000131 A0B707 mov al,[0x7b7]
00000134 EB08 jmp short 0x13e
; 打印出错信息(索引号 [0x7b6] = 7B)"Error loading operating system"
00000136 A0B607 mov al,[0x7b6]
00000139 EB03 jmp short 0x13e
; 驱动器号在 0x01-0x7F 范围(索引号 [0x7b5] = 63)"Invalid partition table"
0000013B A0B507 mov al,[0x7b5]
; ==============================
; 打印出错信息
; 计算字符串起始地址:ax = 索引号
0000013E 32E4 xor ah,ah
; 计算字符串起始地址:ax = 0x700 + 索引号
00000140 050007 add ax,0x700
; DS:SI 指向字符串起始地址
00000143 8BF0 mov si,ax
; 把 DS:SI 指向的存储单元读入 AL,然后 SI 自动 +1
; al = [0:0x700]
00000145 AC lodsb
; al = 要显示的字符(判断当前字符是否为 0x00)
00000146 3C00 cmp al,0x0
; 0x00 表示字符串显示完毕,停止 CPU
00000148 7409 jz 0x153
; bh = 0 表示页码,bl = 7 表示背景色
0000014A BB0700 mov bx,0x7
; 功能号 0x0E 表示在 Teletype 模式下显示字符
0000014D B40E mov ah,0xe
; 显示一个字符
0000014F CD10 int 0x10
; 继续显示下一个字符
00000151 EBF2 jmp short 0x145
; 暂停 CPU,等待指令,死循环
00000153 F4 hlt
00000154 EBFD jmp short 0x153
; ==============================
; 等待键盘的“输入缓冲区”为空
; 循环次数(65536 次)
00000156 2BC9 sub cx,cx
; 从 64H 端口读取 1 个字节到 al
; 0x64 端口是“键盘控制器”的“读取状态寄存器”。下面是它各个位的含义:
; 7 = 1 从键盘传输时发生奇偶校验错误
; 6 = 1 常规超时
; 5 = 1 鼠标输出缓冲区已满
; 4 = 0 键盘抑制
; 3 = 1 输入寄存器中的数据为命令
; 0 输入寄存器中的数据为数据
; 2 = 系统标志状态:0=加电或复位,1=自检正常
; 1 = 输入缓冲区已满
; 0 = 输出缓冲区已满
00000158 E464 in al,0x64
; 延时,相当于 nop 指令
0000015A EB00 jmp short 0x15c
; 检查状态寄存器第 2 位(输入缓冲区已满)是否为 0
; 若为 0 则 ZF=1,否则 ZF=0
0000015C 2402 and al,0x2
; CX=CX-1,若 CX != 0 且 ZF=0, 则跳转到 0x158 继续下一轮循环
0000015E E0F8 loopne 0x158
; 直到缓冲区为空,或者 CX 为 0(CX 为 0 表示循环了 65536 次)
00000160 2402 and al,0x2
00000162 C3 ret
; ==============================
; 常量
00000163 49 db "Invalid partition table", 0
0000017B 45 db "Error loading operating system", 0
0000019A 4D db "Missing operating system", 0
000001B3 00 db 0, 0
000001B5 63 db 0x63 ; 第 1 个字符串的地址
000001B6 7B db 0x7B ; 第 2 个字符串的地址
000001B7 9A db 0x9A ; 第 3 个字符串的地址
000001B8 54 db 0x54
000001B9 36 db 0x36
000001BA 55 db 0x55
000001BB 36 db 0x36
000001BC 36 db 0, 0
; ==============================
; 分区表
000001BE 00 times 16 db 0
000001CE 00 times 16 db 0
000001DE 00 times 16 db 0
000001EE 00 times 16 db 0
; ==============================
; 引导标记
000001FE 55AA db 0x55, 0xAA