《汇编语言》- 读书笔记 - 第9章 - 转移指令的原理
- 总结
- 9.1 操作符 offset
- 问题 9.1
- 9.2 jmp 指令
- 9.3 依据位移进行转移的 jmp 指令
- jmp short 标号
- 程序 9.1
- 程序 9.2
- 图 9.2 程序 9.2 的机器码
- jmp near ptr 标号
- 9.4 转移的目的地址在指令中的 jmp 指令
- 如何选择 jmp short、jmp near、jmp far
- 9.5 转移地址在寄存器中的 jmp 指令
- 9.6 转移地址在内存中的jmp 指令
- jmp word ptr 内存单元地址(段内转移)
- jmp dword ptr 内存单元地址(段间转移)
- 检测点 9.1
- 9.7 jcxz 指令
- 1. 循环控制
- 2. 条件分支
- 检测点 9.2
- 9.8 loop 指令
- 检测点 9.3
- 9.9 根据位移进行转移的意义
- 9.10 编译器对转移位移超界的检测
- 实验 8 分析一个奇怪的程序
- 实验 9 根据材料编程
总结
本章主要介绍了转移指令
的原理,这些指令允许程序在执行过程中改变控制流程,实现程序的分支
、循环
和跳转
。掌握这些概念有助于深入理解计算机程序的执行机制和内存管理。
9.1 操作符 offset
问题 9.1
分析:
s
、s0
都在CS
段,偏移量也拿到分别放在了si
、di
中。ax
通用寄存器大小1个字(2字节)够存放这条语句mov ax, bx
- 先把
s
标号处的第一句复制到ax
,再从ax
复制到s0
assume cs:codesg
codesg segment
s: mov ax, bx ; mov ax,bx 的机器码占两个字节
mov si, offset s
mov di, offset s0
mov ax, cs:[si]
mov cs:[di], ax
s0: nop ; nop 的机器码占一个字节
nop
codesg ends
end s
9.2 jmp 指令
jmp
为无条件
转移指令,可以只修改 IP,也可以同时修改 CS 和 IP
jmp
有两种跳转思路:
- 按
偏移量
跳。向前或向后N个字节。(相对跳转) - 按
目录地址
跳。跳到指定位置。(绝对跳转)
通过只修改 偏移地址 IP
还是同时修改 段地址和偏移地址 CS:IP
可以区分段内
、段间
转移。
中文 | 命令 | 说明 | 修改的 寄存器 | 例子(假设有标号叫 label ) |
---|---|---|---|---|
段内短转移 | jmp short 标号 | 根据相对偏移量 在当前段内 进行跳转。IP 修改范围 -128 到 +127 (8位)。 | IP | jmp short label |
段内近转移 | jmp near ptr 标号 | 根据相对偏移量 在当前段内 进行跳转。IP 修改范围 -32768 到 +32767 (16位)。 | IP | jmp near ptr label |
段间转移 (远转移) | jmp far ptr 标号 | 同时修改CS 和IP 为标号 的段地址 和偏移地址 以实现跨段跳转。 | CS:IP | jmp far ptr label |
寄存器 间接转移 | jmp 寄存器 | 将指定寄存器 的值加载到IP 。实现修改偏移地址 (段地址 CS 不变) | IP | jmp dx |
段内 间接转移 | jmp word ptr [内存] | 将指定内存 中的数据l读取到IP 。读取长度一个 word (字,16位) | IP | jmp word ptr [bx+si] |
段间 间接转移 | jmp dword ptr [内存] | 将指定内存 中的数据l读取到CS:IP 。读取长度一个 dword (双字,32位)IP=低16位, CS=高16位 | CS:IP | jmp dword ptr ds:[0] |
————— | ———————————— | ——— | ——————————— |
9.3 依据位移进行转移的 jmp 指令
使用jmp short
还是 jmp near
取决于想要跳转到的目标相对于当前
指令执行位置的距离
(字节)
如果该距离在 -128 ~ 127
(字节)范围内,使用 jmp short
;
如果该距离在 -32768 ~ 32767
(字节)范围内,使用 jmp near
;
jmp short 标号
程序 9.1
此例中因为偏移量为 03
在 -128
~ 127
范围内,所以使用短转移
assume cs:codesg
codesg segment
start: mov ax,0 ; 1. 设置 ax 为 0
jmp short s ; 2. 跳到标号 s 处
add ax,1 ; 3. 被跳过
s: inc ax ; 4. ax 自增 1
codesg ends ; 5. 最终执行结果 ax 为 1
end start
如上图查看 jmp short s
反编译的机器码对应 EB03
,
这里 EB
是 JMP
指令的操作码,对应 jmp short
,
03
就是要跳过的字节数,也就是add ax,1
的机器码83C001
的长度。
执行后 IP + 3 字节,跳过了 add ax,1
指向 inc ax
单步执行,看结果 AX=0001
程序 9.2
我们把程序 9.1 改写一下,变成下面这样:
assume cs:codesg
codesg segment
start: mov ax,0
mov bx,0
jmp short s
add ax,1
s: inc ax
codesg ends
end start
图 9.2 程序 9.2 的机器码
分析 jmp short 标号
前先回忆一下 CPU 执行指令的过程:(详见:2.10 CS和IP)
按照这个步骤,我们参照 【图 9.2】 看一下,程序 9.2 中 jmp short s
指令的读取和执行过程:
- (CS)=0BBDH,(IP)=
0006H
,CS:IP
指向EB 03
(jmp short s
的机器码,长度2字节
); - 读取指令码
EB 03
进入指令缓冲器; - (IP) =
(IP)+所读取指令的长度
=(IP)+2
=0008H
,CS:IP
指向add ax,1
; - CPU 执行
指令缓冲器
中的指令EB 03
; - 指令
EB 03
执行后(IP+=3),(IP)=000BH,CS:IP
指向inc ax
。
jmp near ptr 标号
jmp near
的偏移范围比 jmp short
更大,
如跳转的目标地址在当前代码段内,且偏移量可以使用2字节
表示,则应该使用 jmp near
。
assume cs:codesg
codesg segment
start: mov ax,0 ; 1. 设置 ax 为 0
jmp near ptr s ; 2. 跳到标号 s 处
add ax,1 ; 3. 被跳过
s: inc ax ; 4. ax 自增 1
codesg ends ; 5. 最终执行结果 ax 为 1
end start
这段代码只是修改了 jmp near ptr s
一句。我们看下面截图:
虽然偏移量(操作数)还是 3
但此时占用了2字节
。
9.4 转移的目的地址在指令中的 jmp 指令
jmp far
它直接 修改CS
和IP
实现跨段跳转
。
ssume cs:codesg
codesg segment
start: mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0) ; 被跳过
s: add ax,1 ; ax 中的值 +1
inc ax ; ax 自增 1
codesg ends
end start
查看反汇编效果 jmp far
操作码:EA
,
操作数:偏移地址=010B
段地址=076C
(在内存中存储,低位在前,高位在后)
单步执行,看结果 AX=0002
如何选择 jmp short、jmp near、jmp far
从设计的角度考虑,区分 jmp short
、jmp near
、jmp far
的主要原因是为了节省字节码空间。
根据上面的例子我们看到了:
指令 | 机器码长度 |
---|---|
jmp short | 2字节 |
jmp near | 3字节 |
jmp far | 5字节 |
在早期8086处理器的设计中,内存和存储器资源相对有限,因此指令大小对于程序大小和效率至关重要。
在现代处理器中,尽管内存不再是主要瓶颈,但这样的设计传统仍然保留下来,并且在某些嵌入式系统或对空间极度敏感的应用场景中仍然具有重要意义。
在实际编程中,现代汇编器会根据目标地址与当前指令之间的距离自动选择合适的跳转指令类型。程序员只需要指定目标地址,而无需关心具体的跳转指令类型。
jmp my_label
my_label:
; 业务代码略
当汇编器处理这段代码时,它会计算 jmp
指令与目标标签 my_label
之间的距离。
- 如果距离在
jmp short
指令的偏移量范围内(1 个字节),汇编器会生成jmp short
指令。 - 如果距离超过
jmp short
指令的范围,但仍在jmp near
指令的偏移量范围内(2 个字节),汇编器会生成jmp near
指令。 - 如果距离超过
jmp near
指令的范围,汇编器会生成jmp far
指令。
9.5 转移地址在寄存器中的 jmp 指令
jmp
指令将程序的控制流跳转到 BX 寄存器
中存储的地址。
assume cs:codesg
codesg segment
start: mov bx,s ; 把标号 s 的地址存到 bx
mov ax,0
jmp bx ; 跳转到 bx 寄存器中存储的地址
add ax,1 ; 这句被跳过
s: inc ax ; ax 自增 1
codesg ends
end start
单步执行,看结果 AX=0001
9.6 转移地址在内存中的jmp 指令
jmp word ptr 内存单元地址(段内转移)
修改 IP 段内转移
assume cs:code, ds:data
data segment
t dw offset s ; 定义一个内存变量 t 保存标号 s 的偏移地址。
data ends
code segment
start: mov ax, data ; 数据段偏移量存入 ax
mov ds, ax ; 设置数据段 ds 地址
jmp word ptr [t] ; 段内跳转,到内存变量 t 保存的址
mov ax, 0 ; 这句被跳过
s: mov ax, 1 ; 最终结果 ax 为 1
code ends
end start
jmp dword ptr 内存单元地址(段间转移)
同时修改 CS:IP
段间转移
assume cs:code, ds:data
data segment
t dd 12345678h
data ends
code segment
start: mov ax, data ; 数据段偏移量存入 ax
mov ds, ax ; 设置数据段 ds 地址
jmp dword ptr [t] ; 段内跳转,到内存变量 t 保存的址
code ends
end start
这里就确认一下 jmp 后 CS
和IP
都正确的修改了。
检测点 9.1
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 9.1
9.7 jcxz 指令
指令格式:jcxz 标号
jcxz
(Jump if CX is Zero)指令在x86汇编语言中主要用于条件
跳转,当CX
寄存器的值为0
时执行跳转。否则啥也不做,继续执行下一句。(所有的有条件转移指令都是短转移
,对 IP
的修改范围都为: -128 ~ 127
)
它的常见用途包括:循环控制、条件分支
1. 循环控制
在编写循环结构时,jcxz
可以用来检测循环计数器是否已经减至0
,从而决定是否结束循环。
如下示例代码,循环累加 AX
。
assume cs:code
code segment
start:
mov ax,0 ; 初始化 ax 为 0
mov cx,3 ; 初始化循环次数 3 到 CX寄存器
loop_start:
; 循环体开始
inc ax ; ax 自增
; 循环体结束
dec cx ; 每次循环后递减CX
jcxz loop_end ; 如果 CX为0,则跳转到 loop_end
jmp loop_start ; 否则跳转到 loop_start 处继续下一次循环
loop_end:
mov ax,4c00H
int 21H
code ends
end start
等价于JS的 do{...}while(ax != 0)
var ax = 0, cx = 3;
do {
console.log(++ax);
} while (--cx);
#include <stdio.h>
int main() {
int ax = 0, cx = 3;
do {
printf("Current value: %d\n", ++ax);
} while (--cx); // cx 不等于 0 就继续
return 0;
}
2. 条件分支
在某些算法或数据处理中,CX
可能被当作条件判断的标志位。当CX
代表某种条件成立与否时,可以用jcxz
来进入相应的处理分支。
assume cs:code
code segment
start:
mov cx,0 ; 将标志变量加载到 CX 寄存器中
jcxz branch_B ; 如果 CX 为 0,则跳转到 分支B
branch_A:
mov ax,6666H
jmp branch_end ; 跳转到结束
branch_B:
mov ax,3333H
branch_end:
mov ax,4c00H
int 21H
code ends
end start
分别演示 cx
为 0
和 1
的效果。
总之,jcxz
是一个用于简化基于CX寄存器值进行条件跳转的指令,它在需要根据CX是否为0来进行决策的场景中非常有用。不过,现代编程中往往使用高级循环结构和条件判断语句,但在底层编程和特定优化场合,jcxz仍有其独特的应用价值
检测点 9.2
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 9.2
9.8 loop 指令
loop
指令是一种条件循环指令,主要用于实现计数型循环。它依赖于CX
寄存器作为循环计数器。
- 在执行
loop
指令之前,需要先将循环次数
(通常是递减的次数)加载到CX
寄存器中。 - 每次执行
loop 标号
时,CPU会自动将CX
寄存器中的值减1
。 - 然后检查
CX
寄存器是否为0
。
3.1. 如果CX != 0
,则跳转到
由标号
指定的地址处继续执行循环体内的代码;
3.2. 否则CX == 0
,程序流程将继续向下执行,即跳出循环。
所有的循环指令都是短转移
,对 IP
的修改范围都为:-128 ~ 127
。
偏移量的计算方式为:标号地址
- loop 下一句的地址
以补码形式保存,在编译时获得。
loop 标号
的功能相当于:
(cx)--;
if((cx) != 0){
jmp short 标号;
}
检测点 9.3
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 9.3
9.9 根据位移进行转移的意义
jmp short 标号
,jmp near ptr 标号
,jcxz 标号
,loop 标号
这几种汇编指令,都是相对跳转。
在对应的机器码中不包含
转移的目的地址
,而是包含
到目的地址的偏移量
(字节)。
这种设计,方便了程序段在内存中的浮动装配
,如:
这段代码中,loop s
用的相对跳转,记录的是 -4
(字节) 而不是 s
的地址,这样无论这段程序加载到内存中任何位置,都能正常跳到 s
。
但如果使用 s
的目标地址来跳转,比如 s
的地址是2000:0000
那这个地址就被限制死了。
这段程序只能加载到内存这个位置才能正常跑。如果这段内存被其它程序占用,就没法玩了。
F | C | 说明 |
---|---|---|
1111 | 1100 | 补码 |
1111 | 1011 | 反码 = 补码 - 1 |
1000 | 0100 | 原码 = 反码按位取反(最左侧的符号位不动) |
- | 4 | 结果为 -4 |
9.10 编译器对转移位移超界的检测
根据位移
进行相对跳转
的指令,转移范围都是有限制的,
编译器会在编译时
检测越界
问题,如果触发会报·编译错误
。
所以我们只要知道偏移量
是如何计算即可。实际操作中编译器
在编译时会自动算好。
实验 8 分析一个奇怪的程序
《汇编语言》- 读书笔记 - 实验8 分析一个奇怪的程序
实验 9 根据材料编程
《汇编语言》- 读书笔记 - 实验9 根据材料编程