之前汇编课都没听,啥也不会,距离上机考试就剩几天了,希望能快速速成一下。下面是本人考前的速通笔记。
(考完补充:本人上机结果还不错,全班第二个打完,可见汇编语言还是可以速通的!)
(汇编的课本写得极其诡异,需要自己细品一下就能看懂。)
环境与DOSBOX
启动、编辑、运行命令
(这里的命令只是用于我自己备忘,具体配置需自行处理。)
启动命令:
mount c: d:/masmc:编辑、编译运行命令
edit <文件名>.asmmasm <文件名>.asm # 汇编link <文件名>.obj # 链接<文件名>.exedebug命令:
debug program.exer # 查看当前寄存器状态r ax
t # 单步执行d cs:0100 # 查看内存
u CS:IP # 查看当前CS:IP开始的几行汇编代码DOSBOX
全屏/退出全屏:alt+enter
释放鼠标:ctrl+F10
汇编语言入门:a+b与Hello world!
一般一开始学习一个语言最重要的就是数据类型。
在汇编中,实际上是和高级语言中数据类型很不同的,汇编主要处理的是内存和寄存器中的原始字节。
数据定义指令
数据定义指令用于在数据段中初始化数据。
DB(字节,Define Byte)
格式是DB <值1>, <值2>, ..., <值N>
例如:
myChar DB 65 ; 分配一个字节,并初始化为65(ASCII码的'A')myChars DB 'A', 'B', 'C' ; 分配并初始化为三个字符DW(字,Define Word)
格式是DW <值1>, <值2>, ..., <值N>(汇编语言中,一个字通常是2个字节。)
例如:
myWord DW 0x1234 ; 分配一个字,并初始化为16位数0x1234myWords DW 0x1234, 0x5678 ; 分配两个字,并分别初始化寄存器的含义
上述定义的数据,在汇编语言中,通常是不能直接在数据段中定义的数据上运算的。因为大多数处理器的指令集,并不支持直接在内存地址上执行算术或逻辑运算。
通常运算需要如下步骤:
① 将数据从内存中加载到寄存器中。
② 在寄存器上执行运算。
③ 把结果存回内存(如果有必要)。
可以把寄存器视为用于工作的桌子,内存视为放材料的柜子。这个桌子的空间非常有限(因为寄存器的数量有限),但是访问非常快捷高效,就像你可以随手拿起桌子上的东西一样。内存则像是一个储存材料的柜子。它的空间相对较大,可以存放很多材料(数据和程序代码),但是从柜子中取材料或放回材料的速度没有桌子上操作那么快。程序运行时需要的数据大多数时间都是存放在这里的。
寄存器感觉是很多的,所以这里先掠过,后面细写。先记住最基础的数据寄存器AX,BX,CX,DX这4个即可。
简单a+b程序、基础代码结构
下面是一个简单的a+b程序,主要逻辑仅仅使用了AX寄存器。为了简单这里也没有任何直接输出的代码,因此运行结束后用debug来查看结果。
;--------数据段开始----------datasg segment a dw 5 ; 初始化 a 为 5 b dw 3 ; 初始化 b 为 3 result dw 0 ; 用于存储结果datasg ends
;--------代码段开始---------codesg segmentassume cs:codesg, ds:datasgmain proc far ; 初始化数据段寄存器 push ds sub ax, ax push ax mov ax, datasg mov ds, ax
; 计算 a + b mov ax, [a] ; 将 a 的值加载到 ax add ax, [b] ; 将 b 的值添加到 ax mov [result], ax ; 将结果存储到 result 变量中
; 结束程序 mov ax, 4C00h ; 返回操作系统命令 int 21h ; 调用中断main endpcodesg endsend maindebug时,输入u命令,查看当前代码段,然后一直t,可以看见最后AX变成了额3+5=8。

在上面可以看见result的地址是0004,下面查看这个位置的结果:

这里列出了ds:0004开始后面的所有结果,可以看出来result确实是08。
重新来看上述的代码结构:
;--------数据段开始----------datasg segment a dw 5 ; 初始化 a 为 5 b dw 3 ; 初始化 b 为 3 result dw 0 ; 用于存储结果datasg ends
;--------代码段开始---------codesg segmentassume cs:codesg, ds:datasgmain proc far ; 初始化数据段寄存器 push ds sub ax, ax push ax mov ax, datasg mov ds, ax
; 计算 a + b mov ax, [a] ; 将 a 的值加载到 ax add ax, [b] ; 将 b 的值添加到 ax mov [result], ax ; 将结果存储到 result 变量中
; 结束程序 mov ax, 4C00h ; 返回操作系统命令 int 21h ; 调用中断main endpcodesg endsend main除了datasg数据段外,主要关注代码段。
codesg segment ; 代码段开始 assume cs:codesg, ds:datasg main proc far ; 过程开始 ; 程序指令 main endp ; 过程结束codesg ends ; 代码段结束end main ; 程序结束,入口点为 main上述指令中,end告诉汇编器编译结束位置,并指定程序的入口点(起始执行位置)。
寄存器

1. 通用寄存器
数据寄存器
也就是前面的AX,BX,CX,DX四个寄存器,每个是十六位的,也就是两个字节,也就是一个字。
间接寻址寄存器
两个基址寄存器BX、BP和两个变址寄存器SI、DI构成。
除了可以用来存放要访问的内存单元的偏移地址外,还可以作为通用寄存器使用,来存放操作数和计算结果。和数据寄存器在这一点上没差别。
(一般来讲,SI用于源数据的索引,指向要读取的数据。DI作为目标数据索引,指向要写入的数据。)
指针寄存器
堆栈指针SP和基指针BP。(Stack Pointer, Base Pointer)
也可以作为通用寄存器使用,来存放操作数和计算结果。和数据寄存器在这一点上没差别。
2. 段寄存器
我们知道,CPU会把主存划分为若干段,把要运行的程序各部分放在不同的存储段中,每个存储段用一个段寄存器来指示首地址。

CS:代码段寄存器,用于指向包含程序执行代码的段。指令指针寄存器 IP(或在更高级的模式下的 EIP、RIP)与 CS 一起决定了当前执行指令的完整地址。
DS:数据段寄存器,用于指向程序中适用的大部分变量数据所在的段。在访问内存数据时,没有指定段寄存器,CPU 通常会使用 DS 作为默认寄存器。
ES:附加段寄存器。通常用于在数据传输中作为一个附加的段寄存器,特别是在执行字符串和其他大块数据操作时,它可以作为源地址或目标地址的段。
SS:堆栈段寄存器。堆栈用于存储局部变量、返回地址、历史寄存器状态等。堆栈指针寄存器 SP(或 ESP, RSP)和 SS 一起决定了当前堆栈的完整地址。
一般来讲,初学时,最重要的是CS和DS。
在前面的a+b程序中,可以看见这二者是怎么被设置的:
;--------数据段开始----------datasg segment a dw 5 b dw 3 result dw 0datasg ends
;--------代码段开始---------codesg segmentassume cs:codesg, ds:datasgmain proc far ; 初始化数据段寄存器 push ds sub ax, ax push ax mov ax, datasg mov ds, ax
; 计算 a + b mov ax, [a] ; 将 a 的值加载到 ax add ax, [b] ; 将 b 的值添加到 ax mov [result], ax ; 将结果存储到 result 变量中
; 结束程序 mov ax, 4C00h ; 返回操作系统命令 int 21h ; 调用中断main endpcodesg endsend main在上述程序中,这句话
assume cs:codesg, ds:datasg告诉了汇编器段寄存器(如 CS, DS, ES, SS)当前假设(assume)指向的段。这里,当前的 CS 寄存器被假设为指向 codesg 段,而 DS 被假设为指向 datasg 段。
但是,实际上,尽管 assume 指令对汇编器的地址解析非常有用,但它并不会改变或设置处理器中实际的段寄存器值。(仅仅只是一个声明!)
因此,当程序在 CPU 上运行时,实际的段寄存器需要通过指令显式设置。也就是后面的语句:
mov ax, datasg mov ds, axCS作为代码段寄存器,与其他段寄存器不同,它是不需要显式设置的,我们最后的end main,不仅规定了程序的结束点,也规定了程序的起点main。操作系统在加载程序到内存并执行时,会自动将代码段寄存器(CS)设置为对应的值,确保执行从 main 开始。因此,程序员不需要(也不应该)在程序中手动设置 CS 寄存器,
但是DS寄存器不同。这句话
mov ax, datasg mov ds, ax将ds设置成了datasg的地址。为什么要用ax做中间的寄存器?因为在x86架构中,段寄存器(如DS、ES、SS等)不能直接接收一个立即数(即直接编码在指令中的数值)或内存地址作为操作数。这就需要通用寄存器来中转。
除此之外,ss(堆栈段寄存器)也是无需显式设置的,在大多数操作环境中,尤其是在现代操作系统上运行的程序,操作系统在启动任何程序之前已经为其配置好了堆栈。这包括设置SS(堆栈段寄存器)和SP(堆栈指针寄存器)。
3. 指令指针IP
IP是程序不能访问的寄存器。是指令的指针地址,在程序运行期间,CPU会自动修改IP的值,使得它始终是正在执行指令的下一条指令代码的起始字节的偏移量。
有些指令可以自动修改IP的值,例如JMP,JNE等。
执行调用子程序指令CALL时,IP原有内容自动压入堆栈,把子程序入口地址偏移量自动送入IP。从子程序返回主程序时,返回指令RET又会自动从堆栈弹出原有IP的内容送回IP。
4. 标志寄存器

进位标志CF:最高位产生进位或借位,或者位移指令中最低位和最高位移出的代码。
奇偶标志:操作结果含1是偶数,则是1。
辅助进位AF:低半字节和高半字节产生进位或借位。只和低八位有关。
符号标志:与结果的最高位有关。
指令系统
80x86的指令系统可以分为:数据传送指令、算数指令串处理指令、
(一)数据传送指令
1. 通用数据传送指令:MOV、PUSH、POP、XCHG
① MOV
WARNING
MOV等双操作数指令,不允许两个操作数都使用存储器,必须至少有一个是寄存器。立即数和内存地址,不能直接传入段寄存器,只能是寄存器传入。例如,我们前面程序这句话:
mov ax, datasgmov ds, ax
MOV不影响标志位。
MOV BX, OFFSET TABLE ; 把偏移地址传入BXMOVSX和MOVZX是带符号和带零扩展指令。
② PUSH、POP
假设我们有一个主程序,需要调用一个子程序来计算两个数的和。在调用子程序之前,主程序可能会使用某些寄存器,而子程序也需要使用寄存器来进行计算。为了避免子程序的操作影响到主程序中寄存器的值,我们需要在调用子程序之前保存寄存器的状态,并在子程序返回后恢复这些寄存器。这时,PUSH 和 POP 指令就显得非常重要。
2. 累加器专用传送指令:IN、OUT、XLAT
① IN、OUT
IN 指令:从I/O端口读取数据到累加器(AL 或 AX)。
OUT 指令:将累加器中的数据(AL 或 AX)写入到I/O端口。
IN AL, DX ; 从端口地址由DX指定的I/O端口读取一个字节到ALIN AX, DX ; 从端口地址由DX指定的I/O端口读取两个字节到AX
OUT DX, AL ; 将AL中的一个字节数据写入端口地址由DX指定的I/O端口OUT DX, AX ; 将AX中的两个字节数据写入端口地址由DX指定的I/O端口I/O操作专用:IN 和 OUT 是专门用于与硬件设备的I/O端口进行通信的指令。这些指令直接与CPU的I/O总线交互,而 MOV 指令只能在CPU内部或内存与寄存器之间传输数据,无法直接与I/O端口通信。
硬件交互:IN 和 OUT 允许程序与外部设备(如键盘、显示器、硬盘控制器等)进行数据交换,这是 MOV 无法实现的功能。
② XLAT
Translate
XLAT(Translate)指令用于将一个字节的值从查找表中翻译为另一个值。它通过 AL 寄存器中的索引值,从查找表(由 BX 寄存器指向)中加载对应的字节,并将其存储回 AL。
在使用前,应该先建立一个字节表格,表格首地址提前存入BX或EBX。需要转换的代码(也就是相对于首地址的偏移量)存入AL寄存器。
3. 地址传送指令
① LEA
把源操作数的有效地址送到指定寄存器。
LEA BX, [BX+SI+0F62H]; 执行后 (BX)=(BX)+(SI)+0F62H
LEA BX, LISTMOV BX, OFFSET LIST; 上述是等价的② LDS、LES…
4. 标志寄存器传送指令
5. 类型转换指令:CBW、CWD/CWDE
本组指令不影响标志位。
(二)算数指令
① ADD、SUB
除此之外还有ADC(带进位加法),DST <- SRC + DST + CF
SBB(带借位减法),DST <- DST - SRC - CF
② INC、DEC
INC不影响CF标志位。
③ NEG
求补。也就是取反后+1.
④ CMP
进行减法计算,但不保留结果,只会根据结果设置条件标志位。可以在后面跟上条件转移指令,比如JMP
⑤ MUL、IMUL
格式:
MUL SRC存的结果会自动扩两倍。
字节操作数:(AX) <- (AL) * (SRC)
字操作数:(DX, AX) <- (AX) * (SRC)
双字操作数:(EDX, EAX) <- (EAX) * (SRC)
IMUL是带符号数乘法。
TIP
MUL和IMUL指令可以通过CF和OF均为0来判断扩展出来的字或字节是否有被用到。
⑥ DIV
DIV SRC根据SRC的长度来分配2倍长度的AX或(DX, AX)或(EDX, EAX)做被除数。
商存在与SRC同样长度的AL或AX或EAX。
⑦ 十进制调整指令
我们知道BCD码是用二进制编码的十进制。压缩的BCD码是我们常见的那种,4位为一个数字。非压缩的BCD码,一个数字也是一个字节,低4位表示数值,高4位没有意义。因此ASCII码也是一种非压缩的BCD码,高4位为0011,低4位是数值。
(三)逻辑指令
① AND、OR、NOT、XOR、TEST
② SHL、SAL(逻辑和算术左移),SHR、SAR(逻辑和算术右移)
SHL OPR, CL/1③ ROL、ROR(循环左右移动),RCL、RCR(带CF进位)
(四)串处理指令
在汇编语言中,“串”(String)通常指的是一系列连续存储的字符数据。串在汇编程序中的处理方式稍有不同于高级编程语言,但基本概念类似。串可以是任何长度,通常由程序员定义,并且可以包括字符、数字和其他数据类型。在 x86 汇编中,串操作是通过一组特定的指令来实现的,这些指令专门用于处理内存中的连续字节序列。
① MOVS:串移动
在数据段中有 一个字符串,要求把它转送到附加段的缓冲区中。
dataseg segment mess1 db 'Hello world!', '$' ; 长度为13dataseg ends
extra segment mess2 db 30 dup (?)extra ends
codeseg segment assume cs: codeseg, ds: dataseg, es: extramain proc far push ds sub ax, ax push ax
mov ax, dataseg mov ds, ax
mov ax, extra mov es, ax
lea si, mess1 lea di, mess2 mov cx, 13 cld rep movsb
pop ax pop ds
mov ax, 4C00h int 21h
main endpcodeseg ends
end main如果想把字符串反向移动过去呢?
在数据段中有 一个字符串,要求把它反向后,转送到附加段的缓冲区中。
这里因为标志寄存器是同时影响di和si的,所以需要手动写一个循环。
dataseg segment mess1 db 'Hello world!', '$' ; 长度为13,实际使用12字符dataseg ends
extra segment mess2 db 30 dup (?) ; 为目标字符串预留空间extra ends
codeseg segment assume cs: codeseg, ds: dataseg, es: extramain proc far push ds sub ax, ax push ax
mov ax, dataseg mov ds, ax
mov ax, extra mov es, ax
lea si, [mess1 + 11] ; 将 SI 初始化为源字符串的最后一个有效字符 ('!') lea di, mess2 ; 将 DI 初始化为目标区域的开始 mov cx, 12 ; 设置计数器为有效字符长度,不包括结尾的 '$'
reverse_copy_loop: mov al, [si] mov es:[di], al dec si inc di loop reverse_copy_loop
pop ax pop ds
mov ax, 4C00h int 21h
main endpcodeseg ends
end main
如果是字移动,也一样的。
dataseg segment mess1 dw 1, 2, 3, 4, 5 ; 定义一个包含5个字的数组dataseg ends
extra segment mess2 dw 5 dup (?) ; 为目标数组预留5个字的空间extra ends
codeseg segment assume cs: codeseg, ds: dataseg, es: extramain proc far push ds xor ax, ax push ax
mov ax, dataseg mov ds, ax
mov ax, extra mov es, ax
lea si, mess1 + 8 ; mess1 + (5-1)*2 = mess1 + 8,指向最后一个字 lea di, mess2
mov cx, 5
reverse_copy_loop: mov ax, [si] ; 从 DS:[SI] 读取一个字到 AX mov es:[di], ax ; 将 AX 中的字存入 ES:[DI] sub si, 2 ; SI 递减2,指向前一个字 add di, 2 ; DI 递增2,指向下一个目标位置 loop reverse_copy_loop ; CX 减1,若不为0则继续循环
pop ax pop ds
mov ax, 4C00h int 21h
main endpcodeseg ends
end main
② STOS、LODS:串赋值、串取值
STOS DSTSTOSBSTOSWSTOSD可以将DST地址赋值为AL或AX(是什么取决于STOSB或STOSW)。
LODSB ; 将 [DS:SI] 指向的一个字节加载到 AL 中,并把 SI + 1 或 -1LODSW ; 将 [DS:SI] 指向的一个字加载到 AX 中,并把 SI + 2 或 - 2LODSD ; 将 [DS:SI] 指向的一个双字加载到 EAX 中,并把 SI + 4 或 - 4一般来讲,这个不和REP联用,可以用于逐次取出测试时。
dataseg segment mess1 dw 1, 3, 5, 7, 9 ; 定义一个包含5个字的数组dataseg ends
extra segment mess2 dw 5 dup (?) ; 为目标数组预留5个字的空间extra ends
codeseg segment assume cs: codeseg, ds: dataseg, es: extramain proc far push ds xor ax, ax push ax
mov ax, dataseg mov ds, ax
mov ax, extra mov es, ax
lea si, mess1 + 8 ; mess1 + (5-1)*2 = mess1 + 8,指向最后一个字 lea di, mess2
mov cx, 5reverse_copy_loop: std lodsw cld stosw loop reverse_copy_loop
pop ax pop ds
mov ax, 4C00h int 21h
main endpcodeseg ends
end main
③ REPE、CMPS、SCAS
REPE与REP几乎一致,只是增加了除了Count Reg=0的结束条件外,ZF!=0也可以结束条件(也就是说,两数相等就可以继续比较,不等就提前结束)。除此之外,还可以写作REPZ。
编写一个汇编程序,在一个较大的目标字符串
target中查找子字符串pattern的位置。若找到,返回子字符串在目标字符串中的起始索引;否则,返回-1。示例数据:
data_segment segmenttarget db 'The quick brown fox jumps over the lazy dog',0pattern db 'brown',0index dw ?data_segment ends
dataseg segment target db 'The brown fox.','$' pattern db 'brown','$' index dw -1
dataseg ends
codeseg segment assume cs:codeseg, ds:dataseg, es: datasegmain proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax mov ax, dataseg mov es, ax
lea bx, target ; 这里没限制cx可能会有点问题,后面想起来search: mov si, bx lea di, pattern mov cx, 5 repe cmpsb je is_match inc bx
cmp byte ptr [bx], '$' jz over
jmp search
is_match: mov index, bxover: pop ax pop ds mov ax, 4C00h int 21h
main endpcodeseg ends
end main
编写一个汇编程序,在一个较大的目标字符串
target中查找字符char_count的出现次数。
dataseg segment target db 'The quick brown fox jumps over the lazy dog', '$' char_count db 'o' count dw 0dataseg ends
codeseg segment assume cs:codeseg, ds:dataseg, es: datasegmain proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax mov ax, dataseg mov es, ax
mov al, char_count lea di, target mov cx, [char_count - target] mov bx, 0
search_loop: repne scasb jne over inc bx jmp search_loop
over: pop ax pop ds mov ax, 4C00h int 21h
main endpcodeseg ends
end main
(五)控制转移指令
(六)子程序
① 基本结构
; 主程序段main proc ; 调用子程序 call Subroutine
; 其他代码
; 程序结束 mov ax, 4C00h int 21hmain endp
; 子程序段Subroutine proc ; 子程序的代码 ; ...
; 返回主程序 retSubroutine endp② 参数传递
可以通过寄存器传递、堆栈传递、内存地址传递、
(七)中断
中断中最常用的是int 21h,是DOS提供的主要中断,用于执行各种系统调用,文件操作、设备输入输出、内存管理等。具体功能由 AH 寄存器的值指定。
① 打印字符到屏幕
在前面都是用debug命令调试来看寄存器值,这里可以用这个中断打印到屏幕。
寄存器设置:
AH = 09hDX指向以$结尾的字符串
dataseg segment message db 'Hello world!', '$'dataseg ends
codeseg segment assume cs:codeseg, ds:datasegmain proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax
mov ah, 09h lea dx, message int 21h
pop ax pop ds mov ax, 4C00h int 21h
main endpcodeseg ends
end main
② 从标准输入读取字符
寄存器设置:
AH=01h,返回值是AL,等于读取的字符。AH=0Ah,读取一个字符串,需要特定的缓冲区:第一个字节是缓冲区的最大字符数,第二个字节由DOS填充,是实际输入的字符数,后续字节用于存储输入的字符。
下面做一个输出输入字符串的代码。
dataseg segment prompt_input db 'Enter a string: ', '$' buffer_input db 50 buffer_input_len db 0 buffer_input_data db 50 dup(?)
prompt_output db 'You entered: ', '$'
dataseg ends
codeseg segment assume cs:codeseg, ds:datasegmain proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax
; 打印输入提示 mov ah, 09h lea dx, prompt_input int 21h ; 读取输入 mov ah, 0Ah lea dx, buffer_input int 21h
call newline
; 打印输出提示 mov ah, 09h lea dx, prompt_output int 21h ; 添加 $ lea si, buffer_input_data xor bx, bx mov bl, buffer_input_len add si, bx mov byte ptr [si], '$' ; 打印字符串 mov ah, 09h lea dx, buffer_input_data int 21h
pop ax pop ds mov ax, 4C00h int 21h
main endp
newline proc near push ax push dx mov ah, 02h
mov dl, 0Dh int 21h mov dl, 0Ah int 21h
pop dx pop ax retnewline endp
codeseg ends
end main
系统功能调用
中断
中断分为硬件中断和软件中断。
硬件中断:由硬件设备(如键盘、定时器等)发出信号,CPU 响应中断并跳转到相应的中断服务程序(Interrupt Service Routine, ISR),来处理硬件事件。
软件中断:由软件使用 INT n 指令主动触发,并由操作系统或 BIOS 提供的中断向量来执行特定功能。例如在 DOS 下常见的 INT 21h,可用于文件操作、控制台输入输出等。这些指令实际上是“系统提供的功能接口”。
在 8086 环境中,「系统功能调用」通常是通过软件中断(Software Interrupt)来完成的。
在现代操作系统中,系统功能调用往往是一种“用户态向内核态请求服务”的方法。它可以通过多种技术实现,比如特殊指令 (如 Linux 的 syscall)、中断指令 (如早期 DOS、BIOS 环境下的 INT 21h、INT 10h 等)。
对于 8086/DOS 编程时代,“系统功能调用”大多数情况下就是通过软件中断(比如 INT 21h)来执行的,也就是说想要访问操作系统提供的功能(读写文件、输出字符串、获取系统时间等),就会写一个 INT 21h,并通过寄存器传递不同的功能号和参数。
所以在 DOS/BIOS 里常见的系统服务(读写磁盘、文本输出等)需要用 INT n 指令才能实现,这和 Linux/Windows 里的系统调用机制在概念上相似,只不过实现方式和调用约定不同。
程序直接控制I/O
外设接口由一组寄存器(数据寄存器/状态寄存器/命令寄存器)组成,每个寄存器有一个端口地址(端口号)。

I/O指令
在 8086 体系结构中,硬件设备可以通过“端口(I/O port)”来与 CPU 交互。
8086 CPU 在汇编层面提供了 IN/OUT 指令用于访问 I/O 端口,原理上它与访问内存的 MOV 指令是不同的总线周期。
AL 是 8 位寄存器,AX 是 16 位寄存器。
当我们执行 IN AL, port,表示从端口读取 8 位数据放到 AL;如果是 IN AX, port,则读取 16 位数据放到 AX(相当于先读端口低字节到 AL,再读端口高字节到 AH)。
类似地,OUT port, AL 是输出 8 位;OUT port, AX 是输出 16 位。
PORT 是一个在指令中直接写死的 8 位立即数(只支持 0~255 范围内的端口号)。
DX 可以在程序中通过 mov dx, SomePortValue 动态设置更大的端口号(0~65535)。
当端口号大于 255 时,必须用 DX 传递端口号,而不能直接用 PORT 立即数。
示例:控制喇叭

; 读端口0x61,然后翻转bit1产生方波mov dx, 61h ; 端口0x61放到DXin al, dx ; AL ← (0x61)xor al, 00000010b ; 翻转第1位out dx, al ; (0x61) ← AL完整喇叭控制代码:
dataseg segmentdataseg ends
codeseg segmentassume cs:codeseg, ds:dataseg
main proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax
; --- 发声循环开始 --- mov dx, 100h ; 控制循环次数 in al, 61h ; 读 0x61 端口 and al, 11111100b ; 先清除 bit0/bit1,关闭喇叭 out 61h, al
sound_loop: xor al, 00000010b ; 翻转 bit1 (产生方波) out 61h, al mov cx, 140h ; 延时,影响音调delay1: loop delay1
dec dx jne sound_loop ; --- 发声循环结束 ---
; 退出前关掉喇叭 in al, 61h and al, 11111100b out 61h, al
pop ax pop ds mov ax, 4C00h int 21h
main endpcodeseg ends
end main示例:打印字符

数据寄存器的端口地址为378H
start: mov si , offset mess ;message offset mov cx , count ;count of charnext: mov dx , 379h ;状态寄存器wait1: in al , dx test al , 80h ;最高位是否为1 je wait1 ;yes, test again mov al , [si] ;no, read a char. mov dx , 378h ;数据寄存器 out dx , al ;putout to data port mov dx , 37ah ;控制寄存器 mov al , 0dh ;00001101 out dx , al ;send a strobe = 1 mov al , 0ch ;00001100 out dx , al ;strobe = 0 inc si ;addr. of mess increment loop next ;next char;---------------------------------------------; 简要说明:; 1) 本代码示范如何通过并行端口 LPT1 (0x378 ~ 0x37F); 输出数据(字符),并使用状态寄存器(0x379)、控制寄存器(0x37A); 2) 假设 LPT1 的地址为 0x378; - 数据寄存器:0x378; - 状态寄存器:0x379 (只读); - 控制寄存器:0x37A (读/写); 3) 打印流程简化:先读状态(忙碌位?),如果忙则等待,不忙则输出数据并“触发选通(strobe)”;---------------------------------------------
dataseg segment ; 待输出字符串(示例) mess db 'Hello LPT1!', 0Dh, 0Ah, '$' ; 用 '$' 结尾是常见习惯(如DOS字符串),也可以不这样做
; 计算字符串长度(不含 '$' 本身) count equ ($ - mess - 1)dataseg ends
codeseg segmentassume cs:codeseg, ds:dataseg
start: ;------------------------------ ; (1) 初始化数据段 DS ;------------------------------ mov ax, dataseg mov ds, ax
;------------------------------ ; (2) 准备要输出的字符串 ;------------------------------ mov si, offset mess ; SI 指向 mess 首地址 mov cx, count ; CX=字符数量
next_char: ;------------------------------ ; (3) 读 LPT 状态端口(0x379) ; bit7 (0x80) 通常表示打印机忙(Busy)或空闲 ;------------------------------ mov dx, 379h ; 并行口状态寄存器地址wait_busy: in al, dx ; 读状态 test al, 80h ; 检查最高位(Busy位)是否为1 je wait_busy ; 如果=1(忙), 就一直等
;------------------------------ ; (4) 将一个字符送到 数据寄存器(0x378) ;------------------------------ mov al, [si] ; AL = mess[SI],取当前要打印的字符 mov dx, 378h ; 并行口数据寄存器 out dx, al ; 向数据寄存器输出一个字节
;------------------------------ ; (5) 设置控制寄存器(0x37A),产生一次 strobe 脉冲 ; 一般手法:先置 strobe=1,再置回0 ; 0x0D = 0000 1101b ; 0x0C = 0000 1100b ; 具体bit定义因硬件而异,但普遍bit0为Strobe(取反)、bit1 = AutoFeed、bit2=Init、bit3=Select等等 ;------------------------------ mov dx, 37Ah ; 并行口控制寄存器 mov al, 0Dh ; 0000_1101b : strobe=1, ... out dx, al mov al, 0Ch ; 0000_1100b : strobe=0, ... out dx, al
;------------------------------ ; (6) 准备打印下一个字符 ;------------------------------ inc si ; SI++ loop next_char ; CX自动减1,不为0则继续
;------------------------------ ; (7) 程序结束,返回DOS ;------------------------------ mov ah, 4Ch int 21h
codeseg endsend start中断
在 8086/DOS 那个时代,硬件端口(I/O Port) 和 中断(Interrupt) 都可能用于访问或控制同一个设备,但它们是从不同层面实现的两种机制。
程序直接控制 I/O 端口:程序可以完全按自己的节奏来访问硬件寄存器,操作非常“底层”和“直接”。不依赖操作系统或 BIOS 的服务,想怎么读写端口就怎么写。对于裸机环境(没有操作系统)或极其底层的驱动编程,需要直接操作硬件时非常方便。
但是需要了解硬件细节:端口号、寄存器各位的含义、初始化顺序等。不同硬件、不同主板可能差异较大,如果写错或者没有相应权限(在现代操作系统中一般会被限制),程序就会出错或无法访问。程序自己要处理所有可能的异常和同步问题(如键盘按键还没到达,或者打印机忙等),实现复杂。
中断:
硬件中断:
当硬件事件发生时,CPU 会异步地中断当前程序,跳转到对应的中断服务例程 (ISR)去处理。例如:
-
键盘按下产生硬件中断 (IRQ1 → INT 09h),ISR 会读取 0x60 端口里的扫描码,放到缓冲区;
-
时钟芯片(8253/8254)在固定频率下产生时钟中断 (IRQ0 → INT 08h),用来更新系统时间;
-
硬盘/软盘控制器、网卡等也可能产生中断通知 CPU “我已经传完数据了”,ISR 去做后续处理。
在这些过程中,对端口的读写依然是存在的,只不过不一定是你的主程序直接执行
IN/OUT,而是中断服务例程在适当时机去访问。
WARNING在传统的 8086/PC 架构下,按键输入首先会触发硬件中断(IRQ1),CPU 接收到此硬件中断信号后,会跳转到键盘中断服务例程(通常对应 INT 09h)去读取键盘端口(0x60)的数据,并保存到键盘缓冲区。应用程序往往再通过 BIOS 或操作系统提供的软件中断(如
INT 16h)读取已经保存在缓冲区里的按键信息。因此,从硬件层面看,键盘按下 = 硬件中断; 而从应用程序角度去取按键数据时,通常是软件中断或 API 调用(比如 BIOS 的
INT 16h,DOS 的INT 21h等),但那只是读取已经由硬件中断收集到的数据。
中断向量表:是 CPU 在执行中断时,用来“找到对应中断处理例程入口地址(CS
在 DOS 时代,操作系统为我们提供了一系列“功能号(Function Number)”,用来做文件操作、打印字符、读键盘、获取时间等等。这些功能统一通过 INT 21h 这个“中断号”来调用:你把对应的功能号放进寄存器 AH,执行 int 21h,DOS 内部就会去做相应的功能。
mov ah, 09h+int 21h→ DOS 显示字符串(直到遇到$)。mov ah, 4Ch+int 21h→ DOS 退出程序并返回控制给操作系统。- 还有很多其他,如
AH=1表示从键盘读一个字符等。
中断向量表
在 8086 实模式下,内存最开始的一段区域(从物理地址 0000:0000 开始)专门存放了 256 个“中断向量”——也可以理解为 256 个“指针”,每个指针占 4 个字节(前2字节是偏移 IP,后2字节是段地址 CS)。
“中断号”从 0 到 255,每个号对应这表里的一条记录(n*4处)。CPU 执行 INT n 或响应硬件中断 n 时,就去这条记录里取出真正的处理函数入口 CS
比如 键盘中断 通常是 IRQ1,对应的是“中断号 09h (INT 9)”。按下键后,键盘控制器发出硬件信号 -> CPU 响应中断 09h -> 去中断向量表的 09h 项里,找到 BIOS/OS 对键盘的处理函数 -> 执行读 0x60 端口、存缓冲的过程。
处理完后,用 IRET 返回原程序。
当指令是 INT 21h,CPU 依然会去“向量表中第 21h 条”找到地址,然后跳过去。DOS 正是把“21h 这条中断向量”指向了它的内核函数入口。
这样,每次执行 int 21h,就能调到 DOS 做事了。
例8.4 用DOS功能调用存取中断向量
MOV AL, N ; N为要挂接的中断号MOV AH, 35H ; DOS 功能号 35h = “获取指定中断号的中断向量”,返回的段:偏移地址存放在 ES:BX。INT 21H ; 取原中断向量,返回ES:BX = 原中断服务例程地址PUSH ESPUSH BX ; 保存原中断向量
; 准备新中断向量指向INTHAND (本段中的例程)PUSH DS ; 暂存当前DSMOV AX, SEG INTHANDMOV DS, AXMOV DX, OFFSET INTHAND
; 把中断号 N 的入口设为 DS:DXMOV AL, NMOV AH, 25H ; DOS功能:Set Interrupt VectorINT 21H ; 设置新的中断向量
POP DS……POP DXPOP DSMOV AL, NMOV AH, 25HINT 21H ; 恢复原中断向量RET;INTHAND: …… IRET示例:闹钟程序
编写一个中断处理程序,要求在主程序运行期间,每隔 10秒响铃一次,同时显示
‘The bell is ring!’
核心思路是:
- 挂接 INT 1Ch:这是 BIOS 定时器中断“钩子”向量,每秒会被调用约 18.2 次;
- 在自定义中断处理程序里用一个
count计数,累积到约 10 秒时就执行“响铃+打印”的动作,然后重置计数; - 主程序只是在空循环中“做事”,真正的周期动作全靠时钟中断来触发;
- 退出前若需要,可恢复原 INT 1Ch 向量。
下面的代码尽量简化,以便更好理解。可用 MASM 或 TASM 编译为 .EXE(在 DOSBox 或真实 DOS 下运行),能看到每 10 秒打印一次提示并响铃。
提示:由于这是挂接中断,需要“允许IRQ0”与
sti。在 DOSBox 中应该默认打开时钟中断,但若有问题,可检查in/out 0x21屏蔽位。
dataseg segment msg db 'The bell is ring!', 0ah, 0dh, '$' count dw 182 ; 18.2 * 10 ≈ 182, 表示大约10秒
oldvect dw 2 dup(?)dataseg ends
codeseg segmentassume cs:codeseg, ds:dataseg, es:dataseg
main proc far push ds xor ax, ax push ax mov ax, dataseg mov ds, ax mov es, ax
mov al, 1ch mov ah, 35h int 21h
mov [oldvect], bx mov [oldvect+2], es
mov bx, word ptr count
mov dx, offset newIntHandler mov ax, seg newIntHandler mov ds, ax mov al, 1ch mov ah, 25h int 21h
mov ax, dataseg mov ds, ax
sti
main_loop: nop jmp main_loop
pop ax pop ds ret
main endp
ring proc near push dx push cx push ax
push ds
mov ax, dataseg mov ds, ax
; --- 发声循环开始 --- mov dx, 100h ; 控制循环次数 in al, 61h ; 读 0x61 端口 and al, 11111101b ; 先清除 bit0/bit1,关闭喇叭 out 61h, al
sound_loop: xor al, 00000010b ; 翻转 bit1 (产生方波) out 61h, al mov cx, 140h ; 延时,影响音调 delay1: loop delay1
dec dx jne sound_loop ; --- 发声循环结束 ---
; 退出前关掉喇叭 in al, 61h and al, 11111101b out 61h, al
pop ds pop ax pop cx pop dx retring endp
newIntHandler proc near
dec bx jnz skip
call ring lea dx, msg mov ah, 09h int 21h mov bx, word ptr count
skip:
iretnewIntHandler endp
codeseg endsend main