本文是笔者学习《操作系统真象还原》的读书笔记,若有侵权,请联系删除。笔者也是处于学习阶段,所陈述的内容如有错误,欢迎指正!
目录
一、地址、section、vstart
1.1、地址
地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离。而给各符号编址的工作由编译器完成。代码经过编译之后,编译器提供的所有伪指令都将转化成CPU可以识别的东西。
下面我们就结合《操作系统真象还原》中的例子来体会地址的含义。源代码text.s如下,反汇编结果如截图所示,第一行为地址、第二行为机器码、第三行为反汇编代码。
mov ax, $$
mov ds, ax
mov ax, [var]
label: mov ax, $
jmp label
var dw 0x99
由截图可知,源程序中的$$、$、var、label等伪指令,全部被其对应的地址所代替。[]的作用是取所在地址处的内容。顺带说一句,CPU不能识别二进制串代表的是指令还是数据,它只知道一直往下执行,我们写汇编程序的时候,应该跳过数据区,否则,可能会发生灾难性错误。
1.2、section
section称为节,是伪指令。有的编译器同时支持segment和section,这两者的功能都是在程序中宣称一个区域。一般section的应用场景是根据不同属性人为划分为不同的部分,使得代码结构清晰,便于维护。但是,也可以用section将程序切得零零碎碎。
下面我们就结合《操作系统真象还原》中的例子来体会section。源代码text2.s如下,反汇编结果如截图所示,第一行为地址、第二行为机器码、第三行为反汇编代码。
section code
mov ax, $$
mov ax, section.data.start
mov ax, [var1]
mov ax, [var2]
label: jmp label
section data
var1 dd 0x4
var2 dw 0x99
由上述截图可知,最后四行,起始都是数据,但是机器码“碰巧”和一些指令的相同,所以,当CPU“误”把数据当指令来执行,可能会出错。section.data.start是获得名为data的section在本文件中的真实偏移量,根据反汇编截图可知,其与var1的偏移量是一致的。所以,关键字section并没有对程序中的地址产生影响,section中的数据地址依然是相对与整个文件的顺延,仅仅是起到逻辑上让开发人员梳理程序。
1.3、vstart
vstart的作用是为section内的数据指定一个虚拟的起始地址,也就是根据此地址,在文件中是找不到相关数据的。
vstart = ***的功能是告诉编译器,将本section后面的所有数据(指令和变量)的地址以***为起始编址,不再以0开始编址。加载器会将该段程序加载到***处。
使用vstart的时机是,我们预先知道我们的程序会被加载到某处。例如,我们知道mbr会被BIOS(加载器)加载到0x7c00处,所以我们前面写mbr时,第一行写着“section mbr vstart=0x7c00”
下面我们就结合《操作系统真象还原》中的例子来理解vstart。源代码text3.s如下,反汇编结果如截图所示,第一行为地址、第二行为机器码、第三行为反汇编代码。
section code vstart=0x7c00
mov ax, $$
mov ax, section.code.start
mov ax, section.data.start
mov ax, $
mov ax, [var1]
mov ax, [var2]
jmp $
section data vstart=0x900
var1 dd 0x4
var2 dw 0x99
分析:$$在反汇编中变成了我们指定的“0x7c00”。“section.节名.start”可以获取该section在文件中真实的地址(相对文件开头的偏移量)。var1和var2对应的地址也是以0x7c00为起始地址的顺延。然而,需要注意的是,jmp $的反汇编指令变成了“jmp short 0x12”,此处的$对应的是相对文件开头的偏移量,这是不正确的。机器码“E8”对应的是相对短转移,jmp short是相对短转移,操作数是相对于跳转目标地址的偏移量。此处需修改成“jmp short -2”
二、CPU的实模式
实模式是指8086CPU的寻址方式、寄存器大小、指令用法等,是用来反映CPU在该环境下如何工作的概念。实模式被保护模式淘汰的原因是,是安全隐患。在实模式下,用户程序和操作系统可以说是同一特权级的程序。
2.1CPU工作原理
CPU大体上可划分为三个部分:控制单元、运算单元、存储单元。
控制单元大致由指令寄存器、指令译码器、操作控制器组成。
当前指令执行完毕后,控制单元要取下一条待运行的指令,该指令的地址在程序计数器PC中,对于X86CPU而言看,程序计数器就是cs:ip。读取ip寄存器后,将此地址送上地址总线,CPU据此地址便可得到指令,将其存入指令寄存器。此时需要指令译码器根据指令格式来确定指令操作码和操作数类型。若操作数在内存中,需要将操作数从内存中取回放入自己的存储单元,若操作数在寄存器中,则无需去操作数的步骤。此时已经确定操作数和操作码,由操作控制器向运算单元下达命令,运算单元便开始执行相关操作。ip寄存器的值加上当前指令的大小,得到下一条指令的地址(当前指令为jmp指令时,会跟新cs和ip的值,直接得到下一条指令的地址)。
2.2实模式下的寄存器
CPU中有缓存。CPU有一级缓存L1、二级缓存L2,它们都是SRAM,它是最快的存储器了,由寄存器来存储数据的。SRAM和CPU的速度是同一级别的。
根据程序员是否能够使用,将寄存器分为“对程序员可见”和“对程序员不可见”。对程序员可见的寄存器是我们在写汇编代码时可以操作的寄存器,比如段寄存器和通用寄存器。虽然我们不能使用对程序员不可见的寄存器,但是这类寄存器有些需要我们来初始化。
实模式下,默认用到的寄存器都市16位宽的。
段寄存器:CS、DS、ES、FS、GS、SS。对于16位CPU,只有一个附加寄存器ES,FS和GS是32位CPU中增加的。我们使用32位CPU,但不代表32位CPU在实模式下的16位环境中就不能使用FS和GS。
通用寄存器:AX(AH&&AL)、BX(BH&&BL)、CX(CH&&CL)、DX(DH&&DL)、SI、DI、BP、SP
寄存器 | 助记符 | 功能描述 |
ax | 累加器 | 算数运算、逻辑运算、保存与外设输入输出的数据据 |
bx | 基址 | 存储内存地址,用此地址作为基址遍历一片内存区域 |
cx | 计数器 | 存储循环指令中的循环次数 |
dx | 数据 | 用于保存外设控制器的端口号地址 |
si | 源变址 | 常用于存储字符串操作中的数据的源地址值 |
di | 目的变址 | 和si一样,但di用来表示数据的目的地址 |
sp | 栈指针 | 段基址是SS,用来指向栈顶。pop和push会改变sp |
bp | 基址指针 | bp默认段寄存器是SS,可读写栈底和栈顶之间的数据 |
2.3实模式下CPU内存寻址方式
在CPU眼中只有二进制数,所以寻找的是源操作数或者目的操作数的地址。CPU访问数据的方式看着很死板,这是因为,一种寻址方式对应了一种电路实现,增加一种寻址方式,就会增加硬件电路的复杂性,所以寻址方式是有限的。8086在寻址方面的电路做的简单有限。
2.3.1寄存器寻址
最直接的寻址方式就是寄存器寻址,它是指“数”在寄存器中,直接从寄存器拿数据即可。只要指令牵扯到寄存器的操作,无论其是源操作数还是目的操作数,都是寄存器寻址。示例如下:
mov ax, 0x10
mov dx, 0x9
mul dx
第一第二条指令的源操作数都是立即数,所以也数据立即数寻址。
补充:8086汇编中的乘法指令MUL(被乘数*乘数=积)
- 被乘数与乘数均为8位时,被乘数默认存放在AL中,乘数存放在内存(需要指明操作数宽度)或者8位寄存器中,结果存放在AX中
- 被乘数与乘数均为16位时,被乘数默认存放在AX中,乘数存放在内存(需要指明操作数宽度)或者16位寄存器中,结果的高16位存于DX中、低16位存于AX中
2.3.2立即数寻址
立即数就是常数。当操作数在寄存器或内存中时,是间接给出的,得到数需要花费时间。当操作数“直接”存在指令中,立即就能使用。示例如下:
mov ax, 0x18
mov ax, macro_selector ;macro_selector是宏定义
mov ax, label_start ;label_start是标号
当操作数中有立即数、宏、标号(其值就是地址)时,就属于立即数寻址。在编译阶段,宏和标号会转化为数字,在可执行文件中依然是立即数。
2.3.3内存寻址
操作数在内存中的寻址方式称为内存寻址。访问内存是以“段基址:段内偏移地址”的形式,默认情况下数据段寄存器是DS,一般情况下只需要给出段内偏移地址就可以访问内存了。
①直接寻址
直接寻址,就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉CPU,取此地址中的值作为操作数。示例如下:
mov ax, [0x1234]
mov ax, [fs:0x1234]
对于第一条指令,0x1234是段内偏移地址,其默认段基址是DS。第二条指令,是有了跨段前缀fs,所以段基址变为了fs。
②基址寻址
基址寻址,就是操作数中用寄存器bx或者寄存器bp作为地址的起始,地址的变化以它为基础。在实模式下,用寄存器作为内存寻址,必须用bx或者bp,在保护模式下没有此限制,基址寄存器可以是所有的通用寄存器。
寄存器bx的段寄存器默认是DS,寄存器bp的段寄存器默认是SS。虽然称bx或者bp为基址寄存器,但是,bx、bp中的内容依然是段内偏移地址,段基址依然在DS、SS中。此处的“基址”可以理解为以bx或者bp中的偏移地址为基准,通过位移可以得到新的偏移地址。示例如下:
;对于以寄存器bx作为基址寄存器
buffer dw 0x20, 0x10, 0x0f, 0x30
mov bx, buffer
mov cx, 0x04
lpinc:
inc word [bx]
add bx, 0x02
loop lpinc
;对于以寄存器bp作为基址寄存器
mov ax, 0x5000
push ax
mov bp, sp
mov ax, 0x7000
push ax
mov dx, [bp]
mov dx, [bp-2]
第一段代码,我们令bx指向所有数据的起始位置,这是基准地址。但是在循环lpinc中,我们不断设置新的基准地址,并通过新的寄存地址来访问后面的每一个字。
第二段代码,我们在将0x5000压入栈后,立即将sp的值保存到bp。后面尽管栈顶数据0x7000没有出栈,通过bp也可以将在0x7000之下的数据0x5000甚至栈中的其它数据取出来。这样,我们就可以将栈当成普通的数据段那样访问了。bp作为基址寄存器,不但可以用来访问栈中的元素,还可以间接修改eflags的内容。
③变址寻址
与基址寻址类似,只是用到的通用寄存器不同。变址寄存器是SI和DI,与基址寄存器一致,除非使用段超越前缀,否则处理器会访问由段寄存器DS指向的数据段。变址寄存器也允许带一个偏移量。示例如下:
mov [si], dx
add ax, [di]
xor word [si], 0x8000
mov [si+0x100], al
and byte [di+label_a], 0x80 ;虽然使用了标号label_a,但是本质上属于一个编译阶段确定的数值
④基址变址寻址
这种寻址方式是基址寻址和变址寻址的结合,即基址寄存器bx或bp加一个编址寄存器si或di。示例如下:
mov [bx+di], ax
add [bx+si], ax
2.4什么是栈
栈的定义:栈首先需要是线性结构,并且数据的存取在线性结构的一端进行。需要维护一个指针,用它来指向线性结构的一端,数据存取都通过此指针。
上述是逻辑上的栈,硬件是如何实现栈的呢?
首先,栈是一片内存区域,内存本身就具有线性的特性,满足要求。栈在内存中是向下扩展的,访问栈也是采取“SS:SP”的形式。
硬件提供了相应的方法来存取栈,即push和pop指令。push把数据压向哪里?pop如何取得栈顶数据?这些都依赖栈顶指针sp。sp的值是段内偏移地址,是栈顶相对于栈底的偏移量。
2.5实模式下的ret
CPU中提供了改变程序执行流的指令有ret、jmp、call等指令。虽然这三条指令只是一条指令,但是其内部做的操作并不少,它们都在原理上修改了寄存器CS和IP的值,将CPU导向新的位置。
当我们要调用某个函数或某段程序时,需要执行call指令,然而,在call之前,我们理应保存下返回地址,即call指令的下一条指令的地址。因为在执行完调用之后,需要提供返回地址。
call指令将返回地址压入栈中,为将来能够回到当前执行位置埋下伏笔。ret指令则负责回来。
返回指令分为ret(近返回,调用时不需要跨段)和retf(远返回,调用时跨段)
ret和retf的功能是在栈顶(ss:sp)弹出若干字节的内容来修改CPU的执行流(修改PC),不仅如此,它们还负责维护栈顶指针,需要移动sp指向新的栈顶。
ret弹出两字节(该内容应该为ip),retf弹出四字节(改内容应该为cs:ip)。为什么说是应该呢?因为ret指令并不管里面的内容是什么,这些需要程序员写下合理的程序。例如,执行call指令后应当执行ret而不是retf。否则,CPU的运行情况可能会失去掌控。
2.6实模式下的call
以下提供四个指令,读者能说出分别是类型的call吗?"call near near_prog"、"call [0x1234]"、"call 0x0000:0x1234"、"call far [0x1234]"
2.6.1实模式相对近调用
强调两个概念,“近”和“相对”
“近”指的是call指令调用的目标函数和当前代码段是同一个段,不需要换段基址,只需给出段内偏移地址即可。
何为“相对”?由于在同一个代码段,所以只要给出目标函数的相对地址即可。编译后的操作数,不是目标函数的绝对地址,而是call指令相对于目标地址的偏移量。既然是偏移量,那么此指令的操作数是有符号数。
call相对近调用时,CPU会根据给出的偏移量计算出目标函数的绝对地址。CPU会当前IP寄存器值压入栈,再把计算出的绝对地址载入IP寄存器,CPU的航线就会被改变,此处压栈是为后续ret埋下伏笔。
示例如下:
mov ax, 0x1234
call near near_prog
jmp $
addr dd 4
near_prog:
mov ax, 0x5678
ret
结合以上程序和反汇编代码对“相对”这一概念做进一步说明。看到“call near near_prog”指令的机器指令是“0xE80600”,其中E8是机器码,0x0600是操作数,由于采用小端字节序,所以地址应该是0x06。该数字怎么来的呢?看到标号“near_prog”的值为0x0c,call指令的地址为0x03,而call指令的长度为0x03,所以,就是0x0c-0x03-0x03所得。
《操作系统真象还原》中提供了逐字节查看文件的xxd.sh脚本文件,内容如下
#usage: sh xxd.sh 文件 起始地址 长度
xxd -u -a -g 1 -s $2 -l $3 $1
#-u use upper case hex letters. Default is lower case.
#
#-a | -autoskip
# toggle autoskip: A single ’*’ replaces nul-lines. Default off.
#
#-g bytes | -groupsize bytes
# separate the output of every <bytes> bytes (two hex characters or eight bit-digits each) by a whitespace. Specify -g 0 to
# suppress grouping. <Bytes> defaults to 2 in normal mode and 1 in bits mode. Grouping does not apply to postscript or
# include style.
#
#-c cols | -cols cols
# format <cols> octets per line. Default 16 (-i: 12, -ps: 30, -b: 6). Max 256.
#
#-s [+][-]seek
# start at <seek> bytes abs. (or rel.) infile offset. + indicates that the seek is relative to the current stdin file position
# (meaningless when not reading from stdin). - indicates that the seek should be that many characters from the end of
# the input (or if combined with +: before the current stdin file position).
# Without -s option, xxd starts at the current file position.
笔者将xxd.sh文件放在mylab路径下,让各个分实验需要时再使用即可。在分实验文件中,也许直接使用xxd.sh会出现不能使用的情况,执行“chmod +x xxd.sh路径”在执行xxd.sh即可。操作截图如下,0和16分别是被查看文件的起始地址和想要查看的文件长度。可以通过“ll -b 文件名”查看文件长度。可以结合xxd.sh和反汇编来查看相关指令。
2.6.2实模式间接绝对近调用
“近”依然是当前代码段和目标函数在同一段内,只需要给出段内偏移即可。
“间接”是目标函数的地址不再是以立即数的形式给出,而是通过寄存器或者内存给出。
“绝对”是指目标函数的地址是绝对地址,不再是相对地址了
示例如下:
section call_test vstart=0x900
mov word [addr], near_prog
call [addr] ;通过内存来调用目标函数
mov ax, near_prog
call ax ;通过寄存器来调用目标函数
jmp $
addr dd 4
near_prog:
mov ax, 0x1234
ret
数据类型伪指令byte(1字节)、word(2字节)、dword(4字节)、qword(8字节)等,它们用在操作数前,相当于做数据类型强制转换
near、far、short,用在调用或者转移中的修饰符,其意义就是数据类型转换。例如,call near ax表示在ax寄存器中取2字节,相当于给ax寄存器中的值做了类型转换。near的范围可正可负,所以不等同于数据类型word。far表示取4字节,short取1字节。
2.6.3实模式直接绝对远调用
“直接”表示操作数以立即数的形式给出
“绝对”表示给出的目标函数绝对地址
“远”表示目标函数与当前指令不在同一段内,需要跨段了。故而先将cs压栈,再将ip压栈
调用形式“call (far)段基址(立即数):段内偏移地址(立即数)”
说明:即使段基址填入的仍然是当前段的段基址,也不会有什么问题,cpu不负责识别两个段基址是否相同,它只做硬件规定的事情
2.6.4实模式间接绝对远调用
此处和第三种方式的区间,在于由“直接”变成“间接”。操作数不再由立即数提供,而是由内存提供,注意,此方式操作数不能让寄存器提供。
调用形式“call far 内存寻址”,例如call far [bx],call far [0x1234]。如果没有段跨域前缀,段基址默认是ds。
对于call far [0x1234],由ds:0x1234得到物理地址,再到该物理地址处读取新的偏移地址和段基址,以该物理地址为起始的2字节是偏移地址,段基址是紧跟在偏移地址后的两个字节。为了记得来时路,需要将当前指令的cs和ip分别压栈
2.7实模式下的jmp
无条件跳转,是指改变cpu航向后,将不再返回,是一去不回头的指令。
2.7.1实模式相对短转移
相对短转移的操作数占1字节,跳转的范围只能是1字节有符号数所表示的范围,即-128~127。若指令带有short,操作数超过这个范围,将会报错
“相对”意味着操作数是个相对增量,而不是绝对地址
“短‘”意味着只能在段内转移,不需要跨段
指令格式为“jmp short 立即数地址”。short可以省略,但是省略后并不能保证nasm依然把它编译成相对短的形式。
示例如下:
section jmp_test vstart=0x900
jmp short start
times 127 db 0
start:
mov ax, 0x1234
jmp $
此处,分析一下,为什么“jmp $”的反汇编是“jmp -2”。假设jmp的相对文件头的偏移量是x,jmp的指令长度为2,跳转到的地址-当前指令的地址-指令长度=x-x-2=-2
2.7.2实模式相对近转移
相对近转移相较于相对短转移,只是操作数的宽度由1字节变成2字节,表示范围-32768~32767。
指令格式为“jmp near 立即数地址”
2.7.3实模式间接绝对转移
该指令的目标地址是绝对地址,并且未在指令中直接给出,而是存在寄存器或者内存中。该绝对地址,就是段内偏移地址,是“CS:IP”中的IP值。只需从寄存器或者内存中取出两字节写入IP即可。
指令格式为“jmp near 寄存器”或者“jmp near 内存寻址”
示例如下:
section jmp_2.test vstart=0x900
;寄存器方式
mov ax, start
jmp near ax
;内存寻址方式
mov word [addr], start
jmp near [addr]
times 128 db 0
addr dw 0
start:
mov ax, 0x1234
jmp $
2.7.4实模式直接绝对转移
"直接"是操作数不仅是立即数,CPU可以直接拿来用。
“绝对”是提供的操作数是绝对地址。
“远”是指目的地址和当前指令不在同一个段,所以操作数要包括新的段基址和段内偏移。
指令格式为“jmp 立即数形式的段基址:立即数形式的段内偏移地址”
2.7.5实模式间接绝对转移
与“直接绝对”的区别是,此指令的操作数不再是由立即数给出,而是由内存给出。
指令格式为“jmp far 内存寻址”
2.8标志寄存器flags
上述讲述的jmp是无条件跳转,在指令中,亦有有条件跳转,而这些条件就存储在flags中。
实模式下,flags占16位,而保护模式下,eflags占32位。
flags寄存器中存储的信息,只是结果的特征,而并非真正的结果
CF | 进位,用于检查无符号数的加减法是否溢出,CF=1时表示溢出 |
PF | 奇偶位,标记结果中低8位1的个数,PF=1表示有偶数个1 |
AF | 辅助进位标志,记录结果低4位的进、借位,有则AF=1 |
ZF | 零标志位,结果为0时,ZF=1 |
SF | 符号标志位,运算结果为负数时,SF=1 |
TF | 陷阱标志位,TF=1,CPU为单步运行,否则连续工作。与debug有关 |
IF | 中断标志位,IF=1,表示中断开启,否则,不相应外部可屏蔽中断 |
DF | 方向标志位,用于字符串操作指令。DF=1,指令中的操作数会自动减少一个单位,否则会自动增加一个单位 |
OF | 溢出标志位,OF=1表示有溢出,专门用于检测有符号数运算结果 |
IOPL | 用在有特权级概念的CPU中,有四个特权级 |
NT | 任务嵌套标志,NT=1时表示一个任务可以嵌套另一个任务 |
RF | 恢复标志位,用于程序调试,RF=1时指令忽略调试故障,否则接受 |
VM | 是实模式向保护模式过渡的产物,允许将此位一直置1 |
AC | 对齐检查,AC=1时会对地址进行对齐检查 |
VIF | 虚拟中断标志位,虚拟模式下的中断标志 |
VIP | 虚拟中断挂起标志位,在多任务下,为OS提供虚拟中断挂起信息 |
ID | 识别标志位,ID=1表示当前CPU支持CPU id指令 |
2.9有条件转移
有条件转移不是一个指令,而是一个指令族,此处简单称为jxx
格式为“jxx 目标地址”。若条件满足则跳转到目标地址,否则顺序执行下一条指令。
目标地址只能是段内偏移地址。在实模式下,由编译器根据当前指令与目标指令的偏移量,自行编译成短转移或近转移。保护模式下,不再区分转移方式。
转移指令 | 条件 | 意义 |
jz/je | ZF=1 | 相等时转移 |
jnz/jne | ZF=0 | 不相等时转移 |
js | SF=1 | 负数时转移 |
jns | SF=0 | 正数时转移 |
jo | OF=1 | 溢出时转移 |
jno | OF=0 | 不溢出时转移 |
jp/jpe | PF=1 | 低字节中有偶数个1时转移 |
jnp/jpo | PF=0 | 低字节中有奇数个1时转移 |
jbe/jna | CF=1或ZF=1 | 不大于时转移 |
jnbe/ja | CF=ZF=0 | 大于时转移 |
jc/jb/jnae | CF=1 | 进位时转移 |
jnc/jnb/jae | CF=0 | 未进位时转移 |
jl/jnge | SF!=OF | 小于时转移 |
jnl/jge | SF=Of | 不小于时转移 |
jle/jng | ZF!=OF或ZF=1 | 不大于时转移 |
jnle/jg | SF=OF且ZF=0 | 大于时转移 |
jcxz | CX寄存器值=0 | cx寄存器值=0时转移 |
三、通过显卡输出字符
3.1CPU如何与外设通信——IO接口
外部设备同类繁多,各个外设之间工作原理、信号、数据格式、时序等差异较大,CPU不可能与外设直接进行IO操作。任何不兼容问题,都由CPU与外设之间的一层——IO接口来解决。
IO接口时连接CPU和外部设备的逻辑控制部件。硬件部分做的是实质具体的工作,其功能是协调CPU和外部设备之间的种种不协调。IO接口内部由软件来控制运作。软件是用来控制接口电路工作的驱动程序以及完成内部数据传输所需要的程序。
IO接口芯片可分为可编程接口芯片和不可编程接口芯片。当外部设备非常简单时,不需要设定就能直接使用,则可以用不可编程接口芯片与处理机连接。
IO接口的功能
- 设置数据缓冲,解决CPU与外设的速度不匹配
- 设置信号电平转化电路
- 设置数据格式转换
- 设置时序控制电路来同步CPU和外部设备
- 提供地址译码(一个IO接口包含多个端口)
CPU如何访问IO接口?
同一时刻,CPU只能和一个IO接口通信,当很多IO接口需要和CPU通信时,需要由输入输出控制中心,即南桥芯片来决定CPU与哪一个IO接口通信。在南桥芯片内部集成了一些IO接口,南桥芯片还提供了专门用于扩展的接口。
IO接口被设计成通过寄存器的方式同CPU通信,其内部有专门用于数据交互的寄存器,又称为端口。如何访问端口呢?把一些内存地址作为端口的映射,或者将端口独立编址。通过内存映射的方式,端口可以使用mov指令来操作,而独立编址需要使用in和out来读写端口。
in指令从端口读数据
- "in al, dx"or"in ax, dx"
- in指令的源操作数必须是dx,目的操作数由dx端口指代的寄存器的宽度决定
out指令将数据写入端口
- "out dx, al"or"out dx, ax"or"out 立即数, al"or"out 立即数, ax"
- out指令的目的操作数是dx或者立即数,源操作数由dx或立即数指代的寄存器宽度决定
3.2显卡、显存、显示器
显卡是CPU与显示器之间的IO接口。它提供给我们的可编程接口是IO端口和显存。
外部设别都是通过软件指令的形式与上层接口通信的,所以显卡也有自己的BIOS。显卡支持文本模式、黑白图像模式和彩色图形模式。我们只关注文本模式。
0xB8000~0xBFFFF,这32KB是内存区域用于文本显示的。我们往里输入字符直接会写入显存。显卡加电之后,默认设置为80*25,即一屏2000个字符。
在文本模式下,一个字符对应两个字节,低字节是字符ASCII码,高字节是字符属性。
3.3让MBR直接操作显卡
先导知识点
- 显存文本模式中,其内存地址为0xb8000
- 段跨越前缀。例如,数据段的默认段寄存器为DS,但是如果显示地使用其它段寄存器来充当数据段地段基址寄存器,则称为段跨越前缀
- 当目的操作数和源操作数均无法确定操作数宽度时,需要使用伪指令以确定数据宽度
实验源代码
section mbr vstart=0x7c00
;initialize sreg
mov ax, cs
mov ds, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
;let gs point to video memory
mov ax, 0xb800
mov gs, ax
;;;;;;;;;;clear screen;;;;;;;;;;
;ah = function number:0x06-->ah = 0x06
;al = number of rows to roll(0 means all)
;bh = scroll line properties
;(cl,ch) = the (x,y) position in the bottom left corner of the window
;(dl,dh) = the (x,y) position in the top right corner of the window
mov ax, 0x600;
mov bx, 0x700
mov cx, 0
mov dx, 0x184f ;the default screen has 25row*80column
int 0x10
;output strings with red letters on green background through the graphics card
mov byte [gs:0x00], 'R'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'M'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'B'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4
jmp $ ;let the program hover
times 510-($-$$) db 0 ;fill the program to 510 bytes size
dw 0xaa55 ;fill in the magic number
创建磁盘命令:/home/oskiller/bochs/bin/bximage
编写配置文件:bochsrc.disk
megs: 32
romimage: file=/home/oskiller/bochs/share/bochs/BIOS-bochs-latest
vgaromimage:file=/home/oskiller/bochs/share/bochs/VGABIOS-lgpl-latest
#floppya: 1_44=a.img, status=inserted
boot: disk #改为从硬盘启动。我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作。
log: bochs.out
mouse: enabled=0
keyboard_mapping:enabled=1,map=/home/oskiller/bochs/share/bochs/keymaps/x11-pc-us.map
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master:type=disk, path="/home/oskiller/bochs/mylab/lab2/hd60M.img", mode=flat, cylinders=121, heads=16, spt=63 #对于不同目录下的配置文件,仅需要修改磁盘路径即可
#gdbstub:enabled=1, port=1234, text_base=0, data_base=0, bss_base=0
编译:nasm -o mbr_1.bin mbr_1.s
写入磁盘:dd if=/home/oskiller/bochs/mylab/lab2/mbr_1.bin of=/home/oskiller/bochs/mylab/lab2/hd60M.img bs=512 count=1 conv=notrunc
运行:/home/oskiller/bochs/bin/bochs -f bochsrc.disk
运行结果:
上述两幅截图,可以说明字符是在闪烁的。
四、让MBR使用硬盘
4.1硬盘介绍
磁盘是随机存取的。磁盘的随机存取是靠磁头臂不断移动实现的,磁头臂移动到目标位置的时间称为寻道时间,若数据是不连续的,那么磁头就要不停的移动,这就是磁盘速度瓶颈所在。
4.1.1硬盘工作原理
盘面与磁头是一一对应的,故用磁头号表示盘面。
一方面盘片的自转,另一方面磁头的摆动,这两种运动的合成,使得磁头能够读取盘片上的任何位置的数据。
盘片表面是用于存储数据的磁性介质,为了更有效管理磁盘,将整个盘面划分为多个同心圆环,这就是磁道。对于不同盘面,相同磁道组成的管状区域就称为柱面。以同心画扇形,扇形与每个同心环相交的弧状区域,是扇区,是最基本的数据存取单元。
如果数据按柱面存取,则减少了寻道次数和寻道时间。
磁头何如找到所需要的扇区?借助扇区头部信息,包括磁头号、磁道号和扇区号。
4.1.2硬盘控制器端口
硬盘控制器主要端口寄存器,如下所示:
Command Block registers向硬盘驱动器写入命令字或从硬盘控制器获得硬盘状态。Control Block registers用于控制磁盘工作状态。
端口是按照通道给出的,一个通道上的主、从两块硬盘都用这些端口号。
data寄存器是负责管理数据的,作用是读取或写入数据。data寄存器宽度为16位。在读数据时,读此寄存器就能读出缓冲区的数据;在写数据时,往此寄存器写数据就能被写入缓冲区。
在读硬盘时,只有读取失败时,才会用到error寄存器,里面存放失败的信息,尚未读取的扇区数在sector count中。在写操作时,Features中存放了命令需要指定的额外参数。error和Features是同一个寄存器。
sector count用来指定待读入或待写入的扇区数,中途失败时,记录尚未完成的扇区。值为0时,表示要操作256个扇区。
“柱面-磁头-扇区”,即CHS,寻找扇区对于我们来说并不直观。LBA,逻辑块地址,磁盘中的扇区从0开始依次递增编号,不用考虑扇区所在的物理结构。本实验使用LBA28,用28位比特描述一个扇区的地址,最大支持128GB。LBA寄存器有三个,LBA low、LBA mid、LBA high,分别存储28位地址的第0~7位、第8~15位、第16~23位。24~27位存储在device中的低4位。device的第4位指定主(0)、从(1)盘,第6位设置是否开启LBA模式(1表示启动),第5、7位固定为1。
在读硬盘时,status用来给出硬盘的状态信息。第0位是err,若为1表示出错,出错信息在error中;第3位是data request位,若为1表示硬盘把数据准备好了;第6位是DRDY,表示硬盘就绪;第7位BSY,否为1,表示硬盘正忙。在写硬盘时,command用来存储让硬件执行的命令。status和command是同一个寄存器。
本实验用到的让硬件执行的命令只有以下三条:
- identify: 0xEc,即硬盘识别
- read sector: 0x20,即读扇区
- write sector: 0x30,即写扇区
4.1.3硬盘操作方法
读、写硬盘的基本步骤:
- 先选择通道,往该通道的sector count中写入待操作的扇区数
- 往该通道的三个LBA寄存器写入扇区起始地址的低24位
- 往device寄存器中写入LBA的24~27位地址,选择LBA模式和主、从盘
- 往通道上的command写入操作命令
- 读取该通道的status,判断硬盘工作是否完成
- 若以上是读操作,则进入下一步,否则完成操作
- 将硬盘数据读出
当磁盘工作完成之后,我们如何获取数据?
常用的数据传递方式:
- 无条件传送:数据源设备必须是随时准备好数据的,CPU随时取走数据
- 查询传送:CPU需要不断查询设备状态,当准备好后方可取走数据
- 中断传送:当数据源设备准备好数据后,通过发送中断通知CPU取数据。CPU需要执行压栈、执行传输指令、恢复现场等工作
- 直接存储器存取:不需要让CPU参与传输,由数据源设备与内存直接传输。但是CPU仍需要完成数据交换、组合、校验等工作
- I/O处理机传送:可以随时处理数据,CPU可以完全不知道有传输这回事
第一种不适合磁盘获取数据,磁盘的数据不是随取随有的。本实验采用第2、3中方式获取数据。
4.2改造MBR
先导知识:
- 熟悉硬盘的IO端口
- 会读写端口:"out"&&"in"
- 对于loop,cx寄存器的内容作为循环次数,每循环一次,cx自减1
- 熟悉读取硬盘的操作步骤
本实验分为boot.inc、mbr_2.s和loader.s三个文件。本实验,在mbr中通过操作显卡显示“Real MBR”并将磁盘中的loader.s加载到以0x900为起始地址的内存中,然后跳转执行loader.s的内容,在loader.s中输出“Real loader”。实现了由MBR向加载器loader的交权,当然,我们此处的loader是非常简陋,只是实现了输出,在未来,我们将会完善loader的内容。
代码源码:
boot.inc源码:
;-------------------loader&&kernel-------------------
Loader_Base_Addr equ 0x900
Loader_Start_Sector equ 0x2
Loader_Sector_Cnt equ 0x1
mbr_2.s源码:
%include "boot.inc"
section mbr vstart=0x7c00
;initialize sreg
mov ax, cs
mov ds, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
;let gs point to video memory
mov ax, 0xb800
mov gs, ax
;;;;;;;;;;clear screen;;;;;;;;;;
;ah = function number:0x06-->ah = 0x06
;al = number of rows to roll(0 means all)
;bh = scroll line properties
;(cl,ch) = the (x,y) position in the bottom left corner of the window
;(dl,dh) = the (x,y) position in the top right corner of the window
mov ax, 0x600;
mov bx, 0x700
mov cx, 0
mov dx, 0x184f ;the default screen has 25row*80column
int 0x10
;output strings with red letters on green background through the graphics card
mov byte [gs:0x00], 'R'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'M'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'B'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4
mov eax, Loader_Start_Sector
mov bx, Loader_Base_Addr
mov cx, Loader_Sector_Cnt
call rd_disk
jmp Loader_Base_Addr ;let the program go to Loader
rd_disk:
;first: set the number of read sectors
mov dx, 0x1f2
mov di, ax
mov al, cl
out dx, al
mov ax, di
;second: set LBA's address
;write the 0 to 7 bits of the LBA's address through port 0x1f3
mov dx, 0x1f3
out dx, al
;write the 8 to 15 bits of the LBA's address through port 0x1f4
mov cl, 0x08
shr eax, cl
mov dx, 0x1f4
out dx, al
;write the 16 to 23 bits of the LBA's address through port 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
;write device through port 0x1f6
shr eax, cl
mov dx, 0x1f6
and al, 0x0f
or al, 0xe0
out dx, al
;third: the read command, 0x20, is written to port 0x1f7
mov dx, 0x1f7
mov al, 0x20
out dx, al
;fourth: check the status of the hard disk
.not_ready:
nop
in al, dx
and al, 0x88 ;only the 3rd and 7th positions will be focused here
cmp al, 0x08
jnz .not_ready
;fifth: read data from port 0x1f0
;count reads times, cx get read times
mov ax, Loader_Sector_Cnt
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.read:
in ax, dx
mov [bx], ax
add bx, 2
loop .read
ret ;return to "jmp Loader_Base_Addr"
times 510-($-$$) db 0 ;fill the program to 510 bytes size
dw 0xaa55 ;fill in the magic number
loader.s源码
%include "boot.inc"
section loader vstart=Loader_Base_Addr
;output strings with red letters on green background through the graphics card
mov byte [gs:0xa0], 'R'
mov byte [gs:0xa1], 0xA4
mov byte [gs:0xa2], 'e'
mov byte [gs:0xa3], 0xA4
mov byte [gs:0xa4], 'a'
mov byte [gs:0xa5], 0xA4
mov byte [gs:0xa6], 'l'
mov byte [gs:0xa7], 0xA4
mov byte [gs:0xa8], ' '
mov byte [gs:0xa9], 0xA4
mov byte [gs:0xaa], 'L'
mov byte [gs:0xab], 0xA4
mov byte [gs:0xac], 'o'
mov byte [gs:0xad], 0xA4
mov byte [gs:0xae], 'a'
mov byte [gs:0xaf], 0xA4
mov byte [gs:0xb0], 'd'
mov byte [gs:0xb1], 0xA4
mov byte [gs:0xb2], 'e'
mov byte [gs:0xb3], 0xA4
mov byte [gs:0xb4], 'r'
mov byte [gs:0xb5], 0xA4
jmp $
编译阶段:
nasm -o mbr_2.bin mbr_2.s
nasm -I include/ -o loader.bin loader.s
写入磁盘:
dd if=/home/oskiller/bochs/mylab/lab2/mbr_2.bin of=/home/oskiller/bochs/mylab/lab2/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/oskiller/bochs/mylab/lab2/loader.bin of=/home/oskiller/bochs/mylab/lab2/hd60M.img bs=512 count=1 seek=2 conv=notrunc
运行:
/home/oskiller/bochs/bin/bochs -f bochsrc.disk
运行结果:
五、Bochs调试
①单步执行命令“s”
②断点指令“b”,要实现设置一个内存地址。例如:“b 0x7c00”
③持续执行命令“c”。事先执行命令“b”再执行命令“c”,可以使得程序执行到预设的断点处才停下来
④显示寄存器命令“r”,可以显示通用寄存器的内容
⑤显示段寄存器命令“sreg”
⑥显示内容内容命令“xp”,命令xp每次只显示一个双字,需要显示多个双字时,需要用“/”附加数量。还应当指定一个物理内存。例如:xp/2 0xb800