Drollery Medieval drollery of a knight on a horse

🏆 欢迎来到本站: https://xuchangwei.com/希望这里有你感兴趣的内容

flowery border with man falling
flowery border with man falling

Assembly: x86实模式

实模式

主要内容

虚拟机的安装和使用

主要内容

  • 安装VirtualBox虚拟机管理器
  • 创建VirtualBox虚拟机
  • 虚拟硬盘简介
  • 在Windows下创建虚拟硬盘并安装操作系统
  • 在Linux下创建虚拟硬盘并安装操作系统

安装VirtualBox虚拟机管理器

虚拟机软件:

  • VMWare
  • Windows Virtual PC
  • VirtuaBox
  • Bochs
  • … …

官网:https://www.virtualbox.org/

windows安装virtualbox,可自定义安装目录,其他默认。

Ubuntu安装virtuabox,系统界面找到Ubuntu软件搜索virtuabox并安装

创建VirtualBox虚拟机

打开VirtuaBox,点击新建

新建一台没有虚拟硬盘的虚拟机

  • 虚拟电脑名称和系统类型
    • 名称:任意,learn
    • 文件夹:默认或自定义,如D:\VirtuaBox VMs
    • 虚拟光盘:无
    • 类型:按需求。Linux
    • Subtype: 按需求。Ubuntu
    • 版本:按需求。Ubuntu(64-bit)
  • 自动安装,无
  • 硬件
    • 内存:自定义,2G
    • 处理器:自定义,2核心,尽量不超过宿主机核心
      • 宿主机核心,我的电脑右键–>属性–>设备管理器–>处理器
  • 虚拟硬盘
    • 勾选,不添加虚拟硬盘

虚拟机设置

  • 系统-主板:
    • 启动顺序:去掉软驱
  • 显示-屏幕
    • 显存大小:32M
  • 存储
    • IDE光盘控制器,SATA机械硬盘控制器,稍后添加

虚拟硬盘简介

认识虚拟硬盘并介绍VHD格式的虚拟硬盘

和真实的计算机一样虚拟机也需要一块硬盘,但这个硬盘并不是真实的硬盘,而是用文件模拟的硬盘(虚拟硬盘)。

不同公司开发的虚拟机软件对于虚拟硬盘格式的标准各不相同

  • VMDK(VMWare虚拟机)
  • VDI(VirtuaBox虚拟机)
  • VHD(Virtual-PC/Hyper-V虚拟机)
  • … …

VHD格式虚拟机硬盘

VHD相对简单

  • Virtual Hard Disk Image Format Specification
  • 文件前面数据区0面0柱1扇区、0面0柱2扇区等,最后512字节结尾,以conectix开头表示这是一个合法的VHD文件,包含创建时间大小等信息。
img_20241114_070855.png

创建 VHD(了解,不操作)

  • 打开磁盘管理。 在任务栏的搜索框中,输入“计算机管理”,然后选择“存储”>“磁盘管理”。
  • 在“操作”菜单上,选择“创建 VHD”

在Windows下创建虚拟硬盘并安装操作系统

创建虚拟硬盘

  • 管理–>工具–>虚拟介质管理–>创建
    • 虚拟硬盘文件类型:VHD
    • 虚拟硬盘文件位置:自定义,D:\VirtuaBox VMs\unbutu.vhd
    • 虚拟硬盘文件大小:自定义,10G

虚拟机关联虚拟硬盘

  • 选择已经创建的虚拟硬盘。虚拟机设置–>存储–>SATA控制器

光盘安装操作系统

  • 选择已经下载的ISO文件。虚拟机设置–>存储–>IDE控制器
  • 启动虚拟机
  • 安装操作系统
    • 安装过程略

操作系统IOS

在Linux下创建虚拟硬盘并安装操作系统

同上,虚拟硬盘创建过程直接在virtuabox的对应虚拟机设置中操作,过程略

汇编语言程序的调试

主要内容

  • 带调试功能的虚拟机
  • 安装Bochs虚拟机
  • 为Bochs虚拟机安装虚拟硬盘
  • 创建主引导扇区程序
  • 将程序写入硬盘主引导扇区
  • 用调试器观察程序的执行

带调试功能的虚拟机

回顾一下:

二进制指令文件编译.

nasm exam.asm -f bin -o exam.bin

编译的文件不能在任何操作系统中运行。因为他里面只包含了处理器指令。而操作系统要求可 执行程序里必须包含一些额外的内容来知道系统如何加载这个程序。

计算机启动过程: 程序内容写入主引导扇区,因为在计算机加电或者复位后,将首先从ROM BIOS中执行,然后硬盘主引导扇区的内容读到内存,并执行读取后的指令

由于它破坏了真实的计算机,所以可用虚拟机来做这样的事。

如果想看到指令执行过程,可以使用另一种带调试过程的虚拟机Bochs

  • 显示指令执行过程
  • 显示机器的状态
  • 显示寄存器的内容

安装Bochs虚拟机

Bochs官网:https://bochs.sourceforge.io/

Windows下载,这里下载的2.8版本,安装时自定义安装路径其他默认就好。

Ubuntu安装Bochs虚拟机

sudo apt install bochs

#启动bochs
bochs #选择3来编辑硬件

Bochs是一台完整的虚拟计算机,第一次启动时需要配置一套自己的硬件配置。

  • bochsdbg.exe 具有调试功能的虚拟机
  • bochs.exe 虚拟机

为Bochs虚拟机安装虚拟硬盘

创建VDH格式的硬盘,大小20MB

  • 打开VirtuaBox虚拟机软件
    • 菜单管理–>工具–>虚拟介质管理–>创建。硬盘类型VDH,大小20MB,预先分配空间。方便写入数据
    • D:\VirtualBox VMs\learn.vhd

查看VDH硬盘信息。最后512字节为硬盘信息

  • fixvhdwr.exe 自己编写的查看VDH硬盘信息工具
虚拟硬盘: D:\VirtualBox VMs\learn.vhd
VHD规范的原始创建者标识: conectix
创建此文件的程序:vbox
创建此文件的程序版本:07.01
该虚拟磁盘创建于 2024-11-16 3:05:01。
602个柱面;4个磁头;每磁道有17个扇区。
总容量为 20 MB(兆字节)。
该磁盘为固定磁盘(容量固定,文件大小不变)

配置Bochs配置硬盘

  • 打开Bochs,编辑Disk&Boot
  • Floppy Options 软盘选项,关闭
    • Type of floppy drive: none
  • ATA channel 0 硬盘控制器通道0
    • ATA channel 0: 启动,勾选Enable
    • First HD/CD on channel 0:
      • Type of ATA device: disk 设备类型硬盘
      • Path or physical device name: 物理设备名的路径,选择刚创建的固定大小20MB的HDV格式硬盘
      • Type of disk image: flat 硬盘镜像类型选择flat或者vpc
      • Cylinders: 602 柱面数,填写刚创建虚拟硬盘对应的信息
      • Heads: 4 磁头数,填写刚创建虚拟硬盘对应的信息
      • Sector per track: 17 每道扇区数,填写刚创建虚拟硬盘对应的信息
      • Sector size: 512 扇区大小,填写刚创建虚拟硬盘对应的信息
  • Boot Options 计算机启动顺序
    • 依次为disk, cdrom,noe

quit 退出

退出后启动需要重新设置,可 Save 保存配置到bochs工作目录下, 保存名称为bochsrc.bxrc,这是bochs默认启动的配置。

创建主引导扇区程序

16进制工具:

创建主引导扇区程序exam.asm

mov ax, 0x30      ;将立即数传送到AX寄存器
mov dx, 0xC0
add ax, dx  ;将寄存器ax和dx的值相加,结果在ax
D:\project\assemblyprojs>nasm  exam.asm -f bin -o exam.bin  #得到8个字节的数据。
$ hexdump -C exam.bin
00000000  b8 30 00 ba c0 00 01 d0                           |.0......|
00000008

#8086是低端字节序的。
b8 30 00 就是 b8 0030
ba 00c0
0d0a

主引导扇区512个字节。一个有效的主引导扇区其最后2个字节的数据必须是55AA。exam.bin程序总长8个字节。缺少的502个字节用0填充。

mov ax, 0x30      ;将立即数传送到AX寄存器
mov dx, 0xC0
add ax, dx  ;将寄存器ax和dx的值相加,结果在ax

times 502 db 0 ;重复执行502次后面的指令,db 0 向程序添加1字节数据

db 0x55
db 0xAA ;主引导扇区有效标识55AA
db #伪指令,来向程序中添加1个字节的数据。数据可以是0,55等一个字节的数据。伪指令不是真正处理器指令而是只对编译器有效的指令。
times n #伪指令,重复后指令次数,n为次数

重新编译

D:\project\assemblyprojs>nasm  exam.asm -f bin -o exam.bin
jasper@jasper MINGW64 /d/project/assemblyprojs
$ hexdump -C exam.bin
00000000  b8 30 00 ba c0 00 01 d0  00 00 00 00 00 00 00 00  |.0..............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200

将程序写入硬盘主引导扇区

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

LBA

传统上,为了读写硬盘必须指定磁头号、柱面号、扇区号。但这样非常麻烦,可以指定逻辑扇区号。

1个扇区的大小512字节,可以看成一个数据块Block。所以可以认为硬盘是典型的块设备。

采用柱面(Cylinder)、磁盘(Header)和扇区(Sector)来访问硬盘的方法叫做CHS模式。每次访问读写 磁盘都要考虑这些不是很方便,传统的CHS采用24bit寻址,这样硬盘容量太小

  • 传统的CHS采用 24 bit位寻址
  • 其中前10位表示cylinder,中间8位表示head,后面6位表示sector
  • 最大寻址空间 8 GB. echo 2^24*512/1024/1024|bc

如果将磁盘的所有扇区统一编号,在读写时只用指定的统一编号将方便很多。

逻辑块地址(LBA): logical block addressing

  • LBA是一个整数,通过转换成 CHS 格式完成磁盘具体寻址
  • ATA-1规范中定义了28位寻址模式,以每扇区512位组来计算,ATA-1所定义的 28位LBA上限达到128 GiB。2002年ATA-6规范采用48位LBA,同样以每扇区512 位组计算容量上限可达128Petabytes

范例:逻辑扇区编号

假定某硬盘有2个磁头,100个柱面,每磁盘有17个扇区。那么:
逻辑0扇区对应着0面0道1扇区;
逻辑1扇区对应着0面0道2扇区;
......
逻辑16扇区对应着0面0道16扇区;
逻辑17扇区对应着1面0道1扇区;
逻辑18扇区对应着1面0道2扇区;
......
逻辑33扇区对应着1面0道17扇区;
逻辑34扇区对应着0面1道1扇区;
逻辑35扇区对应着0面1道2扇区;
......
要注意,扇区在编号时,是以柱面为单位的。即,先是0面0道,接着是1面0道,直到把所有
盘面上的0磁道处理完,再接着处理下一个柱面。之所以这样,是因为要加速硬盘的访问速度,
最好是尽可能不移动磁头。
因为这里总共有3400(2*100*17)个扇区,故最后一个逻辑扇区的编号是3399,它对应着1面99道17扇区,
这也是整个硬盘上最后一个物理扇区。

若已知物理扇区的位置是对于H面(头)、C道的第S个扇区,则它的逻辑扇区号是:

\begin{equation*} C \times 磁头总数 \times 每道扇区数 + H \times 每道扇区数 + (S - 1) \end{equation*}

用调试器观察程序的执行

通过调试主引导扇区程序,来了解Bochs虚拟机的软件调试环境,以及软件调试的一般过程

8086处理器之后的32位64位寄存器

Intel 8086 registers
19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 (bit position)
Main registers
  AH AL AX (primary accumulator)
0 0 0 0 BH BL BX (base, accumulator)
  CH CL CX (counter, accumulator)
  DH DL DX (accumulator, extended acc)
Index registers
0 0 0 0 SI Source Index
0 0 0 0 DI Destination Index
0 0 0 0 BP Base Pointer
0 0 0 0 SP Stack Pointer
Program counter
0 0 0 0 IP Instruction Pointer
Segment registers
CS 0 0 0 0 Code Segment
DS 0 0 0 0 Data Segment
ES 0 0 0 0 Extra Segment
SS 0 0 0 0 Stack Segment
Status register
  - - - - O D I T S Z - A - P - C Flags
Table 1: 通过寄存器
64位寄存器 低32位 低16位 低8位
rax eax ax al
rbx ebx bx bl
rcx ecx cx cl
rdx edx dx dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl

ax、bx、cx 和 dx 的高 8 位仍可寻址为 ah、bh、ch、dh,但不能与所有类型的操作数一起使用。

Table 2: 指令指针寄存
64位寄存器 低32位 低16位 低8位
rip eip ip  

x86体系结构(32位)

x86体系结构源自1980年代,它是一种CISC(复杂指令集计算)架构,广泛应用于个人电脑中。在32位的x86体系结构中,主要包括以下几类寄存器:

  • 通用寄存器(EAX, EBX, ECX, EDX):用于多种算术和逻辑运算。
  • 索引寄存器(ESI, EDI):主要用于指针操作和字符串处理。
  • 指针寄存器(ESP, EBP):用于管理堆栈操作。
  • 段寄存器(CS, DS, SS, ES, FS, GS):用于存储内存段的地址。
  • 指令指针(EIP):存储即将执行的下一条指令的地址。
  • 标志寄存器(EFLAGS):存储当前状态标志,如零标志、符号标志等。

x86-64体系结构(64位)

随着技术的发展,64位计算成为必要,因此x86架构被扩展到了x86-64,也称为AMD64或Intel 64。这个架构不仅增加了寄存器的数量,而且扩展了寄存器的大小。在64位架构中,主要的变化包括:

  • 扩展的通用寄存器:每个32位寄存器(如EAX)都有对应的64位版本(如RAX)。
  • 新增的通用寄存器:R8 到 R15。
  • 扩展的索引寄存器:ESI、EDI、EBP、ESP都有对应的64位版本(如RSI、RDI、RBP、RSP)。
  • 扩展的指令指针和标志寄存器:RIP 和 RFLAGS。

这些改进提供了更大的地址空间和更高的数据处理能力,使得64位处理器可以高效地处理更大的数据集和更复杂的程序。

参考:x64 Architecture

boch开机

已经准备

  • 虚拟硬盘文件
  • 主引导扇区程序

之后就可在Bochs虚拟中调试了。打开具有调试功能的虚拟机 bochsdbg.exe

与真正的处理器不同,bochs在取第1条指令之前会停下来,等待你的调试命令。

Next at t=0  #时钟滴答的第1次,即下一条指令在t=0的基础上执行。每模拟1次,时钟会滴答1次
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0  #这行表示即将执行的指令
<bochs:1>

[0x0000fffffff0] #这条指令所在物理内存地址。仔细观察可发现,与下面的逻辑地址不一致, 逻辑地址对应ffff0的物理地址。这是有原因的,只在处理器刚启动时发生,暂不讨论
f000:fff0 #逻辑地址,可以理解为段寄存器CS内容F000, 指令指针寄存器IP内容FFF0。不像8086处理器段寄存器FFFF,指令指针寄存器0。
jmpf 0xf000:e05b #这条指令的汇编语言形式。跳转指令,目标位置0xf000:e05b
;  #分号为注释
ea5be000f0  #这条指令的机器码

bochs调试命令

sreg   #显示段寄存器内容
r    #显示通过寄存器
s  #step 单步执行
b #break断点指令,设置一个内存地址,当前处理器执行到此位置将停一下来
c #countine持续执行,如果遇到断点会停下来

bochs开始调试

#显示段寄存器
<bochs:1> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
        Data segment, base=0xffff0000, 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=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        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=0x0000000000000000, limit=0xffff
idtr:base=0x0000000000000000, limit=0xffff

#cs, ds 早在8086处理器有有
#es, ss, fs, gs, tr, gdtr, idtr 是8086之后处理器新增的

#显示通用寄存器
<bochs:2> r
rax: 00000000_00000000
rbx: 00000000_00000000
rcx: 00000000_00000000
rdx: 00000000_00000000
rsp: 00000000_00000000
rbp: 00000000_00000000
rsi: 00000000_00000000
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_0000fff0  #指令指针寄存器内容fff0
eflags: 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf

#bochs开机后,所有的通过寄存器内容都是0

#单步执行指令
#这里执行了 jmpf 0xf000:e05b 指令,可以看到下一条将执行指令是 xor ax, ax
<bochs:3> s
Next at t=1 #时钟滴答的第2次,即下一条指令在t=1的基础上执行。
(0) [0x0000000fe05b] f000:e05b (unk. ctxt): xor ax, ax                ; 31c0
#可以看到,f000:e05b 和 物理地址fe05b一致了
#从地址可以看出,目前还是在 ROM BIOS中执行

Next at t=2
(0) [0x0000000fe05d] f000:e05d (unk. ctxt): out 0x0d, al              ; e60d
<bochs:5> s
Next at t=3
(0) [0x0000000fe05f] f000:e05f (unk. ctxt): out 0xda, al              ; e6da
<bochs:6> s
Next at t=4
(0) [0x0000000fe061] f000:e061 (unk. ctxt): mov al, 0xc0              ; b0c0

定位到离开ROM BIOS最后一条指令

因为计算机启动后,总是把主引导扇区内容读取物理内存地址7c00处,然后从主引导扇区开始取指令并执行指令。 可以将这个地址设置为断点。

b #break断点指令,设置一个内存地址,当前处理器执行到此位置将停一下来
c #countine持续执行,如果遇到断点会停下来

<bochs:1> b 0x7c00
<bochs:2> c
... ...
#已经停在断点处
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0030            ; b83000

可以看到下一次指令就是主引导扇区第1条指令 mov ax, 0x30, 也就是我们前面编写的主引导扇区程序。

执行程序

<bochs:3> r
rax: 00000000_0000aa55  #内容是在执行ROM BIOS时遗留的,不用管
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

<bochs:4> s  #执行mov ax, 0x30
Next at t=17175564
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov dx, 0x00c0            ; bac000
<bochs:5> r
rax: 00000000_00000030  #可以看到rax的最低16位内容 0030,说明刚才mov ax, 0x30已经成功执行
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_00007c03
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

<bochs:6> s #执行 mov dx, 0xc0
Next at t=17175565
(0) [0x000000007c06] 0000:7c06 (unk. ctxt): add ax, dx                ; 01d0
<bochs:7> r
rax: 00000000_00000030
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_000000c0  #rdx寄存器内容低16位 00c0
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_00007c06
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

<bochs:8> s #执行add ax, dx
Next at t=17175566
(0) [0x000000007c08] 0000:7c08 (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000  #下一条将执行指令,都是0不是有效指令
<bochs:9> r
rax: 00000000_000000f0  #rax内容  ax + dx = 0x30 + 0xc0 = 0xf0
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_000000c0
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_00007c08
eflags: 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf

#退出,按回车键结束本次调试
<bochs:10> q

Bochs is exiting. Press ENTER when you're ready to close this window.

在屏幕上显示文本

主要内容

  • 显卡和显存
  • 准备访问文本模式下的显存
  • 字符的编码和显示属性
  • 文本模式下的显存操作
  • MOV指令的形式和机器码
  • 列表文件的创建和使用
  • 在汇编程序中使用标号
  • 段间直接绝对跳转指令
  • 在Bochs中运行和调试写屏程序
  • 在VirtualBox中运行写屏程序
  • 主引导扇区执行时的内存布局
  • 使用标号计算跳转的偏移地址
  • 使用寄存器的绝对间接近跳转
  • 使用相对偏移量的短跳转和近跳转

显卡和显存

显卡

为了显示文字,通常需要两种硬件,一种是显示器,一种是显卡。

  • 显卡:的职责是为显示器提供内容并控制显示器的显示模式和状态。
    • 独立显卡,独立生产销售的部件,需要插在主板上才能工作。
    • 集成显卡,焊在主板上,是主板的一部分
  • 显示器的职责是将那些内容以视觉可见的方式呈现在屏幕上。

显存

img_20241118_234410.png

显卡控制显示器的最小单位是像素,这每一个原点都是一个像素,假定这是一个显示器。一个像素对应着屏幕上的一个点,屏幕上通常有数十万乃至更多的像素。通过控制每个像素的明暗和颜色,我们就能让这大量的像素形成美丽的图案和文字。

不过一个很容易想到的问题是如何来控制这些像素呢?答案是显卡都有自己的存储器,因为它位于显卡上,所以称之为显示存储器,简称显存。

计算机显示文本和图像的基本原理:

  • 要显示的内容都预先写入显存。
  • 和其他半导体存储器一样,显存并没有什么特殊的地方,也是一个按字节组织的存储期间。因此我们可以用显存里的数据来控制每一个像素,使它呈现不同的亮度和颜色

如果显示器只能显示黑白两种颜色,那么只需要控制每个像素是亮还是不亮。我们可以用显存里的每一个比特控制一个像素,如果比特是0,则像素是不亮的。如果是1,则像素是亮的。

例如

img_20241118_234822.png
10001011 #1为亮,0暗

在这里这一串二进制比特用来控制各自的像素,比如比特为1时,对应的像素是亮的。比特是0时对应像素是暗的,看不见,后面的也一样。

img_20241118_234917.png

有些显示器需要显示灰度,也就是纯黑和纯白之间的中间色。计算机的显示系统可以提供 256级灰度,这需要为一个像素提供8比特的数据,显存里的一个字节对应着显示器上的一个像素.

如上图。这个二进制数的大小决定了像素的灰度级别,数比较大的对应的像素比较亮。数字稍微小一点,像素就稍微暗一点。后面的也是一样。

img_20241118_235105.png

现在的显示器可以显示彩色,这些显示器的每个像素是由红、绿、蓝三原色组成。每个颜色还有 256 级的深度,也就是颜色的深浅,或者专业的说是饱和度。

因为每个颜色特别小,那么三种不同深浅的颜色就会合成一个具有特定颜色的像素,这就是色彩混合的原理。

上图中

  • 左侧是早期的阴极射线管显示器的像素组成,它由三个原色组成,共同组成一个像素,每个像素是由三个原色,由红、绿、蓝三个原色组成。每个原色的成分都是圆的,这个圆点是由电子数来控制的,所以打成一个圆点。
  • 右侧是现代的液晶显示器的像素。每个原色是长方形的,因为每个液晶分子是长方形的。这样的话这三个长方形的原色就组成了一个液晶显示器的像素。

因为每个像素是由三种原色组成,而且每一种原色有 256 级的饱和度,所以在显存里每个像素需要三个字节,分别对应于红、绿、蓝三种颜色。每一个字节所表示的数字的大小就是各自原色的饱和度。

11111111 #第1个字节对应红像素,每个字节的大小就他们各自原色的饱和度
11111111 #第2个字节对应蓝像素
11111111 #第3个字节对应绿像素

比如在这里这三个字节共同表示一个像素。那么每个字节的大小就是红、绿、蓝三原色的饱和度。第一个字节它对应于红像素。第二个字节对应有绿像素。第三个字节它对应于蓝像素。那么每个字节的大小就是它们各自原色的饱和度。这样的话因为三种颜色的饱和度不一样,它们共同组成了一个具有特定颜色的像素。

img_20241118_235754.png

显然,不管你想显示什么东西,是文字还是图片,都必须小心细致的组织显存的内容,安排每个像素。不管是显示图片还是显示文字,对于显示器来说没有什么不同,因为所有的内容都是由像素组成,区别仅仅在于这些像素组成的是什么。有时候人们会说显示的是一棵树,有时候人们会说显示的是一个字母h。如果是要显示图片,这是必须要做的工作,必须要安排每一个像素。但是如果只是显示文字,这样做就太过于麻烦了,因为文字的数量是有限的,不像图片那样千变万化。

那么有没有一种方法可以简化文本的显示呢?这答案是可以的。答案是有的,工程师们将显卡的工作模式分成两种, 一种是文本模式,一种是图像模式,而且它们的显存也是分开的,有文本模式下的显存和图像模式下的显存。

  • 在文本模式下显存的内容是字符的代码。
  • 在图像模式下显存的内容是像素的颜色,就像一个二进制数,既可以是一个普通的数,也可以代表一条处理器指令一样,每个字符也可以表示成一个数字。

比如十六进制数字 4C 就代表字符l,那么这个数就是字符 l 的代码,或者说字符编码。如图所示,我们可以将字符的编码放到显存里。比如在这里我们依次存放了字符h、字符 e 和 字符l的代码,或者说编码。

在显卡上有字符发声器,它可以根据字符的编码来控制屏幕上的像素,使它们共同组成字符的轮廓。比如说当字符发声器接到字符 h 的编码的时候,那么就将它就控制一部分像素,将它显示成字符 h 的轮廓,当它接到字符 e 的编码的时候,就控制一部分像素将它显示成字符 e 的轮廓。

为了给出要显示的字符,处理器需要访问显存,把字符的编码写进去。然而显存是位于显卡上的,访问显存需要和显卡这个外围设备打交道,同时多一道手续自然是不好的,这当中最重要的考量是速度和效率。

为了实现一些快速的游戏动画效果,或者是播放高码率的电影,不直接访问显存是办不到的。为此,计算机系统的设计者们决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里。

B8000 一直到BFFFF是留给显卡的

img_20241119_000336.png

如图所示,我们知道 8086 可以访问一兆字节的内存这一部分,其中从地址 00000 开始到 A0000 结束的这一部分是常规的内存,是由内存条来提供的。从 F0000 到 FFFFF 之间的这一部分属于 ROM BIOS,它是由主板上的一个芯片来提供,也就是ROM BIOS 芯片。那么这样一来,中间还有一个 320 千字节的空洞,它的地址范围是 A0000一直到F0000,传统上这一段地址空间是由特定的外围设备来提供,其中就包括显卡,因为显示功能对于现代计算机来说实在是太重要了。

由于历史的原因,所有在个人计算机上使用的显卡在家电自检之后都会把自己初始化到 80 乘以 25 的文本模式。在这一种模式下,屏幕上可以显示 25 行,每行 80 个字符,所以叫做 80 乘以 25 的文本模式。那么这样的话,每屏总共可以显示 2, 000 个字符。所以一直以来,从地址 B8000一直到BFFFF的这一部分空间是留给显卡的,是由显卡来提供的,是将显卡上的显存文本模式的显存映射过来的,用来显示文本。这一段空间足以用来保存 2, 000 个字符以及它们的属性。

准备访问文本模式下的显存

文本模式下的显卡被映射到处理器的内存空间B8000到BFFFF,共320kB。为了访问显存也要使用逻辑地址。

BFFFF   #逻辑地址 B800:7FFF
.....
B8000 #逻辑地址 B800:0000

这一段内存可以看成为段地址为B800,偏移地址从0延伸到7FFF的区域,320千字节。

确定目标就可以编写程序了。

exam.asm

; 为访问文本模式下的显存做准备工作
mov ax, 0xb800  ; 把段地址0xb800传送给寄存器AX
mov ds, ax   ;将寄存器AX中的内容传送给数据段寄存器DS

访问数据要使用数据段寄存器DS,在intel中不允许让立即数传送到数据段寄存器。

# mov ds, 0xb800 #intel处理器,不存在这样的指令
#只允许如下指令

#mov 段寄存器, 通用寄存器
mov ds, ax
mov ds, bx
mov ds, cx

#mov 段寄存器, [内存地址]
mov es, [0x6C]

字符的编码和显示属性

通信的俩端都使用相同的字符编码方案才能正确的显示内容。

我们知道文字的信息是以数字的形式存储在计算机里。那么用哪个数字代表哪个字符呢?

必须要制定一个全球都认可的编码方案和编码标准,否则设备之间的文字交流将无法实现。

必须完成字符的收集和整理工作,使之形成一个字符的清单或者成为字符集。计算机
发展的早期字符是的制定是小范围的事,那时大型机是主流。为了使用大型机,需要
使用终端通过电缆和电话线与主机相连。

那个时候的终端都是电传打字机,少数带有显示器,电传打字机带有键盘可以向主机
发送命令,主机将处理结果送回终端。为了在主机和终端之间通信,有个双方都能理解
的码表,这就是早期的字符集和编码方式。

这个字符集用于在主机和终端之间传送控制命令和文字信息。最著名的是IBM公司的码表,
1967年 ASCII码.

ASCII字符集,共128个字符。 控制字符用来控制电传打字机的动作,如确认、响铃、查询、同步、回车、换行等。

ASCII字符集用代码点得到字符编码的实例

现今绝大数字符集都兼容ASCII字符集。

字符的编码

https://en.wikipedia.org/wiki/ASCII

img_20241121_002011.png

如果给每个字符按顺序编号,从16进制00到7F,每个字符的编码就是它在字符集中的位置编号

20 空格
21 字符!
30 数字字符0
3F 问号

字符编码、显存和显示器之间的关系

屏幕上的每个字符对应着显存中的2个连续字节。

  • 第1个字节为屏幕的ascii编码
  • 第2个字节为字符的属性,包括字符的颜色、前景色和底色(也就是背景色)。

范例1:

img_20241121_090154.png

例如上图中,前2个字节对应屏幕左上角第1个字符位置。后俩个字节对应着屏幕第2个显示字符的位置。后面以此类推。 在显存中最后2个字节对应着屏幕右下角最后一个字符的位置。

范例2:

img_20241121_090856.png

例如上图中,从显存的起始处理存放了一些字符的编码和属性。显存的开始位置逻辑地址是B800:0000,即段地址是 B800,偏移地址是0。

  • 第1个字节存放的是字符h的编码,16进制的48
  • 第2个字节存放的是字符的显示属性,16进制的07

这俩个字节就对应于屏幕上的第1个字符。从偏移地址为2的地方存放的是字符e的编码,16进制的65,它的第2个字节 存放的是它的显示属性,16进制的07。这俩个字节共同组成了屏幕上第2个字符e。以此类推。

字符的显示属性

字符编码前面已经了解过,现在我们重点讲一下字符的显示属性。

所有的属性都是1个字节长,分成2部分,低4位定义的是前景色,高4位定义的是背景色。

色彩主要是以由背景色的RGB和前景色的RGB来决定。我们知道所有颜色可以由红、绿、蓝 来调配,所以背景色有RGB这3原色,前景色也有RGB这3原色。除了RGB这3原色外,背景色 中有K位,K位是闪烁位,当K不等于0时背景不闪烁;K为1是背景闪烁。在前景色中有I位, I位是亮度位,当I为0时是正常亮度,当I为1时高亮。

对于前景色来说亮度不同会影响颜色的效果。

属性字节的组合

img_20241121_092740.png
Table 3: 80X25 文本模式下的颜色表
R G B 背景色 前景色  
      K=0不闪烁,K=1闪烁 I=0 I=1
0 0 0
0 0 1 浅蓝
0 1 0 绿 绿 浅绿
0 1 1 浅青
1 0 0 浅红
1 0 1 品(洋)红 品(洋)红 浅品(洋)红
1 1 0
1 1 1 亮白

上图是属性字节的搭配。

0x07 0000 0111  #高4位0000是背景色不闪烁黑色,低4位0111是前景色白色

黑底白字不闪烁

文本模式下的显存操作

exam.asm

mov ax, 0xb800
mov ds, ax

mov byte [0x00], 0x41  ;字符A的ASCII编码。指令的意思是把A的编码传送到B8000处
mov byte [0x01], 0x04  ;黑底红字,无闪烁

mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73  , 汇编语言编译器会把 s 转换成字符的编码
mov byte [0x03], 0x04

mov byte [0x04], 's'
mov byte [0x05], 0x04

mov byte [0x06], 'e'
mov byte [0x07], 0x04

mov byte [0x08], 'm'
mov byte [0x09], 0x04

mov byte [0x0a], 'b'
mov byte [0x0b], 0x04

mov byte [0x0c], 'l'
mov byte [0x0d], 0x04

mov byte [0x0e], 'y'
mov byte [0x0f], 0x04

mov byte [0x10], '.'
mov byte [0x11], 0x04

为了访问内存单元,需要给出段地址和偏移地址。这条指令 mov byte [0x00], 0x41 偏移地址是0x00, 段地址一般情况下,如果没有附加任何指示,那么段地址默认是在段寄存DS中。这里DS是0xb800

当这条指令执行时,将段寄存器DS中的内容做为段地址。具体操作是处理器把段寄存器DS中的内容 B800左移4位形成B8000,然后加指令中的偏移地址0,形成物理地址B8000,从这个位置开始的2个字节 单元对应屏幕左上角第1个字节。

在这条指令中 0x41 的长度不明确,可以认为它是一个字节长,也可以认为是2个字节长。

内存操作数部分 [0x00] 只是用来指定起始的偏移地址没有更多的信息,这个起始地址可以是1个字节 的起始地址,也可以是一个字的起始地址。

为此必须使用关键字byte来修饰目的操作数。

mov byte [0x00], 0x41 #偏移地址0x00是一个字节单元的偏移地址,本次传送是以字节方式进行
#一但目的操作数0x00被指定是byte,那么0x41也是一个字节长

mov ax, 0xb800 #目的操作数ax是16位长度,因此源操数0xb800是必须是16位的长度,并不需要指明宽度

#下面2条指令同样不用指明宽度
mov [0x00], al    ;按字节操作
mov ax, [0x02]   ;按字操作

MOV指令的形式和机器码

针对mov指令做个总结

MOV  目的操作数,  源操作数

目的操作数相当于容器,可以是:寄存器、内存地址
源操作数可以是:寄存器、内存地址、立即数

传送指令只会改变目的操作数的内容,不会改变源操作数内容

范例: mov指令在寄存器之间传送数据,但是寄存器的宽度必须相同

mov ah, bh
mov ax, dx

mov ax, bl ;报错,寄存器的宽度必须相同

范例:mov指令可以将内存中数据传送到寄存器

mov al, [0xf000] ;把内存里偏移地址为f000的数据传送到寄存器al中,在传送时段地址在段寄存器DS中。
        ;目的操作数是8位的寄存器al,所以传送是以字节为单位进行的,是从偏移地址f000处取一个字节传送
mov bx, [0x08]

范例:mov指令可以将一个立即数传送到寄存器

mov al, 0x07  ;将立即数0x07传送到寄存器al。因为al是8位寄存器,因此0x07的长度被视为一个字节
mov bx, 0x08

范例:mov指令可以将指定的寄存器内容传送到指定的内存地址处,传送时数据宽度取决于寄存器的宽度

mov [0x0c], dx  ;将寄存器dx中的内容传送到内存中,偏移地址为0x0c处。因为dx是16位的,
        ;因此传送的数据长度也是16位的,将占用偏移地址0x0c处2个连续的最小的单元。
mov [0x2000], ah
img_20241121_233211.png

范例:mov指令可以把立即数传送到指定的内存地址处,使用关键字来修饰地址操作数

mov byte [0x02], 0xe9 ;将字节长度的立即数0xe9传送到起始地址为0x02字节单元
mov byte [0x06], 0x3c ;将一个字长度的立即数0x3c传送到内存中偏移地址为0x06处连续的2个最小单元

范例:mov指令的目的操作数不允许是立即数,并且目的操作数和源操作数不能同时为内存单元

; mov 0x07, al  ; 目的操作数不允许是立即数
; mov [0x01], [0x02] ;目的操作数和源操作数不能同时为内存单元

可以用2个指令分阶段实现
mov ax, [0x02]
mov [0x01], ax

指令指针寄存器IP是在指令中不可直接访问的,不能出现在任何指令中,而只能间接地修改

;mov ip, 0xf000 ; 非法的,指令指针寄存器IP不能在指令中直接访问

;如果目的操作是段寄存器,源操作数必须是通用寄存器或者内存地址
mov ds, ax
mov es, [0x3002]

axam.asm的机器码、操作码是不一样的,观察编译后的mov指令

#编译,机器码看来不够直观
D:\project\assemblyprojs\assembly>nasm  exam.asm -f bin -o exam.bin

#-l 选项生成列表文件,包含了汇编指令和机器码

D:\project\assemblyprojs>nasm exam.asm -l exam.lst

D:\project\assemblyprojs>type exam.lst
   第1列为行号; 第2列为每条指令相对文件起始处的偏移量(汇编地址,程序汇编阶段所确认的地址);
   第3列为每条指令的机器码;第4列为每条指令的汇编语言格式;第5列为注释
     1 00000000 B800B8                  mov ax, 0xb800
     2 00000003 8ED8                    mov ds, ax
     3                                  
     4 00000005 C606000041              mov byte [0x00], 0x41  ;字符A的ASCII编码. 指令的意思是把A的编码传送到B8000处
     5 0000000A C606010004              mov byte [0x01], 0x04  ;黑底红字,无闪烁
     6                                  
     7 0000000F C606020073              mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
     8 00000014 C606030004              mov byte [0x03], 0x04
     9                                  
    10 00000019 C606040073              mov byte [0x04], 's'
    11 0000001E C606050004              mov byte [0x05], 0x04
    12                                  
    13 00000023 C606060065              mov byte [0x06], 'e'
    14 00000028 C606070004              mov byte [0x07], 0x04
    15                                  
    16 0000002D C60608006D              mov byte [0x08], 'm'
    17 00000032 C606090004              mov byte [0x09], 0x04
    18                                  
    19 00000037 C6060A0062              mov byte [0x0a], 'b'
    20 0000003C C6060B0004              mov byte [0x0b], 0x04
    21                                  
    22 00000041 C6060C006C              mov byte [0x0c], 'l'
    23 00000046 C6060D0004              mov byte [0x0d], 0x04
    24                                  
    25 0000004B C6060E0079              mov byte [0x0e], 'y'
    26 00000050 C6060F0004              mov byte [0x0f], 0x04
    27                                  
    28 00000055 C60610002E              mov byte [0x10], '.'
    29 0000005A C606110004              mov byte [0x11], 0x04

列表文件的创建和使用

完善主引导扇区程序

我们知道主引导扇区是512个字节,其中最后2个字节是主引导扇区的有效标志55和AA。

要填充的字节数为512字节减去55AA的2个字节,再减去程序的字节数。

在用nasm生成的列表文件里,有每一条指令的汇编地址,它是这一条汇编指令相对于程序开头处的偏移量, 通过偏移量就可以算出程序代码有多少字节。

nasm exam.asm -f bin -o exam.bin -l exam.lst

cat exam.lst
     1 00000000 B800B8                  mov ax, 0xb800
     2 00000003 8ED8                    mov ds, ax
     3                                  
     4 00000005 C606000041              mov byte [0x00], 0x41  ;字符A的ASCII编码. 指令的意思是把A的编码传送到B8000处
     5 0000000A C606010004              mov byte [0x01], 0x04  ;黑底红字,无闪烁
     6                                  
     7 0000000F C606020073              mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
     8 00000014 C606030004              mov byte [0x03], 0x04
     9                                  
    10 00000019 C606040073              mov byte [0x04], 's'
    11 0000001E C606050004              mov byte [0x05], 0x04
    12                                  
    13 00000023 C606060065              mov byte [0x06], 'e'
    14 00000028 C606070004              mov byte [0x07], 0x04
    15                                  
    16 0000002D C60608006D              mov byte [0x08], 'm'
    17 00000032 C606090004              mov byte [0x09], 0x04
    18                                  
    19 00000037 C6060A0062              mov byte [0x0a], 'b'
    20 0000003C C6060B0004              mov byte [0x0b], 0x04
    21                                  
    22 00000041 C6060C006C              mov byte [0x0c], 'l'
    23 00000046 C6060D0004              mov byte [0x0d], 0x04
    24                                  
    25 0000004B C6060E0079              mov byte [0x0e], 'y'
    26 00000050 C6060F0004              mov byte [0x0f], 0x04
    27                                  
    28 00000055 C60610002E              mov byte [0x10], '.'
    29 0000005A C606110004              mov byte [0x11], 0x04
    30                                  
    31                                  ;times 510-? db 0   ; 要填充的字节数为512字节减去55AA的2个字节, 再减程序的长度。 
    32 0000005F 00<rep 1FEh>            times 510 db 0
    33                                  
    34                                  
    35 0000025D 55AA                    db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

我们说过,每一条指令的汇编地址是这条指令相对于文件开始处的偏移量。可以看到程序的长度为5F

exam.asm

mov ax, 0xb800
mov ds, ax

mov byte [0x00], 0x41  ;字符A的ASCII编码. 指令的意思是把A的编码传送到B8000处
mov byte [0x01], 0x04  ;黑底红字,无闪烁

mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
mov byte [0x03], 0x04

mov byte [0x04], 's'
mov byte [0x05], 0x04

mov byte [0x06], 'e'
mov byte [0x07], 0x04

mov byte [0x08], 'm'
mov byte [0x09], 0x04

mov byte [0x0a], 'b'
mov byte [0x0b], 0x04

mov byte [0x0c], 'l'
mov byte [0x0d], 0x04

mov byte [0x0e], 'y'
mov byte [0x0f], 0x04

mov byte [0x10], '.'
mov byte [0x11], 0x04

times 510-0x5f db 0   ; 要填充的字节数为512字节减去55AA的2个字节, 再减程序的长度。 

db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

jasper@jasper MINGW64 /d/project/assemblyprojs
$ hexdump.exe  -C exam.bin
00000000  b8 00 b8 8e d8 c6 06 00  00 41 c6 06 01 00 04 c6  |.........A......|
00000010  06 02 00 73 c6 06 03 00  04 c6 06 04 00 73 c6 06  |...s.........s..|
00000020  05 00 04 c6 06 06 00 65  c6 06 07 00 04 c6 06 08  |.......e........|
00000030  00 6d c6 06 09 00 04 c6  06 0a 00 62 c6 06 0b 00  |.m.........b....|
00000040  04 c6 06 0c 00 6c c6 06  0d 00 04 c6 06 0e 00 79  |.....l.........y|
00000050  c6 06 0f 00 04 c6 06 10  00 2e c6 06 11 00 04 00  |................|
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200
# 可以看到程序大小正好是512个字节

在汇编程序中使用标号

完善主引导扇区程序,了解什么是标号,并使用标号来计算数据和代码的长度

程序的指令长度是可变化的,自己算程序的长度是很麻烦的。

在文本编辑器里,汇编语言程序是分行的,每一行包括3个部分

  • 标号:一串文本。通常是以冒号结尾。可选
  • 指令:可选
  • 注释:以分号开始,后面是说明性文字。可选

标号代表离它最近指令的汇编地址。使用标号可以做到真正的一劳永逸。

exam.asm

start:
        mov ax, 0xb800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.指令的意思是把A的编码传送到B8000处
        mov byte [0x01], 0x04  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04
current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

cat exam.lst

     1                                  start:
     2 00000000 B800B8                          mov ax, 0xb800
     3 00000003 8ED8                            mov ds, ax
     4                                  
     5 00000005 C606000041                      mov byte [0x00], 0x41  ;字符A的ASCII编码.指令的意思是把A的编码传送到B8000处
     6 0000000A C606010004                      mov byte [0x01], 0x04  ;黑底红字,无闪烁
     7                                          
     8 0000000F C606020073                      mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
     9 00000014 C606030004                      mov byte [0x03], 0x04
    10                                          
    11 00000019 C606040073                      mov byte [0x04], 's'
    12 0000001E C606050004                      mov byte [0x05], 0x04
    13                                          
    14 00000023 C606060065                      mov byte [0x06], 'e'
    15 00000028 C606070004                      mov byte [0x07], 0x04
    16                                          
    17 0000002D C60608006D                      mov byte [0x08], 'm'
    18 00000032 C606090004                      mov byte [0x09], 0x04
    19                                          
    20 00000037 C6060A0062                      mov byte [0x0a], 'b'
    21 0000003C C6060B0004                      mov byte [0x0b], 0x04
    22                                          
    23 00000041 C6060C006C                      mov byte [0x0c], 'l'
    24 00000046 C6060D0004                      mov byte [0x0d], 0x04
    25                                          
    26 0000004B C6060E0079                      mov byte [0x0e], 'y'
    27 00000050 C6060F0004                      mov byte [0x0f], 0x04
    28                                          
    29 00000055 C60610002E                      mov byte [0x10], '.'
    30 0000005A C606110004                      mov byte [0x11], 0x04
    31                                  current: 
    32 0000005F 00<rep 19Fh>                    times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 
    33                                          
    34 000001FE 55AA                            db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

jasper@jasper MINGW64 /d/project/assemblyprojs
$ hexdump.exe  -C exam.bin
00000000  b8 00 b8 8e d8 c6 06 00  00 41 c6 06 01 00 04 c6  |.........A......|
00000010  06 02 00 73 c6 06 03 00  04 c6 06 04 00 73 c6 06  |...s.........s..|
00000020  05 00 04 c6 06 06 00 65  c6 06 07 00 04 c6 06 08  |.......e........|
00000030  00 6d c6 06 09 00 04 c6  06 0a 00 62 c6 06 0b 00  |.m.........b....|
00000040  04 c6 06 0c 00 6c c6 06  0d 00 04 c6 06 0e 00 79  |.....l.........y|
00000050  c6 06 0f 00 04 c6 06 10  00 2e c6 06 11 00 04 00  |................|
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200

段间直接绝对跳转指令

断续完善主引导扇区程序,并认识段间直接绝对跳转指令

汇编程序是按顺序执行的,当执行到我们写的填充位置就会出现不可预见的问题。写完这些指令后我们的工作就做完了, 我们的任务很简单只要在屏幕上显示完文本Assebly就可以了。

想象一下我们的程序是完整的主引导扇区内容。在计算机处理器加电或者复位后,将首先从ROM BIOS中执行,然后把硬盘主引导扇区的内容读到读取物理内存地址7c00处,然后从主引导扇区开始取指令并执行指令。

物理地址7c00的逻辑地址可以表示成 0x0000:0x7c00,我们可以执行一段跳转指令重新从7c00处理读取程序代码 jmp 0x0000:0x7c00

exam.asm

start:
        mov ax, 0xb800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.指令的意思是把A的编码传送到B8000处
        mov byte [0x01], 0x04  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

        jmp 0x0000:0x7c00 ;循环从主引导扇区起始处读取指令。段间直接绝对跳转指令

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

这样就组成了段间直接绝对跳转指令。

  • 段间指的是这条指令将跳到另外一个段里执行。段地址0x0000和偏移地址0x7c00。当处理器执行这条 跳转指令时,用指令中给出的段地址修改代码段寄存器CS,用指令中的偏移地址修改指令指针寄存器IP
  • 直接绝对是指跳转的目标位置,也就是段地址0x0000和偏移地址0x7c00是直接在指令中给出的不需要计算的绝对地址。

编译观察

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

jasper@jasper MINGW64 /d/project/assemblyprojs
$ hexdump.exe -C exam.bin
00000000  b8 00 b8 8e d8 c6 06 00  00 41 c6 06 01 00 04 c6  |.........A......|
00000010  06 02 00 73 c6 06 03 00  04 c6 06 04 00 73 c6 06  |...s.........s..|
00000020  05 00 04 c6 06 06 00 65  c6 06 07 00 04 c6 06 08  |.......e........|
00000030  00 6d c6 06 09 00 04 c6  06 0a 00 62 c6 06 0b 00  |.m.........b....|
00000040  04 c6 06 0c 00 6c c6 06  0d 00 04 c6 06 0e 00 79  |.....l.........y|
00000050  c6 06 0f 00 04 c6 06 10  00 2e c6 06 11 00 04 ea  |................|   #跳转指令,EA是操作码
00000060  00 7c 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |.|..............| #00 7c是偏移地址, 00 00 是段地址。因为是
#intel处理器是低端字节序的,处理器的低字节在内存的低地址处,处理器的高字节在内存的高地址处。
#所以 00 7c 00 00 , 读的时候是从后往前读,读成 0000  7c00
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200

在Bochs中运行和调试写屏程序

调试程序,观察写屏过程,进一步了解程序调试的基本技巧

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

bochs调试命令

sreg   #显示段寄存器内容
r    #显示通过寄存器
s  #step 单步执行
b #break断点指令,设置一个内存地址,当前处理器执行到此位置将停一下来
c #countine持续执行,如果遇到断点会停下来

帮助 help, help 命令
n #next,写step命令功能类似
xp /字节数和显示方式 物理内存地址 #查看内存中内容,如xp /512xb 0x7c00  ,其中x以16进制显示, b以字节为单位显示
u #后续的机指令反汇编成汇编代码。一个u反汇编一条指令。连续反汇编32条指令, u/32

bochs开机后调试界面

与真正的处理器不同,bochs在取第1条指令之前会停下来,等待你的调试命令。

Next at t=0  #时钟滴答的第1次,即下一条指令在t=0的基础上执行。每模拟1次,时钟会滴答1次
#处理器准备执行第1条指令。CS内容为f000, IP内容为fff0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0  #这行表示即将执行的指令
<bochs:1>

[0x0000fffffff0] #这条指令所在物理内存地址。仔细观察可发现,与下面的逻辑地址不一致, 逻辑地址对应ffff0的物理地址。这是有原因的,只在处理器刚启动时发生,暂不讨论
f000:fff0 #逻辑地址,可以理解为段寄存器CS内容F000, 指令指针寄存器IP内容FFF0。不像8086处理器段寄存器FFFF,指令指针寄存器0。
jmpf 0xf000:e05b #这条指令的汇编语言形式。跳转指令,目标位置0xf000:e05b
;  #分号为注释
ea5be000f0  #这条指令的机器码

bochs开机后,准备跳到ROM BIOS中执行,ROM BIOS要做很多工作 ,其中最后一项工作是从虚拟硬件的主引导 扇区读取一个扇区数据,把主引导扇区读入到内存的物理地址0x7c00。

这时ROM BIOS还没有执行,查看7c00是什么,使用调试命令xp /512xb 0x7c00, 以16进制显示512个字节

xp /字节数和显示方式 物理内存地址 #查看内存中内容,如xp /512xb 0x7c00 ,其中x以16进制显示, b以字节为单位显示

帮助 help, help 命令

#查看7c00处理内容,以16进制显示512个字节
<bochs:3> xp /512xb 0x7c00
[bochs]:
0x0000000000007c00 <bogus+       0>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x0000000000007c08 <bogus+       8>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
... ...
0x0000000000007df0 <bogus+     496>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x0000000000007df8 <bogus+     504>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
<bochs:4>

bochs开始调试

因为计算机启动后,总是把主引导扇区内容读取物理内存地址7c00处,然后从主引导扇区开始取指令并执行指令。 可以将这个地址设置为断点。

#设置断点7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
<bochs:4> b 0x7c00
<bochs:5> c #处理器连续地执行,遇到断点停下
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

u 后续的机指令反汇编成汇编代码。一个u反汇编一条指令。连续反汇编32条指令, u/32

<bochs:30> u/32 #反汇编后续机器指令, 找到程序最后一条指令所在内存物理地址
0000000000007c00: (                    ): mov ax, 0xb800            ; b800b8
0000000000007c03: (                    ): mov ds, ax                ; 8ed8
...
#最后一条指令的物理地址是 7c5f,方便设置断点
0000000000007c5f: (                    ): jmpf 0x0000:7c00          ; ea007c0000


#查看7c00处理内容,以16进制显示512个字节. 可以看到我们写的主引导扇区程序指令
<bochs:6> xp /512xb 0x7c00
[bochs]:
0x0000000000007c00 <bogus+       0>:    0xb8    0x00    0xb8    0x8e    0xd8    0xc6    0x06    0x00
0x0000000000007c08 <bogus+       8>:    0x00    0x41    0xc6    0x06    0x01    0x00    0x04    0xc6
0x0000000000007c10 <bogus+      16>:    0x06    0x02    0x00    0x73    0xc6    0x06    0x03    0x00
......
0x0000000000007df0 <bogus+     496>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x0000000000007df8 <bogus+     504>:    0x00    0x00    0x00    0x00    0x00    0x00    0x55    0xaa

单步运行程序指令, 数据段寄存内容为b800

#单步执行指令
<bochs:7> s
Next at t=17175564
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov ds, ax                ; 8ed8
<bochs:8> s
Next at t=17175565 #跟我们写的指令有区别,但总体一样的,是把字母A的编码0x41传送到偏移地址0,
#但这里给出了默认的数据段寄存器ds,意思是传送到ds所指向的内存段,段中偏移地址0处
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): mov byte ptr ds:0x0000, 0x41 ; c606000041

虚拟机的显示器已经显示了很多内容,这是启动信息。这些启动信息位于内存物理地址也就是显存0xb8000处
#查看显示器上已有内容,b800处的编码和颜色属性
<bochs:10> xp /16xb 0xb8000
[bochs]:
0x00000000000b8000 <bogus+       0>:    0x42    0x0b    0x6f    0x0b    0x63    0x0b    0x68    0x0b
0x00000000000b8008 <bogus+       8>:    0x73    0x0b    0x20    0x0b    0x56    0x0b    0x47    0x0b
#0x42是显示器上字母B的编码,0x0b(0000 1011)是字母B的颜色属性(黑底青色)
#0x6f是显示器上字母o的编码,0x0b(0000 1011)是字母o的颜色属性(黑底青色)
<bochs:10>

继续运行程序,在屏幕左上角显示新的文本

#使用单步调试命令s,写入显存的指令时,写入的内容不能在屏幕上看出来。这里换成相似功能的n命令
<bochs:11> n #字母A的编码0x41写入到显存, ds:0x0000处,ds为0xb800
Next at t=17175566
(0) [0x000000007c0a] 0000:7c0a (unk. ctxt): mov byte ptr ds:0x0001, 0x04 ; c606010004
<bochs:12> n #字母A的颜色属性0x04写入到显存,0000 0100黑底红字
Next at t=17175567
(0) [0x000000007c0f] 0000:7c0f (unk. ctxt): mov byte ptr ds:0x0002, 0x73 ; c606020073

将程序最后一条指令设为断点,快速运行完程序

#将程序最后一条指令设为断点,快速运行完程序
<bochs:32> b 0x7c5f
<bochs:33> c
Next at t=17175604
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmpf 0x0000:7c00          ; ea007c0000

#继续运行,又跳到7c00处,开始循环执行程序指令
<bochs:34> s
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=17175605
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

在VirtualBox中运行写屏程序

打开Virtualbox, 创建硬件的虚拟机或者移除我们之前创建的虚拟机硬盘,添加已经写入主导扇区程序的虚拟硬盘文件learn.vhd

img_20241123_112350.png

启动虚拟机,观察屏幕是否显示文本信息Assembly.

img_20241123_112705.png

显示正常,强制退出虚拟机。

主引导扇区执行时的内存布局

根据主引导扇区加载后的内存布局来修改最后一条JMP指令,使它重复执行自己

将主引导扇区程序编译生成列表文件,观察一下内存布局,找到最后一条指令物理地址。

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

     1                                  start:
     2 00000000 B800B8                          mov ax, 0xb800 ;段地址B800
     3 00000003 8ED8                            mov ds, ax
     4                                  
     5 00000005 C606000041                      mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
     6 0000000A C606010004                      mov byte [0x01], 0x04  ;黑底红字,无闪烁
     7                                          
     8 0000000F C606020073                      mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
     9 00000014 C606030004                      mov byte [0x03], 0x04
     ......
    32 0000005F EA007C0000                      jmp 0x0000:0x7c00 ;循环从主引导扇区起始处读取指令。段间直接绝对跳转指令
    33                                  
    34                                  current: 
    35 00000064 00<rep 19Ah>                    times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 
    36                                          
    37 000001FE 55AA                            db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

实际上我们没有跳那么远程让程序重新执行,可以反复跳到他自己。将跳转的目标地址改为这条指令自身的地址。

列表文件中第1条指令,汇编地址是0,jmp指令的汇编地址是5f。

img_20241123_123925.png

这是内存最低端的视图,因为物理地址是从0开始。主引导扇区加载的位置是从内存物理地址7c00处开始一直到7e00结束。主引导扇区 第1条指令是从7c00处开始,在源文件中的汇编地址是0。指令 jmp 0x0000:0x7c00 在源文件的汇编地址是5f,这条指令在内存中物理 地址是 7c00 + 5f = 7c5f 。得到地址,我们将程序修改如下。

start:
        mov ax, 0xb800 ;段地址B800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
        mov byte [0x01], 0x0c  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

        jmp 0x0000:0x7c5f ;跳转到自己。段间直接绝对跳转指令

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

#反汇编32条指令
<bochs:3> u /32
0000000000007c00: (                    ): mov ax, 0xb800            ; b800b8
0000000000007c03: (                    ): mov ds, ax                ; 8ed8
0000000000007c05: (                    ): mov byte ptr ds:0x0000, 0x41 ; c606000041
0000000000007c0a: (                    ): mov byte ptr ds:0x0001, 0x0c; c60601000c
0000000000007c0f: (                    ): mov byte ptr ds:0x0002, 0x73 ; c606020073
0000000000007c14: (                    ): mov byte ptr ds:0x0003, 0x04 ; c606030004
0000000000007c19: (                    ): mov byte ptr ds:0x0004, 0x73 ; c606040073
0000000000007c1e: (                    ): mov byte ptr ds:0x0005, 0x04 ; c606050004
0000000000007c23: (                    ): mov byte ptr ds:0x0006, 0x65 ; c606060065
0000000000007c28: (                    ): mov byte ptr ds:0x0007, 0x04 ; c606070004
0000000000007c2d: (                    ): mov byte ptr ds:0x0008, 0x6d ; c60608006d
0000000000007c32: (                    ): mov byte ptr ds:0x0009, 0x04 ; c606090004
0000000000007c37: (                    ): mov byte ptr ds:0x000a, 0x62 ; c6060a0062
0000000000007c3c: (                    ): mov byte ptr ds:0x000b, 0x04 ; c6060b0004
0000000000007c41: (                    ): mov byte ptr ds:0x000c, 0x6c ; c6060c006c
0000000000007c46: (                    ): mov byte ptr ds:0x000d, 0x04 ; c6060d0004
0000000000007c4b: (                    ): mov byte ptr ds:0x000e, 0x79 ; c6060e0079
0000000000007c50: (                    ): mov byte ptr ds:0x000f, 0x04 ; c6060f0004
0000000000007c55: (                    ): mov byte ptr ds:0x0010, 0x2e ; c60610002e
0000000000007c5a: (                    ): mov byte ptr ds:0x0011, 0x04 ; c606110004
0000000000007c5f: (                    ): jmpf 0x0000:7c5f          ; ea5f7c0000
......

#设置断点7c5f, 再执行到断点处
<bochs:4> b 0x7c5f
<bochs:5> c
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175583
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmpf 0x0000:7c5f          ; ea5f7c0000

#可以看到屏幕上已经显示自定义文本信息

#下一条将执行的指令一直是跳到物理地址7c5f
<bochs:6> s
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175584
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmpf 0x0000:7c5f          ; ea5f7c0000
<bochs:7> s
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175585
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmpf 0x0000:7c5f          ; ea5f7c0000
<bochs:8> s
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175586
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmpf 0x0000:7c5f          ; ea5f7c0000

#退出
<bochs:9> q

打开Virtusbox虚拟机观察。

img_20241123_130511.png

使用标号计算跳转的偏移地址

使用标号来计算JMP指令跳转的偏移地址

开始之前,先看一下,主引导扇区开始执行时,段寄存器CS和指令指针寄存器IP的内容是什么。

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
#设置断点7c00 再执行到断点处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

# mov ax, 0xb800~ 这是主引导扇区的第1条指令,它的逻辑地址是 ~0000:7c00~ , 换句话
#说段寄存器CS的内容是0,指令指针寄存器IP的内容是7c00

#查看段寄存器, CS的内容是0000
<bochs:3> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
        Data segment, base=0x00000000, 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=0x00000000000f9e67, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff

#查看通用寄存器,rip的低16位对应着8086的IP的内容为7c00
<bochs:4> 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
#退出
<bochs:5> q
img_20241123_132228.png

上图,是位于内存最低端的段。段地址是0000,偏移地址从0000到FFFF,64千字节的段。 在计算机处理器加电或者复位后,将首先从ROM BIOS中执行,然后把硬盘主引导扇区的内容加载到内存里,而且是从段内偏移地址7c00处开始加载的。 当主引导扇区执行时,处理器CS是指向这个段的(0000),同时指令指针寄存器指向这个加载的位置,ip的内容是7c00。 jmp 0x0000:0x7c5f 跳转 之后还是在这个段里,段内偏移地址7c5f = 源文件指令的汇编地址5f +第1条指令段内偏移地址7c00

标号代表着它所在那条指令的汇编地址,使用了标号再也不用手工计算偏移地址了,还可以向任何地方跳转。

5f可以变成一个标号。程序修改如下

start:
        mov ax, 0xb800 ;段地址B800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
        mov byte [0x01], 0x0c  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

again:
        jmp 0x0000:0x7c00+again ;跳转到自己。段间直接绝对跳转指令

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

使用bochs虚拟机进行调试。过程略

使用寄存器的绝对间接近跳转

认识和使用另一种段内的跳转指令:绝对间跳转

jmp 0x0000:0x7c00+again ;跳转到自己。段间直接绝对跳转指令

段间直接绝对跳转指令是从一个代码段跳转到另一个代码段地址处执行,直接给出了目标位置的绝对地址。

就我们这个程序而言是很费事的,因为这些指令都在一个段内,可以在不改变段寄存器CS的情况,从一个地方跳到另一个地址就行了。 相对于段之间的跳转,在段内的跳转更常见,只是在一个代码段内跳转。

8086处理器提供了一种新的跳转形式,绝对间接近跳转,可以根据通用寄存器的内容来跳转。如可以根据bx的内容进行跳转。

  • 绝对,给出的地址是目标地址的实际偏移地址,绝对地址
  • 间接,跳转的目标位置不是在指令中直接给出的,而是根据寄存器中内容间接给出
  • 近,跳转到不太远的地方,段的内容

程序修改如下

start:
        mov ax, 0xb800 ;段地址B800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
        mov byte [0x01], 0x0c  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

        mov bx, 0x7c00+again

again:
        jmp bx ;跳转到自己。绝对间接近跳转指令

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

编译

nasm exam.asm -f bin -o exam.bin -l exam.lst

将程序写入主引导扇区,打开bochs调试

#设置断点7c00 再执行到断点处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

#反汇编32条指令,观察指令和对应的物理地址,jmp指令的物理地址是7c62,跳转自己
<bochs:3> u /32
0000000000007c00: (                    ): mov ax, 0xb800            ; b800b8
0000000000007c03: (                    ): mov ds, ax                ; 8ed8
0000000000007c05: (                    ): mov byte ptr ds:0x0000, 0x41 ; c606000041
0000000000007c0a: (                    ): mov byte ptr ds:0x0001, 0x0c ; c60601000c
0000000000007c0f: (                    ): mov byte ptr ds:0x0002, 0x73 ; c606020073
0000000000007c14: (                    ): mov byte ptr ds:0x0003, 0x04 ; c606030004
0000000000007c19: (                    ): mov byte ptr ds:0x0004, 0x73 ; c606040073
0000000000007c1e: (                    ): mov byte ptr ds:0x0005, 0x04 ; c606050004
0000000000007c23: (                    ): mov byte ptr ds:0x0006, 0x65 ; c606060065
0000000000007c28: (                    ): mov byte ptr ds:0x0007, 0x04 ; c606070004
0000000000007c2d: (                    ): mov byte ptr ds:0x0008, 0x6d ; c60608006d
0000000000007c32: (                    ): mov byte ptr ds:0x0009, 0x04 ; c606090004
0000000000007c37: (                    ): mov byte ptr ds:0x000a, 0x62 ; c6060a0062
0000000000007c3c: (                    ): mov byte ptr ds:0x000b, 0x04 ; c6060b0004
0000000000007c41: (                    ): mov byte ptr ds:0x000c, 0x6c ; c6060c006c
0000000000007c46: (                    ): mov byte ptr ds:0x000d, 0x04 ; c6060d0004
0000000000007c4b: (                    ): mov byte ptr ds:0x000e, 0x79 ; c6060e0079
0000000000007c50: (                    ): mov byte ptr ds:0x000f, 0x04 ; c6060f0004
0000000000007c55: (                    ): mov byte ptr ds:0x0010, 0x2e ; c60610002e
0000000000007c5a: (                    ): mov byte ptr ds:0x0011, 0x04 ; c606110004
0000000000007c5f: (                    ): mov bx, 0x7c62            ; bb627c
0000000000007c62: (                    ): jmp bx                    ; ffe3
0000000000007c64: (                    ): add byte ptr ds:[bx+si], al ; 0000


#设置断点,执行到mov bx, 0x7c62停下来
<bochs:4> b 0x7c5f
<bochs:5> c
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175583
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): mov bx, 0x7c62            ; bb627c

#不同的跳转指令,他们的操作码是不一样的。跳转到bx的操作码是ffe3
<bochs:6> s
Next at t=17175584
(0) [0x000000007c62] 0000:7c62 (unk. ctxt): jmp bx                    ; ffe3
<bochs:7> s
Next at t=17175585
(0) [0x000000007c62] 0000:7c62 (unk. ctxt): jmp bx                    ; ffe3
<bochs:8> s
Next at t=17175586
(0) [0x000000007c62] 0000:7c62 (unk. ctxt): jmp bx                    ; ffe3
<bochs:9> s
Next at t=17175587
(0) [0x000000007c62] 0000:7c62 (unk. ctxt): jmp bx                    ; ffe3
#同时要说明的是,这条指令执行时不改变段寄存器CS的内容,只是改变
#指令指针寄存器IP的内容,所以它是段内绝对间接近跳转指令

#只要虚拟机不关机就会永远执行这条指令

#退出
<bochs:11> q

使用相对偏移量的短跳转和近跳转

引入另一种更方便的跳转形式,这种跳转形式不使用绝对地址,而是使用 到目标位置的相对偏移量。

段间直接绝对跳转和绝对间接近跳转都要使用绝对段内地址很不方便。主引导扇区程序有点特殊,我们知道加载的位置是 7c00,可以用7c00加上汇编地址加得出段的偏移地址。但是,在多时候程序从什么地址加载是不知道的, 它的偏移地址很难计算。

使用标号是最方便的。

目标位置相对于这条跳转指令,它们之间的距离是不变的。也就是相对偏移量是不会改变的。

jmp 标号

像这种指令,在编译时采用的不是绝对偏移地址,也不是标号的汇编地址,而是相对偏移量。

相对偏移量计算:标号所在指令的汇编地址减去jmp指令后面的汇编地址。相减的结果可能是正数也可能是负数。

如果跳不太远,相减后小128小于-127,对应的机器指令是2个字节,第1个字节是EB,第2个字节是8位的相对偏移量。这样的跳转称为 相对短跳转。指令的汇编格式是 jmp short start ,其中short可不写,编译器可以自己选择。

jmp short 标号 ;短跳转。机器指令2个字节,EB 和 8位的相对偏移量
jmp near 标号  ;近跳转。操作码E9,和16位的相对偏移量

程序修改如下

start:
        mov ax, 0xb800 ;段地址B800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
        mov byte [0x01], 0x0c  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

again:
        jmp near start ;跳转到自己。相对偏移量跳转。E9 16位的相对偏移量

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

编译

nasm exam.asm -f bin -o exam.bin -l exam.lst

将程序写入主引导扇区,打开bochs调试

#设置断点7c00 再执行到断点处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8

#反汇编32条指令,观察指令和对应的物理地址
<bochs:3> u /32
0000000000007c00: (                    ): mov ax, 0xb800            ; b800b8
0000000000007c03: (                    ): mov ds, ax                ; 8ed8
0000000000007c05: (                    ): mov byte ptr ds:0x0000, 0x41 ; c606000041
0000000000007c0a: (                    ): mov byte ptr ds:0x0001, 0x0c ; c60601000c
0000000000007c0f: (                    ): mov byte ptr ds:0x0002, 0x73 ; c606020073
0000000000007c14: (                    ): mov byte ptr ds:0x0003, 0x04 ; c606030004
0000000000007c19: (                    ): mov byte ptr ds:0x0004, 0x73 ; c606040073
0000000000007c1e: (                    ): mov byte ptr ds:0x0005, 0x04 ; c606050004
0000000000007c23: (                    ): mov byte ptr ds:0x0006, 0x65 ; c606060065
0000000000007c28: (                    ): mov byte ptr ds:0x0007, 0x04 ; c606070004
0000000000007c2d: (                    ): mov byte ptr ds:0x0008, 0x6d ; c60608006d
0000000000007c32: (                    ): mov byte ptr ds:0x0009, 0x04 ; c606090004
0000000000007c37: (                    ): mov byte ptr ds:0x000a, 0x62 ; c6060a0062
0000000000007c3c: (                    ): mov byte ptr ds:0x000b, 0x04 ; c6060b0004
0000000000007c41: (                    ): mov byte ptr ds:0x000c, 0x6c ; c6060c006c
0000000000007c46: (                    ): mov byte ptr ds:0x000d, 0x04 ; c6060d0004
0000000000007c4b: (                    ): mov byte ptr ds:0x000e, 0x79 ; c6060e0079
0000000000007c50: (                    ): mov byte ptr ds:0x000f, 0x04 ; c6060f0004
0000000000007c55: (                    ): mov byte ptr ds:0x0010, 0x2e ; c60610002e
0000000000007c5a: (                    ): mov byte ptr ds:0x0011, 0x04 ; c606110004
0000000000007c5f: (                    ): jmp .-98  (0x00007c00)    ; e99eff
0000000000007c62: (                    ): add byte ptr ds:[bx+si], al ; 0000
#e99eff一共3个字节。其中e9是操作码,9eff是相对偏移量。

偏移量计算, 查看编译后的列表文件

 1                                  start:
 2 00000000 B800B8                          mov ax, 0xb800 ;段地址B800
 ......
32                                  again:
33 0000005F E99EFF                          jmp near start ;跳转到自己。相对偏移量跳转。E9 16位的相对偏移量
36 00000062 00<rep 19Ch>                    times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

相对偏移量 = 目标汇编地址(0) - jmp指令之后的汇编地址(62) = FFFF FFFF FFFF FF9E = 指令中只能取16位ff9e = -98

8086是低端字节序的,读的时候从向前读。 9eff 。 16进制的ff9e等于10进制-98

这条jmp指令之后的汇编地址相对于第1条指令,它们的相对偏移量是98个字节。因为方向原因所以是个负数。

设置新断点

<bochs:4> b 0x7c5f
<bochs:5> c
(0) Breakpoint 2, 0x0000000000007c5f in ?? ()
Next at t=17175583
(0) [0x000000007c5f] 0000:7c5f (unk. ctxt): jmp .-98  (0x00007c00)    ; e99eff

#当前 jmp -98执行时,是用指令指针寄存器IP的内容+指令中指令的偏移量得到目标位置处的内存偏移地址。
#指令指针寄存器IP的内容不是7c5f,而是IP的内容等于IP的内容+当前指令的长度,即IP内容自动变为7c5f+16=7c62
#所以目标位置内存偏移地址为=7c62+ff9e=17c00,指令指针的内容只有16位,取7c00
<bochs:6> s
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=17175584
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xb800            ; b800b8
<bochs:7> s
Next at t=17175585
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov ds, ax                ; 8ed8
<bochs:8> s
Next at t=17175586
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): mov byte ptr ds:0x0000, 0x41 ; c606000041

所以跳转到自己话,程序修改如下

start:
        mov ax, 0xb800 ;段地址B800
        mov ds, ax

        mov byte [0x00], 0x41  ;字符A的ASCII编码.意思是把A的编码传送到内存物理地址B8000处
        mov byte [0x01], 0x0c  ;黑底红字,无闪烁

        mov byte [0x02], 's'   ;等同于 mov [0x02], 0x73
        mov byte [0x03], 0x04

        mov byte [0x04], 's'
        mov byte [0x05], 0x04

        mov byte [0x06], 'e'
        mov byte [0x07], 0x04

        mov byte [0x08], 'm'
        mov byte [0x09], 0x04

        mov byte [0x0a], 'b'
        mov byte [0x0b], 0x04

        mov byte [0x0c], 'l'
        mov byte [0x0d], 0x04

        mov byte [0x0e], 'y'
        mov byte [0x0f], 0x04

        mov byte [0x10], '.'
        mov byte [0x11], 0x04

again:
        jmp near again ;跳转到自己。相对偏移量跳转。E9 16位的相对偏移量

current: 
        times 510-(current-start) db 0   ;要填充的字节数为512字节减去55AA的2个字节, 再减程序长度。 

        db 0x55, 0xaa ;用一个db可以定义多个字节,用逗号分隔

在屏幕上显示数字

主要内容

  • 显示数字的基本原理
  • 无符号数除法指令div
  • 在调试器里验证除法操作
  • 异或指令xor的用法
  • 加法指令add的用法
  • 使用标号访问内存数据
  • 段超越前缀的使用
  • 显示标号的汇编地址

显示数字的基本原理

认识数字和数字字符的区别,以及如何将数字拆分为独立的数位,为显示数字做准备

数字1 -> 0000 0001 (0x01)
字符1 -> 0011 0001 (0x31)

在ASCII字符集中只有0到9这10个数字字符。这意味着如果想在屏幕上显示数字125,需要在屏幕分别显示3个数字字符1,2,5。 也就是分别向显存写入这3个字符的编码和颜色属性。

125如何变成3个独立的字符?分2步进行

  • 第一步:将数字125分解为3个数字1,2,5
125 -> 0111 1101
  1 -> 0000 0001
  2 -> 0000 0010
  5 -> 0000 0101
  • 第二步:将数字1、2和5分别转换为对应的数字字符
    • 数字0到9对应的数字字符编码为0x30到0x39。因此,只需要将数字加上0x30得到了 它对应的数字字符编码。
    • 比如数字5的二进制是0000 0101,加上0x30后的结果为0011 0101,这就是数字字符5的编码。

第一步:将数字125分解为3个数字1,2,5

传统上,要分解数字的每一个数位,要除以10,取余。

范例:125分解

125 / 10 = 商12  余 5
12 / 10  = 商1 余2
1 / 10 = 商0 余1

无符号数除法指令div

了解无符号整数除法指令div

div指令只能用于无符号整数的操作。div指令只需要一个操作数,即除数所在寄存器或者内存地址。 被除数在哪里要由除数的长度决定。

格式

div 除数所在的寄存器或者内存地址

除数长度是8位

  • 如果在指令中指定的是8位寄存器或者8位操作数的内存地址,则意味着被除数在寄存器AX里。
  • 相除后,商在寄存器AL里,余数在寄存器AH里。

范例:div 除数长度为8位

div bh
#bh是8位的寄存器,存放的是除数,那么被除数存放在AX里
#AX中内容除以BH中内容,相除后,商在AL中,余数在AH中

div byte [0x2002]
#除数在内存中,从内存地址0x2002处取1个字节
#AX中内容除以0x2002地址的8位除数,相除后,商在AL中,余数在AH中

除数长度是16位

  • 如果在指令中指定的是16位寄存器或者16位操作数的内存地址,则意味着被除数是32位的,低 16位在寄存器AX里;高16位在寄存器DX里
  • 相除后,商在寄存器AX里,余数在寄存器DX里。
img_20241124_131246.png

范例:div 除数长度为16位

div bx
#bx是16位的寄存器,存放的是除数,那么被除数是32位的,低16位在寄存器AX里;高16位在寄存器DX里
#用DX和AX组合成32位的被除数除以BX中内容,相除后,商在AX中,余数在DX中

div word [0x2002]
#除数在内存中,从内存地址0x2002处取1个字
#用DX和AX组合成32位的被除数除以0x2002地址的16位除数,相除后,商在AX中,余数在DX中

除数长度是32位

8086不支持的,它是16位处理器,从80386开始支持

  • 如果在指令中指定的是32位寄存器或者32位操作数的内存地址,则意味着被除数是64位的,低 32位在寄存器EAX里;高32位在寄存器EDX里
  • 相除后,商在寄存器EAX里,余数在寄存器EDX里。

范例:div 除数长度为32位

div ebx
#ebx是32位的寄存器,存放的是除数,那么被除数是64位的,低32位在寄存器EAX里;高32位在寄存器EDX里
#用EDX和EAX组合成64位的被除数除以有EBX中内容,相除后,商在EAX中,余数在EDX中

div dword [0x2002]
#除数在内存中,从内存地址0x2002处取4个字节组合出32位操作数
#用EDX和EAX组合成64位的被除数除以0x2002地址的32位除数,相除后,商在EAX中,余数在EDX中

除数长度是64位

8086和32位处理器不支持,只有64位处理器支持

  • 如果在指令中指定的是64位寄存器或者64位操作数的内存地址,则意味着被除数是128位的,低 64位在寄存器RAX里;高64位在寄存器RDX里
  • 相除后,商在寄存器RAX里,余数在寄存器RDX里。

范例:div 除数长度为64位

div rbx
#rbx是64位的寄存器,存放的是除数,那么被除数是128位的,低64位在寄存器RAX里;高64位在寄存器RDX里
#用RDX和RAX组合成128位的被除数除以有RBX中内容,相除后,商在RAX中,余数在RDX中

div qword [0x2002]
#除数在内存中,从内存地址0x2002处取8个字节组合出64位操作数
#用RDX和RAX组合成128位的被除数除以0x2002地址的64位除数,相除后,商在RAX中,余数在RDX中

在调试器里验证除法操作

通过在调试器里跟踪除法指令的执行,来加深对这条指令的认识

;计算378除以37的结果
mov ax, 378  ;378传送到ax,被除数
mov bl, 37  ;37传送到bl,除数
div bl     ;AL=商(10), AH=余数(8)

将这个3行代码放在调试器中运行。为此把代码放到主引导扇区里,做成主引导扇区程序,再写入到虚拟硬盘文件中,通过bochs来调试。

exam.asm

start:
        ;计算378除以37的结果
        mov ax, 378
        mov bl, 37
        div bl     ;AL=商(10), AH=余数(8)

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563  #16进制的17a等于10进制的378
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x017a            ; b87a01

#单步执行
<bochs:3> s
Next at t=17175564
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov bl, 0x25              ; b325
#查看mov ax, 0x017a执行后,寄存器内容
<bochs:4> r
rax: 00000000_0000017a #rax的低16位ax内容为017a,10进制的378
rbx: 00000000_00000000
.....
rip: 00000000_00007c03

#查看mov bl, 0x25执行后,寄存器内容
<bochs:5> s
Next at t=17175565
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): div al, bl                ; f6f3
<bochs:6> r
rax: 00000000_0000017a
rbx: 00000000_00000025 ;rbx的低8位bl内容为25,即10进制37
......
rip: 00000000_00007c05

#查看div al, bl执行后,寄存器内容。这是bochs虚拟机自己定义的格式,实际上是div bl
<bochs:7> s
Next at t=17175566
(0) [0x000000007c07] 0000:7c07 (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000
<bochs:8> r
rax: 00000000_0000080a  #378除以37,商在al中,rax的低8位0a,10进制的10; 余数在ah中,rax的高8位08,10进制的8
rbx: 00000000_00000025
......
rip: 00000000_00007c07

异或指令xor的用法

;计算65535除以10的结果
mov ax, 65535
mov bl, 10
div bl     ;AL=商(6553), AH=余数(5)

AL是8位的不能存6553这么大的数字。改用16位除数。

start:
        ;计算65535除以10的结果
        mov ax, 65535
        mov dx, 0
        mov bx, 10
        div bx     ;AX=商(6553), DX=余数(5)

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

上面 mov dx, 0 也可以改成 xor dx, dx, 执行速度比 mov dx, 0要快

异或=XOR=eXclusive OR

img_20241124_154319.png
Figure 19: 异或电路
异或电路有2个输入1个输出。当输入相同时,输出为0;当输入不同时,输出为1

0 ^ 0 = 0
1 ^ 1 = 0
0 ^ 1 = 1
1 ^ 0 = 1

#异或操作可在2个数之间进行
129 ^ 127 = ?
1000 0001 
0111 1111 ^
---------
1111 1110  254

异或指令格式

xor r/m, r/m/imm

#说明
r 寄存器
m 内存地址
imm 立即数

异或的结果保存在左操作数中。

注意:2个操作数所指定的数据长度必须相同

范例:xor异或

xor bh, al  #bh的值和al的值做异或操作,结果在bh中

xor cx, dx #cx的值和dx的值做异或操作,结果在cx中

xor ax, 3 #ax的值和立即数3做异或操作,结果在ax中. 因为ax是16位寄存器,
#所以为了保持一致,立即数3被当成16位的数字参与运算

xor word [0x2002], 67 #把从内存地址2002开始的一个字和立即数67做异或操作,结果返回到原来的内存地址保存。
#因为内存操作数是用word来修饰的,因此操作数的长度是16位的字,为了保持一致,立即数
#被当成16位的数字参与运算

xor si, [0x2002] #把si和内存里的数字做异或运算,结果在si中。
#因为si的长度是16位的,所以必须从内存地址2002处读取一个16位的字参与运算。 

xor dx, dx 两个操作数都一样做异或,生成一个所有比特为0的结果,覆盖寄存器dx中原有的内容。2个操作数都是寄存器,执行起来比较快。

exam.asm 可修改为

start:
        ;计算65535除以10的结果
        mov ax, 65535
        xor dx, dx   ;等价于mov dx, 0, 执行速度更快
        mov bx, 10
        div bx     ;AX=商(6553), DX=余数(5)

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

加法指令add的用法

因为工作需要引入加法指令add并介绍它的功能和用法

在屏幕上显示数字65535,就必须把它的每一个数位分解出来。上节已经用div分解出第一个 数位5。 这个数位保存在寄存器DX中,只需要取DX的低8位DL。要在屏幕上显示这个数位,就要加上0x30就可以转换为数字字符编码。

DX
00000000 00000101
高8位DH   低8位DL

exam.asm可修改为

start:
        ;在屏幕上显示数字65535
        mov ax, 65535
        xor dx, dx          ;等价于mov dx, 0
        mov bx, 10
        div bx              ;AX=商(6553), DX=余数(5)

        add dl, 0x30        ;将数字转换为对应的数字字符

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

add指令介绍

add格式

add r/m, r/m/imm

#说明
r 寄存器
m 内存地址
imm 立即数

结果保存在左操作数中

说明:2个操作数所指的数据长度必须相同,操作数不能同时为内存地址

范例:add指令

add bh, al  #bh的值和al的值相加,结果在bh中

add cx, dx #cx的值和dx的值相加,结果在cx中

add ax, 3 #ax的值和立即数3相加,结果在ax中. 因为ax是16位寄存器,
#所以为了保持一致,立即数3被当成16位的数字参与运算

add word [0x2002], 67 #把从内存地址2002开始的一个字和立即数67相加,结果返回到原来的内存地址保存。
#因为内存操作数是用word来修饰的,因此操作数的长度是16位的字,为了保持一致,立即数
#被当成16位的数字参与运算

add si, [0x2002] #把si和内存里的数字相加,结果在si中。
#因为si的长度是16位的,所以必须从内存地址2002处读取一个16位的字参与运算。 

使用标号访问内存数据

如何在程序中保留内存空间,并使用标号访问这些内存空间

每分解一位就显示的话,在屏幕上显示是反的。可以把每次分解的数位转换成字符编码后,临时存在在别的地方。 全部转换完成后,再按正解的顺序加以显示。

保存到哪?8086的寄存器肯定不是够用的,我们已经占用了bx, dx,ax,剩下的是si,di,dp,sp,但剩下的都是16位的而且只能做为16位来用,不用做为8位。 所以用来保存8位的字符编码并不是很方便。

img_20241124_180758.png

考虑到内存空间很大,可以将这些数位保存到内存里。可以伪指令db来开辟内存空间 buffer db 0, 0, 0, 0, 0 。使用标号来找到它们。 我们知道标号代表汇编地址,这标号buffer仅仅代表第一个字节的汇编地址,即第1个0的汇编地址。

在访问内存时,标号可以用来计算程序运算时的偏移地址。db 0,0, 0,0,0 中第1个0,它的段地址是0,段偏移地址是 0x7c00+buffer。

exam.asm

start:
        ;在屏幕上显示数字65535
        mov ax, 65535
        xor dx, dx          ;等价于mov dx, 0
        mov bx, 10
        div bx              ;AX=商(6553), DX=余数(5)

        add dl, 0x30        ;将数字转换为对应的数字字符

        ;指定数据段寄存ds内容, 0
        mov cx, 0
        mov ds, cx

        mov [0x7c00+buffer], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第1个0处

        ;分解第2个数位,将数位对应字符存放在 db 0,0,0,0,0 的第2个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+1], dl ;将dl中的字符编码0x33传送到段内这个偏移地址处。即第2个0处

        ;分解第3个数位,将数位对应字符存放在 db 0,0,0,0,0 的第3个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+2], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第3个0处

        ;分解第4个数位,将数位对应字符存放在 db 0,0,0,0,0 的第4个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+3], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第4个0处

        ;分解最后一个数位,将数位对应字符存放在 db 0,0,0,0,0 的第5个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+4], dl ;将dl中的字符编码0x36传送到段内这个偏移地址处。即第5个0处

buffer  db 0, 0, 0, 0,0     ;开辟5个字节空间

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

段超越前缀的使用

如何使用段超越前缀在两个数据段之间协作工作

上一节中,我们已经完成了数位的分解并将数字字符存放在内存中。这一节我们将数字字符写入显存,然后就能在显示器上看到了。

因为intel处理器不允许指定的2个操作数都是内存地址,所以要使用寄存器中转。

取字符,传送第1个字符

  • 首先把最后一次分解出来的字符,临时保存到寄存器al中。 mov al, [0x7c00+buffer+4]
  • 将寄存器的字符编码传送到显存内偏移地址为0的地方。显存的访问也是按照段地址加偏移地址进行
    • 显存的段地址是B800。在访问显存前,必须把B800传送到数据段寄存器DS中,传送段地址也必须要用通用寄存器中转,不能直接传送。 mov cx, 0xb800 , mov ds, cx
  • 最后,将寄存器AL中的字符编码写入显存内偏移地址为0的地方。 mov [0x00], al
    • 同时再写入一个颜色属性, mov byte [0x01], 0x2f 绿底白字

取下一个字符时

  • 将数据段寄存器变回来 mov cx, 0 , mov ds, cx ,才能取到第2个字符
  • 取第2个字符 mov al, [0x7c00+buffer+3]
  • 指向显存的段地址 mov cx, 0xb800 , mov ds, cx
  • 存放 mov [0x02], al , mov byte [0x03], 0x2f

以此类推,exam.asm修改如下

start:
        ;在屏幕上显示数字65535
        mov ax, 65535
        xor dx, dx          ;等价于mov dx, 0
        mov bx, 10
        div bx              ;AX=商(6553), DX=余数(5)

        add dl, 0x30        ;将数字转换为对应的数字字符

        ;指定数据段寄存ds内容, 0
        mov cx, 0
        mov ds, cx

        mov [0x7c00+buffer], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第1个0处

        ;分解第2个数位,将数位对应字符存放在 db 0,0,0,0,0 的第2个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+1], dl ;将dl中的字符编码0x33传送到段内这个偏移地址处。即第2个0处

        ;分解第3个数位,将数位对应字符存放在 db 0,0,0,0,0 的第3个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+2], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第3个0处

        ;分解第4个数位,将数位对应字符存放在 db 0,0,0,0,0 的第4个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+3], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第4个0处

        ;分解最后一个数位,将数位对应字符存放在 db 0,0,0,0,0 的第5个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+4], dl ;将dl中的字符编码0x36传送到段内这个偏移地址处。即第5个0处

        ;分别将字符传送到显存中
        ;第1个字符
        mov al, [0x7c00+buffer+4]

        mov cx, 0xb800
        mov ds, cx

        mov [0x00], al
        mov byte [0x01], 0x2f

        ;第2个字符
        mov cx, 0
        mov ds, cx

        mov al, [0x7c00+buffer+3]

        mov cx, 0xb800
        mov ds, cx

        mov [0x02], al
        mov byte [0x03], 0x2f

        ; 一直到第5个字符....

buffer  db 0, 0, 0, 0,0     ;开辟5个字节空间

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

将数据段寄存器DS的值变来变去,很麻烦。为了在2个段之间进行操作,8086处理还多准备了一个数据段寄存器ES,附加段寄存器。

  • 为了取数字字符可以使用段寄存器DS
  • 为了操作显存可以使用附加段寄存器ES

exam.asm修改如下

start:
        ;在屏幕上显示数字65535
        mov ax, 65535
        xor dx, dx          ;等价于mov dx, 0
        mov bx, 10
        div bx              ;AX=商(6553), DX=余数(5)

        add dl, 0x30        ;将数字转换为对应的数字字符

        ;指定数据段寄存ds内容, 0
        mov cx, 0
        mov ds, cx

        mov [0x7c00+buffer], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第1个0处

        ;分解第2个数位,将数位对应字符存放在 db 0,0,0,0,0 的第2个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+1], dl ;将dl中的字符编码0x33传送到段内这个偏移地址处。即第2个0处

        ;分解第3个数位,将数位对应字符存放在 db 0,0,0,0,0 的第3个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+2], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第3个0处

        ;分解第4个数位,将数位对应字符存放在 db 0,0,0,0,0 的第4个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+3], dl ;将dl中的字符编码0x35传送到段内这个偏移地址处。即第4个0处

        ;分解最后一个数位,将数位对应字符存放在 db 0,0,0,0,0 的第5个字节里
        xor dx, dx          ;dx清零,因为它是被除数的高16位
        div bx
        add dl, 0x30        ;将数字转换为对应的数字字符
        mov [0x7c00+buffer+4], dl ;将dl中的字符编码0x36传送到段内这个偏移地址处。即第5个0处


        ;分别将字符传送到显存中
        mov cx, 0xb800
        mov es, cx ;将显存段地址b800传送到附加段寄存器ES中

        ;第1个字符
        mov al, [0x7c00+buffer+4]   ;如果没有特别指名,默认使用段寄存器DS
        mov [es:0x00], al           ;使用段寄存器es来填充显存
        mov byte [es:0x01], 0x2f    ;写入颜色属性

        ;第2~5个字符
        mov al, [0x7c00+buffer+3]
        mov [es:0x02], al
        mov byte [es:0x03], 0x2f

        mov al, [0x7c00+buffer+2]
        mov [es:0x04], al
        mov byte [es:0x05], 0x2f

        mov al, [0x7c00+buffer+1]
        mov [es:0x06], al
        mov byte [es:0x07], 0x2f

        mov al, [0x7c00+buffer]
        mov [es:0x08], al
        mov byte [es:0x09], 0x2f

again:
        jmp again

buffer  db 0, 0, 0, 0,0     ;开辟5个字节空间

current: 
        times 510-(current-start) db 0

        db 0x55, 0xaa

[段寄存器:偏移地址] 叫做段超越前缀。如果没有使用段超越前缀,默认是在ds中。

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563  #16进制的17a等于10进制的378
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0xffff            ; b8ffff

#反汇编查看一下
<bochs:3> u /32
0000000000007c00: (                    ): mov ax, 0xffff            ; b8ffff
0000000000007c03: (                    ): xor dx, dx                ; 31d2
0000000000007c05: (                    ): mov bx, 0x000a            ; bb0a00
0000000000007c08: (                    ): div ax, bx                ; f7f3
......

#
<bochs:4> s
Next at t=17175564
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): xor dx, dx                ; 31d2
<bochs:5> s
Next at t=17175565
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): mov bx, 0x000a            ; bb0a00
<bochs:6> s
Next at t=17175566
(0) [0x000000007c08] 0000:7c08 (unk. ctxt): div ax, bx                ; f7f3
<bochs:6> c
img_20241124_202546.png

显示标号的汇编地址

其他例子

       ;代码清单5-1 
       ;文件名:c05_mbr.asm
       ;文件说明:硬盘主引导扇区代码

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

       ;以下显示字符串"Label offset:"
       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

       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 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

 infi: jmp near infi                 ;无限循环

number db 0,0,0,0,0

times 203 db 0
          db 0x55,0xaa

阶段性重点内容总结

标号

标号可以由以下字符组成
字母  数字  _ $ # @ ~ . ?

其中,可以做打头字符的是
字母 . _ ?

标号后面可以后一个冒号,但是它不是标号的一部分

标号的作用是定伪指令或数据。在编写程序时,标号代表指令或者数据的汇编地址。

在程序运行时,处理器使用段地址和段内偏移地址。程序在运行前要先加载到某个内存段,如果是从段的起始处加载, 那么汇编地址等于偏移地址。

数据长度

  • 在需要两个操作数的指令中,如果至少有一个是寄存器,则不需要长度修饰符。如
mov ah, al
mov [buffer], ax
xor byte [buffer], 0x55
  • 如果只有一个操作数且不是寄存器,必须使用长度修饰符。如
div word [divisor]

伪指令定义数据

  • 伪指令db用来定义字节(8位)长度的数据。如db 0x55
  • 伪指令dw用来定义字(16位)长度的数据。如db 0x55aa
  • 伪指令dd用来定义双字(32位)长度的数据。如db 0xabcd1234
  • 伪指令dq用来定义四字(64位)长度的数据。如db 0x12345678aabbccdd
  • 伪指令times用来重复后面的指令若干次。如
times 5 mov ax, bx

等价于
mov ax, bx
mov ax, bx
mov ax, bx
mov ax, bx
mov ax, bx

循环批量传送和条件转移

主要内容

  • 跳过非指令的数据区
  • 逻辑段地址的重新设定
  • 串传送指令和标志寄存器
  • NASM的$和$$记号
  • 使用循环指令LOOP分解数位
  • 基址寻址和INC指令
  • 数字的显示和DEC指令
  • 基址变址寻址和条件转移指令

跳过非指令的数据区

当数据和指令混合时,如何避免执行到这些指令的数据。

mov byte [0x00], 'L'
mov byte [0x01], 0x2f

字符和指令绑定在一起很原始也很笨拙,如果要显示的内容很多,不但需要大量的指令而且修改也很麻烦。

这里将文本内容和显示他们的指令分开。需要专门定义一个存放字符串的数据区,当我们要显示它们时再用指令取出。

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

字符的字面形式和显示的颜色属性。其中 \ 为续行符,表明下一行与当前行是一行。也可以这样写

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

start:
      xx指令 

逻辑段地址的重新设定

重新设定数据段寄存器的逻辑地址以方便通过标号访问数据

为了简化后面程序的设计,

exam.asm

        jmp start

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

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

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

为什么将7c0传送到数据段寄存器ds?段地址不是0吗,怎么是7c0呢?

因为是主引导扇区程序,加载的逻辑段地址是0,没有从段内偏移地址为0的位置加载,而是从段内偏移地址7c00开始加载,mytext的偏移地址是7c00+mytext

img_20241126_211156.png

0000:7c00的物理地址是7c00,我们也可以看成另外一个段07c0:0000,在访问标号mytext时就不用加上7c00了。标号的汇编地址就是段内偏移地址。

串传送指令和标志寄存器

学习串传送指令MOVSB和MOVSW的用法,并了解8086的标志寄存器FLAGS

exam.asm

        jmp start

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

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

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

        cld
        mov si,mytext
        mov di,0
        mov cx,(start-mytext)/2     ;实际上等于 13
        rep movsw

数据串的传送指令,用于把一串或者一批数据从内存中一个地址批量地复制到内存中另一个地方。在处理器看来被传送的内容就是数据串。如mytext标志的这一行指令

8086处理器支持的两种串传送指令

  • movsb 按字节传送 s表示string, b表示byte
  • movsw 按字传送. s表示string, w表示word

传送前准备工作

串指令传送前的准备工作(8086处理器)

DS:SI  原始数据串的段地址: 偏移地址

ES:DI 目标位置的段地址: 偏移地址
mov si,mytext       ;将文本的起始偏移地址放到SI
mov di,0               ;将0传送给DI. 这意味着文本的传送是从显存的起始位置开始的
mov cx,(start-mytext)/2     ;实际上等于 13
rep movsw

设置传送的方向

正向传送是原始位置和目标位置同时从内存低地址向高地址推进。反向传送正好相反。这个方向还影响SI和DI的内容,这样做是为了自动指向下一个要传送的位置方便传送。

  • 正向传送时,movsb, movsw每执行一次SI和DI的值自动加上传送的字节数
  • 反向传送时,movsb, movsw每执行一次SI和DI的值自动减去传送的字节数

8086的标志寄存器FLAGS

这是一个16位的寄存器,每位用做一个有特定含意的标志

15 14 ..  10 ... 6 ... 0
          DF     ZF

位6:零标志(zero flag) 当处理器执行一条算数或逻辑运算执行后,如果计算结果为0,位6设置为1
位10:方向标志(direction flag) 控制内存操作的方向,0为正方向
cld  ;方向标志清零指令,将FLAGE寄存器的DF标志清零,0为正方向。与其相反的是STD置方向指令,将FLAGE寄存器DF标志置为1,反方向
mov cx,(start-mytext)/2     ;实际上等于 13
rep movsw

movsw, movsb指令只能执行一次,重复执行需要使用rep指令。rep指令重复的次数由寄存器cx指定,为0时结束。

NASM的$和$$记号

通过阶段性的运行和调试来深入观察批量传送指令的执行特点,并引入NASM编译器提供的$和$$记号

完美程序,作为主引导扇区程序。

exam.asm

        jmp start

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

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

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

        cld
        mov si,mytext               ;将文本的起始偏移地址放到SI
        mov di,0                    ;将0传送给DI. 这意味着文本的传送是从显存的起始位置开始的
        mov cx,(start-mytext)/2     ;实际上等于 13
        rep movsw                   ;重新执行movsw, 寄存器cx为执行次数

        jmp $

        times 510-($-$$) db 0
        db 0x55, 0xaa
$ 代表当前行的汇编地址

$$ 表示当前程序段的汇编地址

这里整个程序是一个自然的段,$$代表这个程序的起始汇编地址,这程序的段的起始汇编地址是0

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmp .+29  (0x00007c1f)    ; eb1d

<bochs:3> s
Next at t=17175564
(0) [0x000000007c1f] 0000:7c1f (unk. ctxt): mov ax, 0x07c0            ; b8c007
<bochs:4> s
Next at t=17175565
(0) [0x000000007c22] 0000:7c22 (unk. ctxt): mov ds, ax                ; 8ed8
<bochs:5> s
Next at t=17175566
(0) [0x000000007c24] 0000:7c24 (unk. ctxt): mov ax, 0xb800            ; b800b8
<bochs:6> s
Next at t=17175567
(0) [0x000000007c27] 0000:7c27 (unk. ctxt): mov es, ax                ; 8ec0
<bochs:7>
Next at t=17175568
(0) [0x000000007c29] 0000:7c29 (unk. ctxt): cld                       ; fc

#查看cld执行前标志寄存器的状态
<bochs:8> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf
#各个标志用名字做显示的,小写的说明该标志是0,大写是1

#查看cld执行后标志寄存器的状态,zf是0没错
<bochs:9> s
Next at t=17175569
(0) [0x000000007c2a] 0000:7c2a (unk. ctxt): mov si, 0x0002            ; be0200
<bochs:10> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf


<bochs:11> r
rax: 00000000_0000b800
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_00007c2a
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf
<bochs:12> s
Next at t=17175570
(0) [0x000000007c2d] 0000:7c2d (unk. ctxt): mov di, 0x0000            ; bf0000
<bochs:13> r
rax: 00000000_0000b800
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0002
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_00007c2d
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

#观察si di内容
<bochs:14> s
Next at t=17175571
(0) [0x000000007c30] 0000:7c30 (unk. ctxt): mov cx, 0x000e            ; b90e00
<bochs:15> s
Next at t=17175572
(0) [0x000000007c33] 0000:7c33 (unk. ctxt): rep movsw word ptr es:[di], word ptr ds:[si] ; f3a5
<bochs:16> s
Next at t=17175573
(0) [0x000000007c33] 0000:7c33 (unk. ctxt): rep movsw word ptr es:[di], word ptr ds:[si] ; f3a5
<bochs:17> r
rax: 00000000_0000b800
rbx: 00000000_00000000
rcx: 00000000_0009000c #cx内容在变
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0004  #自动变为0004
rdi: 00000000_00000002  #自动变为0002
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_00007c33
eflags: 0x00010082: id vip vif ac vm RF nt IOPL=0 of df if tf SF zf af pf cf

使用循环指令LOOP分解数位

exam.asm

        jmp start

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

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

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

        cld
        mov si,mytext               ;将文本的起始偏移地址放到SI
        mov di,0                    ;将0传送给DI. 这意味着文本的传送是从显存的起始位置开始的
        mov cx,(start-mytext)/2     ;实际上等于 13
        rep movsw                   ;重新执行movsw, 寄存器cx为执行次数

        ;得到标号所代表的汇编地址
        mov ax, number              ;标号和数字是等效的,程序编译后标号会转换为数字。

        ;分解各个数位
        mov bx,ax
        mov cx,5                    ;循环次数
        mov si,10                   ;除数
digit:
        xor dx,dx                   ;此时dx和ax一起形成32位被除数
        div si                      ;商在ax,余数在dx
        mov [bx],dl                 ;保存数位
        inc bx
        loop digit

        jmp $

number  db 0, 0, 0, 0, 0

        times 510-($-$$) db 0
        db 0x55, 0xaa

循环是由循环指令loop来完成的。

loop 标号

标号为跳转目标位置

loop指令的机器码:  E2 8位相对偏移量

8080上,LOOP指令的执行过程

  • 将寄存器CX的内容减1
  • 如果CX内容不为零,转移到指定位置处执行,否则顺序执行后面的指令。

基址寻址和INC指令

使用基址寄存器BX访问内存的方法,以及INC指令的用法

        mov [bx],dl                 ;保存数位
; 将bx内容取出,当作偏移地址。 这个偏移地址是number标号的汇编地址
  • 在8086处理器上,如果要用寄存器来提供偏移地址,只能使用BX, SI, DI, BP,不能使用其它寄存器。
  • 寄存器BX在设计之初的作用之一就是用来提供数据访问的基地址,所以又叫基址寄存器(Base Address Register)
  • 在设计8086处理器时,每个寄存器都有自己的特殊用途,比如
    • AX是累加器(Accumulator),与它有关的指令还会做指令长度的优化(较短);
    • CX是计数器(Counter);
    • DX是数据(Data)寄存器,除了作为通用寄存器使用外,还专门用于和外设之间进行数据传送;
    • SI是源索引寄存器(Source Index);
    • DI是目标索引寄存器(Destination Index),用于数据传送操作

inc 指令将指定内容加1

inc  r/m

inc al
inc di
inc byte [0x2002]

数字的显示和DEC指令

显示分解出来的数位,在这个过程中认识DEC指令

在完成了数位分解工作后,接下来的工作是将它们传送给显示器。

传送可以用LOOP指令构造一个循环执行

exam.asm修改如下

                jmp start

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

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

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

        cld
        mov si,mytext               ;将文本的起始偏移地址放到SI
        mov di,0                    ;将0传送给DI. 这意味着文本的传送是从显存的起始位置开始的
        mov cx,(start-mytext)/2     ;实际上等于 13
        rep movsw                   ;重新执行movsw, 寄存器cx为执行次数

        ;得到标号所代表的汇编地址
        mov ax, number              ;标号和数字是等效的,程序编译后标号会转换为数字。

        ;分解各个数位
        mov bx,ax
        mov cx,5                    ;循环次数
        mov si,10                   ;除数
digit:
        xor dx,dx                   ;此时dx和ax一起形成32位被除数
        div si                      ;商在ax,余数在dx
        mov [bx],dl                 ;保存数位
        inc bx
        loop digit

        ;开始显示各个数位
        mov cx,5
show:
        dec bx                      ;bx内容减1,bx保存了指向number标号的位置,上次数位分解结束后指向number最后一个位置
        mov al,[bx]
        add al,0x30
        mov ah,04
        mov [es:di],ax
        add di,2                    ;指向显存下一个位置
        loop show

        jmp $

number  db 0, 0, 0, 0, 0

        times 510-($-$$) db 0
        db 0x55, 0xaa

DEC指令格式

减1指令
dec r/m

dec al
dec di
dec byte [0x2002]
img_20241127_225519.png

基址变址寻址和条件转移指令

换种方法显示分解出来的数位,在这个过程中认识基址变址寻址和条件转移指令

bx的值并没有沿用前面代码所遗留的值,而是重新设置的。一共有5个数字要显示, 它们在当前数据段内起始偏移地址就是标号number的汇编地址

exam.asm

        jmp start

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

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

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

        cld
        mov si,mytext               ;将文本的起始偏移地址放到SI
        mov di,0                    ;将0传送给DI. 这意味着文本的传送是从显存的起始位置开始的
        mov cx,(start-mytext)/2     ;实际上等于 13
        rep movsw                   ;重新执行movsw, 寄存器cx为执行次数

        ;得到标号所代表的汇编地址
        mov ax, number              ;标号和数字是等效的,程序编译后标号会转换为数字。

        ;分解各个数位
        mov bx,ax
        mov cx,5                    ;循环次数
        mov si,10                   ;除数
digit:
        xor dx,dx                   ;此时dx和ax一起形成32位被除数
        div si                      ;商在ax,余数在dx
        mov [bx],dl                 ;保存数位
        inc bx
        loop digit

        ;开始显示各个数位
        mov bx,number
        mov si,4
show:
        mov al,[bx+si]              ;si的值相当于索引
        add al,0x30
        mov ah,04
        mov [es:di],ax              ;ax内容为字符编码和颜色属性
        add di,2                    ;指向显存下一个位置
        dec si
        jns show                    ;跳转指令,如果标志寄存器的位7的SF不为1就一直跳转。si显示完最后一个数位后,最高位为1

        jmp $

number  db 0, 0, 0, 0, 0

        times 510-($-$$) db 0
        db 0x55, 0xaa

在8086处理器上,只允许以下几种基址变址的组合

bx + si
bx + di
bx + si
bx +di

非法的例子
bx + ax
ax + cx

8086的标志寄存器FLAGS

这是一个16位的寄存器,每位用做一个有特定含意的标志

15 14 ..  10 ... 6 ... 0
          DF     ZF

位6:零标志(zero flag) 0 时ZF,1时zf。当处理器执行一条算数或逻辑运算执行后,如果计算结果为0,位6设置为1
位7:符号标志Sign Flag  SF。在执行算数逻辑运算后,如果结果的最高位是0,位7置为0
位10:方向标志(direction flag) DF控制内存操作的方向,0为正方向

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmp .+26  (0x00007c1c)    ; eb1a

#观察,跳转在持续进行
<bochs:4> u /52
0000000000007c00: (                    ): jmp .+26  (0x00007c1c)    ; eb1a
......
0000000000007c58: (                    ): dec si                    ; 4e
0000000000007c59: (                    ): jns .-15  (0x00007c4c)    ; 79f1
0000000000007c5b: (                    ): jmp .-2  (0x00007c5b)     ; ebfe
0000000000007c5d: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c5f: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c61: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c63: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c65: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c67: (                    ): add byte ptr ds:[bx+si], al ; 0000
<bochs:5> b 0x7c58

<bochs:6> c
(0) Breakpoint 2, 0x0000000000007c58 in ?? ()
Next at t=17175621
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): dec si                    ; 4e
<bochs:7> c
(0) Breakpoint 2, 0x0000000000007c58 in ?? ()
Next at t=17175628
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): dec si                    ; 4e
<bochs:8> c
(0) Breakpoint 2, 0x0000000000007c58 in ?? ()
Next at t=17175635
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): dec si                    ; 4e
<bochs:9> c
(0) Breakpoint 2, 0x0000000000007c58 in ?? ()
Next at t=17175642
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): dec si                    ; 4e
<bochs:10> c
(0) Breakpoint 2, 0x0000000000007c58 in ?? ()
Next at t=17175649
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): dec si                    ; 4e

#观察符号标志SF
<bochs:11> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf
<bochs:12> s
Next at t=17175650
(0) [0x000000007c59] 0000:7c59 (unk. ctxt): jns .-15  (0x00007c4c)    ; 79f1
<bochs:13> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf SF zf AF PF cf

<bochs:14> r
rax: 00000000_00000433
rbx: 00000000_0000005d
rcx: 00000000_00090000
rdx: 00000000_00000000
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000effff  #si 最高位是1
rdi: 00000000_00000024
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_00007c59
eflags: 0x00000096: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf AF PF cf

<bochs:15> s
Next at t=17175651
(0) [0x000000007c5b] 0000:7c5b (unk. ctxt): jmp .-2  (0x00007c5b)     ; ebfe
<bochs:16> s
Next at t=17175652
(0) [0x000000007c5b] 0000:7c5b (unk. ctxt): jmp .-2  (0x00007c5b)     ; ebfe
<bochs:17> s
Next at t=17175653
(0) [0x000000007c5b] 0000:7c5b (unk. ctxt): jmp .-2  (0x00007c5b)     ; ebfe
<bochs:18>

计算机中的负数

主要内容

  • 无符号数和有符号数
  • 减法指令SUB和求补指令NEG
  • 计算机如何区分对待无符号数和有符号数
  • 有符号数除法指令IDIV
  • 有符号数的符号扩展指令

无符号数和有符号数

用汇编语言表示负数

        jmp start
data1   db -1
data2   dw -25 ;用伪指令dw在程序中开辟一个字的空间,并保存-25
start:
        mov ax,-78 ;将-78传送到寄存器ax

在程序编译时,负数会被自动转换成2进制形式。

在计算机里,整数分成2种,无符号数和有符号数。

无符号数

在无符号的2进制形式中没有符号位,所有比特都用来表示数字的大小。

范例:一个字节所能表示的无符号数

1111 1111 255 (0xFF)
1111 1110 254 (0xFE)
1111 1101 253 (0xFD)
......
0000 0010 2 (0x02)
0000 0001 1 (0x01)
0000 0000 0 (0x00)

无符号2进制数转10进制

从右向左,每个2进制数位都有一个序号,序号从右往左依次增大。每个数位都有一个加权值。

范例: 1111 1101

最左面最高位的加权值为数位本身1乘以以2为底以它的序号为指数的幂。

\(1 \times 2^7 + 1 \times 2^6 + 1 \times 2^5 + 1 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = 253\)

有符号数

和无符号数不同,有符号数是分正负的。它最左边的那一位即最高位,不但要表示数的大小,还要 表示数的正负。最高位是0,这个数是正数;如果最高位是1,这个数是负数。

范例:一个字节所能表示的有符号数

0111 1111  +127  (0x7F)
......
0000 0010  +2  (0x02)
0000 0001  +1  (0x01)
0000 0000  0  (0x00)
1111 1111  -1  (0xFF)
1111 1110  -2  (0xFE)
......
1000 0000  -128  (0x80)

范例:一个字所能表示的有符号数

0111 1111 1111 1111  +32767  (0x7FFF)
......
0000 0000 0000 0010  +2  (0x0002)
0000 0000 0000 0001  +1  (0x0001)
0000 0000 0000 0000  0  (0x0000)
1111 1111 1111 1111  -1  (0xFFFF)
1111 1111 1111 1110  -2  (0xFFFE)
......
1000 0000 0000 0000  -32767  (0x8000)

有符号2进制数转10进制

从右向左,每个2进制数位都有一个序号,序号从右往左依次增大。每个数位都有一个加权值。但是,符号位加权值比较特殊,必须添加一个负号

范例: 1111 1101

最左面符号位的加权值为数位本身1乘以以2为底以它的序号为指数的幂,前面加一个负号

\(-(1 \times 2^7) + 1 \times 2^6 + 1 \times 2^5 + 1 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = -3\)

范例:1111 1111 1111 1110

\(-(1 \times 2^{15}) + 1 \times 2^{14} + 1 \times 2^{13} + ...... + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = -2\)

减法指令SUB和求补指令NEG

了解计算机内部如何得到一个正数的负值,在这个过程中认识SUB和NEG指令

对于计算机来说生成和表示一个负数很简单

做2进制减法

-1 等于 0 - 1

           0
        -  1
------------借位
....11111111


-2 等于 0 - 2
           0
        - 10
------------借位
....11111110  

二进制0减去二进制1 。需要不停的借位。因为寄存器和内存空间是8位、16位、32位、64位,相减的结果取8位就是1111 1111, FF,取16位是1111 1111 1111 1111 ,FFFF。

减法指令SUB

mov ax, 0
sub ax,dx

格式

sub   r/m,  r/m/imm

sub al,35
sub dx,ax
sub dx, [0x2002]
sub byte [0x2002],37
sub byte [0x2002],al

将左操作数和右操作数相减,结果在左操作数

注意:2个操作数宽度要一致,而且不能同时是内存地址

NEG指令

mov ax, 0
sub ax,dx
mov dx,ax  ;相减的结果再返回到dx中

上面3条指令可以用一条完成

neg dx ;求负数指令或对面的补码指令

格式

net r/m

neg al 
neg di
neg byte [0x2002]

用0减去指令中的操作数,然后用相减的结果替换操作数中的内容

neg al
如al 内容为 0000 1000 (8), 指令执行后al内容为1111 1000(-8)
如al内容为 1100 0100 (-60), 指令执行后al内容为 0011 1100 (60)

计算机如何区分对待无符号数和有符号数

范例:

mov ax, -1
1111 1111 1111 1111

mov bx,65535
1111 1111 1111 1111

-1和65535在计算机内部有相同的表示,那么计算机如何区分它们呢?

计算机不关心这个问题,最终这个事件取决于你自己。你编写的程序,一个数有没有符号你清楚。指令在执行的时候有自己的法则, 你的任务是根据你的情况选择不同的指令来完成不同的目的。

对于处理器的多数指令来说,执行的结果和操作数的类型没有关系,也就是无论是从符号的角度看还是从有符号角度看,指令执行的 结果都是正解无误的。如mov ax,bx

mov ah,0xf0  ;1111 0000 可以解释成-16 或240
inc ah ; ah加1到内容为 1111 0001 可认为是-15或241

mov ah,0xf0 ; 1111 0000
add ah,0x03 ; 1111 0011 可认为是-13或243
  • 可以说,大多数指令既适用于无符号整数,也适用于有符号整数。指令执行的结果不管是用无符号来解释,还是用有符号来 解释,都是正确的。
  • 但是,也有一些指令不能同时应付无符号和有符号数,需要根据你的实际情况,选择它们的无符号版本和有符号版本。比如 无符号乘法指令mul和有符号乘法指令imul,以及无符号除法指令div和有符号除法指令idiv

范例:div只适用于无符号除法

mov ax,0x0400
mov bl,0xf0
div bl

times 510-($-$$) db 0
db 0x55,0xaa

有符号数除法指令IDIV

格式

idiv  r/m

除数长度是8位

  • 如果在指令中指定的是8位寄存器或者8位操作数的内存地址,则意味着被除数在寄存器AX里
  • 相除后,商在寄存器AL里,余数在寄存器AH里

范例:div 除数长度为8位

idiv bh
#bh是8位的寄存器,存放的是除数,那么被除数存放在AX里
#AX中内容除以BH中内容,相除后,商在AL中,余数在AH中

idiv byte [0x2002]
#除数在内存中,从内存地址0x2002处取1个字节
#AX中内容除以0x2002地址的8位除数,相除后,商在AL中,余数在AH中

除数长度是16位

  • 如果在指令中指定的是16位寄存器或者16位操作数的内存地址,则意味着被除数是32位的,低 16位在寄存器AX里;高16位在寄存器DX里
  • 相除后,商在寄存器AX里,余数在寄存器DX里。

范例:div 除数长度为16位

div bx
#bx是16位的寄存器,存放的是除数,那么被除数是32位的,低16位在寄存器AX里;高16位在寄存器DX里
#用DX和AX组合成32位的被除数除以BX中内容,相除后,商在AX中,余数在DX中

div word [0x2002]
#除数在内存中,从内存地址0x2002处取1个字
#用DX和AX组合成32位的被除数除以0x2002地址的16位除数,相除后,商在AX中,余数在DX中

除数长度是32位

8086不支持的,它是16位处理器,从80386开始支持

  • 如果在指令中指定的是32位寄存器或者32位操作数的内存地址,则意味着被除数是64位的,低 32位在寄存器EAX里;高32位在寄存器EDX里
  • 相除后,商在寄存器EAX里,余数在寄存器EDX里。

范例:div 除数长度为32位

div ebx
#ebx是32位的寄存器,存放的是除数,那么被除数是64位的,低32位在寄存器EAX里;高32位在寄存器EDX里
#用EDX和EAX组合成64位的被除数除以有EBX中内容,相除后,商在EAX中,余数在EDX中

div dword [0x2002]
#除数在内存中,从内存地址0x2002处取4个字节组合出32位操作数
#用EDX和EAX组合成64位的被除数除以0x2002地址的32位除数,相除后,商在EAX中,余数在EDX中

除数长度是64位

8086和32位处理器不支持,只有64位处理器支持

  • 如果在指令中指定的是64位寄存器或者64位操作数的内存地址,则意味着被除数是128位的,低 64位在寄存器RAX里;高64位在寄存器RDX里
  • 相除后,商在寄存器RAX里,余数在寄存器RDX里。

范例:div 除数长度为64位

div rbx
#rbx是64位的寄存器,存放的是除数,那么被除数是128位的,低64位在寄存器RAX里;高64位在寄存器RDX里
#用RDX和RAX组合成128位的被除数除以有RBX中内容,相除后,商在RAX中,余数在RDX中

div qword [0x2002]
#除数在内存中,从内存地址0x2002处取8个字节组合出64位操作数
#用RDX和RAX组合成128位的被除数除以0x2002地址的64位除数,相除后,商在RAX中,余数在RDX中

商和余数的符号性

  • 如果被除数和余数的符号相同,商为正数,否则商为负数
  • 余数的符号始终和被除数相同

范例:有符号指令idiv执行特点

exam.asm

;以下为有符号除法
mov ax,0x0400       ;AX内容为有符号数1024
mov bl,0xf0         ;BL内容为有符号-16
idiv bl             ;1024/-16 AL = 0xC0

;也可以写为下面3行
mov ax,1024
mov bl,-16
idiv bl

times 510-($-$$) db 0
db 0x55,0xaa

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

列表文件exam.lst, 可以看到机器码是一样的

 1                                          ;以下为有符号除法
 2 00000000 B80004                          mov ax,0x0400       ;AX内容为有符号数1024
 3 00000003 B3F0                            mov bl,0xf0         ;BL内容为有符号-16
 4 00000005 F6FB                            idiv bl             ;1024/-16 AL = 0xC0
 5                                  
 6                                          ;也可以写为下面3行
 7 00000007 B80004                          mov ax,1024
 8 0000000A B3F0                            mov bl,-16
 9 0000000C F6FB                            idiv bl
10                                  
11 0000000E 00<rep 1F0h>                    times 510-($-$$) db 0
12 000001FE 55AA                            db 0x55,0xaa

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0400            ; b80004
<bochs:3> s
Next at t=17175564
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov bl, 0xf0              ; b3f0
<bochs:4> s
Next at t=17175565
(0) [0x000000007c05] 0000:7c05 (unk. ctxt): idiv al, bl               ; f6fb

#在相除之前查看通用寄存器中的内容
<bochs:5> r
rax: 00000000_00000400 #被除数在AX中0400
rbx: 00000000_000000f0 #除数在BL寄存串f0
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_00007c05
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf

#执行指令 idiv al,bl后,查看通用寄存器中的内容
<bochs:6> s
Next at t=17175566
(0) [0x000000007c07] 0000:7c07 (unk. ctxt): mov ax, 0x0400            ; b80004
<bochs:7> r
rax: 00000000_000000c0  #商在AL中c0,余数在AH中0
rbx: 00000000_000000f0
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_00007c07
eflags: 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf
<bochs:8> q

有符号数的符号扩展指令

  • 8位有符号数的范围:-128…..0…..+127
  • 16位有符号数的范围:-32768……0……+32768
  • 32位有符号数的范围:-2147483648……0……+2147483647
+7
0000 0111                                 0x07
0000 0000 0000 0111                       0x0007
0000 0000 0000 0000 0000 0000 0000 0111   0x00000007

-3
1111 1101                                 0xFD
1111 1111 1111 1101                       0xFFFD
1111 1111 1111 1111 1111 1111 1111 1101   0xFFFFFFFD

我们发现将8位的有符号数加长之后,等于是符号位做为扩展,是将符号位复制并扩展到左边高位部分。

符号扩展指令

cbw          ;将AL中的有符号数扩展到AX
              若AL=FD(-3),则扩展后,AX=FFFD(-3)

cwde         ;将AX中的有符号数扩展到EAX
              若AX=FFFD(-3),则扩展后,EAX=FFFFFFFD(-3)

cdqe         ;将EAX中有符号数扩展到RAX
              若EAX=FFFFFFFD(-3),则扩展后,RAX=FFFFFFFFFFFFFFFD(-3)

cwd          ;将AX中的有符号数扩展到DX:AX
              若AX=FFFD(-3),则扩展后,DX=FFFF, AX=FFFD

cdq         ;将EAX中的有符号数扩展到EDX:EAX
              若EAX=FFFFFFFD(-3),则扩展后,EDX=FFFFFFFF,EAX=FFFFFFFD

cdo         ;将RAX中有符号数扩展到RDX:RAX
              若RAX=FFFFFFFFFFFFFFFD(-3),则扩展后,RDX=FFFFFFFFFFFFFFFF, RAX=FFFFFFFFFFFFFFFD(-3)

范例:

;以下为有符号数除法
mov ax,-6002
cwd
mov bx,-10
idiv bx

times 510-($-$$) db 0
db 0x55,0xaa

阶段性知识总结和拓展

主要内容

  • 8086的标志寄存器
  • 条件转移指令和CMP指令

8086的标志寄存器

8086的标志寄存器FLAGS

这是一个16位的寄存器,每位用做一个有特定含意的标志

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
            OF DF IF TF SF ZF    AF    PF    CF
  • CF: 位0,进位标志(Carry Flag)。当一个算术操作在结果的最高位产生进位或者借位时,此标志是1;否则是0

范例:

若AL中的内容是二进制数1000 0000
则指令的 add al,al 执行后,CF=1

1000 0000
1000 0000
----------add
0000 0000
  • PF: 位2,奇偶标志(Parity Flag)。当一个算术操作的结果在低8位中有偶数个“1”,此标志是1;否则为0

范例:

若AL中的内容是二进制数0010 0110
则指令的 xor al,3 执行后,PF=0

0010 0110
0000 0011
----------xor
0010 0101
  • OF: 位11,溢出标志(Overflow Flag)。对任何一个算术操作,假定它进行的是有符号运算。那么,当结果超出目标位置所能 容纳的最大正数或者最小负数时,此标志为1,表示有符号整数运算的结果已经溢出;否则为0.

范例:

若AH中的内容是二进制数1111 1101
则指令的 add ah,5 执行后,OF=0

-3
+5
---------add
+2

mov ah,115
add ah,ah  ;此指令执行后,OF=1

mov ax,-56
add ax,axx  ;此指令执行后,OF=0
  • ZF: 位6,零标志(zero flag)。 当运算的结果为0,此标志为1;否则为0

范例:

mov ax,25
sub ax,25  ;此指令执行后,ZF=1
  • SF: 位7,符号标志(Sign Flag)。用运算结果的最高位来设置此标志(一般来说,这一位是有符号数的符号位。0表示正数,1表示负数)

范例:

mov ah,127
add ah,1  ;此指令执行后,SF=1

0111 1111
0000 0001
----------add
1000 0000
  • AF: 位4,调整标志(Adjust Flag)。当一个算术操作在结果的位3产生进位或者借位,此标志为1;否则是0。此标志用于 二进制编码的十进制数算法里。

范例:

mov ah,247
sub ah,8  ;此指令执行后,AF=1


1111 0111
0000 1000
----------sub
1110 1111
  • DF: 位10,方向标志(direction flag) 。控制内存操作的方向,0为正方向

现有指令对标志位的影响

  • cbw/cwde/cdqe/cwd/cdq/cqo 不影响任何标志位
  • cld DF=0,对CF、OF、ZF、AF和PF的影响未定义
  • std DF=1,不影响其他标志位
  • inc/dec CF标志不受影响;对OF、SF、ZF、AF和PF的影响依计算结果而定
  • add/sub OF、SF、ZF、AF、CF和PF的状态依计算结果而定
  • div/idiv 对CF、OF、SF、ZF、AF和PF的影响未定义
  • mov/movs 这类指令不影响任何标志位
  • neg 如果操作数为0,则CF=0,否则CF=1;对OF、SF、ZF、AF和PF的影响依计算结果而定
  • xor OF=0, CF=0;对SF、ZF和PF依计算结果而定;对AF的影响未定义。

范例:

mov al,11111000b
add al,00001000b

times 510-($-$$) db 0
db 0x55,0xaa

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
Next at t=17175563
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov al, 0xf8              ; b0f8

#反汇编3条指令
<bochs:3> u /3
0000000000007c00: (                    ): mov al, 0xf8              ; b0f8
0000000000007c02: (                    ): add al, 0x08              ; 0408
0000000000007c04: (                    ): add byte ptr ds:[bx+si], al ; 0000

#观察标志寄存里标志位变化
<bochs:4> s
Next at t=17175564
(0) [0x000000007c02] 0000:7c02 (unk. ctxt): add al, 0x08              ; 0408
<bochs:5> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf
<bochs:6> s
Next at t=17175565
(0) [0x000000007c04] 0000:7c04 (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000
<bochs:7> info eflags
id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF AF PF CF

条件转移指令和CMP指令

条件转移指令

js 符号标志SF为1则转移 ;  jns 符号标志SF不为1(为0)则转移

jz 零标志ZF为1则转移;     jnz 零标志ZF不为1(为0)则转移

jo 溢出标志OF为1则转移;   jno 溢出标志OF不为1(为0)则转移

jc 进位标志CF为1则转移;   jnc 进位标志CF不为1(为0)则转移

jp 奇偶标志PF为1则转移;   jnp 奇偶标志PF不为1(为0)则转移

转移指令本身不影响任何标志位,但是它们必须依赖标志位才能工作。因此,它们必须出现在影响标志位的指令之后。

范例:

dec si
jns show

CMP指令

为方便起见,我们更喜欢将俩个数加以比较,判断它们的关系。根据比较结果判断是否转移。那么就可以使用CMP指令。

cmp指令格式

cmp  r/m, r/m/imm

影响到CF、OF、SF、ZF、AF和PF标志位

cmp al,35
cmp dx,ax
cmp dx,[0x2002]
cmp byte [0x2002],37
cmp [0x2002],ax

CMP指令在功能上和减法指令SUB是相同的。CMP指令在指令执行时是做减法操作,唯一不同是cmp仅仅根据计算结果来 设定标志位,而不保存计算结果,因此也就不会改变2个操作数的原有内容。会把操作数看成有符号数和无符号数比较。

范例

cmp dh,0
jl negb ;如果小于0,则转移到negb标号处执行

cmp dh,0x80
jae negb  ;按无符号比较,高于等于转移

受CMP指令执行结果影响的条件转移指令及其依赖的标志位状态

记住指令和比较结果就好。

指令 英文描述 比较结果 相关标志位的状态
je Jump if Equal 等于 相减结果为零才成立,故要求ZF=1
jne Jump if Not Equal 不等于 相减结果不为零才成立,故要求ZF=0
jg Jump if Greater 大于 适用于有符号数比较。要求:ZF=0(两个数不同,相减的结果不为零),并且SF=OF(如果相减后溢出,则结果必须是负数,说明目的操作数大;如果相减后未溢出,则结果必须是正数,也表明目的操作数大些)
jge Jump if Greater or Equal 大于等于 适用于有符号数的比较。要求: SF=OF
jng Jump if Not Greater 不大于 适用于有符号数的比较。要求:ZF=1(两个数相同,相减的结果为零),或者SF≠OF(如果相减后溢出,则结果必须是正数,说明源操作数大;如果相减后未溢出,则结果必须是负数,同样表明源操作数大些)
jnge Jump if Not Greater or Equal 不大于等于 适用于有符号数的比较。要求:SF≠OF
jl Jump if Less 小于 适用于有符号数的比较,等同于“不大于等于”。要求:SF≠OF
jle Jump if Less or Equal 小于等于 适用于有符号数的比较,等同于“不大于”。要求:ZF=1(两个数相同,相减的结果为零),或者SF≠OF(如果相减后溢出,则结果必须是正数,说明源操作数大;如果相减后未溢出,则结果必须是负数,同样表明源操作数大些)
jnl Jump if Not Less 不小于 适用于有符号数的比较,等同于“大于等于”。要求:SF=OF
jnle Jump if Not Less or Equal 不小于等于 适用于有符号数的比较,等同于“大于”。要求:ZF=0(两个数不同,相减的结果不为零),并且SF=OF(如果相减后溢出,则结果必须是负数,说明目的操作数大;如果相减后未溢出,则结果必须是正数,也表明目的操作数大些)
ja Jump if Above 高于 适用于无符号数的比较。要求:CF=0(没有进位或借位)而且ZF=0(两个数不相同)
jae Jump if Above or Equal 高于等于 适用于无符号数的比较。要求:CF=0(目的操作数大些,不需要借位)
jna Jump if Not Above 不高于 适用于无符号数的比较,等同于“低于等于”(见后)。要求:CF=1或者ZF=1
jnae Jump if Not Above or Equal 不高于等于 适用于无符号数的比较,等同于“低于”(见后)。要求:CF=1
jb Jump if Below 低于 适用于无符号数的比较。要求:CF=1
jbe Jump if Below or Equal 低于等于 适用于无符号数的比较。要求:CF=1或者ZF=1
jnb Jump if Not Below 不低于 适用于无符号数的比较,等同于“高于等于”。要求:CF=0
jnbe Jump if Not Below or Equal 不低于等于 适用于无符号数的比较,等同于“高于”。要求:CF=0而且ZF=0
jpe Jump if Parity Even 校验为偶 要求:PF=1
jpo Jump if Parity Odd 检验为奇 要求:PF=0

条件转移指令

jcxz (jump if CX is zero),意思是当CX寄存器的内容为0时则转移。执行这条指令时,处理器先测试寄存器是否为0.

范例

jcxz show

从 1 加到 100 并显示结果

主要内容

  • 字符串的定义和累加过程
  • 栈的原理和使用
  • 栈在数位分解和显示中的应用
  • 在调试器里观察栈操作的状态
  • 进一步认识栈和栈操作的特点
  • 逻辑或指令OR和逻辑与指令AND

字符串的定义和累加过程

1+2+3+......+98+99+100=?

数学家高斯 50 * 101 = 5050。 但是计算机不懂规则,只能从1一直加到100. 它的强项就是速度不怕麻烦。

通过1加到100学习很重要的数据结构,栈。而且了解处理器为访问栈提供怎样的支持。

范例:主引导扇区程序

exam.asm

        ;从1加到100并显示累加结果
        jmp start

message db '1+2+3+...+100='         ;等同于 db '1','+','2','+','3','+','.','.','.','+','1','0','0','='

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

        mov ax,0xb800       ;设置附加段基址到显示缓冲区
        mov es,ax

        ;以下显示字符串
        mov si,message
        mov di,0
        mov cx,start-message
showmsg:
        mov al,[si]
        mov [es:di],al
        inc di
        mov byte [es:di],0x07
        inc si
        inc di
        loop showmsg

        ;以下计算1到100的和
        xor ax,ax           ;AX用于存放累加结果
        mov cx,1
summate:
        add ax,cx
        inc cx
        cmp cx,100
        jle  summate

        jmp $

        times 510-($-$$) db 0
        db 0x55,0xaa

在设计8086处理器时,每个寄存器都有自己的特殊用途,比如

  • AX是累加器(Accumulator),与它有关的指令还会做指令长度的优化(较短);
  • CX是计数器(Counter);
  • DX是数据(Data)寄存器,除了作为通用寄存器使用外,还专门用于和外设之间进行数据传送;
  • SI是源索引寄存器(Source Index);
  • DI是目标索引寄存器(Destination Index),用于数据传送操作

栈的原理和使用

上一节里我们已经完成了字符串的显示和从1加到100的累加过程,累加完后,接下来就要分解数位,为显示结果做准备。

我们知道,最先分解出来的数位是最右边的数位,最后分解出来的数位是最左边的数位,为此我们需要先将数位临时保存 起来,然后再按相反的顺序显示在屏幕上。之前是在程序中开辟一个空间来保存数位,这里我们用特殊的方法,将数位 保存在一个特殊的内存区域,栈。

栈的概念来自于生活,比如说草堆或者柴火堆。在柴火堆上,下面的草被上面的草压着是拔不出来的,只有将上面的草先移走,先扒走,下面的草才能露出来。 在计算机中,栈是一种特殊的数据存储结构,数据的存取只能从一端进行,这样最先进去的数据只能最后出了,最后进去的数据倒是先出了,这叫做后进先出 last in first out (LIFO)。

img_20241201_123802.png

如这一幅图所示,我们可以把栈看成是一个只有一端开口的瓶子,比如这个瓶子,这是一个瓶子,只有一端开口,下面是密封的。在这个瓶子里, 1 号球最先放进来, 3 号球最后放进来。显然只有在 2 号球和 3 号球都取出来之后,才能够把 1 号球取出来,按顺序必须先把 3 号球取出才能取 2 号球, 3 号球和 2 号球都取出之后,才能够取 1 号球。显然最后进去的必须先出来,先进去的只能最后出来,这就是栈的特点。听起来像是在讲如何往盒子里放东西,或者从盒子里面取东西,实际上我们还是在讲内存,只不过是另外一种特殊的读写方式而已。

img_20241201_125432.png

计算机的内存是可以自由读写的,在使用内存时,我们根据需要用内存中的不同区域来存储不同的内容,从而形成了代码段、数据段。为了模仿栈的先进后出,后进先出,我们也要在内存里画一部分区域,这个区域叫做栈段。我们知道在处理器里段寄存器 CS 保存着当前正在使用的代码段在内存里的起始位置。段寄存器 DS 或者 ES 保存着当前正在使用的数据段在内存里的起始位置。同样的,在处理器内部有一个段寄存器SS。段寄存器 SS 保存着当前正在使用的栈段,在内存里的起始位置。

听起来似乎有些复杂,但是栈的使用是极其简单的,它只有两个动作

  • 第一个动作是把数据推入栈中,这叫做推栈,或者叫压栈。
  • 另一个动作是把数据从栈中取出了,这叫做弹栈,或者叫出栈。
压栈的指令push
push r/m

压栈指令的操作是将寄存器的内容或者内存中的内容存入栈中。

push dx
push word [0x2002] 将偏移地址 2002 处的一个字取出了,然后压入栈中。

出栈指令pop
pop r/m

从栈中取出一个字,把它存入寄存器或者存入内存地址处。

pop ax
pop word [0x08] 从栈中取出一个字,保存到偏移地址 08 处

压栈和出栈的细节通常是不需要关心的,这个过程是由处理器来自动完成,但是它的原理非常重要,是必须要掌握的。当压栈时,数据是压入栈段的,出栈也是如此,但是处理器如何跟踪这些数据呢?处理器如何知道下一个数据应该压入哪里?下一个要出站的数据位于什么地方呢?答案是在 8086 处理器内部有一个特殊的寄存器,叫做栈指针寄存器SP(stack pointer)。

  • SP: 栈指针寄存器 SP 用来提供访问栈段的偏移地址。就像指令指针寄存器 IP 一样, SP 是用来提供偏移地址,是访问栈段的偏移地址
  • SS: 栈段的段地址用 SS 来提供

用段地址SS左移4位加上栈指针寄存器 SP 的内容就形成了访问内存所需要的 20 位物理地址。

push 和 pop 指令的执行过程

那么压栈和出栈时, 8086 处理器是如何用段寄存器 SS 和栈指针寄存器 SP 来访问栈段的呢?

push的执行过程:
(a) SP <- SP - 操作数的大小(字节数);
(b) 段寄存器SS左移4位,加上SP里的偏移地址,生成物理地址;
(c) 将操作数写入上述地址处。

pop 的执行过程:
(a) 段寄存器SS左移4位,加上SP里的偏移地址,生成物理地址;
(b) 从上述地址处取得数据,存入由操作数提供的目标位置处 
(c) SP <- SP + 2

下面我们通过一段代码来加深对压栈和出栈操作的认识

img_20241201_131918.png

这是内存的示意图,假定在执行这一段代码的时候,栈段寄存器SS的值是256A,这是逻辑段地址256A。栈指针寄存器 SP 的内容是0200。这意味着栈段在内存中的起始物理地址时 256 A0。因为我们知道断地址是去掉了最后一个 0 的物理地址,因为断地址是256A,所以物理地址是 256A0。后面这些都是物理地址。

  • 第1条指令 push ax 在执行这条指令的时候是将 SP 的内容减 2, 得到01FE,将SS的值256A左移四位生成256A0,再加上01FE是指向物理地址2589E。这个位置叫做站顶。所以我们把寄存器 SP 的值叫做栈顶,而且寄存器 SP 叫做栈顶寄存器,它指示了栈顶部。从这里压入,那么写入之后这两个单元它的内容合成一个字,内容是25。
  • 第2条指令push bx。这一条指令执行的时候,再将SP的值减2,减了之后是 01FC,将SS的值256A左移四位生成256A0,再加上01FC是指向物理地址2589C。然后将寄存器bx的值 30 压入从这个位置处开始的一个字,用来保存30。
  • 第3条指令 push cx。这一条指令执行的时候,再将SP的值减2,减了之后是 01FA,将SS的值256A左移四位生成256A0,再加上01FA是指向物理地址2589A。然后将寄存器cx的值 35 压入从这个位置处开始的一个字,用来保存30。
  • 第1条指令pop DX。 这一条指令执行的时候,首先用 SS 的内容左移4位生成256A0,再加上寄存器SP的当前值01FA生成20位的物理地址2589A。然后从2589A外取出一个字35,传送到寄存器d x。等于是从栈中把数据弹到了DX中。弹完了之后,这条指令的最后一步是将SP的内容加2再存回。 SP 的内容是01FA,加2之后是01FC。
  • 第2条指令pop SI。这一条指令执行的时候,使用SS的值左移4位生成256A0,加上SP的当前值01FC生成物理地址2589C。然后从物理地址2589C处取出一个字30传送到SI中。传送之后SI的值1030,紧接着将 SP 的值加2,再返回SP,那么01FC加2之后是01FE
  • 第3条指令pop di。这一条指令执行的时候,首先用 SS 的内容左移4位生成256A0,再加上寄存器SP的当前值01FE生成20位的物理地址2589E。然后从物理地址2589E处取出一个字25传送到DI中。紧接着将 SP 的值01FE加2,得到0200,然后存回SP中。

通过这个例子可以看出与代码段和数据段不同,栈段的扩展和推进方向是向下的,从高地址向低地址方向推进。而代码段和数据段则不一样,他们是从低地址向高地址方向推进

栈在数位分解和显示中的应用

exam.asm

        ;从1加到100并显示累加结果
        jmp start

message db '1+2+3+...+100='         ;等同于 db '1','+','2','+','3','+','.','.','.','+','1','0','0','='

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

        mov ax,0xb800       ;设置附加段基址到显示缓冲区
        mov es,ax

        ;以下显示字符串
        mov si,message
        mov di,0
        mov cx,start-message
showmsg:
        mov al,[si]
        mov [es:di],al
        inc di
        mov byte [es:di],0x07
        inc si
        inc di
        loop showmsg

        ;以下计算1到100的和
        xor ax,ax           ;AX用于存放累加结果
        mov cx,1
summate:
        add ax,cx
        inc cx
        cmp cx,100
        jle  summate

        ;以下分解累加和的每个数位
        xor cx,cx           ;设置栈段的段基地址
        mov ss,cx
        mov sp,cx

        mov bx,10
        xor cx,cx
decompo:
        inc cx
        xor dx,dx
        div bx
        add dl,0x30
        push dx
        cmp ax,0
        jne decompo

        ;以下显示各个数位
shownum:
        pop dx
        mov [es:di],dl
        inc di
        mov byte [es:di], 0x07
        inc di
        loop shownum

        jmp $

        times 510-($-$$) db 0
        db 0x55,0xaa
img_20241201_142845.png
img_20241201_142642.png

在调试器里观察栈操作的状态

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmp .+14  (0x00007c10)    ; eb0e

<bochs:3> u /50
0000000000007c00: (                    ): jmp .+14  (0x00007c10)    ; eb0e
0000000000007c02: (                    ): xor word ptr ss:[bp+di], bp ; 312b
0000000000007c04: (                    ): xor ch, byte ptr ss:[bp+di] ; 322b
0000000000007c06: (                    ): xor bp, word ptr ss:[bp+di] ; 332b
0000000000007c08: (                    ): sub si, word ptr cs:[bx+di] ; 2e2e2e2b31
0000000000007c0d: (                    ): xor byte ptr ds:[bx+si], dh ; 3030
0000000000007c0f: (                    ): cmp ax, 0xc0b8            ; 3db8c0
0000000000007c12: (                    ): pop es                    ; 07
0000000000007c13: (                    ): mov ds, ax                ; 8ed8
0000000000007c15: (                    ): mov ax, 0xb800            ; b800b8
0000000000007c18: (                    ): mov es, ax                ; 8ec0
0000000000007c1a: (                    ): mov si, 0x0002            ; be0200
0000000000007c1d: (                    ): mov di, 0x0000            ; bf0000
0000000000007c20: (                    ): mov cx, 0x000e            ; b90e00
0000000000007c23: (                    ): mov al, byte ptr ds:[si]  ; 8a04
0000000000007c25: (                    ): mov byte ptr es:[di], al  ; 268805
0000000000007c28: (                    ): inc di                    ; 47
0000000000007c29: (                    ): mov byte ptr es:[di], 0x07 ; 26c60507
0000000000007c2d: (                    ): inc si                    ; 46
0000000000007c2e: (                    ): inc di                    ; 47
0000000000007c2f: (                    ): loop .-14  (0x00007c23)   ; e2f2
0000000000007c31: (                    ): xor ax, ax                ; 31c0
0000000000007c33: (                    ): mov cx, 0x0001            ; b90100
0000000000007c36: (                    ): add ax, cx                ; 01c8
0000000000007c38: (                    ): inc cx                    ; 41
0000000000007c39: (                    ): cmp cx, 0x0064            ; 83f964
0000000000007c3c: (                    ): jle .-8  (0x00007c36)     ; 7ef8
0000000000007c3e: (                    ): xor cx, cx                ; 31c9
0000000000007c40: (                    ): mov ss, cx                ; 8ed1
0000000000007c42: (                    ): mov sp, cx                ; 89cc
0000000000007c44: (                    ): mov bx, 0x000a            ; bb0a00
0000000000007c47: (                    ): xor cx, cx                ; 31c9
0000000000007c49: (                    ): inc cx                    ; 41
0000000000007c4a: (                    ): xor dx, dx                ; 31d2
0000000000007c4c: (                    ): div ax, bx                ; f7f3
0000000000007c4e: (                    ): add dl, 0x30              ; 80c230
0000000000007c51: (                    ): push dx                   ; 52
0000000000007c52: (                    ): cmp ax, 0x0000            ; 83f800
0000000000007c55: (                    ): jnz .-14  (0x00007c49)    ; 75f2
0000000000007c57: (                    ): pop dx                    ; 5a
0000000000007c58: (                    ): mov byte ptr es:[di], dl  ; 268815
0000000000007c5b: (                    ): inc di                    ; 47
0000000000007c5c: (                    ): mov byte ptr es:[di], 0x07 ; 26c60507
0000000000007c60: (                    ): inc di                    ; 47
0000000000007c61: (                    ): loop .-12  (0x00007c57)   ; e2f4
0000000000007c63: (                    ): jmp .-2  (0x00007c63)     ; ebfe
0000000000007c65: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c67: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c69: (                    ): add byte ptr ds:[bx+si], al ; 0000
0000000000007c6b: (                    ): add byte ptr ds:[bx+si], al ; 0000

#设置分解数位处为断点 xor cx,cx
<bochs:4> b 0x7c47
<bochs:5> c
(0) Breakpoint 2, 0x0000000000007c47 in ?? ()
Next at t=17176075
(0) [0x000000007c47] 0000:7c47 (unk. ctxt): xor cx, cx                ; 31c9
<bochs:6> n
Next at t=17176076
(0) [0x000000007c49] 0000:7c49 (unk. ctxt): inc cx                    ; 41
<bochs:7> n
Next at t=17176077
(0) [0x000000007c4a] 0000:7c4a (unk. ctxt): xor dx, dx                ; 31d2
<bochs:8> n
Next at t=17176078
(0) [0x000000007c4c] 0000:7c4c (unk. ctxt): div ax, bx                ; f7f3
<bochs:9> n
Next at t=17176079
(0) [0x000000007c4e] 0000:7c4e (unk. ctxt): add dl, 0x30              ; 80c230
<bochs:10> n
Next at t=17176080
(0) [0x000000007c51] 0000:7c51 (unk. ctxt): push dx                   ; 52

#观察压栈前,栈段寄存器SS和栈指针寄存器SP的值
<bochs:11> sreg
es:0xb800, dh=0x0000930b, dl=0x8000ffff, valid=7
        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=1  #SS值为0000
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x07c0, dh=0x00009300, dl=0x7c00ffff, valid=3
        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=0x00000000000f9e67, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff
<bochs:12> r
rax: 00000000_000001f9
rbx: 00000000_0000000a
rcx: 00000000_00090001
rdx: 00000000_00000030
rsp: 00000000_00000000 #栈指针寄存器SP为0000
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001c
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_00007c51
eflags: 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf

#观察压栈后,栈段寄存器SS和栈指针寄存器SP的值
<bochs:13> n
Next at t=17176081
(0) [0x000000007c52] 0000:7c52 (unk. ctxt): cmp ax, 0x0000            ; 83f800
<bochs:14> r
rax: 00000000_000001f9
rbx: 00000000_0000000a
rcx: 00000000_00090001
rdx: 00000000_00000030
rsp: 00000000_0000fffe  #SP = SP-2
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001c
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_00007c52
eflags: 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf
<bochs:15> print-stack  #观察栈内容, print-stack每次显示栈顶10个数据
Stack address size 2
 | STACK 0xfffe [0x0030] (<unknown>)  #栈顶。fffe处压入了一个字,0x0030
 | STACK 0x10000 [0x0000] (<unknown>)
 | STACK 0x10002 [0x0000] (<unknown>)
 | STACK 0x10004 [0x0000] (<unknown>)
 | STACK 0x10006 [0x0000] (<unknown>)
 | STACK 0x10008 [0x0000] (<unknown>)
 | STACK 0x1000a [0x0000] (<unknown>)
 | STACK 0x1000c [0x0000] (<unknown>)
 | STACK 0x1000e [0x0000] (<unknown>)
 | STACK 0x10010 [0x0000] (<unknown>)
 | STACK 0x10012 [0x0000] (<unknown>)
 | STACK 0x10014 [0x0000] (<unknown>)
 | STACK 0x10016 [0x0000] (<unknown>)
 | STACK 0x10018 [0x0000] (<unknown>)
 | STACK 0x1001a [0x0000] (<unknown>)
 | STACK 0x1001c [0x0000] (<unknown>)


 <bochs:16> n
Next at t=17176082
(0) [0x000000007c55] 0000:7c55 (unk. ctxt): jnz .-14  (0x00007c49)    ; 75f2
<bochs:17> n
Next at t=17176083
(0) [0x000000007c49] 0000:7c49 (unk. ctxt): inc cx                    ; 41
<bochs:18> n
Next at t=17176084
(0) [0x000000007c4a] 0000:7c4a (unk. ctxt): xor dx, dx                ; 31d2
<bochs:19> n
Next at t=17176085
(0) [0x000000007c4c] 0000:7c4c (unk. ctxt): div ax, bx                ; f7f3
<bochs:20> n
Next at t=17176086
(0) [0x000000007c4e] 0000:7c4e (unk. ctxt): add dl, 0x30              ; 80c230
<bochs:21> n
Next at t=17176087
(0) [0x000000007c51] 0000:7c51 (unk. ctxt): push dx                   ; 52
<bochs:22> n
Next at t=17176088
(0) [0x000000007c52] 0000:7c52 (unk. ctxt): cmp ax, 0x0000            ; 83f800
<bochs:23> r
rax: 00000000_00000032
rbx: 00000000_0000000a
rcx: 00000000_00090002
rdx: 00000000_00000035
rsp: 00000000_0000fffc
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001c
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_00007c52
eflags: 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf
<bochs:24> print-stack
Stack address size 2
 | STACK 0xfffc [0x0035] (<unknown>)
 | STACK 0xfffe [0x0030] (<unknown>)
 | STACK 0x10000 [0x0000] (<unknown>)
 | STACK 0x10002 [0x0000] (<unknown>)
 | STACK 0x10004 [0x0000] (<unknown>)
 | STACK 0x10006 [0x0000] (<unknown>)
 | STACK 0x10008 [0x0000] (<unknown>)
 | STACK 0x1000a [0x0000] (<unknown>)
 | STACK 0x1000c [0x0000] (<unknown>)
 | STACK 0x1000e [0x0000] (<unknown>)
 | STACK 0x10010 [0x0000] (<unknown>)
 | STACK 0x10012 [0x0000] (<unknown>)
 | STACK 0x10014 [0x0000] (<unknown>)
 | STACK 0x10016 [0x0000] (<unknown>)
 | STACK 0x10018 [0x0000] (<unknown>)
 | STACK 0x1001a [0x0000] (<unknown>)


 #观察POP出栈
 <bochs:25> u/10
0000000000007c52: (                    ): cmp ax, 0x0000            ; 83f800
0000000000007c55: (                    ): jnz .-14  (0x00007c49)    ; 75f2
0000000000007c57: (                    ): pop dx                    ; 5a
0000000000007c58: (                    ): mov byte ptr es:[di], dl  ; 268815
0000000000007c5b: (                    ): inc di                    ; 47
0000000000007c5c: (                    ): mov byte ptr es:[di], 0x07 ; 26c60507
0000000000007c60: (                    ): inc di                    ; 47
0000000000007c61: (                    ): loop .-12  (0x00007c57)   ; e2f4
0000000000007c63: (                    ): jmp .-2  (0x00007c63)     ; ebfe
0000000000007c65: (                    ): add byte ptr ds:[bx+si], al ; 0000
<bochs:26> b 0x7c57
<bochs:27> c
(0) Breakpoint 3, 0x0000000000007c57 in ?? ()
Next at t=17176104
(0) [0x000000007c57] 0000:7c57 (unk. ctxt): pop dx                    ; 5a
<bochs:28> r
rax: 00000000_00000000
rbx: 00000000_0000000a
rcx: 00000000_00090004
rdx: 00000000_00000035
rsp: 00000000_0000fff8  #栈顶fff8
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001c
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_00007c57
eflags: 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
<bochs:29> print-stack
Stack address size 2
 | STACK 0xfff8 [0x0035] (<unknown>)
 | STACK 0xfffa [0x0030] (<unknown>)
 | STACK 0xfffc [0x0035] (<unknown>)
 | STACK 0xfffe [0x0030] (<unknown>)
 | STACK 0x10000 [0x0000] (<unknown>)
 | STACK 0x10002 [0x0000] (<unknown>)
 | STACK 0x10004 [0x0000] (<unknown>)
 | STACK 0x10006 [0x0000] (<unknown>)
 | STACK 0x10008 [0x0000] (<unknown>)
 | STACK 0x1000a [0x0000] (<unknown>)
 | STACK 0x1000c [0x0000] (<unknown>)
 | STACK 0x1000e [0x0000] (<unknown>)
 | STACK 0x10010 [0x0000] (<unknown>)
 | STACK 0x10012 [0x0000] (<unknown>)
 | STACK 0x10014 [0x0000] (<unknown>)
 | STACK 0x10016 [0x0000] (<unknown>)

 #出栈后,观察
 <bochs:30> n
Next at t=17176105
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): mov byte ptr es:[di], dl  ; 268815
<bochs:31> r
rax: 00000000_00000000
rbx: 00000000_0000000a
rcx: 00000000_00090004
rdx: 00000000_00000035
rsp: 00000000_0000fffa  #栈顶fffa
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001c
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_00007c58
eflags: 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
<bochs:32> print-stack
Stack address size 2
 | STACK 0xfffa [0x0030] (<unknown>)
 | STACK 0xfffc [0x0035] (<unknown>)
 | STACK 0xfffe [0x0030] (<unknown>)
 | STACK 0x10000 [0x0000] (<unknown>)
 | STACK 0x10002 [0x0000] (<unknown>)
 | STACK 0x10004 [0x0000] (<unknown>)
 | STACK 0x10006 [0x0000] (<unknown>)
 | STACK 0x10008 [0x0000] (<unknown>)
 | STACK 0x1000a [0x0000] (<unknown>)
 | STACK 0x1000c [0x0000] (<unknown>)
 | STACK 0x1000e [0x0000] (<unknown>)
 | STACK 0x10010 [0x0000] (<unknown>)
 | STACK 0x10012 [0x0000] (<unknown>)
 | STACK 0x10014 [0x0000] (<unknown>)
 | STACK 0x10016 [0x0000] (<unknown>)
 | STACK 0x10018 [0x0000] (<unknown>)


<bochs:33> n
Next at t=17176106
(0) [0x000000007c5b] 0000:7c5b (unk. ctxt): inc di                    ; 47
<bochs:34> n
Next at t=17176107
(0) [0x000000007c5c] 0000:7c5c (unk. ctxt): mov byte ptr es:[di], 0x07 ; 26c60507
<bochs:35> n
Next at t=17176108
(0) [0x000000007c60] 0000:7c60 (unk. ctxt): inc di                    ; 47
<bochs:36> n
Next at t=17176109
(0) [0x000000007c61] 0000:7c61 (unk. ctxt): loop .-12  (0x00007c57)   ; e2f4
<bochs:37> n
(0) Breakpoint 3, 0x0000000000007c57 in ?? ()
Next at t=17176110
(0) [0x000000007c57] 0000:7c57 (unk. ctxt): pop dx                    ; 5a
<bochs:38> n
Next at t=17176111
(0) [0x000000007c58] 0000:7c58 (unk. ctxt): mov byte ptr es:[di], dl  ; 268815
<bochs:39> r
rax: 00000000_00000000
rbx: 00000000_0000000a
rcx: 00000000_00090003
rdx: 00000000_00000030
rsp: 00000000_0000fffc
rbp: 00000000_00000000
rsi: 00000000_000e0010
rdi: 00000000_0000001e
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_00007c58
eflags: 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf
<bochs:40> print-stack
Stack address size 2
 | STACK 0xfffc [0x0035] (<unknown>)
 | STACK 0xfffe [0x0030] (<unknown>)
 | STACK 0x10000 [0x0000] (<unknown>)
 | STACK 0x10002 [0x0000] (<unknown>)
 | STACK 0x10004 [0x0000] (<unknown>)
 | STACK 0x10006 [0x0000] (<unknown>)
 | STACK 0x10008 [0x0000] (<unknown>)
 | STACK 0x1000a [0x0000] (<unknown>)
 | STACK 0x1000c [0x0000] (<unknown>)
 | STACK 0x1000e [0x0000] (<unknown>)
 | STACK 0x10010 [0x0000] (<unknown>)
 | STACK 0x10012 [0x0000] (<unknown>)
 | STACK 0x10014 [0x0000] (<unknown>)
 | STACK 0x10016 [0x0000] (<unknown>)
 | STACK 0x10018 [0x0000] (<unknown>)
 | STACK 0x1001a [0x0000] (<unknown>)

 #结束调试
 <bochs:41> c
(0) Breakpoint 3, 0x0000000000007c57 in ?? ()
Next at t=17176116
(0) [0x000000007c57] 0000:7c57 (unk. ctxt): pop dx                    ; 5a
<bochs:42> c
(0) Breakpoint 3, 0x0000000000007c57 in ?? ()
Next at t=17176122
(0) [0x000000007c57] 0000:7c57 (unk. ctxt): pop dx                    ; 5a
<bochs:43> c

进一步认识栈和栈操作的特点

在8086上,push和po指令的操作数只能是16位的。

push cs
pop ds

结果与以下2条指令执行的结果相同
mov ax,cs
mov ds,ax

栈在本质上也只是普通的内存区域。之所以用push和pop访问是因为你把它看成栈而已。把栈看成普通的数据忘掉它是一个栈,它将不再神秘。这样做只是为了方便程序开发。

push ax

;等价
sub sp,2
mov bx,sp
mov [ss:bx],ax

pop ax

;等价
mov bx,sp
mov ax,[ss:bx]
add sp,2

必须保持栈平衡

压栈和出栈必须是配对的。有一个压栈操作必须有一个出栈操作。

充分估计需要的栈空间

在编写程序之前,必须充分估计所需要的栈空间。避免有重叠部分的时候数据造成混乱。

img_20241201_150726.png

逻辑或指令OR和逻辑与指令AND

逻辑或指令OR

或门(OR-GATE)

只要有一个输入是1,输出是1;输入都为0,输出为0

img_20241201_150937.png
0 or 0 = 0
0 or 1 = 1
1 or 0 = 1
1 or 1 = 1

或操作可以在2个数之间进行

129 or 127 = ?

1000 0001
0111 1111
----------or
1111 1111  255

or指令格式

or  r/m, r/m/imm

或运算的结果在左操作数。

注意:俩个操作数长度一致

or bh,al
or cx,dx
or ax,3
or word [0x2002],67
or si,[0x2002]

逻辑或指令or执行之后

OF=0, CF=0;
SF、ZF和PF依计算结果而定
AF的状态未定义

逻辑与指令AND

与门(AND-GATE)

除非俩个输入是1,输出才是1.

img_20241201_152005.png
0 and 0 = 0
0 and 1 = 0
1 and 0 = 0
1 and 1 = 1

逻辑与操作可以在俩个数之间进行

129 and 127 = ?

1000 0001
0111 1111
----------and
0000 0001  1

在处理器中,引入一个指令做逻辑与操作,and

and指令格式

and  r/m,  r/m/imm

与运算的结果在左操作数。

注意:俩个操作数长度一致

and bh,al
and cx,dx
and ax,3
and word [0x2002],67
and si,[0x2002]

INTEL 8086 处理器的寻址方式

主要内容

  • 寄存器、立即数和直接寻址
  • 基址寻址
  • 变址寻址
  • 基址变址寻址
  • 练习

寄存器、立即数和直接寻址

什么是寻址方式?

处理器的任务是取指令并执行指令,指定了处理器的操作。很多操作和数字有关,所以必须在指令中指定被操作 的数字从哪里来,操作完后把结果送到哪里去。这叫做寻址方式,Addressing Mode.

简单的说寻址方式就是如何找到要操作的数据,如何找到要存放结果的地址。

寄存器寻址

寄存器寻址:操作的数位于寄存器中

范例:

mov ax,cx  ;左右操作数都是寄存器寻址
add bx,0xf000 ;左操作或者说目的操作数是寄存器
inc dx

立即数寻址

立即数寻址:操作数是由立即数指定的

范例:

add bx,0xf000 ;右操作数或者说源操作数是立即数
mov dx, mydata ;右操作数是标号,标号是汇编地址,在本质上是数字。在程序编译阶段标号会被转换为立即数

直接(内存)寻址

内存寻址:操作数在内存里

8086处理器在访问内存时,采用的是段地址左移4位,然后再加上偏移地址来形成20位的物理地址。段地址是由 CS,DS,ES,SS之一来提供,偏移地址必须由指令来提供。因此所谓的内存寻址实际上是寻找偏移地址,通过偏移地址 来寻找操作数。

如果在指令中直接给出来偏移地址,这叫直接寻址。

范例

mov ax,[0x5c0f] ;源操作数采用直接寻址。指令执行时,数据段寄存器的内容左移4位,再加上偏移地址50c0f,得到物理地址。从这个地址处取得一个字传到ax中。
add word [0x0230],0x5000
xor byte [es:mydata],0x05 ;直接寻址. 指令编译时,标号mydata被转换成数值,这个数值就是偏移地址。这里采用了段超越前缀es。当指令执行时,将段寄存器
                                              ES的内容左移4位,再加上偏移地址形成20位的物理地址。将源操作数0x05传送到这个内存地址处。

基址寻址

在8086处理器中,基址寻址是用基址寄存器BX或者BP来提供操作数的偏移地址。偏移地址通常也叫做有效地址(Effective Address)。

    buffer dw 0x20,0x100,0x0f,0x300,0xff00

    ;以下使用直接寻址方式将所有数据加1
    inc  word [buffer]
    inc  word [buffer+2]
    inc  word [buffer+4]
    inc  word [buffer+6]
    inc  word [buffer+8]
    
    ;以下使用基址寻址方式将所有数据加1
    mov bx,buffer
    mov cx,5
lpinc:
    inc word [bx]
    add bx,2
    loop lpinc

    ;基址寄存器bx提供有效地址,默认段寄存器是DS
    mov ax,0x5000
    mov bx,0x7000
    mov cx,0x8000

    push ax
    push bx
    push cx

    mov bx,sp
    mov dx,[ss:bx+2]

    pop ax
    pop bx
    pop cx

    ;基址寄存器bp提供有效地址,默认段寄存器是SS,可省略段超越前缀SS
    mov ax,0x5000
    mov bx,0x7000
    mov cx,0x8000

    push ax
    push bx
    push cx

    mov bp,sp
    mov dx,[bp+2]

    pop ax
    pop bx
    pop cx

变址寻址

变址寻址类似于基址寻址,唯一不同之处在于这种方式使用的是SI和DI,或者叫索引寄存器。

范例:

;使用变址寻址
mov [si],dx ;目的操作数是一个地址,来自于索引寄存器SI
add ax,[di] ;源操作数来自于寻址寄存器DI,
xor word [si],0x8000 ;目的操作数是一个地址,来自于索引寄存器SI

;如果在指令中使用了SI和DI来提供偏移地址,而且没有使用段超越前缀,则默认使用段寄存器DS来提供段地址。

;使用变址寻址
mov [si+0x100],al ;目的操作数是一个内存地址,偏移地址是由变址寄存器SI加上偏移量0x100的结果。
and byte [di+mydata],0x80 ;标号代表汇编地址,本质上是一个数字。

让处理器支持多种寻址方式会增加硬件的复杂性,但是可以增强处理器的处理能力。

范例:主引导扇区程序 exam.asm

        ;就地反转字符串的内容
        jmp start

string  db 'abcdefghijklmnopqrstuvwxyz'

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

        mov ax,cs           ;设置栈段的段基地址
        mov ss,ax           ;这样,栈段SS的内容就和代码段CS内容一致了
        mov sp,0            ;初始化栈顶指针

        mov cx,start-string  ;循环次数,从26到1,共26次
        mov bx,string       ;数据区首地址(基地址)
lppush:
        mov al,[bx]
        push ax
        inc bx
        loop lppush         ;循环压栈

        mov cx,start-string
        mov bx,string
lppop:
        pop ax
        mov [bx],al
        inc bx
        loop lppop          ;循环出栈

        jmp $

        times 510-($-$$) db 0 
        db 0x55,0xaa

这里的bx使用了基址寻址,可以换成si或者di,就是变址寻址方式了。

编译

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

将2进制程序写入虚拟硬盘文件learn.vhd

  • 方法1 fixvhdwr.exe 用自定义的程序导入。
    • LBA连续直写模式,起始LBA区号为0
  • 方法2 其他有效方法

打开bochs调试器bochsdbg.exe进行调试。

#设置断点到物理地址7c00,因为ROM BIOS执行后最后的工作是把主引导扇区数据读入到内存的物理地址7c00处
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1> b 0x7c00
<bochs:2> c #处理器连续地执行,遇到断点停下
......
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmp .+26  (0x00007c1c)    ; eb1a
...

基址变址寻址

有时候为了更灵活地访问数据需要将基址寄存器和变址寄存器结合起来,这样就成了基址变址寻址方式。

在使用基址变址寻址方式时,操作数的有效地址可以使用BX和SI相加等到、BX+DI、BX+SI+偏移量、BX+DI+偏移量

[bx+si]
[bx+di]
[bx+si+偏移量]
[bx+di+偏移量]

如:
mov ax, [bx+si+0x03]

基址寄存器也可以是BP,默认使用栈段寄存器SS

[bp+si]
[bp+di]
[bp+si+偏移量]
[bp+di+偏移量]

如:
mov ax, [bp+si+0x03]

范例:主引导扇区程序 exam.asm

        ;就地反转字符串的内容--采用基址变址寻址方式
        jmp start

string  db 'abcdefghijklmnopqrstuvwxyz'

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

        mov bx,string       ;数据区首地址
        mov si,0            ;正向索引
        mov di,start-string ;反向索引

rever:
        mov ah,[bx+si]
        mov al,[bx+di]
        mov [bx+si],al
        mov [bx+di],ah
        inc si              ;以上4行用于交换首尾数据
        dec di
        cmp si,di
        jl rever            ;首尾没有相遇,或者没有超越,继续交换

        jmp $

        times 510-($-$$) db 0 
        db 0x55,0xaa

硬盘和显卡的访问与控制

主要内容

  • 离开主引导扇区
  • 给汇编语言程序分段
  • 控制段内元素的汇编地址
  • 加载器和用户程序头部段
  • 加载器的工作流程和常数声明
  • 确定用户程序的加载位置
  • 外围设备及其接口
  • 输入输出端口的访问
  • 通过硬盘控制器端口读扇区数据
  • 过程和过程调用
  • 过程调用和返回的原理
  • 加载整个用户程序
  • 用户程序的重定位
  • 比特位的移动指令
  • 转到用户程序内部执行
  • 8086的无条件转移指令
  • 用户程序的执行过程
  • 验证加载器加载和执行用户程序的过程
  • 用户程序概述
  • 与文本显示有关的回车、换行与光标控制
  • 回车的光标处理和乘法指令MUL
  • 换行和普通字符的处理过程与滚屏操作
  • 8086的过程调用方式
  • 通过RETF指令转到另一个代码段内执行
  • 在程序中访问不同的数据段
  • 使用新版FixVhdWr写虚拟硬盘并运行程序
  • 练习

离开主引导扇区

在硬盘上,0面0道1扇区是主引导扇区,主引导扇区是处理器迈向广阔天地的第一块跳板。离开主引导扇区后, 前方就是操作系统的森林,即windows,unix, linux。操作系统可以看成是一个程序,只是非常复杂庞大,包含了 更多的指令。

和主引导扇区程序一样,操作系统也是位置于硬盘上。操作系统需要安装到硬盘上的,这个安装过程不但要把 操作系统的指令和数据写入硬盘,通常还要更新主引导扇区的内容,好让这个跳板把操作系统运行起来。

操作系统通常肩负着处理器、内存和显卡声卡等外围设备的控制管理任务,也负责各种应用程序的加载和调度 工作。应用程序千千万万,像办公软件、游戏娱乐软件他们也需要安装在硬盘上。应用程序需要操作系统负责 管理,它们的加载和运行都是由操作系统负责的。

凭个人之力写一个非常完善的操作系统,这几乎是不可能的事。但是写一个小程序模拟一下它的哪个功能,这 是可以的。编译好的程序通常存放在像硬盘这样的载体上,需要加载到内存之后才能执行。这个过程并不简单。

  • 首先需要读取硬盘,决定把它加载到内存的什么位置。最重要的是,程序通常是分段的,载入内存之后还要 重新计算段地址。这个过程叫做段的重定位。

下一步的目标

  • 模拟操作系统加载应用程序的过程,演示段的重定位方法,最终彻底理解8086处理器的分段管理机制;
  • 学习x86处理器过程调用的程序执行机制;
  • 以读硬盘扇区和控制屏幕光标为实例,了解x86处理器访问外围硬件的方法;
  • 总结JMP和CALL指令的全部格式;
  • 认识更多的x86处理器指令

给汇编语言程序分段

如何编写具有多个段的汇编语言程序

8086处理器已经是我们很熟悉的朋友了,我们了解它的秉性,它可以访问1MB字节的内存,地址范围从 00000一直到FFFFF。但是它没有能力将这1MB字节看成一个连续的空间,而只能最多将它划分为16个片段来 访问。因为他们必须采用段地址加偏移地址的方式来访问内存。段地址用段寄存器来提供,但是这些段寄存 器都是16位的。

段与段之间也是不同的

  • CS代码段寄存地址:保存代码段的段地址。
  • DS数据段寄存器:用来保存数据段的段地址
  • ES附加段寄存器:用来保存数据段的段地址
  • SS:用来保存栈段的段地址
img_20241217_205442.png

因为处理器的工作模式是将内存分成段,代码段、数据段和栈段,相对应的一个规范的汇编语言程序也应该包括 代码段、数据段、附加段和栈段。这样一来,段的划分和段与段之间的界线在程序加载到内存之前就已经准备好了。

在实际编程时,代码可能很长超过了64KB,数据也可能很多超过64KB。因为一个段最多64千字节,在这种情况下, 必须分成多个段来容纳。

范例: nasm编译器,划分段

SECTION data1
mydata  dw 0xFACE

SECTION data2
string  db 'hello'

section code
        mov bx,string
        mov si,string

Nasm编译器使用SECTION或者SEGMENT来定义段,段名只要不重复就可以了。一旦定义了段,后面的内容则都属于这个段。 如果程序中没有段定义语句,那么整个程序自成一个段。Nasm编译器对段的数量没有限制。

D:\project\assemblyprojs>nasm  exam.asm -f bin -o exam.bin -l exam.lst

$ hexdump.exe -C exam.bin
00000000  ce fa 00 00 68 65 6c 6c  6f 00 00 00 bb 04 00 be  |....hello.......|
00000010  04 00                                             |..|
00000012

#在我的机器上,段的长度必须是4的倍数
#ce fa 00 00 #data1段内容
#68 65 6c 6c  6f 00 00 00#data2段内容, hello是5字节不能被4整除,需要被3个字节用0填充
#bb 04 00 be 04 00 #bb 04 00 是mov bx,string 指令;be 04 00 是mov si,string指令

对于8086处理器来说,要求段的物理地址必须是16的倍数。按16字节对齐。可以在段定义中使用 ALINE=n子句

范例: nasm编译器,划分段

SECTION data1    ALIGN=16
mydata  dw 0xFACE

SECTION data2    ALIGN=16
string  db 'hello'

section code     ALIGN=16
        mov bx,string
        mov si,string
D:\project\assemblyprojs>nasm  exam.asm -f bin -o exam.bin -l exam.lst

$ hexdump.exe -C exam.bin
00000000  ce fa 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  68 65 6c 6c 6f 00 00 00  00 00 00 00 00 00 00 00  |hello...........|
00000020  bb 10 00 be 10 00                                 |......|
00000026

#为了16字节对齐,中间必须用0来填充

控制段内元素的汇编地址

如果没有特别的指令程序中所有指令他们的汇编地址都是相对于程序开头的。

按道理来说,段内元素的汇编地址都是相对于当前段的,再不应该是相对于程序开头处。

在定义段时,还可以包含一个VSTART子句

范例:

SECTION data1    ALIGN=16   VSTART=0
mydata  dw 0xFACE

SECTION data2    ALIGN=16   VSTART=0
string  db 'hello'

section code     ALIGN=16   VSTART=0
        mov bx,string
        mov si,string

VSTART=0是虚拟的汇编地址,在这个段内所有元素的汇编地址都从这个指定的数值开始计算。

D:\project\assemblyprojs>nasm exam.asm -f bin -o exam.bin -l exam.lst

$ hexdump.exe -C exam.bin
00000000  ce fa 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  68 65 6c 6c 6f 00 00 00  00 00 00 00 00 00 00 00  |hello...........|
00000020  bb 00 00 be 00 00                                 |......|

可以看到 mov si,string 的机器码为be 00 00,string是从0开始计算的。

练习:

section s1
offset  dw str1,str2,num

section s2    align=16   vstart=0x100
str1    db 'hello'
str2    db 'world'

section s3    align=16
num     dw  0xbad

在标号offset处定义了3个字,请定出他们具体数值

str1 0x100
str2 0x105
num  20

加载器和用户程序头部段

如果计算机配备了硬盘,那么主引导扇区是必须要执行的,但是主引导扇区太小了只有512个字节,容纳不了多少指令。 因此,主引导扇区通常只用来做为一个跳板,加载和引导操作系统。操作系统就不说了就是我们自己。

如果我们的程序很大主引导扇区放不下,该怎么办呢?很简单,硬盘很大,我们可以把程序放到硬盘的其他扇区里,我们 称之为用户程序。然后把主引导扇区改造为一个程序加载器或者说是一个加载程序,它的作用是加载用户程序并执行。

img_20241219_200402.png

一般来说加载器和用户程序是在不同的地方由不同的人或者公司来开发的。加载器和用户程序彼此之间都是墨盒的,并不 知道对方是做什么的。加载器需要一些必要的信息来知道如何加载用户程序。所以在用户程序头部。这是双方都知道的 协议部分。

范例:用户程序 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:

这个程序有5个段header, code, data,stack,trail

用户程序头部起码要包含以下信息

  • 程序的总长度
  • 入口点
  • 段重定位表项数
  • 段重定位表
    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:

- 程序的总长度
    program_length  dd  program_end                             ;程序总长度[0x00]

    程序的总长度用标号来定位就是可以的。标号program_end在数值上等于整个程序的长度

- 入口点
入口点包含段地址和段内偏移地址。
    code_entry      dw  start                                   ;偏移地址[0x04]
                    dd  section.code.start                      ;段地址[0x06] 
   偏移地址是用标号start所代表的汇编地址来填写
   段地址是用表达式section.code.start得到的。


- 段重定位表
    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:

段重定位表以标号segtb1_begin开始以标号segtb1_end结束,标号之间是表项,
每个表项用来记录一个段的汇编地址。

- 段重定位表项数
    realloc_tbl_len dw  (segtbl_end-segtbl_begin)/4             ;段重定位表项个数[0x0A]

用户定义的段在数量上是不确定的。因此必须在用户程序头部定义段重定位表项数。
segtbl_end-segtbl_begin是那块区域的总长度,区域内每个段重定位表项用dd 4个字节来记录,所以除以4就是表项数

nasm计算段的汇编地址的表达式

section.段名字.start
两边是固定写法,用来计算这个段相对于整个程序开头的汇编地址,实际上是相对程序开头处的偏移量。
也可以理解成这个段距离整个程序开头的字节数。

编译

D:\project\assemblyprojs>nasm userapp.asm -f bin -o userapp.bin -l userapp.lst
userapp.asm:55: warning: uninitialized space declared in stack section: zeroing [-w+zeroing]

忽略警告

加载器的工作流程和常数声明

  • 读取用户程序的起始扇区
  • 把整个用户程序都读入内存
  • 计算段的物理地址和逻辑地址(段的重定位)
  • 转移到用户程序执行(将处理器的控制权交给用户程序)
- 读取用户程序的起始扇区
这个扇区包含了用户的头部段内容。

- 把整个用户程序都读入内存
读取完后需要取出并检查用户程序的大小,并知道还需要读取多少扇区。
把整个用户程序都读到内存中,具体内存位置可以自由选择。

- 计算段的物理地址和逻辑段地址(段的重定位)
加载器还要根据段的汇编地址来计算段的实际物理内存地址,并将物理地址转换成
段的逻辑段地址。这样,用户程序就可以将逻辑段地址加载到DS,ES,SS并访问这些段

- 转移到用户程序执行(将处理器的控制权交给用户程序)

规定用户程序从硬盘逻辑扇区号100开始

常数声明,用equ来声明,不占空间。方便来统一修改程序,防止漏掉。

范例:用户加载器 mbr.asm

         ;代码清单8-1
         ;文件名:c08_mbr.asm
         ;文件说明:硬盘主引导扇区代码(加载程序) 

         app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                         ;常数的声明不会占用汇编地址

SECTION mbr align=16 vstart=0x7c00                                     

         ;设置堆栈段和栈指针 
         mov ax,0      
         mov ss,ax
         mov sp,ax

         mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
         mov dx,[cs:phy_base+0x02]
         mov bx,16        
         div bx            
         mov ds,ax                       ;令DS和ES指向该段以进行操作
         mov es,ax                        

         ;以下读取程序的起始部分 
         xor di,di
         mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
         xor bx,bx                       ;加载到DS:0x0000处 
         call read_hard_disk_0

         ;以下判断整个程序有多大
         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                          ;恢复数据段基址到用户程序头部段 

         ;计算入口点代码段基址 
   direct:
         mov dx,[0x08]
         mov ax,[0x06]
         call calc_segment_base
         mov [0x06],ax                   ;回填修正后的入口点代码段基址 

         ;开始处理段重定位表
         mov cx,[0x0a]                   ;需要重定位的项目数量
         mov bx,0x0c                     ;重定位表首地址

 realloc:
         mov dx,[bx+0x02]                ;32位地址的高16位 
         mov ax,[bx]
         call calc_segment_base
         mov [bx],ax                     ;回填段的基址
         add bx,4                        ;下一个重定位项(每项占4个字节) 
         loop realloc 

         jmp far [0x04]                  ;转移到用户程序  

;-------------------------------------------------------------------------------
read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                         ;输入:DI:SI=起始逻辑扇区号
                                         ;      DS:BX=目标缓冲区地址
         push ax
         push bx
         push cx
         push dx

         mov dx,0x1f2
         mov al,1
         out dx,al                       ;读取的扇区数

         inc dx                          ;0x1f3
         mov ax,si
         out dx,al                       ;LBA地址7~0

         inc dx                          ;0x1f4
         mov al,ah
         out dx,al                       ;LBA地址15~8

         inc dx                          ;0x1f5
         mov ax,di
         out dx,al                       ;LBA地址23~16

         inc dx                          ;0x1f6
         mov al,0xe0                     ;LBA28模式,主盘
         or al,ah                        ;LBA地址27~24
         out dx,al

         inc dx                          ;0x1f7
         mov al,0x20                     ;读命令
         out dx,al

  .waits:
         in al,dx
         and al,0x88
         cmp al,0x08
         jnz .waits                      ;不忙,且硬盘已准备好数据传输 

         mov cx,256                      ;总共要读取的字数
         mov dx,0x1f0
  .readw:
         in ax,dx
         mov [bx],ax
         add bx,2
         loop .readw

         pop dx
         pop cx
         pop bx
         pop ax

         ret

;-------------------------------------------------------------------------------
calc_segment_base:                       ;计算16位段地址
                                         ;输入:DX:AX=32位物理地址
                                         ;返回:AX=16位段基地址 
         push dx                          

         add ax,[cs:phy_base]
         adc dx,[cs:phy_base+0x02]
         shr ax,4
         ror dx,4
         and dx,0xf000
         or ax,dx

         pop dx

         ret

;-------------------------------------------------------------------------------
         phy_base dd 0x10000             ;用户程序被加载的物理起始地址

 times 510-($-$$) db 0
                  db 0x55,0xaa

确定用户程序的加载位置

外围设备及其接口

输入输出端口的访问

通过硬盘控制器端口读扇区数据

过程和过程调用

过程调用和返回的原理

加载整个用户程序

用户程序的重定位

比特位的移动指令

转到用户程序内部执行

8086的无条件转移指令

用户程序的执行过程

验证加载器加载和执行用户程序的过程

用户程序概述

与文本显示有关的回车、换行与光标控制

回车的光标处理和乘法指令MUL

换行和普通字符的处理过程与滚屏操作

8086的过程调用方式

通过RETF指令转到另一个代码段内执行

在程序中访问不同的数据段

使用新版FixVhdWr写虚拟硬盘并运行程序

练习