汇编语言

前言

​ 本文档为大连理工大学软件学院24级Briteny所总结编写,内容基于汇编语言(王爽)的第四版,加入个人理解和总结,如有问题,欢迎及时指正。

​ 由于书的缘故,本文档内容可能和现在的设备相差较大,但对于入门学者,可以初步奠定对计算机的汇编语言以及相关的基本结构逻辑的认识,这个速通版还是可以读一读,毕竟虽然有点过时,但是毕竟计算机系统结构没有大改,所以基础的逻辑没变,所以我认为本书还是值得看一看。

​ 另外本书介绍了debug的使用,但本菜鸡只是略看一下,没有太多认识,且该debug对于二进制网络安全方向学习并不是很便利,所以不建议学习,其实我在电脑上系统更新把这个程序优化掉了。如果想要学习,请安装DOS并在CSDN上查询教程安装,在此我便不误人子弟了。

阅读建议

全篇读完后可以尝试看看书上的思考题,来确保自己正真掌握了这些相关的知识

[TOC]

基础知识

汇编语言的指令组成:

1.汇编指令:机器码的助记符,有对应的机械码。

2.伪指令:没有对应的机械码,由编译器执行,计算机并不运行。

3.其他符号:如+,-,*,/等,由编译器识别,没有对应的机械码。

存储器(内存):指令和数据在存储器中存放。

指令和数据:在内存或磁盘上,数据和命令没有区别,都是二进制信息。

存储单元:

电子计算机的最小信息单位是bit,也就是一个二进制位。

微型机存储器的存储单元可以存储一个byte,即八个bit。

一个存储器有128个存储单元,也就是128字节。

CPU对存储器的读写:

CPU进行读写的要求:1.存储单元的地址(地址信息),2.器件的选择,读或写得命令(控制信息),3.读或写的数据(数据信息)。

示例如下:[CPU从三号单元读取数据]

过程:先地址线过程,再控制线过程,随后数据线过程。

总线:地址总线,控制总线,数据总线。

地址总线:导线可以传输的稳定状态只有两种,高电平或低电平。用二进制表示就是1和0.

CPU的寻址能力就是2的N次方(N是地址线个数)。

数据总线:8088CPU的数据总线宽度为8,一次只能传输一个字节;8086CPU的数据总线宽度为16,一次可以传输两个字节.

示例:两种CPU传输89D8H

控制总线:有多少根控制总线,就意味着CPU对外界有多少中控制。

主板:主板上有核心器件和一些主要器具,他们通过总线相连。

接卡口:CPU对外部设备是无法直接控制的,直接控制这些外部设备的是接卡口,CPU通过直接控制接卡口来间接控制外部设备。

各类存储芯片:随机存储器(RAM)和只读存储器(ROM)。

RAM:可读可写,但必须带电存储。

ROM:只可读取不可写入,关机后不丢失。

根据功能和连接方式又可分为:

随机存储器:存放CPU的绝大部分数据,主存储器一般由两个位置上的RAM组成——装在主板上的RAM和装在接卡口的RAM。

装有BIOS(基本输入输出系统)的ROM

接卡口上的RAM:对大批量的输入输出数据暂时存储

内存地址空间:

CPU将系统内的各类存储器看作逻辑存储器:

寄存器

寄存器是CPU的主要部件,进行数据的存储。

通用寄存器:AX,BX,CX,DX这四个用来存放一般性的数据,称为通用寄存器。

[8086CPU上一代寄存器都是8位的,8086CPU寄存器16位,可分为两个八位可独立的八位寄存器使用分为H,L]

两个字节成为一个字。

几条简单的汇编指令:[在写汇编指令时不需要区分大小写]

mov ax,18 将18传入ax寄存器中

add ax,bx 等效于ax=ax+bx

物理地址:存储空间本质上是一个一维的线性空间,每一个内存地址在这个空间中都有唯一的地址,这个地址就是物理地址。

16位的CPU:一次性可以处理16位的数据,寄存器的宽度为16位;寄存器和运算器的通路为16位。

8086CPU给出物理地址的方法:采取两个16位地址合成一个20位地址,通过地址加法器得到物理地址。

地址加法器运算:物理地址=段地址*16+偏移地址。

段:内存并没有分段,只是CPU分段了(段的最大长度为64KB由偏移地址的最大值决定)

例如:地址10000H~100FFH的内存地址组成一个段,这个段的起始基址为10000H,段地址为1000H,大小为100H;

[段地址*16是一定是16的倍数,所以一个段的起始地址必然是16的倍数,这就是段地址如上表示的原因]

段寄存器:8086CPU中有四个段寄存器CS,DS,SS,ES。CPU要访问内存时由这四个段寄存器提供内存单元段地址。

CS,IP指示了当前要读取命令的地址

CS:代码段寄存器,(段地址)

IP:指令指针寄存器,(偏移地址)

CS,IP指令的修改:简单的mov指令并不能修改CS,IP。此时需要用到jmp

jmp的用法:jmp 段地址:偏移地址 意为修改CS,IP为相应的段地址;

​ jmp ax (ax代指合法寄存器) 意思为修改IP为ax中的值。

代码段:可以在内存单元中定义一个段,用于存放代码。

寄存器(内存访问)

字单元:即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。

用16位来寄存一个字。高八位放高位字节,低八位放低位字节。

DS和[address]:

DS:8086CPU中有一个DS寄存器,通常用于存放要访问的数据的段地址。[注意看向ds中放入数据的方式]

示例:

mov bx 1000H

mov ds bx

mov al [0]

此时的[0]中的0就是内存单元的偏移地址,执行命令时,8086CPU会自动读取段地址。

字的传送:

由于8086CPU的16位结构,所以只需要在mov指令中给出16位寄存器就可以进行16位数据的传送了

mov,add ,sub指令:

mov 寄存器,数据

mov 寄存器,寄存器

mov 寄存器,内存单元

mov 段寄存器,寄存器

sub,add指令用法基本相同,一个代表加,一个代表减。

数据段:编程时根据需要,将一组内存单元定义为一个段专门存储数据。

栈:栈是一种具有独特访问方式的存储空间,特殊性就在于最后进入这个空间的数据最先出去(LIFO)

由于这种独特的存储方式,与正常的其他存储不同,在栈中,先放入栈的数据地址是高地址,而读取时则是读取低地址。

CPU提供的栈机制:

8086CPU提供基础的出栈(pop),入栈(push)指令。

SS,SP:任何时刻,SS:SP指向栈顶地址

push 指令按图示完成

pop指令则刚好相反:

栈顶超界问题:

栈满或栈空状态下执行pop,push指令很容易导致栈顶超界。

8086CPU不保证我们对栈的操作不会超界,也就是说,8086CPU只知道栈顶在哪,而不知道我们安排的栈空间有多大。

栈段:将一组内存单元定义为一个段,将其作为栈来使用,从而定义了一个栈段。

第一个程序

源程序从写出到执行的过程:

1.用汇编语言编写源程序。

2.对源程序进行编译连接。

3.执行可执行文件中的程序。

1.伪指令:

segment 段开始

ends 段结束

成对使用,功能是定义一个段。

end 一个汇编程序结束的标志。(注意区分end和ends)

assume 不一定要深入了解,其作用就是将有特定相关用途的段和相关的段寄存器关联起来。

2.汇编语言编写的源程序,包括伪指令和汇编指令。

源文件中的所有内容称为源程序,将源程序中最终由计算机执行,处理的指令或者数据,称为程序。

3.标号:例如codesg这种不是汇编指令,不是伪指令。一个标号代表了一个地址。

例如:codesg 在segment前面作为一个段的名称,这个段的名称最后编译,连接程序处理为一个段的地址。

此处我们不用理会int 21H 的指令含义 只需要知道在程序的末尾加上这两条指令就可以实现程序的返回。

程序过程:

1.编辑源程序——利用任意文本编辑器编辑源程序,最终存储为纯文本文件即可(后缀.asm)

2.进入DOS方式用汇编编译器编译源程序,生成(后缀.obj)的目标文件[在此过程中编译程序可能会提示你输入交叉引用文件的名称,按enter键跳过即可]

目标文件是含有机械代码的目标文件。

3.连接:运行连接器[一个程序]然后输入需要链接的程序的名称和编译相同可能会出现映像文件的生成,同样跳过即可。

另外如果程序中调用了某个库的子程序,也需要将这个库和函数连接起来[写过C语言的都清楚,就类似于#include<stdio.h>]

问题:谁将可执行文件中的程序载入内存并使它运行?

对DOS有了解就知道,DOS程序中有个程序叫command.com

BX和loop指令

[BX]是一个内存单元,它的偏移地址在bx中。

示例: mov al [bx]

示例含义:将一个内存单元送入al中,这个内存单元长度为一个字节(字节单元),这个内存的段地址在ds中,偏移地址在[bx]中。

由此可见[bx]的效果和[0]类似。

为简洁表述,我们用(ax)表示ax寄存器中的内容。

()中的元素一般有三种类型:1.寄存器名,2.段寄存器名,3.内存单元的物理地址

()所表示的数据类型一般为:1.字节,2.字

约定idata 用于表示常量 […]中的…其实就是一个常量。

示例:

mov ax,[bx]

功能:将当前ds:bx中存储的数据放入ax中。用表达式表示就是(ax)=((ds)*16+(bx))

inc bx 的含义是bx中的内容+1

尝试分析下面汇编语言的代码:{书本p97-p99有详细解读,如若不懂可以借阅查看}

loop指令:

格式:loop 标号

CPU对loop指令的处理方式为1.(cx)=(cx)-1 2.判断cx中的值,不为零则转移到标号处进行执行程序,为零则向下执行

我们通常用loop实现循环,cx中存放着循环的次数

示例代码:目标实现123*236的计算

assume cs:code

code segment

​ mov ax,0

​ mov cx,236

​ s: add ax,123

​ loop s

​ mov ax 4c00h

​ int 21h

code ends

end

小问题:你能尝试改进算法吗?

loop与[bx]的联合使用:此部分较简单理解,仅给出示例,自己分析

程序目的:实现ffff:0~ffff:b中的8位数据累加到的dx寄存器中

段前缀:在先前的代码中可以见到明显的标识,如上面示例中的s 。在汇编语言中,用于显式地明显内存单元的段地址的符号[“ds: ,cs: ,ss: ,es: “]在汇编语言中称为段前缀。

在8086模式中,我们随意向内存中写入一段代码是非常危险的,因为这段内存可能原来存放着重要的系统数据或代码。所以我们写入的部位最好是空白的,或者没有程序的数据或者代码的。在DOS模式下,一般 0:200~2ff 空间中没有系统或其他程序的代码和数据,所以可以用这段空间写入代码(书本原话,但不建议练习,毕竟8086模式不一定与你的设备相同匹配)

包含多个段的程序

程序获得合法空间的方式有两种:1.在加载程序是为程序分配;2.再程序执行过程中向系统申请

书中只讨论了第一种方法:在源程序中申请内存空间

范例:

dw的含义是“define word”定义字符型数据

示例中定义了八个字型数据,由于dw放在最前面,所以最初的偏移地址为零。而后每个数据的偏移量增加一个字型长度。

书上还给了一个示例[在上面代码的end的后面加上了一个start,在dw下面一行mov前也加了start]

由此引出end的另一个作用,就是通知编译器程序的入口在什么地方。

由前面也可以知道,在单任务程序系统中,可执行程序的执行过程如下:

1.通过一些工具将可执行程序载入内存;

2.设置CS:IP指向程序的第一条要执行的命令(即程序的入口);

3.结束运行,返回加载者。

end start 解读:指明程序的入口,转化为一个入口地址,存储在可执行文件的描述信息中。

简单构造程序的框架为

assume cs:code

code segment

​ 数据

start:

​ 代码

code ends

end start

在代码段中使用栈:

这也说明,栈的定义并非在我们的源代码中有相应的编写方式,而是根据CPU的某些读取特性来使用的,示例中创造栈的内存空间就是一个很好的说明

数据,代码,栈,放入不同的段

定义多个段分别存不同的类型的二进制信息

定义多个段的方法与定义栈的方法基本相同,只是段需要设置段名

注意比较下面哪个代码是错误的,错误在哪:

(1)

mov ax,data

mov ds,ax

mov bx,ds:[6]

(2)

mov ds,data

mov bx,ds:[6]

错误原因先前内容中有,可自行复习

更灵活的定位内存地址的方法

and 和 or 指令

and 将相应的操作对象设为0,其他位不变
例:

mov al 01100011B

and al 00111011B

执行结果为

al 00100011B

说明:将第六位设置为0的指令是 and al 10111111

​ 将第七位设置为0的指令是 and al 01111111

​ 将第零位设置位0的指令是 and al 11111110

or将相应的操作对象设置为1,其他位不变

ASCII码[一种常见的编码方式]

如果我们要在屏幕上看到a字母,我们就要通过显存给显卡提供a字母的ASCII码

以字符的形式给出的数据

db 是汇编语言中的一个伪指令,用于定义字节(byte)类型的数据。

大小写转化问题:

用and和or指令转变大小写{判断大写小写决定是否转化}

[比较字母大小写之间ASCII码的二进制结果之间的差别]

范例

assume cs: codesg,ds,datasg

datasg segment

​ db ‘BaSiC’

​ db ‘iNfOrMaTiOn’

datasg ends

codesg segment

start : mov ax,datasg

​ mov bx,ax

​ mov bx,0

​ mov cx,5

​ s:mov al,[bx]

​ and al,11011111B

​ mov [bx] al

​ inc bx

​ loop s

​ mov bx,5

​ mov cx,11

​ s0: mov al,[bx]

​ or al 00100000B

​ mov [bx],al

​ inc bx

​ loop s0

​ mov ax,4c00h

​ int 21h

codesg ends

end start

[bx+idata]

[bx+idata]表示一个内存单元,它的地址偏移地址是(bx)+idata

可以用以下几种形式表示:

mov ax,[200+bx]

mov ax,200[bx]

mov ax,[bx].200

用[bx+idata]表示数组

SI和DI

si和di是8086CPU中功能与bx功能相近的寄存器,si和di并不能被分成两个独立的八位寄存器来使用

[bx+si]和[bx+di]

mov ax,[bx+di]的含义:将一个内存单元中的内容送入ax中,这个内存单元的长度为两个字节,存放一个字,偏移地址为bx中的数值加上si中的数值,短地址在ds内。

也可表述为(ax) == ((ds)*16+(bx)+(si))

也可以以这种格式使用:mov ax,[bx] [si]

[bx+si+idata]和[bx+di+idata]

mov ax,[bx+si+idata]

将一个内存单元的内容送入ax中,这个内存单元的长度为2字节,存放一个字,偏移地址为bx中的数值加上si中的数值和idata中的数值,段地址在ds中。

该指令也可写成如下格式:

mov ax,[bx+200+si]

mov ax,[200+bx+si]

mov ax,200[bx] [si]

mov ax,200.[bx] [si]

mov ax,[bx] [si].200

不同寻址方式的灵活运用

1.[idata] 用一个常量来表示地址,可用于直接定位内存单元

2.[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元

3.[bx+idata]用一个变量和一个常量的和表示内存地址,可以在起始地址的基础上用变量间接定位内存单元

4.[bx+si]用两个变量来表示地址

5.[bx+si+idata]用两个变量和一个常量表示地址

一般而言我们要短暂存储数据时,我们都应该使用栈

具体的汇编代码修改示例,参考原书

数据处理的两个基本问题

1.处理的数据在什么位置

2.处理的数据有多长

reg 表示一个寄存器,包括:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di

sreg表示段寄存器,包括:ds,ss,cs,es

bx,si,di,bp

1.在8086CPU中,只有这四个寄存器可以用来寻址

2.在[…]中,这四个寄存器可以单独出现,或者只能以四种组合出现:bx和si,bx和di,bp和si,bp和di

3.只要[…]中出现了bp,而指令中没有显性地给出段地址,段地址就默认在ss中 例:mov ax,[bp+si]

汇编语言中的数据位置表达

1.立即数——idata

2.寄存器

3.段地址(SA)和偏移地址(EA)

存放段地址的寄存器可以是默认的如,DS,SS

也可以显性给出的 如:mov ax,ss:[si] mov ax,es:[idata]

寻址方式总结:

指令要处理的数据有多长

8086CPU的指令,处理尺寸的数据,byte和word

#其实就是根据寄存器的大小决定了处理数据的长度

1.通过寄存器指明要处理数据的尺寸

1>指明字操作

mov ax,1

mov bx,ds:[0]

mov ds,ax

mov ds:[0],ax

inc ax

add ax 1000

2>指明字节操作

mov al,1

mov al,bl

mov al,ds:[0]

mov ds:[0],al

inc al

add al,100

2.没有寄存器名存在的情况下,可以使用操作符 X ptr指明内存单元的长度,X在汇编语言中可以为word或者byte

1>指明字操作

mov word ptr ds:[0],1

inc word ptr [bx]

inc word ptr ds:[0]

add word ptr [bx],2

2>指明字节操作

mov byte ptr ds:[0],1

inc byte ptr [bx]

inc byte ptr ds:[0]

add byte ptr [bx],2

范例对比:

假定debug查看2000:1000的内存结果为:FF FF FF FF FF FF FF FF FF FF ……

1
2
3
4
mov ax,2000
mov ds,ax
mov byte ptr [1000H],1
#在汇编语言中,ptr 是一个关键字,用来指明紧跟其后的数值是一个指针或者地址。这个关键字通常用于指定操作数的大小和类型,尤其是在操作内存时。

这样得到的结果是:2000:1000 01 FF FF FF FF FF ……

1
2
3
mov ax,2000
mov ds,ax
mov word ptr [1000H],1

这样得到的结果是 2000:1000 01 00 FF FF FF FF FF FF ……

对于这个结果再作深入解析一下:

在 x86 架构的汇编语言中,数据在内存中的存储方式通常是小端字节序(Little-Endian)。这意味着较低的地址存放的是数值的低位字节,而较高的地址存放的是数值的高位字节。

  1. mov ax, 2000:将数值 2000 移动到 ax 寄存器中。在十六进制中,2000 表示为 0x2000
  2. mov ds, ax:将 ax 寄存器的值(2000 或 0x2000)移动到数据段寄存器 ds 中。
  3. mov word ptr [1000H], 1:将数值 1 移动到地址 1000H 指向的内存位置,并且指定这是一个字(word)操作,即 16 位。

由于您指定了 word ptr,这意味着您正在操作一个 16 位的值。在小端字节序中,这条指令将数值 1(0x0001)的字节表示放入内存地址 1000H1001H

  • 地址 1000H(较低的地址)将存放数值 1 的低位字节,即 0x01
  • 地址 1001H(较高的地址)将存放数值 1 的高位字节,即 0x00

因此,对于 mov word ptr [1000H], 1 这条指令,放入的是低位字节在较低的地址,高位字节在较高的地址。

小端字节序(Little-Endian)是一种计算机存储多字节数据类型(如整数、浮点数等)的方式。在小端字节序中,一个多字节值的最低有效字节(LSB,即数值最低的字节)存储在最低的内存地址处,而最高有效字节(MSB,即数值最高的字节)存储在最高的内存地址处。

3.其他方法:有些指令默认了访问的是字单元还是字节单元,比如push [1000H]就不用指明访问的是字单元还是字节单元,因为push指令只对字操作

div指令(除法指令)

注意事项:

1.除数:有8位和16位两种,在一个reg或者内存单元中

2.被除数:默认放在AX或者AX和DX中,如果除数是八位,被除数则为16位,默认再AX中存放;如果除数为16位,被除数则为32位,在AX和DX中存放,DX存放高16位AX存放低16位。

3.结果:如果除数为8位,则AL存储计算的商,AH存储计算的余数,如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

使用格式如下:

1
2
3
div reg
div 内存单元
#不用指明除数,因为默认

伪指令dd

dd是用来定义double word(双字)型数据的

范例:

1
2
3
4
5
data segment
db 1
dw 1
dd 1
data ends

上述汇编在data段中定义了三个数据,

第一个数据为01H,在data:0处,占一个字节

第二个数据为0001H,在data:1处,占一个字节

第三个数据为00000001H,在data:2处,占两个字节

dup指令

dup指令和定db,dw,dd一样,也是由编译器识别处理的符号,它是和db,dd,dw等伪指令配合使用的,用于进行数据的重复.

1
2
3
4
5
6
db 3 dup (0)
#定义了三个字节,他们的值都是0,等效于db 0,0,0
db 3 dup (0,1,2)
#定义了9个字节,他们是0,1,2,0,1,2,0,1,2,等效于db 0,1,2,0,1,2,0,1,2
db 3 dup ('abc','ABC')
#定义了18个字节

dup的使用格式:

db 重复的次数 dup (重复的字节型数据)

dw 重复的次数 dup (重复的字型数据)

dd 重复的次数 dup (重复的双字型数据)

转移指令的原理

可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括而言,转移指令就是能控制CPU执行内存内某处代码的指令。

8086CPU的转移行为有以下几类:

1.只能修改IP时,称为段内转移,比如jmp ax

2.同时修改CS和IP时,称为段间转移,比如jmp 1000:0

由于转移指令对IP修改的范围不同,段内转移又分为短转移和近转移

短转移的IP修改范围为-128~127

近转移IP的修改范围为-32768~32767

8086CPU的转移指令又分为以下几类:

1.无条件转移指令(如jmp)

2.条件转移指令

3.循环指令(loop)

4,过程

5.中断

操作符offset

操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址

范例:

1
2
3
4
5
6
assume cs:codesg
codesg sement
start:mov ax,offset start ; 相当于mov ax,0
s:mov ax,offset s;相当于mov ax,3,第一条指令长度为3个字节,所以s的偏移地址为3
codesg ends
end start

jmp指令

jmp为条件跳转指令可以只修改IP,也可以同时修改IP和CS

jmp指令要给出两种信息:

1.转移的目的地址

2.转移的距离(段间转移,段内短转移,段内近转移)

根据位移进行转移的jmp指令

jmp short 标号(转到标号处执行指令)

这种格式的jmp指令是实现段内短转移,他们对IP的修改范围是-128~127,格式中的short符号说明指令是进行段内短转移,标号是代码段中的标号,指明了指令要转移到的目的地,转移指令结束后,CS和IP应该指向该标号处的指令

范例:

1
2
3
4
5
6
7
8
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start

机械指令对比

正常机械指令:

1
2
3
mov ax,0123h        #B8 23 01
mov ax,ds:[0123h] #A1 23 01
push ds:[0123h] #FF 36 23 01

上述范例可以看到,idata无论是一个数据还是地址,都会在对应的机械语言中出现,因为CPU执行命令他必须知道这些数据或地址

范例中的代码段和对应机械码比较:

1
2
3
4
5
#B8 00 00  #0000  #mov ax,0000
#EB 03 #0003 #jmp 0008
#05 01 00 #0005 #add ax,0001
#40 #0008 #inc ax
#上述机械码的结果看似毫无问题,但仔细看就会发现,jmp short s中的s(0008)并没有出现在机械码中,

书本上另给的一个范例作比较;

1
2
3
4
5
6
7
8
9
10
assume cs:codesg

codesg segment
start:mov ax,0
mov bx,0
jmp short s
add ax,1
s:inc ax
codesg ends
end starts

对应成机械码就是:

1
2
3
4
5
#B8 00 00  #0000  #mov ax,0000
#BB 00 00 #0003 #mov bx,0000
#EB 03 #0006 #jmp 000B
#05 01 00 #0008 #add ax,0001
#40 #000B #inc ax

两个功能差别不大的汇编代码,得到同样的结果,说明CPU在执行jmp指令时不需要转移的目标地址,也就是不需要这个地址就能实现对IP的修改

CPU执行指令的过程回顾:

1.从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;

2.IP=IP+(所读命令的长度),从而指向下一条指令

3.执行指令,转到1,重复该过程

比较两段代码后发现,跳转距离相同,都是3,这也就是03的含义

所以jmp short (标号)的功能为:(IP)=(IP)+八位位移

1.八位位移=标号处的地址 - jmp指令后第一个字节的地址;

2.short 指明此处位移为八位位移

3.八位位移的参数范围时-128~127,用补码表示

4.八位位移由程序编译时由编译器算出

还有一种和jmp short 标号功能相近的指令格式,jmp near ptr 标号,它实现的时段内近转移

jmp near ptr 标号 的功能为(IP) = (IP) + 十六位位移

1.16位位移 = 标号处的地址 - jmp 指令后的第一个字节的地址

2.near ptr 指明此处位移为16位位移,进行的是段内近转移

3.段内近转移的范围为-32768~32767,用补码表示

4.16位位移由编译程序在编译时算出

转移的目的地址在指令中的jmp指令

jmp far ptr 标号 实现的是段间转移,又称为远转移

(CS)=标号所在段的段地址 (IP)=标号所在段中的偏移地址

far ptr 指明了指令用标号的段地址和偏移地址修啊给CS和IP

范例:

1
2
3
4
5
6
7
8
9
10
11
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0)
s:add ax,1
inc ax
codesg ends
end start

对应的机械码如下:

1
2
3
4
5
6
7
#B8 00 00         #0000   #mov ax,0000
#BB 00 00 #0003 #mov bx,0000
#EA 0B 01 BD 0B #0006 #jmp 0BBD:010B
#00 00 #000B #add [BX+SI],al
#00 00 #000D #add [BX+SI],al
#00 00 #000F #add [BX+SI],al
#00 00 #0011 #add [BX+SI],al

我们需要注意一下jmp对应的机械码:EA 0B 01 BD 0B 其中包含转移的目的地址”0B 01 BD 0B“是目的地址在指令中的顺序,高地址是’BD 0B’是转移的段地址;

0BBDH;低地址是”0B 01“是偏移地址:010BH

转移地址在寄存器中的jmp指令

命令格式:jmp 16位 reg

功能;(IP)=(16位的reg)

转移地址在内存中的jmp指令

两种形式:

1.jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处存放一个着一个字,这是转移的目的偏移地址

内存单元可以用寻址方式的任意格式给出

范例:

1
2
3
4
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds
#执行后IP为0123H

范例2:

1
2
3
4
mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
#执行后IP为123H

2.jmp dword ptr 内存单元地址(段间转移)

功能:从内存单元处开始存放着两个字,高地址的字是转移的目标地址的段地址,低地址是转移目的的偏移地址

(CS) = (内存单元地址+2)

(IP) = (内存单元地址)

内存单元地址可以任意形式给出

范例:

1
2
3
4
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]

执行后,(CS)=0; (IP)=0123H,CS:IP指向0000:0123

范例2:

1
2
3
4
mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]

执行后,(CS) = 0,(IP) = 0123H,CS:IP 指向0000:0123

jcxz指令(Jump if CX equals Zero)

jcxz指令为有效转移指令,所有的有条件转移的指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127

指令格式:

jcxz 标号 (如果(cx) = 0,转到标号处执行)

操作:当(cx) = 0 时,(IP) = (IP) + 8位位移

8位位移 = 标号处的地址 - jcxz指令后的第一个字节的地址;

8位位移的范围为-128~127,用补码表示;

8位位移由编译程序在编译时算出。

用C语言的形式理解就等效于:

1
if((cx) == 0)jmp short 标号

loop指令

loop指令是循环指令,所有循环指令都是短转移,在对应的机械码中都包含转移的位移,而不是目的地址,IP修改范围是-128~127

根据位进行转移的意义:便于灵活处理,如果机械码中是s的具体内存地址,则对代码段在内存中的偏移地址有着严格的限制(例如:之后想修改代码,而代码段的被修改语句机械码长度与原来不同,这时若机械码还是表示固定的s的原来地址,则会发生报错)

编译器对转移位移超界的检测

书上也没具体介绍原理,只是说明会检测

程序分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:codesg
codesg segment
mov ax,4c00h
int 21h
start: mov ax,0
s:nop
nop

mov di,offset s
mov si,offset s2
mov ax,cs:[si]
mov cs:[di],ax

s0:jmp short s
s1:mov ax,0
int 21h
mov ax,0
s2:jmp short s1
nop

codesg ends
end start

CALL指令和RET指令

call和ret指令都是转移指令,修改IP的内容,从而实现近转移;

retf调用栈中的数据,修改CS和IP的内容,从而实现远转移;

CPU执行ret指令时,进行下面两步操作

1.IP = (ss*16) + (sp)

2.(sp) = (sp) + 2

CPU执行retf指令时,进行一下面四步操作

1.(IP) = (ss)*16 + (sp)

2.(sp) = (sp) + 2

3.(cs) = (ss*16) + (sp)

4.(sp) = (sp) + 2

从汇编的角度解释这两个命令就是:

1
2
3
4
5
#ret
pop IP
#retf
pop IP
pop CS

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code

stack segment
db 16 dup (0)
stack ends

code segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
ret
code ends

end start

ret执行后IP = 0,CS:IP指向代码段的第一条指令

call指令:

CPU执行call指令时,会进行两步操作:

1.将当前(其实是call这句话的下一句)的IP或CS和IP压入栈中;

2.转移

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同

根据位移进行转移的call指令

格式:call 标号

1.(sp) = (sp) - 2

((ss)*16+(sp)) = (IP)

2.IP = IP +16位位移

CPU执行call指令相当于

1
2
push IP
jmp near ptr 标号

转移的目的地址在指令中的call指令:

1
call far ptr 标号

CPU在执行此格式的call指令时会进行一下操作:

1.(sp) = (sp) - 2

​ (ss)*16+(sp) = (cs)

​ (sp) = (sp) +2

​ (ss)*16+(sp) = (IP)

2.(CS) = 标号所在的段地址

(IP) = 标号所在的偏移地址

从汇编的角度看就是

1
2
3
push CS
push IP
jmp far ptr 标号

转移地址在寄存器里的call指令:

指令格式:call 16reg

功能:

(sp) = (sp) - 2

(ss)*16+(sp) = (IP)

IP = (16位reg)

1
2
push IP
jmp 16位reg

转移地址在内存中的call指令:

1.call word ptr 内存单元地址

等效汇编指令:

1
2
push IP
jmp word ptr 内存单元地址

2.call dword ptr 内存单元地址

等效汇编指令:

1
2
3
push CS
push IP
jmp dword ptr 内存单元地址

call和ret的结合使用

范例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start

范例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code

stack segment
db 8 dup (0)
db 8 dup (0)
stack ends

code segment
start:mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call s
mov ax.4c00h
int 21h
s:add ax,ax
ret
code ends

end start

mul指令

乘法指令:

注意事项:

1.相乘的两个数,要么是八位,要么是十六位。若为八位,一个默认放在AL中另一个放在八位reg中或内存字节单元中。

如果是16位,一个默认放在AX中,另一个放在一个16位的寄存器中或内存字单元中

2.结果:8位乘法默认放在AX中;16位乘法高位默认在DX中存放,低位默认在AX中存放

格式:

1
2
mul reg
mul 内存单元#内存单元可由不同的寻址方式给出

参数和结果传递问题

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
code segemnt
start:mov ax,data
mov ds,ax
mov si,0
mov di,16 ;ds:si指向第一组word单元
;ds:si指向第二组dword单元
mov cx,8
s:mov bx,[si]
call cube
mov [di],ax
mov [di].2,ax
add si,2 ;ds:si指向下个word单元
add di,2 ;ds:si指向下个dword单元
loop s

mov ax,4c00h
int 21h

cubu:mov ax,bx
mul bx
mul bx
ret

code ends
end start

批量数据的传递

因为寄存器数量有限,对于参数调用和返回值存储的功能十分有限

这种时候我们选择将批量数据放到内存中,然后将他们所在的内存空间的首地址放在寄存器中,传递给需要的子程序,对于批量的数据返回结果,也可用同样的方法

对于所调用的子程序,需要知道:字符串的内容和长度

考虑到程序需要用到循环,且循环次数次数恰好就是字符串长度,出于方便考虑,可以将字符串的长度放到cx中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
capital: cs:code
db 'conversation'
data segment
start:mov ax,data
mov ds,ax
mov si,0
mov cx,12
call capital
mov ax,4c00h
int 21h

capital:and byte ptr [si],11011111b
inc si
loop capital
ret
code ends
end start

寄存器冲突问题

范例:

##程序目的:将一个全是字母以0结尾的字符串全部转化成大写

程序说明:

#将一个全是字母,以0为结尾的字符串,转化为大写

#参数:ds:si指向字符串的首地址

#结果:没有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
##完整程序
code segment

start: mov ax,data
mov ds,ax
mov bx,0

mov cx,4
s: mov si,bx
call capital
add bx,5
loop s

mov ax,4c00h
int 21h

capital: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital
ok: ret
code ends
end start

范例程序思想上完全正确,但细节上存在错误——子程序和主程序均使用cx进行循环控制

对于这种常见错误我们提出两种解决方式:

1.在编写调用子程序时,注意看看子程序有没有会用到产生冲突的寄存器,如果有,调用者使用别的寄存器;

2.在编写子程序时,不会使用产生冲突的寄存器。

但实际上上面两种方法均不太可能实现

解决方法

在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前恢复。可以用栈来保存寄存器内容。

编写程序的标准框架:

1
2
3
4
5
6
7
8
子程序开始: 子程序中使用的寄存器入栈

子程序内容

子程序中使用的寄存器内容出栈

返回(ret,retf)

改进程序:

1
2
3
4
5
6
7
8
9
10
11
12
capital:  push cx
push si
change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change

ok: pop si
pop cx
ret

标志寄存器

CPU内部存在一种特殊的寄存器,具有以下三种作用:

1.用来存储相关命令的执行结果,

2.为CPU某些行为提供依据,

3.用来控制CPU的相关工作方式.

这种特殊的CPU叫做标志寄存器,8086CPU的标志寄存器有16位,简称flag寄存器。

ZF标志:

flag的第六位是ZF,零标志位,执行相关命令后,检查结果是否为0。如果结果为0,那么zf=1;如果不为0,则zf=0
1
2
3
mov ax,1
sub ax,1
#zf = 1
1
2
3
mov ax,2
sub ax,1
#zf = 0

并不是所有运算都会影响到flag寄存器,影响到flag寄存器的大多数是运算指令,按照逻辑或算数运算;大多数对flag寄存器没有影响的指令都是传送指令

使用指令时要注意这个指令的全部功能,其中包括对flag寄存器的影响

PF标志:

flag的第二位时PF,奇偶标志位,它记录相关指令后,其结果中所有bit位中1的个数是否为偶数。如果一的个数为偶数,pf=1,如果为奇数,那么pf=0。

1
2
3
mov al,1
add al,10
#ax的结果是00001011B,其中有三个1,pf = 0
1
2
3
mov al,1
or al,2
#ax的结果是00000011B,其中有两个1,pf = 1

SF标志

flag的第七位是SF,符号标志位。它记录相关指令后结果是否为负。若结果为负,sf = 1,反之结果则为0

计算机中经常用补码表示一个有符号数据,计算机可以将一个数据看成有符号数,也可以将一个数据看成无符号数

例:

1
2
#00000001B  可以看作无符号数1,也可看做有符号数+1
#10000001B 可以看作无符号数129,也可看作有符号数-127

汇编语言
http://example.com/汇编语言(王爽)知识点总结/
作者
briteny-pwn
发布于
2025年3月31日
许可协议