第八章 硬盘和显卡的访问与控制
8.2 用户程序的结构
处理器对指令和数据的获取一律按“段地址:偏移地址“进行,相对应地,一个规范的程序应该包括代码段、数据段、附加段和栈段。
段定义
NASM编译器使用汇编指令SECTION或者SEGMENT来定义段,格式如下
SECTION 段名
SEGMENT 段名
对齐命令align
Intel处理器要求段在内存中的起始物理地址起码是16字节对齐的,意思是,必须是16的倍数,或者说物理地址必须能被16整除。这在装载时会被操作系统自动装载到对齐后的地址。但是,相应的,汇编语言中定义的每个段,也有对齐的要求。具体是,在段定义中,使用“align=”语句,用于指定某个SECTION的汇编地址对齐方式,比如,“align=16“就表示段是16字节对齐的。
SECTION code align=16
段地址和段开头地址
段的汇编地址就是段内的第一个元素处的汇编地址。每个段的汇编地址是相对于整个程序开头的(即汇编地址0处,推测为程序头部的首地址)。为取得该段的汇编地址,MASM提供了以下表达式,可以用在程序中。
section.段名.start
vstart命令
如果段定义语句包含”vstart=“子句,就可以控制段内标号的汇编地址是相对于段首还是相对于程序开头设置的。例如
SECTION code_1 align=16 vstart=0
使用了“vstart=0”命令后,当前段下的所有标号的汇编地址就是从当前段的开头计数的,而不是从整个程序的开头计数的。
一个小试验
我把c08.mbr编译写入虚拟磁盘,当不改变其中代码,也就是使用vstart=0x7c00时,编译后标号会自动加上0x7c00,变成0x7cc7;而如果没有使用vstart=0x7c00,默认是标号的地址,也就是0x00c7,如下图倒数第二行,是没有使用vstart的情况。测试的汇编语句如下
mov ax,[cs:phy_base]
设置vstart=0,发现结果和上述相同。说明在不设置vstart时,vstart默认为0,标号地址默认从程序代码的开头开始算。
8.3 加载程序(器)的工作流程
这一节的主要内容,是把用户程序从磁盘读取到内存中,并重定位用户程序代码段地址,最后把控制移交给用户程序(即转到用户程序执行)。代码有两个,一个是主引导扇区代码,一个是用户程序代码。
初始化和决定加载位置
这里要注意,分清至少三个地址。首先是主引导扇区代码,固定加载到内存0x7c00处;其次是用户程序代码,之后会被写入硬盘第100逻辑扇区处;最后是用户程序准备加载到内存中的位置,设置为了0x10000
- equ伪指令
可以采用equ伪指令,在汇编语言中声明常数。
app_lba_start equ 100
运行后,后面就能使用app_lba_start来代替100,编译时会自动将它替换为我们定义的常数,此处即为100。和其他伪指令dd、dw等不同,用equ声明的数值不占用任何汇编地址,也不在运行时占用任何内存单元,仅仅代表一个数值。
- 用户程序加载到内存中的地址,必须是16字节对齐的,也就是能够整除16的。
准备加载用户程序
由于在c08_mbr.asm程序第8行已经设置了vstart=0x7c00,后面的所有标号地址都已经加了0x7c00,所以就不需要每条命令都手动写上+0x7c00。
外设、I/O端口访问
ICH(I/o Controller Hub)芯片,即输入输出控制设备集中器,作用是连接不同的总线,并协调各个I/O接口对处理器的访问,在个人计算机上,该芯片就是南桥芯片。
外设和处理器之间通过端口打交道。端口其实本质是寄存器,只不过存在于I/O接口电路中,通过改变这些寄存器的值,就能够控制外设,以及与CPU进行通信。
外设有独立编址和内存映射两种索引方式,计组提过。在Intel处理器中,早期采用独立编址,现在既有独立编址又有内存映射。因为存在独立编址,所以采用in、out指令控制端口。
- in target, source 目的操作数必须是al或者ax,源操作数必须是dx。意思是将源操作数指向的端口值送入目的操作数寄存器,供CPU使用。
- out target, source 目的操作数必须是al或者ax,源操作数必须是dx。意思是将CPU将源操作数寄存器中的值送入目的寄存器指向的端口。
- 外围设备向处理器发出数据用in,处理器向外围设备发出数据用out。in、out指令都不影响任何标志位。
采用访问端口的方式,可以将磁盘中的数据读入内存。具体过程、每个端口含义不再赘述,见书P125~P127,比较容易理解。
过程调用call、ret、retf指令
过程调用可以理解为汇编中的函数调用,调用过程的指令是call。8086处理器支持四种调用方式。
call
- 16位相对近调用。**如果没有提供关键字,那么call默认为近调用。**指令长3字节,操作码1字节,操作数2字节16位,有正负,所以被调用过程只能位于距离当前call指令-32768~32767字节的地方。最终的操作数是一个减出来的偏移量,所以叫做“相对”。
call near 0x0500
- 16位间接绝对近调用。指令中操作数是一个真实的偏移地址,执行指令时从该偏移地址处取出一个字(2字节),用来直接取代指令指针寄存器IP中的内容。
call [0x3000]
- 16位直接绝对远调用。需要关键字far。段间调用,段地址和偏移地址直接在call指令中给出,偏移地址在前,段地址在后,分别占1个字,总共是2个字,即4字节。
call 0x2000:0x0030
- 16位间接绝对远调用。需要关键字far。段间调用,段地址和偏移地址在操作数指向的地址处。同样取出两个字共四字节。
call far [0x2000] ;从0x2000处取得偏移地址和段地址,分别占1个字
上述call指令虽然在具体执行时有细微差别,但是大体上执行时都按照这个流程:将当前的IP(或CS和IP)压栈,再更新CS/IP的值,继续执行。
ret、retf
ret是近返回指令,retf是远返回指令。ret执行时,处理器只是弹出一个字到IP中。retf执行时,处理器弹出四个字节,两两分别到IP和CS中。
用户程序加载和重定位
为什么要有这一步?因为在用户程序加载进去时, 段首地址并不再是原先从这个程序段的开头0x00处计数的了,而是从加载到内存中的地址,此处为0x10000处开始计数的。所以,要重定位段地址。每个段重定位后的地址构成了段重定位表。段地址是每个段的首地址右移四位后的值,所以我们需要用到逻辑右移指令shr和循环右移指令ror。相对应地有shl和rol,大致相同,不再赘述。
shr
将操作数连续右移指定的次数,每移动一次,挤出来的比特位都会被移到标志寄存器CF。
ror
将操作数右移指定的次数,每右移一次,移出的比特既送到CF位,又送到左边空出的位。
无条件转移指令jmp
和call一样,jmp分为相对和绝对两类,或者近转移和远转移两类,共五种转移。
- 相对短转移。必须使用关键字short(最近的版本有改变,见16位相对近转移)。操作数仅1字节,是个有符号偏移量,则范围为-128~127字节。虽然指令中可以用标号,但是编译器会自动计算成偏移量。执行时,处理器计算出新的IP,跳转到那里执行。
- 16位相对近转移。需使用关键字near。属于段内转移,操作数是一个16位有符号数,则范围为-32768~32767字节。最新版本NASM规定,如果没有指定关键字short或者near,编译器会自动根据目标地址距离当前地址的距离在-128~127字节之间还是在-32768~32767字节之间,自动判断采用short还是near。
- 16位间接绝对近转移。只在段内转移,关键字near可以省略。偏移地址在寄存器中,或者在内存地址指向的存储空间中。
- 16位直接绝对远转移。直接在指令中给出段地址和偏移指令。
- 16位间接绝对远转移。需要用到far关键字。从内存地址指向的位置处读出两个字,分别用来代替CS和IP。
8.4 用户程序的工作流程
c08.mbr用户程序要把字符显示在屏幕上。这是用用户程序实现的,而不是直接写在主引导扇区里,所以与之前的实验不同。
首先说回车和换行。这两个概念来源于打字机,回车是指将光标移到当前行的开头,换行是指将光标移到当前列的下一行。我们现在按的“回车键”,其实做的是回车+换行操作。
实际端口调用过程举例
之前没细说过IO接口的调用过程,此处以c08.mbr line51~line56说说。
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al
为了让显示器显示数据,要通过io端口对外设进行读写等控制。但是由于显卡中端口很复杂,寄存器特别多,所以很多寄存器只能通过索引寄存器访问。
索引寄存器端口号为0x3d4,向它写入值,可以指定要操作的寄存器。索引为0x0e和0x0f的寄存器,分别用来提供光标的高8位和低8位。此处以高8位为例。
首先将0x3d4写入dx,指定要操作的端口,之后将0x0e使用out指令写入dx值代表的端口0x3d4,这表示现在选择了代表光标高8位数值的寄存器。之后将0x3d5写入dx,这是个数据端口,再使用in从dx代表的寄存器读数据到al,则读出了高8位,最后一行将光标高8位移到ah,完成。
这说明,只需要将端口地址或索引写入dx,就可以直接把dx当做端口本身来使用in、out操作。
乘法mul指令
像div,mul也有两种格式,如下
mul r/m8 ;AX=AL×r/m8,8位数据乘AL值,保存在AX中
mul r/m16 ;DX:AX=AX×r/m16,16位数据乘AX值,保存在DX:AX中
mul执行后,如果结果的高一半为全0,则OF和CF清零,否则置1。对SF、ZF、AF和PF标志的影响未定义。
一些调试技巧和提示
-
在做最后章末习题时,可能需要bochs调试,此时从c08_mbr跳转到c08的那条指令,jmp far [0x04],位置是0x7c70,也就是说,在最开始设置断点时可直接用b 0x7c70.
-
在执行到loop、call、repmov这种循环和调用时,用n马上结束全部循环是一个节约时间的好方法。