文章目录
- 01、离开主引导分区
- 02、给汇编程序分段
- 03、控制段内元素的汇编地址
- 04、加载器和用户程序头部段
- 05、加载器的工作流程和常数声明
- 06、确定用户程序的加载位置
- 07、外围设备及其接口
- 08、输入输出端口的访问
- 09、通过硬盘控制器端口读取扇区数据
- 10、过程和过程调用
- 11、过程调用和返回的原理
- 12、加载整个用户程序
- 13、用户程序的重定位
- 14、比特位的移动指令
- 15、转到用户程序内部执行
- 16、8086的无条件转移指令
- 17、用户程序的执行过程
- 18、验证加载器加载和执行用户程序的过程
- 19、原书第8章程序概述
- 20、与文本有关的回车、换行与光标控制
- 21、回车的光标处理和乘法指令MUL
- 22、换行和普通字符的处理过程与滚屏操作
- 23、8086的过程调用方式CALL
- 24、通过RETF指令转到另一代码段执行
- 25、在程序中访问不同的数据段
- 26、使用新版的FixVhdWr软件写虚拟硬盘并运行程序
- 27、原始第8章习题
上一节:18、INTEL8086处理器的寻址方式
下一节:20、中断和动态时钟显示
01、离开主引导分区
下一步计划:
02、给汇编程序分段
NASM
编译器使用SECTION
或着SEGMENT
来定义段,使用ALIGN
规定段的对齐方式。
SECTION data1 ALIGN=16
mydata dw 0xFACE
SECTION data2 ALIGN=16
string db 'hello'
section code ALIGN=16
mov bx, mydata
mov si, string
03、控制段内元素的汇编地址
使用VSTART
指定段的起始汇编地址。
SECTION data1 ALIGN=16 VSTART=0
mydata dw 0xFACE
SECTION data2 ALIGN=16 VSTART=0x100
string db 'hello'
section code ALIGN=16 VSTART=0
mov bx, mydata
mov si, string
04、加载器和用户程序头部段
计算段的汇编地址
:
加载器和用户程序的关系:加载器需要用户程序头部段提供的信息决定如何加载并执行用户程序。
其中用户程序头部段
的内容如下所示:
用户程序头部段的内容如下:
05、加载器的工作流程和常数声明
加载器的工作流程:
代码可参考配套程序中的c08_mbr.asm
。
其中常数使用equ
定义,而且常数是不占用内存空间的。
...
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址
...
06、确定用户程序的加载位置
此小节程序见c08_mbr.asm
。
本程序中内存分布:
可用空间为0x10000
~~0x0FFFF
中。
将用户程序的起始物理地址定义在phy_base
标号处的4字节处:
将用户程序地址从内存中取出,传入DX
和AX
寄存器中,用以计算16位的段地址:
07、外围设备及其接口
此小节从硬盘上将用户程序读入并加载到用户程序地址0x100
处。
处理器使用总线和外围设备进行数据交换:
08、输入输出端口的访问
此小节程序见c08_mbr.asm
。
在Intel
的系统中,最多只有65536
个端口,端口号位0
~~65535
。
使用in
指令从端口读取数据:使用AX
(端口数据为16位)、AL
(端口数据为8位)做为目的操作数,DX
做为源操作数。
其中源操作数中的端口号
也可不用DX
存储,可使用8位的立即数直接给出:
使用out
指令向端口发送数据:使用DX
(端口数据为16位)、imm8
(端口数据为8位)做为目的操作数,AX
、AL
做为源操作数。
in
、out
指令都不影响任何标志位。
09、通过硬盘控制器端口读取扇区数据
硬盘读写的基本单位是扇区
。
CHS模式:向硬盘发送磁头号、柱面号、扇区号;
LBA模式:硬盘所有扇区统一编址。最早使用28个比特表示逻辑扇区号,即LBA28
。
发送0表示读取256个扇区、1~255
表示读取1~255
个扇区。
主硬盘控制器被分配了8个端口,从0x1F0
到0x1F7
,以下是从硬盘读数据的具体过程:
- 第1步:设置要读取的扇区数量
- 第2步:设位置起始的LBA扇区号
- 第3步:向端口
0x1f7
写入读命令0x20
- 第4步:等待读写操作完成
0x1f7
即是命令端口也是状态端口。
- 第5步:连续的取出数据
10、过程和过程调用
具体过程见程序:c08_mbr.asm
。
11、过程调用和返回的原理
具体过程见程序:c08_mbr.asm
。
调用过程:
16位的相对近调用,调用当前代码段内的程序;
其中相对偏移量为:标号处的汇编地址 减去 call
指令下一条指令的汇编地址。
call
指令执行时:处理器用IP内容压栈保存之后 加上 call
指令中的16位偏移量得到子程序的偏移地址,之后转到此偏移地址处执行。
过程调用之后栈的变化:
过程返回之后栈的变化:
...
;以下读取程序的起始部分
xor di,di
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
xor bx,bx ;加载到DS:0x0000处
call read_hard_disk_0 ;执行call指令时处理器会将IP寄存器指向下一条指令的位置
;并将IP压栈,由于这条call指令为十六位的近调用
;只能调用当前代码段内的过程,不能调用其他段内过程
;也就是代码调用前后代码段CS内的值不改变,就不去要压栈
;
;入栈之后就会将IP当前内容加上指令中的相对偏移量得到子程序的
;偏移地址,同时处理器将此地址取代IP的原有内容
...
12、加载整个用户程序
具体过程见程序:c08_mbr.asm
。
用户程序头部段的组成:
在程序中就是使用DX:AX
读取用户程序总长度。
其中一个段的大小最大位64KB
,若使用以下方法加载用户程序,则当程序大于64KB
时,会从头在加载一边用户程序。
每读完一个扇区,就将DS
的内容加1,构造一个新的段开始加载剩余的用户程序,这样就可以解决当程序大于64KB
时从头开始的情况:
使用以下程序读取用户程序剩余扇区:
...
;以下判断整个程序有多大
mov dx,[2] ;曾经把dx写成了ds,花了二十分钟排错
mov ax,[0]
mov bx,512 ;512字节每扇区
div bx
cmp dx,0
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec ax ;已经读了一个扇区,扇区总数减1
@1:
cmp ax,0 ;考虑实际长度小于等于512个字节的情况
jz direct
;读取剩余的扇区
push ds ;以下要用到并改变DS寄存器
mov cx,ax ;循环次数(剩余扇区数)
@2:
mov ax,ds
add ax,0x20 ;得到下一个以512字节为边界的段地址
mov ds,ax
xor bx,bx ;每次读时,偏移地址始终为0x0000
inc si ;下一个逻辑扇区
call read_hard_disk_0
loop @2 ;循环读,直到读完整个功能程序
pop ds ;恢复数据段基址到用户程序头部段
...
13、用户程序的重定位
用户程序被读入内存之后,下一步就是计算用户程序中每个段的段基地址。
段的汇编地址是相对于整个程序开头处的偏移量,用户程序各段的汇编地址:
段的汇编地址不是他们在内存中的物理地址,也不是段地址;
用户程序被加载到内存中之后各个段的物理地址为:
将物理地址右移4位得到逻辑段地址,这个过程就是段的重定位。
重定位之前的内存分布,此时DS
指向头部段,此时入口点所在代码段的逻辑地址还是其汇编地址,需要转换为逻辑段地址。
使用下列代码修改程序的段地址:
...
;计算入口点代码段基址
direct:
mov dx,[0x08]
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
...
...
;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
add ax,[cs:phy_base] ;产生进位,标志位CF置1、否则CF置0
adc dx,[cs:phy_base+0x02] ;除了将操作数相加,还要加上进位标志CF的值
shr ax,4 ;逻辑右移,CF标志位 = 最后一个移出的比特位
ror dx,4 ;循环右移,CF标志位 = 最后一个移出的比特位
and dx,0xf000
or ax,dx
pop dx
ret
...
14、比特位的移动指令
逻辑右移指令shr
:
循环右移指令ror
:
代码如下:
;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
add ax,[cs:phy_base] ;产生进位,标志位CF置1、否则CF置0
adc dx,[cs:phy_base+0x02] ;除了将操作数相加,还要加上进位标志CF的值
shr ax,4 ;逻辑右移,CF标志位 = 最后一个移出的比特位
ror dx,4 ;循环右移,CF标志位 = 最后一个移出的比特位
and dx,0xf000
or ax,dx
pop dx
ret
...
逻辑左移、循环左移:
15、转到用户程序内部执行
第13节中完成了用户程序头部段的重定位,这一节完成用户程序剩余所有段的重定位:
....
realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx]
call calc_segment_base ;用段的汇编地址生成逻辑段地址,保存在AX中
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
....
16、8086的无条件转移指令
相对短转移(8位:-128~127
)、相对近转移(16位:-65536~65535
)。
这两个指令都是段内转移指令,只能转移到当前代码段的另一个地方,不能转移到其他段内。这两个指令执行时,都是使用指令指针寄存去IP
中的值加上
指令中的相对偏移量(目标代码处的汇编地址 减去
当前指令的吓下一条指令汇编地址)得到目标位置的偏移地址,使用这个偏移地址修改IP
的值,以此转移到目标位置执行。
如果省略了short
或near
,则由编译器根据目标位置的远近来决定使用哪个形式的转移指令。
16位间接绝对近转移:也是段内近转移(16位:-65536~65535
),因为目标位置的地址通过寄存器或者地址间接给出,并替换IP
内的值。所以叫间接绝对近转移。
16位直接绝对远转移:使用段地址替换CS
内容、偏移地址替换IP
内容,此方式位段间转移
。
16位间接绝对远转移:给出的目标位置为4个字节,高地址2字节处存放段地址、低地址2字节存放偏移地址。使用段地址取代CS
、偏移地址取代IP
。
程序中:在重定位用户程序所有段地址之后,使用间接绝对远转移跳转到用户程序中执行。
- 处理器使用
DS
内容左移4位
加上0x04
构成20位
的有效物理地址,取出此地址出的数据给jmp far
指令使用; jmp far
指令从上面获得的高16位地址
处取出数据替换CS
内容;jmp far
指令从上面获得的低16位地址
处取出数据替换IP
内容;- 使用
CS
中内容左移4位
加上IP
内容构成有效的物理地址,程序将会跳转到此地址处执行。
....
realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx]
call calc_segment_base ;用段的汇编地址生成逻辑段地址,保存在AX中
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
jmp far [0x04] ;转移到用户程序
...
17、用户程序的执行过程
声明未初始化数据的指令:resb
、resw
、resd
,都是跳过此段空间,未初始化,里面的值不确定。
resb 256 = resw 128 = resd 64
,都是保留256个字节的内存空间。
用户程序的具体工作流程看视频,基本就是:
1、设置用户程序自己的栈、代码段、数据段
2、显示数据段中定义的字符
具体以代码查看c08
章中的userapp.asm
:
;包含代码段、数据段和栈段的用户程序
;===============================================================================
SECTION header vstart=0 ;用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code.start ;段地址[0x06]
realloc_tbl_len dw (segtbl_end-segtbl_begin)/4 ;段重定位表项个数[0x0A]
;段重定位表
segtbl_begin:
code_segment dd section.code.start ;[0x0C]
data_segment dd section.data.start ;[0x10]
stack_segment dd section.stack.start ;[0x14]
segtbl_end:
;===============================================================================
SECTION code align=16 vstart=0 ;代码段
start:
;初始执行时,DS和ES指向用户程序头部段
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_pointer ;设置初始的栈顶指针
mov ax,[data_segment] ;设置到用户程序自己的数据段
mov ds,ax
mov ax,0xb800
mov es,ax
mov si,message
mov di,0
next:
mov al,[si]
cmp al,0
je exit
mov byte [es:di],al
mov byte [es:di+1],0x07
inc si
add di,2
jmp next
exit:
jmp $
;===============================================================================
SECTION data align=16 vstart=0 ;数据段
message db 'hello world.',0
;===============================================================================
SECTION stack align=16 vstart=0 ;栈段
resb 256
stack_pointer:
;===============================================================================
SECTION trail align=16 ;尾部
program_end:
18、验证加载器加载和执行用户程序的过程
xp
命令是显示内存中的数据:xp /30xh ds:0
,x
表示16进制、h
表示字节、30表示30个数据、ds:0
表示在ds
偏移地址为0处的数据。
19、原书第8章程序概述
具体以代码查看c08
章中的c08.asm
。
20、与文本有关的回车、换行与光标控制
具体以代码查看c08
章中的c08.asm
。
在数据段中0x0d
、0x0a
分别代表回车和换行,其中段data_1
使用了vstart=0
字句,则标号msg0
的汇编地址就是其段内偏移地址,均为0
。
;===============================================================================
SECTION data_1 align=16 vstart=0
msg0 db ' This is NASM - the famous Netwide Assembler. '
db 'Back at SourceForge and in intensive development! '
db 'Get the current versions from http://www.nasm.us/.'
db 0x0d,0x0a,0x0d,0x0a
db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
db ' xor dx,dx',0x0d,0x0a
db ' xor ax,ax',0x0d,0x0a
db ' xor cx,cx',0x0d,0x0a
db ' @@:',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' add ax,cx',0x0d,0x0a
db ' adc dx,0',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' cmp cx,1000',0x0d,0x0a
db ' jle @@',0x0d,0x0a
db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
db 0
;===============================================================================
光标在屏幕上的位置保存在光标寄存器(2个8位寄存器构成的16位)中:
c08
章中的c08.asm
代码中子程序put_char
的运行流程:
21、回车的光标处理和乘法指令MUL
具体以代码查看c08
章中的c08.asm
。
索引寄存器(索引端口0x3D4
)、数据寄存器(数据端口0x3D5
),光标寄存器位置的索引值(高8位:0x0E
、低8位:0x0F
);
索引端口用来选择显卡内部的寄存器,需要向其提供一个及寄存器的编号,比如是0x0F;
那么数据端口将会和0x0F端口接通,就可以通过数据端口读写0x0F的数据。
代码如下:其中光标的范围是列(0~79
)、行(0~24
)
...
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
...
mul
指令:无符号数乘法指令
imul
指令:有符号数乘法指令
用法与mul
一致。
22、换行和普通字符的处理过程与滚屏操作
具体以代码查看c08
章中的c08.asm
。
滚屏操作:原先的0~23
行上移一行变为1~24
行
23、8086的过程调用方式CALL
CALL
指令形式:
1、16位相对近调用:段内调用
其中相对偏移量为:标号处的汇编地址 减去 call
指令下一条指令的汇编地址。
call
指令执行时:处理器用IP
内容压栈保存之后 加上 call
指令中的16位偏移量得到子程序的偏移地址,CS
左移4位 加上IP
内容形成20位有效物理地址,之后转到目标位置处执行。
2、16位间接绝对近调用:段内调用
call
指令执行时:处理器将IP
内容压栈保存,将操作数中的内容传送到IP
,之后将段寄存器DS
内容左移4位 加上 IP
中的偏移地址 形成20位有效物理地址,即处理器转移到目标位置执行。
3、16位直接绝对远调用:段间调用
call
指向执行时:处理器将代码段CS
、IP
内容压栈,接着用指令中给出的段地址、偏移地址代替CS
、IP
的内容,之后CS
左移4位加上IP
形成20位有效物理地址,即处理器转移到目标位置执行。
4、16位间接绝对远调用:段间调用
call
指向执行时:处理器将代码段CS
、IP
内容压栈,CS
左移4位 加上从指令中给出的偏移地址处取出实际的段地址(高位)和偏移地址(低位),分别传送到CS
和IP
,之后CS
左移4位加上IP
形成20位有效物理地址,即处理器转移到目标位置执行。
24、通过RETF指令转到另一代码段执行
1、近过程调用和返回方式:
ret
指令导致处理器从栈中将返回点的偏移地址弹出到IP
,导致处理器从过程返回。
2、远过程调用和返回方式:
retf
指令导致处理器从栈中将返回点的偏移地址和段地址弹出到IP
和CS
,导致处理器从过程返回。
代码如下:具体以代码查看c08
章中的c08.asm
。
...
push word [es:code_2_segment] ;压入目标位置的 段地址
mov ax,begin
push ax ;压入目标位置的 偏移地址,可以直接push begin, ;80386+
retf ;转移到代码段2执行
;可使jmp far、call far跳转执行
;这里使用retf来模拟段间调用返回的过程
...
25、在程序中访问不同的数据段
代码如下:具体以代码查看c08
章中的c08.asm
和视频。
...
continue:
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax
mov bx,msg1
call put_string ;显示第二段信息
jmp $
...
26、使用新版的FixVhdWr软件写虚拟硬盘并运行程序
使用fixvhdw64.exe
软件加载程序并执行,软件在配套QQ群里。
virtualBox虚拟机运行:
Bochs虚拟机运行:
跳转到用户程序执行之前栈的状态:
使用n
命令持续执行代码:可以看出程序已经在用户程序中执行
转到第2个代码段执行之前:
之后:
其他内容看视频即可。
27、原始第8章习题
第1题:
在data_1
段内定义一个双字的标号entry
:
在代码段code_1
中修改程序:
上一节:18、INTEL8086处理器的寻址方式
下一节:20、中断和动态时钟显示