汇编指令
mov 和 add 指令
转移指令 jmp
JMP
指令是汇编语言中的 无条件转移指令,用于将程序的执行流程跳转到指定的目标地址(代码位置)。当 JMP
指令被执行时,CPU 会将目标地址加载到 指令指针寄存器(IP/EIP/RIP) 中,从而改变程序的执行路径。
下面是 jmp 指令的用法:
-
jmp 指令用于同时修改 CS 和 IP 的内容
-
语法:
jmp 段地址:偏移地址
- jmp 2AE3:3
- jmp 3:0B16
-
功能:用指令给出的段地址修改 CS, 偏移地址修改 IP
-
-
仅修改 IP 的内容
- 语法:
jmp 某一合法寄存器
- jmp ax(类似于 mov IP, ax)
- jmp bx
- 功能:直接跳转到
CS:某一寄存器
指定的地址
- 语法:
举例如下:
汇编语言程序设计
首先给出一段完整可执行的汇编语言程序:
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00h
int 21h
codesg ends
end
该代码的部分:
- 伪指令:编译器执行的指令,没有对应机器码
assume cs:codesg
codesg segment
codesg ends
end
- 汇编指令:编译为机器码的指令
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00h
int 21h
三种伪指令
汇编语言代码示例
编写一个计算 2^3 的代码:
assume cs:asc
abc segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00h
int 21h
abc ends
end
代码解释:
-
伪指令
assume cs:asc
:指令告诉汇编器当前代码段使用的段寄存器是CS
,且段名为asc
;assume
声明仅用来表明段寄存器的逻辑关联,方便汇编器生成正确的代码。abc segment
:声明一个名为abc
的代码段。
-
代码段
- 一次
mov
和两次add
实现了 2^3 的计算
- 一次
-
程序终止
mov ax,4c00h
:用于将4C00h
加载到寄存器AX
,该值是 DOS 系统调用的int 21h
的功能号,用于程序终止int 21h
调用中断服务例程,执行功能号4Ch
[…]和(…)的语法规定
在汇编语言中,方括号通常用于表示内存地址的访问。
语法:
MOV AX, [1234h] ; 将内存地址 1234h 处的值加载到 AX 寄存器
MOV [BX], AL ; 将 AL 的值存储到由 BX 指定的内存地址
在某些汇编器(如 AT&T 语法)中,圆括号用于内存操作的间接寻址,特别是在 x86 AT&T 汇编语法中。
圆括号(…)不属于汇编语言语法,只是表达汇编指令的一个助记符,它表示一个内存单元或者寄存器中的内容
区分有无[ ]:
- 有[]时:
MOV AX, [BX]
表示将BX
寄存器中存储的**地址位置的值(即内存中的数据)**加载到AX
寄存器中- 无[]时:直接使用寄存器的值。比如,
MOV AX, BX
代表将寄存器BX
的值复制到寄存器AX
中可以用 C++ 语法来类比理解汇编语言中寄存器有无方括号的区别。
无方括号:类似于直接使用变量的值。
cpp复制编辑int a = 5; int b = 10;
在汇编中,相当于
MOV AX, BX
,直接将BX
寄存器的值赋给AX
寄存器。有方括号:类似于访问指针所指向的内存中的数据。
int a = 5; int* ptr = &a; // 指针 ptr 指向 a int b = *ptr; // 通过指针访问 a 的值
在汇编中,相当于
MOV AX, [BX]
,假设BX
存储的是某个内存地址,那么[BX]
表示访问该内存地址上的数据。
Loop指令
语法:loop 标号
功能:实现循环(计数型循环)
CPU执行Loop指令的操作:
- (cx)=(cx)-1;
- 判断cx中的值:
- 不为零则转至标号处执行程序
- 如果为零则向下执行
示例代码:
assume cs:code ; 告诉汇编器,代码段寄存器cs指向名为'code'的段
code segment ; 定义一个代码段,命名为'code'
mov ax, 2 ; 将立即数2加载到AX寄存器中
mov cx, 11 ; 将立即数11加载到CX寄存器中,作为循环计数器
s: ; 标签's',标记循环开始的位置
add ax, ax ; 将AX寄存器中的值加上自身(即乘以2)
loop s ; CX寄存器减1,并且如果CX不为零,则跳转到标签's'
mov ax, 4c00h ; 将立即数4C00h加载到AX寄存器中,准备调用DOS中断来正常退出程序
int 21h ; 调用DOS中断21h,功能号在AH寄存器中指定(这里是4Ch,表示程序终止)
code ends ; 结束代码段定义
end ; 汇编结束指令
该程序的主要功能是在AX寄存器中计算 2^11 的值(即2048),并通过DOS中断21h正常退出程序
例:实现123*236的一个汇编程序:
assume cs:code
code segment
mov ax,0 ;初始化结果为0,存在ax中
mov cx,236
s:
add ax,123
loop s
code ends
end
结合[…]语法的示例代码:
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,6
mov al,[bx]
mov ah,0
mov dx,0
mov cx,3
s: add dx,ax
loop s
mov ax,4c00h
int 21h
code ends
end
mov al,[0]的异常情况和段前缀的应用
mov al,[0]
的功能本应该是将偏移地址为0的存储单元的内容赋值给寄存器al,在编译并链接后,debug程序发现该语句被翻译成了mov al ,00
即将0赋值给al
我们可以使用段前缀的方式,即在[data]前显式写上段寄存器
语法:
mov ax, DS:[1234h] ; 使用数据段寄存器 DS
mov ax, ES:[1234h] ; 使用额外段寄存器 ES
连续内存单元的求和(ffff:0~ffff:b)
code segment ; 定义代码段
mov ax, 0ffffh ; 将最大16位值 (0xFFFF) 加载到 AX 寄存器
mov ds, ax ; 将 AX 中的值赋给 DS (数据段寄存器),设置数据段为 0xFFFF
mov ax, 0 ; 初始化 AX 寄存器为 0,用于中间计算
mov dx, 0 ; 初始化 DX 寄存器为 0,用于累加结果
mov cx, 12 ; 初始化循环计数器 CX 为 12,准备执行12次循环
s: ; 定义标签 s,表示循环的开始位置
mov al, [bx] ; 将 BX 寄存器指向的内存单元值加载到 AL(低8位寄存器)
mov ah, 0 ; 清空 AH(高8位寄存器),确保 AX 只包含 AL 的值
add dx, ax ; 将 AX 的值累加到 DX 中
inc bx ; BX 自增 1,指向下一个内存单元
loop s ; 循环 CX 次,每次减 1,直到 CX = 0 跳出循环
mov ax, 4c00h ; 终止程序,设置 AX = 4C00h,其中 4Ch 是退出功能号
int 21h ; 调用中断 21h,终止程序
code ends ; 结束代码段
end ; 程序结束
利用段前缀实现连续内存单元的拷贝
注:es
为附加段寄存器,作为额外的数据段寄存器,通常用于字符串操作或其他需要跨段数据传输的情况
assume cs:code ; 假设代码段寄存器 CS 指向名为 'code' 的段
code segment ; 定义一个代码段,命名为 'code'
; 设置数据段寄存器 DS 为 0FFFFh
mov ax, 0ffffh ; 将立即数 0FFFFh 加载到 AX 寄存器中
mov ds, ax ; 设置 DS 寄存器为 0FFFFh,这意味着我们将访问 0FFFF:0000 开始的内存区域
; 设置附加段寄存器 ES 为 0020h
mov ax, 0020h ; 将立即数 0020h 加载到 AX 寄存器中
mov es, ax ; 设置 ES 寄存器为 0020h,这意味着我们将写入 0020:0000 开始的内存区域
; 初始化偏移地址 BX 和循环计数器 CX
mov bx, 0 ; 将立即数 0 加载到 BX 寄存器中,作为初始偏移地址
mov cx, 12 ; 设置循环计数器 CX 为 12,表示要复制 12 个字节
s: ; 标签 's',标记循环开始的位置
; 从 DS:BX 处读取一个字节的数据到 DL 寄存器
mov dl, [bx] ; 从内存地址 DS:BX 处读取一个字节的数据到 DL 寄存器
; 将 DL 寄存器中的数据写入 ES:BX 处
mov es:[bx], dl ; 将 DL 寄存器中的数据写入内存地址 ES:BX 处
; 增加偏移地址 BX
inc bx ; BX = BX + 1
; 循环控制
loop s ; CX 寄存器减 1,并且如果 CX 不为零,则跳转到标签 's'
; 程序终止
mov ax, 4c00h ; 设置 AX 寄存器的值为 4C00h,准备调用 DOS 中断来正常退出程序
int 21h ; 调用 DOS 中断 21h,功能号 4Ch 表示程序终止
code ends ; 结束代码段定义
end ; 汇编结束指令
在代码段中使用数据
实际上,在汇编语言中直接指明地址是非常危险的行为,现代操作系统都会为每个进程动态分配内存空间,如果手动修改了关键地址(如内存空间)的值,可能会导致访问冲突,甚至是程序崩溃
此外,硬编码的地址难以理解,在后续维护中不易看出该地址数据的意义
例题如下:
dw
关键字可以定义数据,每一个数据为一个字,占两个字节,所以存储偏移地址的寄存器bx
每次要+=2
这里如果直接运行程序,代码段的起始地址在0010,而不是起始地址,如果想运行程序需要在Debug下将IP
设置为10h
,令CS:IP
指向程序中的第一条指令,才能正常运行
我们可以如下改进:
利用start
标号来指定程序运行的第一条指令,后面end start
除了启到终止程序的作用以外,还向编译器告知程序运行的入口
基于此,我们可以这样安排一段基本程序的框架:
assume cs:code
code segment
;定义数据
start:
;定义代码
code ends
end start
在代码段中使用栈
之前我们使用栈段和栈顶寄存器ss
和sp
将一段内存定义为栈,由于用到了真实的物理地址,而不是操作系统分配的内存空间,这种做法也应该抛弃
我们可以在代码段中定义栈,示例代码如下:
assume cs:codesg
codesg segment
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H
dw 0,0,0,0,0,0,0,0,0,0,0,0
start:
mov ax, cs
mov ss, ax
mov sp, 30h
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
代码解释:
-
数据段定义
- 前8个单元存储数据:
0123H
到0987H
。 - 后12个单元初始化为
0
,用于存储处理后的数据。
- 前8个单元存储数据:
-
初始化寄存器
- 设置栈段 (
SS
) 为代码段 (CS
),栈顶指针 (SP
) 为30h
。
- 设置栈段 (
-
入栈过程
- 从偏移
0-15H
的8个数据依次压入栈。 - 每次入栈后,
BX
加 2 指向下一个数据。
- 从偏移
-
出栈过程
- 从栈中依次弹出数据,覆盖原来的偏移
0-15H
数据。
- 从栈中依次弹出数据,覆盖原来的偏移
-
程序结束
-
使用中断
int 21h
正常退出程序。add bx,2
loop s0mov ax,4c00h
int 21h
-
codesg ends
end start
代码解释:
- **数据段定义**
- 前8个单元存储数据:`0123H` 到 `0987H`。
- 后12个单元初始化为 `0`,用于存储处理后的数据。
- **初始化寄存器**
- 设置栈段 (`SS`) 为代码段 (`CS`),栈顶指针 (`SP`) 为 `30h`。
- **入栈过程**
- 从偏移 `0-15H` 的8个数据依次压入栈。
- 每次入栈后,`BX` 加 2 指向下一个数据。
- **出栈过程**
- 从栈中依次弹出数据,覆盖原来的偏移 `0-15H` 数据。
- **程序结束**
- 使用中断 `int 21h` 正常退出程序。