1785 字
9 分钟
从零开始的汇编语言笔记(4)

这里应该就是一些理论的笔记部分。

计算机理论#

小端模式#

下面证明一下“低位对低位,高位对高位”。

C:\> debug
- a
073F:0100 mov ax, 1234
073F:0103 mov [6666], ax
073F:0106 int 3 ; 设置断点
073F:0107 ; 回车退出
- g ; 运行到断点
- d ds:6666

然后可以看见这个ds:6666位置的数,是地址低位是34(数值低位),地址高位是12(数值高位)。

image-20241227150838997

外设接口#

外部设备与主机(CPU和z存储器)的通信是通过外设接口(Interface)进行的,每个接口包括一组寄存器。

数据寄存器:存放外设和主机间传送的数据。

状态寄存器:保存外设或接口的状态信息。

命令寄存器:保存CPU发给外设或接口的控制命令。

外设中每个寄存器有一个端口(Port)地址,构成一个独立于内存的I/O地址空间:0000H ~ 0FFFFH

专用的I/O指令: INOUT

BIOS和DOS功能调用。

CPU和外设(比如键盘、打印机、硬盘等)的工作速度差异很大,而且它们”说的语言”也不同。接口就像是一个翻译和缓冲区,帮助它们能够顺利通信。

接口中的三种关键寄存器各自担任不同的角色:

  1. 数据寄存器: 这就像是一个传递文件的托盘。当键盘输入一个字符时,这个字符会先放在数据寄存器中,然后CPU才能来取走它。反过来,当CPU要向打印机发送数据时,也是先把数据放在这个”托盘”中。
  2. 状态寄存器: 这像是一个状态指示牌,告诉CPU外设当前的工作状态。比如,打印机是否缺纸、键盘是否有按键按下、数据是否准备好等。CPU通过查看状态寄存器,就知道外设是否准备好进行数据传输。
  3. 命令寄存器: 这相当于一个指令箱,CPU通过它发送控制命令给外设。比如,让打印机开始打印、让硬盘开始读取数据等。

端口地址空间(0000H~0FFFFH)是一个很巧妙的设计: 这就像是给每个接口寄存器都分配了一个门牌号。这些地址与内存地址是分开的,形成了一个独立的地址空间。这样,CPU就可以明确知道它是在与内存通信还是在与外设通信。

为了使用这些端口,处理器提供了专门的指令:

  • IN 指令:从指定端口读取数据(就像从外设收信)
  • OUT 指令:向指定端口写入数据(就像给外设发信)

举个具体的例子: 假设键盘的数据端口地址是60H,状态端口是64H。当我们按下一个键时:

  1. CPU通过IN指令从64H端口读取状态,检查是否有新数据
  2. 如果有新数据,就通过IN指令从60H端口读取按键数据

(下面代码抄的, 没跑过)

; 从键盘读取数据的简单例子
mov dx, 60h ; 60h是键盘数据端口
in al, dx ; 从端口读取一个字节到AL寄存器
; 向打印机发送数据的例子
mov dx, 378h ; 378h是打印机数据端口
mov al, 'A' ; 要打印的字符
out dx, al ; 发送字符到打印机端口
; 完整的键盘读取程序
check_keyboard:
mov dx, 64h ; 键盘状态端口
wait_key:
in al, dx ; 读取键盘状态
test al, 1 ; 测试是否有数据可读(第0位)
jz wait_key ; 如果没有数据,继续等待
mov dx, 60h ; 键盘数据端口
in al, dx ; 读取按键扫描码
; 此时AL中包含了按键的扫描码

实模式存储器寻址#

image-20241227203922890

寻址方式#

立即寻址#

(立即数寻址)

mov al, 5
mov ax, 3064h

寄存器寻址#

mov ax, bx
mov al, bh

直接寻址#

mov ax, [2000h]

寄存器间接寻址#

mov ax, [bx]
mov ax, es:[bx]
mov ax, [bp]

寄存器相对寻址#

image-20241227204632197
mov ax, count[si]
mov ax, [count+si]

基址变址寻址#

image-20241227204659985
mov ax, [bx][di]
mov ax, [bx+di]
mov ax, es:[bx][si]
WARNING

必须是一个基址寄存器和一个变址寄存器的组合。

相对基址变址寻址#

image-20241227205126075
mov ax, mask[bx][si]
mov ax, mask[bx+si]
mov ax, [mask+bx+si]

汇编语言语法#

标志位#

伪指令#

1. 数据定义伪指令:DBDWDD#

; DB - 定义字节(1字节)
DATA_BYTE DB 100 ; 定义一个字节,值为100
; DW - 定义字(2字节)
DATA_WORD DW 100, 100H ; 定义两个字
; 重复定义
DATA_DUP DB 100 DUP (?) ; 定义100个未初始化的字节

2. 字符串定义:MESS1 DB 'AB'#

MESS1 DB 'AB' ; 存储为两个字节:41H, 42H
MESS2 DW 'AB' ; 存储为一个字:4241H

3. 类型转换指令:PTR#

OPER1 DB 1, 2
OPER2 DW 1234H, 5678H
; 错误示例
MOV AX, OPER1+1 ; 错误:类型不匹配
MOV AL, OPER2 ; 错误:类型不匹配
; 正确用法
MOV AX, WORD PTR OPER1+1 ; 将字节数据作为字访问
MOV AL, BYTE PTR OPER2 ; 将字数据作为字节访问

4. 标号定义:LABEL#

; 定义一个数据区可以用两种不同的方式访问
BYTE_ARRAY LABEL BYTE
WORD_ARRAY DW 50 DUP (?)

5. 常量定义伪指令:EQU=#

; EQU - 定义常量,不能重定义
CONSTANT EQU 256
ALPHA EQU 7
BETA EQU ALPHA-2
; "=" - 可重定义的常量
EMP = 7
EMP = EMP + 1 ; 允许重新赋值

EQU (equate) 是一个比较严格的常量定义伪指令。“不能重定义”的意思是,一旦你使用 EQU 定义了一个常量,就不能再给这个常量赋新的值。

; 这样是正确的 - 第一次定义
ALPHA EQU 7
; 这样是错误的 - 试图重定义 ALPHA
ALPHA EQU 8 ; 汇编器会报错!
; 这样也是错误的
ALPHA EQU ALPHA + 1 ; 汇编器会报错!

相比之下,使用=定义的常量是可以重新定义的。

; 使用 = 定义常量
COUNT = 7
; 可以重定义
COUNT = 8 ; 正确!
COUNT = COUNT + 1 ; 正确!

这种区别在实际编程中很重要:如果你想定义一个绝对不应该改变的常量(比如硬件端口地址),应该用 EQU;如果你需要一个可以更新的计数器或变量,应该用 “=

这就像现代高级语言中的:

const(常量,不能改变)相当于 EQU

let/var(变量,可以改变)相当于 “=

6. 地址对齐伪指令#

; ORG - 设置起始地址
ORG 100H
START:
; EVEN - 偶地址对齐
A DB 'morning'
EVEN ; 确保下一个数据从偶地址开始
B DW 2 DUP (?)
; ALIGN - 按指定边界对齐
ALIGN 4 ; 按4字节边界对齐

7. 地址计数器:$#

; 跳过空间
ORG $+8 ; 跳过8个字节
; 在数组中使用
ARRAY DW 1, 2, $+4, 3, 4, $+4

8. 基数控制#

.RADIX 16 ; 设置默认数字为16进制
MOV BX, 0FF ; 16进制的FF
MOV BX, 178D ; 10进制的178 (D后缀表示十进制)
从零开始的汇编语言笔记(4)
https://fuwari.vercel.app/posts/course/assembly/assembly-note-4/
作者
wegret
发布于
2025-01-02
许可协议
CC BY-NC-SA 4.0