dustland

dustball in dustland

x86汇编语言 chapter 5 主引导记录

主引导记录

书上借助主引导扇区的例子,讲解了实模式下分段机制,显存的使用等等基本原理

为啥要借助主引导扇区呢?

因为虚拟机开机之后BIOS自动将磁盘的前512字节作为主引导记录装载进入内存的0x7c00位置并从0x7c00处开始执行.

我们的代码直接写到主引导记录中就可以顺理成章地执行了

自举过程

计算机上电之后,如果设置硬盘位首选的启动设备,则ROM-BIOS将视图读取硬盘的逻辑0号扇区,即物理上的0面0道1扇区.这里就是主引导扇区的位置

ROM-BIOS会把这个扇区的512字节搬到内存的0x7c00处,后面就是对内存中的拷贝进行的一系列操作

ROM-BIOS检查这512字节的最后两个字节是否是0x55,0xAA,这是主引导扇区魔数

如果主引导扇区有效则跳转打0x7c00继续执行

主引导扇区的内容一般是检测操作系统在硬盘上的位置,把grub代码加载道内存,跳转grub举起操作系统来

显示

显卡与显示器

显卡负责给显示器提供内容

显示器负责显式

显示器接到显卡上,显卡接到主板上

显卡控制显示器的最小单位是像素,每个像素点需要24比特存储颜色(RGB各8比特).显卡使用字节的显式存储器(显存,video RAM,VRAM)存放像素点的颜色.要显式啥就把啥信息放到显存里.

显示器是一个二维平面,儿显存就是一个内存条子的存储器,它是线性的,这就涉及到一维数组到二维数组的映射关系了

显存相当于一个缓冲区,CPU把想要输出到屏幕的东西放到显存里.显卡周期性地从显存拿出来按顺序显式到屏幕上.

显卡工作模式

显卡的工作模式分为文本模式和图形模式

图形模式下操作的确实是像素点

文本模式下只需要在显存中写入指定的字符的ASCII码,显卡内部经过字符发生器,就可以翻译成一些像素点的组合输出到屏幕上,也就是代替人干了计算每个像素点位置的事,人只需要指定第几行第几列写哪个字符就可以了

一般个人电脑显卡加电自检之后都会把自己初始化为80×25的文本模式

每行可以显示80个字符,一共可以显示25行.满屏是2000个字符

端口映射内存

如果正儿八经地论起来,除了内存条子,其他的设备都是IO外设,CPU和内存条子打交道的速度得是外设的成千上万倍,如果想要实现细致流畅的游戏画面体验,经过IO访问显存是办不到的.于是把显存也和内存统一编址,这就绕开了IO直接让CPU像访问内存一样访问显存.

对于8086来说,其CPU宽20位,可以寻址1M的内存空间0x0到0x9FFFF,

那从0xA0000到0xFFFFF就不是内存条子的事了,比如

0xB8000到0xBFFFF这32KB的地址空间映射给显存

0xF0000到0xFFFFF这64K的地址空间映射给ROM-BIOS

image-20220825153008702

段寄存器

显存入乡随俗,怎么访问内存就得怎么访问显存

访问内存使用段地址:偏移量,

物理地址=段地址×16+偏移量

那么如果要访问显存,就得根据显存的起始地址设置段地址,由于显存起始地址是0xB8000,因此段地址就是0xB800,寻址的时候CPU会自动把0xB800乘16的

怎样让段寄存器等于0xB800呢?

mov es,0xB800这样写吗?不可以,Intel的处理器规定不允许将立即数传递给段寄存器,必须使用通用寄存器或者内存单元中转一下,也就是说

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

字符模式下一个表示字符

字符模式下每个字符需要占据连续的两个字节,低字节是该字符的ASCII代码,高字节是该字符的颜色特征

image-20220825162813158

比如这里0xB8000开始的连续两个字节,

低字节0x48是'H'的ASCII码,

高字节0x07=00000111B表示黑背景色不闪烁,白前景色

显示字符

例子中想要显示一串字符串"Label offset:"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
      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

每两个相邻字节的低字节放字符ASCII代码,高字节调制颜色,都是不闪烁的黑背景,白前景

寻址的时候使用的是[es:0x00],转换成物理地址就是

es*16+0x00,为啥要写es呢?因为段寄存器默认是ds,这里需要段超越前缀指定使用es段

而es=0x8b00,乘以16之后恰好就是显存映射地址的起始位置

用byte关键字来修饰操作数的范围

1
mov byte [es:0x00],'L'

意思是'L'顶多占用一个字节,不能再多了,这条指令只会把0x4C放到es:00上

如果这样写

1
mov word [es:0x00],'L'

则把0x4C放到es:0x00,把0x00放到es:0x01

如果是把寄存器的值放到内存上则不用byte或者word修饰操作数的范围,因为寄存器已经自带宽度了

指令地址

汇编地址

前面字句过程中也已经提到过,BIOS会把MBR这个512个字节放到内存条的0x7c00上.也就是说代码是从0x7c00开始的

刚才显存扯了一大堆,但是都是相当于往数据段放东西,这个数据段位于0xB8000

汇编地址:

编译器会把0x7c00开始的MBR代码作为一个独立的段处理

这样每条指令相对于段地址都有一个汇编地址(段内),在lst文件中可以看到这个汇编地址

1
2
3
4
5
6
7
8
 行号 段内偏移 指令								汇编                                    
6 00000000 B800B8 mov ax,0xb800 ;指向文本模式的显示缓冲区
7 00000003 8EC0 mov es,ax
8
9 ;以下显示字符串"Label offset:"
10 00000005 26C60600004C mov byte [es:0x00],'L'
11 0000000B 26C606010007 mov byte [es:0x01],0x07
12 00000011 26C606020061 mov byte [es:0x02],'a'

第一条有意义的指令是在第6行,段内偏移量为0,指令内容是0xB800B8,翻译成汇编语言就是mov ax,0xb800,由于这条指令长3字节,因此可以计算得到下一条指令的段内偏移量就是3

第七行指令的段内偏移量就是3

标号

如果要在汇编语言中写出跳转功能,就得从一个地址转到另一个地址.

怎么指定目标地址呢?让人手工计算目标地址然后写到jmp后面吗?显然人工计算指令地址不显示,比如一段千八行的汇编语言,有好多指令都是跳转或者调用指令,现在第一行的指令发现错了要改,假设原来两个字节的指令改成了一个字节,那么后面的所有指令地址就都减1,这就需要所有的跳转和调用指令重定位.让人手工一行一行改太慢了

于是就发明了标号这种东西,类似于一个变量符号,作用是记录一个地址,编译器会自动计算该标号的地址,在生成二进制码的时候自动把标号翻译成地址,根宏定义展开一样

比如主引导记录中有这么一句:

1
infi: jmp near infi                 ;无限循环

infi标号的这条指令要跳转到infi标号处的指令,也就是自己跳转到自己的开始,这就形成了无线循环

标号还可以直接写到汇编指令里:

1
2
3
4
dividnd dw 0x3f0
divisor db 0x3f
mov ax,[dividnd]
div byte [divisor]

这里dividnd标号是0x3f0的地址,divisor是0x3f的地址

在编译时就像宏定义展开一样,mov ax,[dividnd]这里的标号就会自动转化成地址

声明并初始化数据

1
number db 0,0,0,0,0

这里number是标号,db是定义字节的伪指令,后面定义了五个字节,都初始化为0,相邻两个字节之间用逗号隔开.6

这里定义的五个字节就在当前代码段中

但是标注的做法应该是代码数据分离,数据专门放在数据段中,使用段超越方法寻址

byte [ds:00]就表示数据段的最开始的字节

意思是从number开始的五个字节都是0

dw声明字,dd声明双字,dq声明四字

db,dw,dd,dq都是伪指令,当编译完成时就找不到影子了

编译完成后的样子,就是5个0

1
100 0000012E 0000000000                number db 0,0,0,0,0

用db声明的变量就必须在字节大小范围内,最小是0x00,最大是0xFF,如果比这还大就会截断高位

比如db 0xFF00,编译之后保留低字节0x00

段超越前缀

如果这样写mov al,[0x00]

意思是把内存中0x00位置的数据搬到al寄存器中,问题是,这个0x00是相对于哪个段的偏移量呢?

DS?CS?

默认的数据段是DS,因此如果不指明段超越前缀,就是访问的数据段

但是如果要访问ES段寄存器指向的段,应该怎么写呢?

mov al,[es:0x00]

这里es:就是段超越前缀

除法指令

除法指令div只有一个操作数作为除数

被除数默认是AX寄存器

8086允许两种类型的整数除法,

其一是16位被除数除以8位除数由于8086的寄存器宽度为8,因此一个AX就可以放开被除数.除完了商放到AL,余数放到AH

其二是32位被除数除以16位除数

此时除数刚好占用一个寄存器,但是被除数就需要放到两个寄存器中了,即DX放高16位,AX放低16位,除完了商放在AX,余数放在DX

处理器怎么区分两种除法应该用哪种呢?看除数的格式,如果除数是一个字寄存器,比如div cx,就得是32位除被数除以16位除数.如果是div cl,就是16位被除数除以8位除数

在主引导扇区代码的第37到47行是这样写的

1
2
3
4
5
6
7
8
9
10
mov ax,number ;这里number是一个地址标号
mov bx,10 ;被除数置为10

mov cx,cs
mov ds,cx ;ds寄存器指向当前cx寄存器的段地址

mov dx,0 ;被除数高位置0
div bx ;DX:AX/BX

mov [0x7c00+number+0x00],dl ;dl放的是余数的低字节,也就是number这个地址的个位数字,放到0x7c00+number+0x00这个位置上

主引导记录中一直重复该过程,意思是十进制分解被除数number标号地址的各位,放到0x7c00+number开始的五个字节上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mov ax,number                 ;?????number???????
mov bx,10


mov cx,cs
mov ds,cx


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


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 [0x7c00+number+0x00],dl这就很诡异了,0x7c00是怎么来的?

偏移量换算

1
mov [0x7c00+number+0x00],dl

这里多了一个0x7c00,而不是直接mov [number+0x00],dl

这是因为,主引导扇区会被bios装载到内存的0x7c00处

此时CS=0x0000,IP=0x7c00

number是相对于mbr.asm这个文件的偏移量12E.

如果写mov [number+0x00],dl.实际上就是mov [0x12E],dl

放到了内存条上的0x12E位置,直接出了主引导区了

image-20220827151725554

mbr.asm程序自始至终没有修改过cs的值,那么这个值就是BIOS设置的,一直是0x0000不变

显示分解出来的各个数位

分解的各个数位按照个十百千万的顺序放到0x7c00+number+0x00到0x7c00+number+0x04的五个字节中

对于万位上的数字,mbr程序是这样写的:

1
2
3
4
mov al,[0x7c00+number+0x04]
add al,0x30
mov [es:0x1a],al
mov byte [es:0x1b],0x04

把万位数字拿出来放到al里,加上0x30转化成这个数字的ASCII码,然后放到es:0x1a位置,由于es早已设置为0xb800,即显存的起始位置,因此es:0x1a就是第一行的第1a个字符

在es:0x1b位置设置的是该字符的显示样式,设置为0x04意思是无背景不闪烁,红色前景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

最后另外放了一个黑底不闪烁白字'D',意思是十进制的缩写(Decimal)

因此打印效果为

image-20220827154928612

无限循环

在主引导记录中打印了number的数位分解之后,立刻进入无线循环

1
infi: jmp near infi  

为啥要这样写呢?

因为如果不写无线循环,那么虚拟机就直接寄了,主引导记录并没有抛砖引玉地加载操作系统,这512个字节跑完之后,后面就全是0了,不存在指令了

段内跳转near

jmp near意思是跳转到当前段内的一个地址,也就是不改变CS段寄存器地址,只改变ip的地址,因此操作数只是infi这个标号,不需要cs:infi指定跳转到哪个段的infi

填充与魔数

前面的代码和数据都写完了也远远达不到512个字节,主引导记录需要最后两个字符是0xAA55才有效.因此中间的字节需要说一些废话填充

1
2
times 203 db 0
db 0x55,0xaa

连着定义了203个字节都置0,最后两个字节是0xAA55魔数

到此主引导记录才有效

Bochs调试

之前我们一直使用的都是virtualBox虚拟机,只能运行不能调试,唯一能够调试运行的虚拟机就是Bochs

bochs配置

将主引导记录写入虚拟磁盘vhd文件之后启动bochs,需要在开始菜单上进行一些设置

image-20220827161343625

Disk&Boot

image-20220827161537279

都2202年了,不会还有人用软盘吧

ATA,硬盘接口标准,PATA是以前的IDE接口,SATA是当前使用的接口标准

每个计算机有多个ATA通道,允许加多块磁盘

ATA channel 0 是必须的,将nobody.vhd挂到ATA channel0上即可

ATA channel 0

image-20220827162047232
image-20220827162545217

这里Cylinders,Heads等参数不是乱写的,需要填虚拟磁盘的实际情况,可以手工分析最后512个字节的几何参数,也可以使用教材配套软件fixvhdwr.exe

image-20220827162429565

Boot Options

image-20220827162851643

保存

image-20220827162908917

前面两个设置完毕之后Save保存设置,下一次开Bochs的时候就不用重新设置了

保存一个bochsrc.bxrc文件,找一个牢稳的地方保存,比如bochs的根目录,或者虚拟机根目录,反正就是下一次启动bochs需要手动load该文件,能找到就行

bochs运行主引导记录

设置好之后就可以从bochs启动了

可以看到第一行打印出了"Label offset:00302D"

image-20220827163212152

bochs调试

使用bochsdbg启动虚拟机,此时除了模拟虚拟机屏幕窗口之外,还有一个终端窗口用于调试

image-20220827163659611

bochs在执行第一条指令前停下来等待调试命令

此时第一条指令位于f000:fff0,指令内容是

1
jmp far 0xf000:e05b

这个cs:ip指向的地址翻译成物理地址就是0xfe05b

而BIOS映射到内存的地址是[0xF0000,0xFFFFF],显然第一条指令跳转到了BIOS中

此后在这个调试终端中就可以输入命令进行调试了

断点

如果想在主引导记录开始下断点,由于主引导记录将被bios装载进入内存的0x7c00位置,

因此可以在调试终端中这样写

1
b 0x7c00

这样就在0x7c00出下拉一个断点,处理器将在执行0x7c00的指令之前停下

执行到断点

在0x7c00下断点后,命令c,一直执行直到遇到断点

1
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

确实是主引导记录的第一条指令,将要通过ax中转设置es为显存基地址

分号后面的十六进制数是该条指令的机器码

查看寄存器

使用r命令查看所有通用寄存器内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bochs:5> r
rax: 00000000_0000aa55
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0000
rdi: 00000000_0000ffac
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_00007c00
eflags 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

此时rax存放的是0xAA55,估计是BIOS检查主引导记录有效性时留下的

rip程序计数器此时指向0x7c00

单步执行

单步执行之后,程序停在0x7c03位置,此处的指令是mov es,ax.

1
2
3
<bochs:6> s
Next at t=17178999
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov es, ax ; 8ec0

那么此时第一条指令已经执行完毕了,ax的值应该是0xb800,r打印一下

1
2
<bochs:7> r
rax: 00000000_0000b800

确实如此

查看段寄存器

单步执行第二条指令mov es,ax.

之后es应该被赋值为0xb800

使用sreg打印所有段寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bochs:17> sreg
es:0xb800, dh=0x0000930b, dl=0x8000ffff, valid=1
Data segment, base=0x000b8000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000000f9ad7, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff

sreg不但打印了段寄存器内容,还打印了段寄存器隐藏的高速缓存器的内容,这是ollydbg做不到的

高速缓存器还有ldtr,gdtr等寄存器都是在保护模式才会用到,现在不管

查看内存

再连续执行两步,写入显存映射区域

1
2
3
4
5
6
7
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): mov byte ptr es:0x0000, 0x4c ; 26c60600004c
<bochs:18> s
Next at t=17179001
(0) [0x000000007c0b] 0000:7c0b (unk. ctxt): mov byte ptr es:0x0001, 0x07 ; 26c606010007
<bochs:19> s
Next at t=17179002
(0) [0x000000007c11] 0000:7c11 (unk. ctxt): mov byte ptr es:0x0002, 0x61 ; 26c606020061

使用xp(examine memory at physical address),显示指定物理内存处内容

1
xp (/数量) <物理地址>

xp默认情况下一次显示一个双字,xp /n就是显示n个双字

使用xp命令观察0xb8000开始的第一个双字

1
2
3
<bochs:24> xp 0xb8000
[bochs]:
0x00000000000b8000 <bogus+ 0>: 0x0b6f074c

低两个字节是0x074c,正好对应

1
2
mov byte ptr es:0x0000, 0x4c
mov byte ptr es:0x0001, 0x07

退出调试

命令q退出

初始化段地址

汇编地址和物理地址

汇编地址就是文件偏移量

实际加载到内存中时的地址不一定是汇编地址,有可能需要重定位基地址

mbr.asm被BIOS加载到内存的0x7c00处时,所有的汇编地址都需要再加上0x7c00才是其准确的物理地址,这是因为,此时的段寄存器是0,

如果想要在寻址的时候不写这个看上去很奇怪的0x7c00,可以将段寄存器改成0x7c0

1
2
mov ax,0x7c0                  ;设置数据段基地址 
mov ds,ax

此后任何汇编地址A就代表了物理地址A+0x7c0*16也就是准确的物理地址

实际上就是从0000:A+0x7c00转变成0x7c00:A

image-20220829151300719

段间数据传送

在机组课本上这叫做"串操作指令"

movs/movsb/movsw

数据流DS:SI->ES:DI

由cld和std两条指令置正方向还是反方向

cld之后DF=0,正向拷贝,movsb每次si和di加一,movsw每次si和di加二

std之后DF=1,反向拷贝,movsw每次si和di减一,movsw每次si和di减二

rep 重复执行,每次cx-1直到cx减为0,cx相当于循环变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
       jmp near start;跳过数据区

mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\
'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
number db 0,0,0,0,0

start:
mov ax,0x7c0 ;设置数据段基地址
mov ds,ax


mov ax,0xb800 ;设置附加段基地址
mov es,ax

cld ;拷贝方向标志清零,正向传送
mov si,mytext
mov di,0
mov cx,(number-mytext)/2 ;实际上等于 13
rep movsw ;重复cx次,每次重复cx-1,直到为0

将当前数据段的mytext标号开始处的(number-mytext)/2个字,正方向拷贝到0xb800:0000开始的内存区域中,

这恰好把mytext完全拷贝到显存区域

使用循环

loop循环

loop 标号,重复执行标号开始的代码,每次cx-1,如果cx降为0则跳出循环

由于loop指令位于循环的末尾,标号开始的代码无论如何和都要执行至少一次,因此实际上相当于do-while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
       ;得到标号所代表的偏移地址
mov ax,number

;计算各个数位
mov bx,ax
mov cx,5 ;循环次数5次
mov si,10 ;除数
digit:
xor dx,dx
div si ;dx:ax/si
mov [bx],dl ;保存数位,就写入number标号的内存区域
inc bx ;下一次要写入的位置
loop digit

条件跳转循环

1
2
3
4
5
6
7
8
9
10
11
12
13
      ;显示各个数位
mov bx,number
mov si,4
show:
mov al,[bx+si]
add al,0x30
mov ah,0x04
mov [es:di],ax
add di,2
dec si
jns show

mov word [es:di],0x0744

把number标号地址放到bx上,也就是分解number地址的十位得到的数字的地址

si作为偏移量,实际上bx[si]就构成了一个数组,si每次减1,直到0.

每次将[bx+si]放到al再加上0x30成为该数字的ASCII码,ah放上0x04即显示样式

然后ax整个两个字节被放到es:di上,就放到了显存中

然后di-2给下一个字符腾地方

每次si-1,直到减为-1是jns不满足跳转条件,跳出循环,执行下一条,即mov指令

跳出循环后的第一条指令是把0x0744这个字放到es:di上,即把'D'及其显示样式放到显存上

美元标记

NASM提供两个想美元符号的标记

$:单个美元符号,表示当前行的行首汇编地址

$$:双美元符号,表示当前段的起始汇编地址

1
2
3
4
5
6
       jmp near $;当前行首隐藏标号

times 510-($-$$) db 0;当前行首隐藏标号减去从头到次的字节数,
;510是去掉0xAA55之后的字节数
;再去掉前面已经占用的字节数,就是还需要填充的字节数
db 0x55,0xaa

jmp near $就是跳转到本行行首,陷入死循环

times 510-($-$$) db 0重复db 0这条指令若干次,

具体次数是510-程序到本指令之前已经有的字节数

也就是空闲的地方填满0

最后两个字节放0x55,0xaa,有效魔数

调试运行

还是在0x7c00下断点,然后c一直运行到该断点处停下

然后一直s单步执行到rep movsw指令

1
2
3
<bochs:11> s
Next at t=17179007
(0) [0x000000007c36] 0000:7c36 (unk. ctxt): rep movsw word ptr es:[di], word ptr ds:[si] ; f3a5

此时用r命令观察一下各个寄存器的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bochs:12> r
rax: 00000000_0000b800
rbx: 00000000_00000000
rcx: 00000000_0009000d
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0003
rdi: 00000000_00000000
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_00007c36
eflags 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

rcx=0x9000d,cx=0x000d,实模式下只用到cx寄存器,高位这个9没有作用

用sreg观察一下段寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bochs:13> sreg
es:0xb800, dh=0x0000930b, dl=0x8000ffff, valid=1
Data segment, base=0x000b8000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x07c0, dh=0x00009300, dl=0x7c00ffff, valid=1
Data segment, base=0x00007c00, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000000f9ad7, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff

es和ds已经设置好了

跳出rep,loop循环n

现在马上要执行rep movsw指令了

如果使用s则会进入循环,如果想步过循环到循环外面的第一条指令,使用n指令

1
2
3
<bochs:14> n
Next at t=17179020
(0) [0x000000007c38] 0000:7c38 (unk. ctxt): mov ax, 0x001d ; b81d00

此时使用xp指令观察显存区域是否已经获得拷贝

1
2
3
<bochs:15> xp /4 0xb8000
[bochs]:
0x00000000000b8000 <bogus+ 0>: 0x0761074c 0x07650762 0x0720076c 0x0766076f

能够使用n跳出的循环有个特点,就是cx作为循环变量,啥时候跳出循环是很清晰的

如果是跳出循环的条件判断在循环体中,比如一个if条件判断,则n指令就行不通了,此时可以使用u+b+c三种指令实现

反汇编u

1
u/<行数>

从当前行开始反汇编若干行指令

在需要跳出条件循环时,可以反汇编若干行,然后在循环外面设置一个断点,然后使用c一直执行到断点停下