3.1 地址、section、vstart 浅尝辄止
3.1.1 什么是地址
地址就相当于门牌号。
编译器的工作就是给各个符号编址。
偏移量:本质就是地址,每个变量的地址是前一个变量的地址+前一个变量的内存空间大小。
1. mov ax, 0x0 说明 $$ 就是在段首。
3. var 处被 0xd 替换,说明var 的地址为 0xd
4. $ 也就是 label 该行代码的地址 0x8
5. jmp short 0x8 ,说明 label 标号也就是 标号处的地址
6. var dw 0x99 就像是 dw var[1] = 0x99 一样,var 数组的首地址就是0x99的地址
也可以通过数地址处的内容:机器码或数据来计算
3.1.2 什么是 section
section称为节
为了不和linux用户进程中的段搞混,只用section来描述汇编语言中的段。
一般的应用场景是根据不同的属性人为地将程序分为几个部分。
使得代码结构更清晰明了,便于维护。
section 存在于 1、8行
第二行 $$ 指的是当前行所在段的首地址。也就是0x00.
第三行的 section.data.start 指的是 第8行 data 段 的地址。
第四行 var1 指的是 var1处的首地址
第五行 var2 指的是 var2处的首地址
第六行 由于是跳到该行的首地址,所以还是该行的首地址
3.1.3 什么是vstart
section 用 vstart=来修饰后,可以被赋予一个虚拟起始地址 virtual start address.
vstart 是
- 虚拟
- 起始地址
第一行 vstart=0x7c00 , 表明该段定为起始地址为0x7c00.
这只是个定义,并不是说加载到内存中后自动存放到0x7c00 处 ,只是当像 $$ 这样的方式访问时候,得到的数据是 以 0x7c00开始的,只是数据上的变化.
第二行 $$ 是 当前段的 地址,使用$ $访问当前段地址,得到的虚拟地址是0x7c00+0x0000,所以是0x7c00 ,也就是0x7c00
第三行 section.code.start 表示 code段距离整个代码开头的距离是多远,是偏移量。因为code段是首段,所以该段的首地址距离整个代码的距离为0x00
第四行 section.data.start 表示 data 段距离整个代码开头的距离是多远,是偏移量。因为data段是第二个段,所以,第一个段的结束,就是第二个段到整个代码的距离 0x14 = 20个字节。
第六行 var1 是 位于 data段 所以虚拟地址为 0x900+距离data段开头的距离 = 0x900+0x00 = 0x900
第七行 var2 是 位于 data段 所以虚拟地址为 0x900+距离data段开头的距离 = 0x900+0x04 = 0x904
第八行,是回到当前行的行首,所以跳到本指令前,也就是说-2字节.
3.2 CPU的实模式
这里的实模式就是8086CPU的寻址方式、寄存器大小、指令用法等。
3.2.1 CPU的工作原理
CPU大体三个部分
- 控制器
- 运算单元
- 指令寄存器IR
- 指令译码器ID
- 操作控制器OC
- 存储单元
- 程序被加载,
- 指令寄存器IP指向内存中下一条待执行指令的地址,将位于内存中的指令装在到指令寄存器中
- 指令译码器将位于指令寄存器中的指令按照指令格式解码。分析出操作码,操作数。
既然指令是存放在指令寄存器中,指令中用到的数据存放在存储单元中。
存储单元是CPU内部的L1、L2缓存及寄存器。这里说的数据是指令中的操作数。
缓存基本上采用的是SRAM存储器,是静态存储功能的存储器。
插在主板上的物理内存是DRAM,动态存储器,必须每个上一段时间就要去刷新(充电)电路,否在存储的数据就会丢失。
寄存器分为:
- 可见寄存器:通用寄存器、段寄存器。
- 不可见寄存器:程序员不可使用,也无法访问的。
总结下CPU的原理:
【控制单元】取下一条待执行的指令,该指令的地址在【程序计数器PC】中,在x86中程序基础其就是cs:ip。
于是将地址送入地址总线,cpu从该地址得到了指令,存放在【指令寄存器IR】中。
【指令译码器】根据指令格式检查指令寄存器中的指令,先去顶操作码是什么,再检查操作数类型,若是再内存中,就将相应操作数从内存中取回放入自己的存储单元,若是操作数是在寄存器中就直接使用了。
【操作控制器】给【运算单元】下令,【运算单元】开始执行指令。
【IP寄存器】的值被加上当前指令的大小,于是ip又开始指向下一条指令的地址。控制单元又要取出下一条指令了。
3.2.2 实模式下的寄存器
- 一类是内部使用的:
全局描述符表寄存器 GDTR、中断描述符表寄存器 IDTR、局部描述符表寄存器 LDTR、任务寄存器TR、控制寄存器 CR0~3、指令指针寄存器 IP、标志寄存器 flags、调试寄存器 DR 0 ~ 7 。
尽管是没法直接使用,但一部分还是要由我们初始化。 - 另一类是对程序员可见的寄存器。
段寄存器、通用寄存器。
- 代码段是指一个段内全是指令
- 数据段的区域内全是数据
- 栈段在内存中,硬盘中没有
- 在16位CPU中,只有一个附加段寄存器——es.
- fs 和 gs 附加段寄存器是在32位寄存器中增加的。但32位CPU在实模式下也可以使用这俩。
- cs:ip,不跨段的时候只修改ip,跨段的时候还要修改cs。
各通用寄存器特定的功能
3.2.3 实模式下的内存分段由来
实模式中的 ‘实’,程序中用到的地址都是真实的物理地址。为了降低程序对地址的过度依赖。
8086的地址总线是20位宽,寻址范围为 220 = 1MB。
为了让16位的寄存器能访问20位地址空间,通过把16位短地址左移4位后变成20位,再加上段内偏移地址,这样便形成了20位地址。
但 0xFFFF*16+0xFFFF=0x10FFEF,这已经是21位了,不过没关系,因为地址总线只有20条时,超过部分的就被丢掉了。
3.2.4 实模式下CPU内存寻址访问
- 寄存器寻址
- 立即数寻址
- 内存寻址
- 直接寻址
- 基址寻址
- 变址寻址
- 基址变址寻址
- 寄存器寻址
数据在寄存器中,直接从寄存器中取出数据
mov ax,0x10
mov dx,0x09
mul dx
以上三个都是。只要牵扯到寄存器的操作,无论其是源操作数,还是目的操作数,都是寄存器寻址。
- 立即数寻址
mov ax,0x18
mov ds,ax
第一条指令,0x18是立即数,所以即是立即数寻址也是寄存器寻址。第二条指令中源和目的都是寄存器,所以是纯粹的寄存器寻址。
另外像是以下这样用宏或标号参与的,也是立即数寻址。会在编译阶段转化为数字。
mov ax,macro_selector
mov ax,label_start
- 内存寻址
以下所说的寻址方式都是在内存中寻址的方法。 - 内存寻址——直接寻址
直接在操作数中给出的数字作为内存地址,通过中括号告诉CPU,取此地址中的值为操作数。
mov ax,[0x1234]
mov ax,[fs:0x5678]
- 内存寻址——基址寻址
操作数用bx寄存器或寄存器为地址的起始,变化以它为基础,这里说的是只能用bx,bp作为基址寄存器。(必须用bx,bp这个限制到保护模式就没有了)
bx 的默认段寄存器是ds,bp的默认段寄存器是ss。
mov [bx],0x1234
对于栈:
push ax;可分为:
sub sp,2
mov [sp],ax
pop ax;分为
mov ax,[sp]
add sp,2
因为sp是一直指向栈顶的,所以可以使用bp来访问栈中的数据。
比方这个函数:
int a = 0;
function (int b, int c){
int d ;
}
a ++
- 内存寻址——变址寻址
其实和基址寻址相似,只是将bx,bp改为了si,di。
si 是 源索引寄存器,di 是 目的索引寄存器,在一些指令中可以成对使用:movsb、movsw、movsd。
mov [di],ax ;将寄存器ax的值存放在ds:di指向的内存中
mov [si+0x1234],ax ;变址中也可以加个偏移量
- 基址变址寻址
mov [bx+di],ax
mov [bx+si],ax
在实模式中用到了call,ret 和 jmp,下面说说它们的用法。
3.2.5 栈到底是什么
CPU中由栈段SS寄存器和栈指针SP寄存器,它们指定当前使用栈的物理地址。
栈是一个线性表,后进先出。SS:SP这个栈的方向是向下的,栈顶地址是小于等于栈顶的。
push把数据压入:先 sp-数据长度 ,再放入数据,
pop将数据弹出:先出数据,再sp+数据长度。
3.2.6 实模式下的 ret
call 负责调用跳转,ret 负责返回。
ret 是 从栈顶弹出2个字节赋值给 IP 寄存器,同时 sp+2。
retf 是栈顶弹出4个字节,栈顶的2字节给 IP寄存器,另外2个给CS寄存器.也要负责维护栈顶指针,所以 retf指令会使 sp+4。
ret 适合近返回, retf 适合远返回
3.2.7 实模式下的 call
8086处理器中,2个指令用于改变程序流程。
- jmp ; 一去不回头
- call ;留下地址,等待 ret 或是 retf 指令返回
- 16 位实模式相对近调用
近调用 : call指令所调用目标函数和当前代码段是同一个段(即在同一个216= 64KB的空间内),所以只给段内偏移地址就可以了,用不到段基址。
名字中带有”近“的,就可以使用near来修饰。这样表示在内存或寄存器中取出2字节。和word作用相同。near可以省略,nasm编译器默认取出2字节。
call near 标号
的时候不要认为是编译后的地址是个绝对地址,在编译后的机器码的操作数中,他是call指令相对于目标地址的偏移量,是个地址差。
首先用目标函数的地址减去当前call指令的地址,所得的差再减去call指令机器码的大小3字节(此类相对近调用机器码大小是3字节),最后得到的就是于目标地址的相对地址增量。
因为是相对地址增量,所以被CPU使用的时候还要还原成绝对地址。段是个16位大小的空间,所以正负数范围 -32768 ~ 32767。
call near near_proc
jmp $
addr dd 4
near_proc:
mov ax,0x1234
ret
00000000: E8 06 00 EB FE 04 00 00 00 B8 34 12 C3 ..........4..
0xe8 就知道是相对近调用指令 06 是偏移量 9 - 0 - 3 = 6
eb fe 是jmp $ 的机器码
04 00 00 00 是4字节数据 addr dd 4
b8 34 12 中的 b8 是 mov 的操作码。但注意由于寻址方式不同mov的机器码也不都是b8。
call 相对近调用的操作数恢复位绝对地址做法:
当前IP指针+操作数+机器码大小=目标函数绝对地址
- 16 位实模式间接绝对近调用
一般形式:call 寄存器寻址、call 内存寻址
call ax ; call [0x1234]
间接绝对近调用 call 内存寻址 操作码 ff16。
机器码还与寄存器名称有关,例如:call ax => ffd0 ; call cx => ffd1 …
因为是近调用,所以还是 near ,保留IP寄存器的值就可以了,将其压入栈后,再用新的偏移地址替换IP的值。
section call_test vstart=0x900
mov word [addr],near_proc
call [addr] // call 内存寻址
mov ax,near_proc
call ax // call 寄存器寻址
jmp $
addr dd 4
near_proc:
mov ax,0x1234
ret
反汇编后的内容
mov word ptr ds:0x911, 0x0915
;c7 06 11 09 15 09 //寻址方式变了,mov 的操作码变成了 c706
call word ptr ds: 0x911
;ff 16 11 09 //用内存寻址 call 操作码 ff16,操作数0x0911
ret
; c3
mov ax,0x0915
;0x0915是函数near_proc的地址
call ax
;ff d0 ,操作码ff,操作数d0
mov ax,-x1234 ; b83412
ret ;c3
jmp .-2
;ebfe //eb是短转移 jmp, 0xfe是-2。
寄存器寻址中,如果寄存器前有 数据类型伪指令,编译器只会发出警告”寄存器大小被忽略“,但不影响编译,编译的机器码依旧是正确的。
数据类型伪指令,byte、word、dword,qword等,用于操作数前,相当于数据类型强转和C语言强转类似 。
near 和 数据类型伪指令 word一样,也是在内存中取出2字节。near 加在寄存器前,如 call near ax
,表示 ax 寄存器取 2字节。
near 若加在寄存器前,call near ax , 表示在ax寄存器前取2字节,相当于给ax寄存器中的值做了类型转化。由于near的范围可正可负,所以不同于world的效果。编译器发现16位的寄存器值精度被破坏了(寄存器中原值不变,但类型被强转了),但如果是word就不会有警告。
同理,far 取4字节、short取1子夜也一样,call far ax, call short ax,编译器同样也会发出警告。如果愿意,就用忽略near的形式。
- 16位实模式直接绝对远调用
”直接“就是指 操作数是立即数。
”远“就是跨段。
- 先把老 CS 压栈,再让老 IP 入栈。
- 用新的 CS 和 IP 寄存器替换。
指令一般形式:
call far 段基址(立即数):段内偏移地址(立即数)
对于直接绝对远调用,far 也可以不加。操作码是 0x9a。
机器码:0x9a+2字节偏移地址+2字节段基址,既偏移在前,段基址在后。和指令的调用形式相反。
section call_test vstart=0x900
call 0: far_proc
jmp $
far_proc:
mov ax,0x1234
retf //因为是远调用,所以用retf
地址 | 指令 | 指令机器码 |
---|---|---|
0000:0900 | call far 0000:0907 | 9a07090000 |
0000: 0907 | mov ax, 0x1234 | b83412 |
0000:090a | retf | cb |
0000:0905 | jmp .-2 | ebfe |
根据第一行中指令机器码,发现偏移地址在前,段地址在后。于汇编指令中给出的顺序是相反的。
操作数中的偏移地址0x907,这是绝对地址,说明了call指令确实通过目标函数的绝对地址来调用的。
- 16位实模式间接绝对远调用
段基址和段内偏移地址,在内存中或是寄存器中。因为至少两个16位寄存器,而寄存器资源过于珍贵。所以设计直接干脆只用内存寻址了。
16位间接绝对远调用指令格式:call far 内存寻址,call far [bx], call far [0x1234]
,操作码是 ff1e。内存中的大小是4字节,前(低)2字节是段内偏移地址,后(高)2字节是段基质。在此调用中一定要加上关键字 far。
像是call far [0x1234]
如果没有段跨越前缀,则默认的段基址寄存器ds*16+1234得到物理地址,在到该地址处取出新的偏移地址和段基址,起始是段内偏移地址,再过2字节是段基址。这时CPU为了记得住回来的路,所以把老CS,IP依次存入栈中。再用新的段基址和段内偏移地址进行替换CS和IP。
section call_test vstart=0x900
call far [addr]
jmp $
addr dw far_proc, 0
far_proc:
mov ax,0x1234
retf
第二行,执行间接绝对远调用,addr是变量,在第四行定义的。所以 其值的低2字节是 far_proc的地址,高2位是0,也就是段基址。
间接绝对远调用 call 的操作码是 ffle。retf的操作码是cb。
3.2.8 实模式下的 jmp
jmp转移指令只需要更新 CS:IP寄存器。
大体分为两类,近调用和远调用。
一共 5 种转移模式。
- 16位实模式相对短转移
像是 jmp $
格式:jmp short 立即数地址(可以是标号)。
既然转移方式是“相对”,所以操作数是相对增量。 范围:-128~127
段内转移。
section call_test vstart=0x900
jmp short start
times 127 db 0
start:
mov ax, 0x1234
jmp $
表 3-6 jmp 相对短转移指令
行 号 | 地 址 | 指 令 | 机 器 码 |
---|---|---|---|
1 | 0000:0900 | jmp .+127 (0x00000981) | eb7f |
2 | 0000:0981 | mov ax, 0x1234 | b83412 |
3 | 0000:0984 | jmp .-2 (0x00000984) | ebfe |
在0x900处指令是jmp .+127,操作数是127。转移的真实地址经过计算得到 0x900+2+127 = 0x981。 | |||
jmp $ 就是 jmp地址0x984+操作数(-2)+此指令长度2字节 = 0x984. |
但如果:
section call_test vstart=0x900
jmp short start
times 128 db 0 ;注意这里是 128 个 1字节
start:
mov ax,0x1234
jmp $
由于 128个1字节和 jmp short start 本身2字节 共130字节,所以操作数超过了范围,编译时会报错。
jmp.S:2: error: short jump is out of range
- 所以将 short 改为 near 就可以了。
- 或是只写 jmp ,让编译器判断short 还是 near。
- 16位实模式相对近转移
相对近转移和相对短转移的区别在于,自身指令长度由2变成3。指令由 short 变为 near。
绝对地址 = 操作数+IP寄存器的值+3
section call_test vstart = 0x900
jmp near start
times 128 db 0
start:
mov ax,0x1234
jmp $
按照2.10.07版本的nasm ,如果超过了16位有符号数字的范围 -32768~32767,虽然不会报错,但会忽略超过16位的部分,只保留16位结果。
- 16位实模式间接绝对近转移
“间接” 是指 内存/寄存器。
”绝对“ 是指 IP值。
采用寄存器寻址的jmp 操作码是0xff,操作数随寄存器不同而不同。
采用内存寻址的jmp 操作码要看段基址寄存器用的是哪个。
section call_test vstart=0x900
mov ax, start ;mov ax,0x0985
jmp near ax ;jmp ax ;ffe0, 操作码是0xff, e0表示寄存器ax。
times 128 db 0
start:
mov ax,0x1234
jmp $
在举个jmp用内存寻址的例子
section call_test vstart=0x900
mov word [addr], start ;mov word ptr ds:0x98a, 0x098c
jmp near [addr] ;jmp word ptr ds:0x98a
times 128 db 0
addr dw 0
start:
mov ax, 0x1234
jmp $
- 16位实模式直接绝对远转移
”直接“ 不仅是立即数,而且拿来就用
”绝对“ 操作数就是绝对地址
”远“ 跨段
格式: jmp 立即数形式的段基址:立即数形式的段内偏移地址 例如jmp 0:0x900
section call _test vstart=0x900
jmp 0: start ;jmp far 0000:0985
times 128:0
start: ;地址 0000:0985
mov ax, 0x1234
jmp $
- 16位实模式间接绝对远转移
”间接“,通过内存读取 段基址和段内偏移地址
section call_test vstart=0x900
jmp far [addr] ;jmp far ds:0x984
times 128 db 0 ;地址 0000:0984
addr dw start, 0
start:
mov ax, 0x1234
jmp $
3.2.9 标志寄存器 flags
实模式下标志寄存器是16位 falgs,在32位保护模式下,扩展(extend)了标志寄存器,称为了32位eflags。
3.2.10 有条件转移
是一个指令族,jxx,条件允许则会跳到指定的位置取执行,否则按顺序执行下一条指令。
格式:
jxx 目标地址
目标地址智能是段内偏移地址。
实模式下,编译器根据当前指令于目标地址的偏移量将其编译成段转移或是近转移。
保护模式下,寄存器宽度32位,32位偏移可以访问32位地址总线的4GB内存空间,编译器不再区分转移方式。
条件转移指令中所说的条件就是指 标志寄存器中的标志位。jxx就是各种各样的条件的分类。每个条件有不同的转移指令。
3.2.11 实模式小结
相当于是复习汇编语言了。
实模式被保护模式淘汰,主要是安全隐患。
实模式中,用户程序与操作系统有一样的特权。导致可以执行一些具有破坏性的指令。
3.3 让我们直接对显示器说点什么吧
之前用的是 BIOS 提供的0x10 中断实现的滚屏和打印字符。
3.3.1 CPU如何与外设通信——IO接口
CPU用的是TTL电平,外设大多是机电设备,所以在CPU和外设之间加个代理,。这个代理层就是IO接口。
IO接口是连接CPU与外部设备的逻辑控制部件,可分为硬件和软件两个方面。
硬件协调CPU和外设之间的种种不匹配,例如速度。实现数据缓冲以减少等待时间,数据格式不匹配,IO接口就在这两种格式间相互转换。
软件就是指用用来控制接口电路工作的驱动程序已完成内部数据传输所需要的程序。
CPU时间宝贵,为了简化CPU访问外设:
大家约定IO接口功能:
- 设置数据缓冲,解决CPU与外设速度不匹配。
- 设置信号电平转电路
- 设置数据格式转换
- 设置时序控制电路来同步CPU和外设
- 提供地址译码
CPU一个时刻只能与一个设备通信,所以仲裁IO接口竞争还要连接各种内部总线的 就是输入输出控制中心,也就是南桥芯片。
而北桥,是用于连接高速设备,如内存。南桥用于连接pci、pci-express、AGP等低速设备。南桥中集成了一些IO接口,并口硬盘PATA(IDE硬盘),串口硬盘SATA,USB、PCI设备、电源管理等接口。
对于非必要设备,南桥提供了专门用于拓展的接口,PCI接口。主板上有很多插槽,是预留的pci接口,pci设备可以即插即用。
如何访问端口:
有些微机系统可以把一些内存地址作为端口的映射,访问这些内存地址就相当于访问了端口。还有一些微机系统给把端口独立编址,从0开始编号。
IA32体系系统中,因为用于存放端口的寄存器是16位的,所以最大能访问65536个端口。0~65535。
要是通过内存映射,端口可以用mov指令操作。
用独立编址,要用CPU提供的单独的指令 in 和 out 。
-
in
从端口中读取数据
in al, dx;
in ax,dx;
al 和 ax 用于存放从端口中获取的数据
源操作数(端口号)必须是dx,al或是ax取决于要取出的数据宽度。 -
out
向端口中写入数据
out dx, al;
out dx, ax;
out 立即数, al / ax
al 和 ax 用于存放从端口中获取的数据
源操作数(端口号)必须是dx,al或是ax取决于要取出的数据宽度。
in 指令中,端口号只能用dx。out中,端口号可以用dx或是立即数。
3.3.2 显卡概述
显卡是显示适配器,用于连接CPU和显示器。
中断向量表只在实模式下存在,BIOS中断是要依赖于中断向量表的。保护模式下就没有中断向量表了,所以无法使用BIOS中断了。
显卡是pic设备,现在的显卡都是串口了,一次只发一位,在目的地处数据进行再组合。尽管是串口,但速度很快。
显卡提供的输入接口是显存和端口。书中主要用显存。
3.3.3 显存、显卡、显示器
显存是显卡内部的内存。显示器通过通过读取这里,显示内容。
24位真彩色就是说用24bit表示一个颜色。
黑白图形模式中,显存位与屏幕像素 1 对 1 的。所以显存中的1 就对应亮,显存中的0 就对应着灭。
显示器不区分图像和文字,只负责显示内容。所以,将一个字符对应一个字节的编码,将编码存放在显存中,这样显示器对照映射关系就知道要显示什么字符了。
<美国信息互换标准代码ASCII码>
显卡各种模式的内存分布
显卡有属于自己的BIOS,位于 0xC0000 ~ 0xC7FFF
支持三种模式:
- 文本模式
- 黑白图模式
- 彩色图形模式
书中主要针对文本模式操作 0xB8000 ~ 0xBFFFF 。
屏幕上显示的字符数:
8025,4025,8043 或是 8050。默认的是80*25。
由于显存 0xB8000 到 0xBFFFF,范围 32KB。所以2000字,每个字符2字节,所以每屏有4000字节,而32KB显存可以容纳32KB/4000B 约为 8 个屏幕。这样就是 alt+fn键 实现tty的切换的原理 。
屏幕上显示的字符编码两个字节:
高字节 字符属性元信息
- 高4位 字符背景色
4:1闪耀,0不闪
321:RGB 红绿蓝三基色调色 - 低4位 字符前景色
4:1亮,0正常亮度
321:RGB 红绿蓝三基色调色
低字节: ASCII
3.3.4 改进MBR,直接操作显卡
改造之前的MBR代码,将数据输出到显存中。
关于中断号 06 就是指定一个范围((cl,ch)行列,(dl,dh)行列),将范围内的资料上卷AL列,卷入新列的颜色放BH。
section MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;使用 int 0x10 功能号 0x06 清屏
mov ah,0x06
mov al,0x00 ;上卷行数 0->全部
mov bl,0x00
mov bh,0x07
mov cx,0x0000 ;左上( 0, 0)
mov dx,0x184f ;右下(80,25)
int 0x10
; ---------- 在显存中输入信息 ---------
; 输出背景为 黄 色,前景 红 色,并且跳动的字符串 "HELLO WORLD"
mov byte [gs:0x00],'H'
mov byte [gs:0x01],1110_1100b
mov byte [gs:0x02],'E'
mov byte [gs:0x03],1110_1100b
mov byte [gs:0x04],'L'
mov byte [gs:0x05],1110_1100b
mov byte [gs:0x06],'L'
mov byte [gs:0x07],1110_1100b
mov byte [gs:0x08],'O'
mov byte [gs:0x09],1110_1100b
mov byte [gs:0x0a],' '
mov byte [gs:0x0b],1110_1100b
mov byte [gs:0x0c],'W'
mov byte [gs:0x0d],1110_1100b
mov byte [gs:0x0e],'O'
mov byte [gs:0x0f],1110_1100b
mov byte [gs:0x10],'R'
mov byte [gs:0x11],1110_1100b
mov byte [gs:0x12],'L'
mov byte [gs:0x13],1110_1100b
mov byte [gs:0x14],'D'
mov byte [gs:0x15],1110_1100b
jmp $
times 510 - ($-$$) db 0
db 0x55, 0xaa
运行结果:
或是
section MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;使用 int 0x10 功能号 0x06 清屏
mov ah,0x06
mov al,0x00 ;上卷行数 0->全部
mov bl,0x00
mov bh,0x07
mov cx,0x0000 ;左上( 0, 0)
mov dx,0x184f ;右下(80,25)
int 0x10
; ---------- 在显存中输入信息 ---------
; 输出背景为 黄 色,前景 红 色,并且跳动的字符串 "HELLO WORLD"
mov bx,0
mov cx,14
mov si,0
print:
mov al,[s+si]
mov [gs:bx],al
inc bx
mov byte [gs:bx],1110_1100b
inc bx
add si,1
loop print
jmp $
s db "HELLO_WORLD!!!"
times 510 - ($-$$) db 0
db 0x55, 0xaa
运行结果:
还可以:
section mbr vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;清屏
mov ah,0x06
mov al,0x00
mov bl,0x00
mov bh,0x07
mov cx,0x0000
mov dx,0x184f
int 0x10
;bx 为屏幕的打印起始位置
;第1行 起始位置为80*2*(1-1)
;第2行 起始位置为80*2*(2-1)
;第x行 起始位置为80*2*(x-1)
mov bx,0
call print_Hello
mov bx,160
call print_Hello
jmp $
print_Hello:
;mov bx,0
mov cx,len
mov si,0
print:
mov al,[string + si]
mov [gs:bx],al
inc bx
mov al,[color]
mov byte [gs:bx],al
inc bx
add si,1
loop print
;jmp $
ret
string db 'HELLO_WORLD!!!'
len equ $-string
color db 1110_1100b
times 510 - ($-$$) db 0
db 0x55,0xaa
3.4 bochs 调试方法
练习上一节改进的MBR,有助于理解bochs用法。
3.4.1 bochs 一般用法
bochs 是开源的x86虚拟机软件
定义了各种数据结构来模拟硬件。因为是虚拟机,所以支持硬件级别的调试。
bochs的硬件调试体现在:
- 调试时可以查看页表、gdt、idt等数据结构 。
- 可以查看栈中数据
- 可以反汇编任意内存
- 实模式、保护模式相互变换时提醒
- 中断发生时提醒
bochs的调试风格是参照gdb来设计的。
bochsdbg.exe 就是调试用的。