kr 第三阶段(一)16 位汇编

2023-10-18 08:28
文章标签 16 汇编 kr 第三阶段

本文主要是介绍kr 第三阶段(一)16 位汇编,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为什么要学习 16 位汇编?

  • 16 位汇编包含了大部分 32 位汇编的知识点。
  • 有助于在学习内核的两种模式。
    • 实模式:访问真实的物理内存
    • 保护模式:访问虚拟内存
  • 有助于提升调试能力,调试命令与 OllyDbg 和 WinDebug 通用。
  • 可以学习实现反汇编引擎(32 位的汇编引擎实现起来比较麻烦)

汇编基础

硬件运行机制

二极管

原则上仅允许电流作单方向传导,它在一个方向为低电阻,高电流,而在另一个方向为高电阻。计算机将高低电压定义为 0 和 1,借助二极管的特性完成运算。这就是为什么计算机以二进制形式运算。
在这里插入图片描述

门电路

门电路(Logic Gate Circuit)是数字电子电路中的基本构建块,用于实现逻辑运算和控制信号。逻辑门根据不同的逻辑运算规则来处理输入信号,并产生相应的输出信号。
在这里插入图片描述
常见的逻辑门包括与门(AND gate)、或门(OR gate)、非门(NOT gate)、异或门(XOR gate)等,上图是与门的实现。

算术/逻辑单元

所有的数学运算都可以由位运算组成。将常用运算符封装成一个器件,称之为单元。
在这里插入图片描述
运算单元通常由两个输入端,一个控制端和一个输出端组成。

  • 输入端用来传输操作数
  • 输出端用来输出运算结果
  • 控制端用于控制运算单元做对应的运算。

机器码

可以控制硬件的二进制数据叫做机器码。

以上面的 ALU 为例,假设该 ALU 能进行 8 位二进制数的运算。
在这里插入图片描述
则我们可以将表达式 15 + 23 与上面的 ALU 的输入作如下对应:

  • 输入1:00001111
  • 输入2:00010111
  • 控制:00

将上面的输入按照 输入1 输入2 控制 的格式可以写作 00 00001111 00010111(为了方便阅读用空格隔开),这就是机器码。

类似的还有如下机器码:

  • 15 & 2310 00001111 00010111
  • 15 ^ 2311 00001111 00010111

助记符

机器码的二进制值难记,因此可以将每种功能的二进制控制码取一个容易记住的名字,这个名字叫做助记符,也称之为指令。
例如:

  • 00add
  • 01sub
  • 10and
  • 11xor

因此上面列举的机器码可以转换为如下汇编:

  • 15 + 23 => 00 00001111 00010111 => add 0fh, 17h
  • 15 & 23 => 10 00001111 00010111 => and 0fh, 17h
  • 15 ^ 23 => 11 00001111 00010111 => xor 0fh, 17h

汇编

硬件不能识别助记符,因此需要将其转换成对应的机器码,这个过程叫做汇编。
在这里插入图片描述
关于汇编代码有几个关键名词,在查阅(反)汇编器文档时会经常遇到:

  • 助记符:Mnemonic
  • 操作数:Operand

微机系统硬件组成

一个典型的硬件系统组成

一个系统不可能由一个硬件单独完成,所以划分出多个硬件模块,然后由一个硬件模块居中调度,称作 CPU(Central Processing Unit)。
在这里插入图片描述

8086 CPU

在这里插入图片描述

  • 8086 是 16 位 CPU,有 20 根地址线(通过段寄存器寻址扩展了 4 位)和 16 根数据线。
  • 前缀为 AD 的引脚是用来寻址或存取数据的引脚。这种一个种引脚承担多种功能的特点称为引脚复用。
  • CLK 引脚是 CPU 的是时钟输入引脚,它接收来自外部的时钟信号,用于同步处理器内部的操作和各种电子元件的工作。时钟信号的频率决定了处理器的工作速度,即指令的执行速度。
  • 80286 是 8086 的后续型号,也是一款 16 位 CPU。它在 8086 的基础上引入了一些新的特性和改进,并提供了更高的性能。
  • 80386 是 Intel 推出的第一个 32 位 CPU,也被称为 386。

IO 桥

所有硬件模块连接到 I/O 桥,由 I/O 桥负责辅助 CPU 与哪一个硬件模块连接。
在这里插入图片描述
以上图为例,s 决定了 out 的输出是 a 还是 b

总线

以下图为例,CPU 有 8 位数据/地址总线,RAM 是一个 256 字节的存储器。
在这里插入图片描述

  • 控制线表示 CPU 的对 RAM 操作,例如寻址,读,写等。
  • 数据地址线表示 CPU 操作的 RAM 的地址。

计算机系统组成

计算机系统分层结构图

在这里插入图片描述

计算机程序的编译运行

以一个 hello.c 程序为例。

  • 编译
    在这里插入图片描述
  • 加载可执行文件
    在这里插入图片描述
  • 执行
    在这里插入图片描述

8086 CPU 组织架构

8086 功能框图

在这里插入图片描述

执行单元(EU)

执行单元(Execution Unit,EU)是8086系列处理器中的一个重要组成部分,它负责执行指令并控制处理器的操作。

  • 算术逻辑单元(Arithmetic Logic Unit,ALU):ALU 是 EU 的核心部件,用于执行算术和逻辑运算。它可以执行加法、减法、乘法、除法等算术操作,以及逻辑操作(如与、或、非、异或等)和移位操作。
  • 寄存器组:EU 包含多个通用寄存器,用于存储数据和中间结果。这些寄存器包括累加器(Accumulator)、通用寄存器(General Purpose Registers)、标志寄存器(Flags Register)等。寄存器提供了快速的数据存储和访问,用于执行指令时的数据操作。
  • 控制单元:EU 的控制单元负责控制指令的执行流程。它根据指令的操作码和其他相关信息,生成相应的控制信号,以控制 ALU、寄存器和其他组件的操作。控制单元还负责处理分支指令、循环指令和异常情况等。
  • 数据通路:EU 内部的数据通路用于将指令和数据在不同的组件之间传递。它包括数据总线、地址总线和控制总线等,用于传输数据、地址和控制信号。

总线接口单元(BIU)

总线接口单元(Bus Interface Unit,BIU)是8086系列处理器中的一个重要组成部分,它负责处理处理器与系统总线之间的接口和通信。

  • 段管理:8086 处理器使用分段的内存管理方式,BIU 负责管理段寄存器和段选择子。它从段寄存器中获取段地址,并与偏移地址组合成物理地址,用于内存访问。
  • 指令预取:BIU 包含一个指令队列(Instruction Queue),用于预取和缓存指令。它从内存中获取指令并将其存储在队列中,以供执行单元(EU)使用。这样可以提高指令的获取速度,减少对内存的访问次数。
  • 总线控制:BIU 负责处理与系统总线的交互,包括地址传输、数据传输和控制信号的生成。它将处理器的地址、数据和控制信号发送到总线上,同时接收来自总线的响应和数据。
  • 内存访问和数据传输:BIU 负责处理对内存和外部设备的访问。它可以执行内存读取和写入操作,并处理与外部设备的数据传输。

寄存器

在这里插入图片描述

流水线

CPU 执行指令的过程可以分为一下 5 个步骤,其中 1,2,4 是必须的。

  1. 取指令
  2. 译码
  3. 取数据
  4. 执行
  5. 存储结果

8086 CPU 将指令的执行分成多个模块,这样可以多个模块同时工作,从而提高效率。
在这里插入图片描述
然而这种优化在程序中分支跳转较多的时候会导致程序运行变慢。因为提前取到的下一条指令是地址与当前指令地址相邻的指令。而当前指令如果为跳转指令则需要消除提前执行的下一条指令的痕迹。因此编译器优化的其中一个方向是尽量减少程序中的分支跳转数量。

debug 的使用

环境配置

获取 debug

在 Windows XP 的 C:\WINDOWS\system32 目录下有一个名为 debug.exe 的程序。debug 是一个命令行工具,它提供了一种简单的方式来执行低级别的调试和汇编操作。

不过这个程序只能在 Windows XP 系统下运行,高版本的 Windows 系统已经不支持该程序的运行。

安装 DOSBox

为了能够让 debug 在高版本的 Windows 系统下运行(方便后续编写汇编程序),需要安装 DOSBox 程序来模拟相应环境。(也可以使用 msdosplayer)

双击在 DOSBox 安装目录下的 DOSBox 0.74-3 Options.bat 可以打开 DOSBox 的配置文件。在文件末尾可以添加 DOSBox 启动时要执行的初始化命令。

这里我添加了如下命令:

mount c "C:\Program Files (x86)\DOSBox-0.74-3\C"
set path=C:
C:
  • DOSBox 需要手动挂载硬盘,即需要将电脑中的某个目录映射到 DOSBox 中作为一个硬盘。这里我将 C:\Program Files (x86)\DOSBox-0.74-3\C(手动创建的一个目录)挂载为 DOSBox 的 C 盘。
  • 将 DOSBox 中的 C: 盘添加为环境变量。
  • 切换当前目录为 C 盘。

另外我还将 Windows XP 中的 debug.exe 复制到 DOSBox 挂载的 C 盘中,这样就可以再 DOSBox 中运行 debug 进行调试了。

debug 常用命令

  • ?:显示 Debug 命令列表。

  • u [range]:反汇编。没有 range 默认从 CS:IP 或上一次反汇编结束位置开始反汇编。

    -u
    0AF1:0100 7419          JZ      011B
    0AF1:0102 8B0ED596      MOV     CX,[96D5]
    0AF1:0106 E313          JCXZ    011B
    0AF1:0108 B01A          MOV     AL,1A
    0AF1:010A 06            PUSH    ES
    0AF1:010B 33FF          XOR     DI,DI
    0AF1:010D 8E06B496      MOV     ES,[96B4]
    0AF1:0111 F2            REPNZ
    0AF1:0112 AE            SCASB
    0AF1:0113 07            POP     ES
    0AF1:0114 7505          JNZ     011B
    0AF1:0116 4F            DEC     DI
    0AF1:0117 893ED596      MOV     [96D5],DI
    0AF1:011B BB3400        MOV     BX,0034
    0AF1:011E E00A          LOOPNZ  012A
    
  • a [addr]:在指定地址写入汇编机器码。

    -a 110
    0AF1:0110 mov ax, ax
    0AF1:0112 mov dx, dx
    0AF1:0114 mov ax, dx
    0AF1:0116
    -u 110 l 6
    0AF1:0110 89C0          MOV     AX,AX
    0AF1:0112 89D2          MOV     DX,DX
    0AF1:0114 89D0          MOV     AX,DX
    
  • r [reg]:显示或改变一个或多个寄存器。

    -r ax
    AX 0000
    :1234
    -r ax
    AX 1234
    :
    
  • d [range]:显示部分内存的内容。

    -d 110
    0AF1:0110  96 F2 AE 07 75 05 4F 89-3E D5 96 BB 34 00 E0 0A   ....u.O.>...4...
    0AF1:0120  C7 96 00 74 03 BB 00 98-BE 77 97 8B 3E B9 98 B9   ...t.....w..>...
    0AF1:0130  08 00 E8 12 00 80 3C 20-74 09 B0 2E AA B9 03 00   ......< t.......
    0AF1:0140  E8 04 00 32 C0 AA C3 B4-00 8A F1 80 FC 01 74 09   ...2..........t.
    0AF1:0150  B4 00 8A 07 E8 DC E2 74-02 FE C4 AC 3C 3F 75 27   .......t....<?u'
    0AF1:0160  80 FC 00 74 20 80 FC 01-75 22 3A CE 75 05 80 3C   ...t ...u":.u..<
    0AF1:0170  20 74 0A 80 3C 3F 75 14-83 F9 01 76 0F 8A 07 AA    t..<?u....v....
    0AF1:0180  43 46 49 FE C4 8A 07 3C-20 74 01 AA 43 E2 BC C3   CFI....< t..C...
    
  • e:修改内存。

    • e addr
      -e 110
      0AF1:0110  96.11   F2.22   AE.33   07.44   75.55
      -d 110 l 5
      0AF1:0110  11 22 33 44 55                                    ."3DU
      
    • e addr val1[逗号|空格 val2 逗号|空格 val3...]
      -e 110 1,2,3,4 5 6 7 8
      -d 110 l 8
      0AF1:0110  01 02 03 04 05 06 07 08                           ........
      
    • e addr "字符串"
      -e 110 "sky123"
      -d 110 l 6
      0AF1:0110  73 6B 79 31 32 33                                 sky123
      
  • g:运行在内存中的可执行文件。

  • t:步入。

  • p:步过。

  • (n,cx,w):写入文件。

    -n text.txt
    -r cx
    CX 0000
    :100
    -w
    Writing 00100 bytes
    
    • n:要写入的文件的名称。
    • cx:要写入的数据的长度。(写完文件之后 cx 寄存器的值不会改变,还是写之前设置的写入长度。)
    • w:写文件命令。

其中 [range] 有下面两种种形式:

  • [startaddr] [endaddr]:从 startaddrendaddr
  • [startaddr l num]:从 startaddrstartaddr + num

标志寄存器

标志寄存器反应 ALU 运算结果的状态。
在这里插入图片描述

  • 条件标志位
    • SFZFOFCFAFPF
    • CPU 执行完一条指令后自动设置。
    • 反映算术、逻辑运算等指令执行完毕后,运算结果的特征。
  • 控制标志位
    • DFIFTF
    • 控制 CPU 的运行方式,工作状态。
标志truefalseName(名称)命题
OFOV(Overflow)NV(Not Overflow)Overflow Flag(是否溢出)存在溢出?
SFNG(Negative)PL(Plus)Sign Flag(结果的符号是正还是负)是负数(正数看做无符号)?
ZFZR(Zero)NZ(Not Zero)Zero Flag(运算结果是否为 0)是 0 ?
PFPE(Event)PO(Odd)Parity Flag(结果中二进制位个数的奇偶性)是偶数个 1 ?
CFCY(Carry yes)NC(Not carry)Carry Flag(进位标志)有进位?
AFAC(Auxiliary Carry)NA(No Auxiliary Carry)Auxiliary Carry Flag(辅助进位标志)发生辅助进位?
DFDN(Down)UP(Up)Direction Flag(方向标志)sidi 递减?
IFEI(Enable Interrupts)DI(Disable Interrupts)Interrupt Flag(中断标志)允许中断?
TFST(Single Step)NT(Non Trap)Trap Flag(陷阱标志)单步调试模式?

进位标志CF(Carry Flag)

如果运算结果的最高位产生了一个进位或借位,那么,CF 的值为 1 ,否则为 0 。(无符号数溢出)
在这里插入图片描述

  • 由于 CF 是针对无符号数来说的,因此无论是加法还是减法参与运算的数都是无符号数(大于等于 0)。
  • 0 减去任何非零的数 CF 都会置 1 。

奇偶标志PF(Parity Flag)

奇偶标志 PF 用于反映运算结果中最低字节中 1 的个数的奇偶性。如果 1 的个数为偶数,则 PF 的值为 1 ,否则其值为 0 。

辅助进位标志AF(Auxiliary Carry Flag)

在发生下列情况时,辅助进位标志 AF 的值被置为 1 ,否则其值为 0 。

  • 在字操作时,发生低字节向高字节进位或借位时
  • 在字节操作时,发生低 4 位向高 4 位进位或借位时。

零标志ZF(Zero Flag)

如果运算结果为 0 ,则其值为 1 ,否则其值为 0 。在判断运算结果是否为 0 时,可使用此标志位。

符号标志SF(Sign Flag)

符号标志 SF 用来反映运算结果的符号位,它与运算结果的最高位相同。

溢出标志OF(Overflow Flag)

溢出标志 OF 用于反映有符号数加减运算所得结果是否溢出。(有符号数溢出)
在这里插入图片描述

分段与寻址

分段

分段寻址

8086 是 16 位 CPU ,但是能访问 1M 的内存,这是因为 8086 将内存划分成多段,通过段基址+段偏移的方式访问。
在这里插入图片描述

地址计算方式

地址计算方式:内存地址 = 段基址 * 10h + 段偏移

  • 段基址+段偏移的方式一般写作 段基址:段偏移 ,称为逻辑地址。
  • 偏移地址称为 EA(Effective Address,在很多库的文档中会出现这个名称)。
  • 通过逻辑地址计算出来的内存地址称为物理地址 PA(Physical Address)。

段的内存分布如下,不同的段之间可以重叠。由于段地址的计算方式,段的起始地址关于 0x10 对齐。

内存分布

在这里插入图片描述

划分段的原则

  • 段大小可以不是 64K
  • 段与段之间不能有重叠(8086 CPU 和 DOS 操作系统都不会管,内存分段靠自己,这样做只是避免程序出问题。)

在这里插入图片描述

逻辑地址与物理地址

一个物理地址可以由多个逻辑地址表示,但基于分段原则,一般编程中不会碰到。
在这里插入图片描述

可用内存

DOS 系统中,应用程序可用内存约 600K 。
在这里插入图片描述

段寄存器

8086 中,段基址都是存储在段寄存器中,段偏移可以用立即数或者通用寄存器指明。

  • DS:数据段,默认使用 DX
  • CS:代码段,绑定 CS:IP 使用。
  • SS:堆栈段,用作函数栈,绑定 SS:SP 使用。
  • ES:扩展段,常用于串操作。

debug 的一些命令也与段寄存器绑定:

  • au:代码段 CS
  • de:数据段 DS

当然我们也可以指定特定的段,例如 d ss:100

8086 有 20 跟地址线,16 根数据线,其中数据线与地址线的低 16 位复用。内部通过地址加法器计算地址。
在这里插入图片描述

寻址

  • 指令中用于说明操作数所在的方式称为寻址方式。
  • 16 位 CPU 的寻址方式有 7 种,32 位 CPU 还会多一种比例因子寻址。

立即寻址

  • 操作数的值存在指令中的方式称作立即寻址。
  • 汇编中整数常量称作立即数。
  • 立即数可以是 8 位数,也可以是 16 位数。

在这里插入图片描述

寄存器寻址

  • 操作数的值存储在寄存器的寻址方式称作寄存器寻址。
  • 寄存器包括通用寄存器和段寄存器。

在这里插入图片描述
注意:

  • 段寄存器之间不能赋值。
  • 指令指针寄存器不能用作寻址(例如不存在 MOV AX, IP 汇编)。

直接寻址

操作数值在内存中,机器码中存储 16 位段内偏移的寻址方式称作直接寻址。
在这里插入图片描述

寄存器间接寻址

  • 操作数值在内存中,段内偏移存储在寄存器中的寻址方式称作寄存器间接寻址。
  • 间接寻址的寄存器有 BXBPSIDI

EA = [ ( BX ) ( BP ) ( SI ) ( DI ) ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ (\text{SI})\\ (\text{DI}) \end{bmatrix} EA= (BX)(BP)(SI)(DI)
在这里插入图片描述

寄存器相对寻址

  • 操作数值在内存中,段内偏移存储由 [寄存器 + 立即数] 计算得来的的寻址方式称作寄存器相对寻址。
  • 寄存器相对寻址的寄存器有 BXBPSIDI
  • 寄存器相对寻址的立即数可以是 8 位,可以是 16 位的。

EA = [ ( BX ) ( BP ) ( SI ) ( DI ) ] + [ 8 位 disp 16 位 disp ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ (\text{SI})\\ (\text{DI}) \end{bmatrix}+\begin{bmatrix} \text{8 位 disp} \\ \text{16 位 disp} \end{bmatrix} EA= (BX)(BP)(SI)(DI) +[ disp16  disp]
在这里插入图片描述

基址变址寻址

  • 操作数值在内存中,段内偏移由 [寄存器 + 寄存器] 计算得来的寻址方式称作基址变址寻址。
  • 可用做基址的寄存器有 BXBP
  • BX 默认 DS 段,BP 默认 SS 段。
  • 可用作变址的寄存器有 SIDI

EA = [ ( BX ) ( BP ) ] + [ ( SI ) ( DI ) ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ \end{bmatrix}+\begin{bmatrix} (\text{SI})\\ (\text{DI}) \end{bmatrix} EA=[(BX)(BP)]+[(SI)(DI)]
在这里插入图片描述

基址变址相对寻址

  • 操作数值在内存中,段内偏移由 [基址寄存器+变址寄存器+偏移常量] 计算得来的寻址方式称作基址变址寻址。
  • 可用做基址的寄存器有 BXBP
  • BX 默认 DS 段,BP 默认 SS 段。
  • 可用作变址的寄存器有 SIDI
  • 可用作常量的数值可以是 8 位,可以是 16 位。

在这里插入图片描述

指令

数据传送类指令

传送指令 MOV (move)

把一个字节或字的操作数从源地址传送至目的地址。
在这里插入图片描述
注意:

  • 不存在存储器向存储器的传送指令。
  • mov 指令源操作数和目的操作数指定的数据长度应一致。
  • 立即数到内存的数据传送指令需要指定数据长度,例如 mov byte ptr ds:[bx], 12h 。其他指令由于寄存器自带长度因此不需要指定数据长度。

交换指令 XCHG(exchange)

情形:

  • 寄存器与寄存器之间对换数据
  • 寄存器与存储器之间对换数据
  • 不能在存储器与存储器之间对换数据

效率:mov 优于 xchg ,因为 xchg 使用了内部暂存器。

换码指令 XLAT

作用:将 BX 指定的缓冲区中 AL 指定的位移处的一个字节取出赋给 AL ,即:al <-- ds:[bx + al] 。该指令无操作数。

用途:键盘的扫描码,需要转为 ASCII 码,可以将扫描码做成表,扫描码作下标可以查到对应的 ASCII 码。

堆栈操作指令

  • 进栈:push reg,相当于 sub sp, 2; mov [sp], reg;
  • 出栈:pop reg,相当于 mov reg, [sp]; add sp, 2;
  • 保存所有寄存器环境
    • 16 位:pusha/popa
    • 32 位:pushad/popad

注意:

  • 对于 8086 CPU,push 指令的操作数只能是长度为 2 字节的寄存器(包括段寄存器)或内存。80286,80386 及以上的 CPU 的 push 指令支持立即数和寄存器。
  • 8086 不支持 pusha 指令,80286 才开始支持该指令。
  • pusha 指令会将 16 位通用寄存器 AXCXDXBXSPBPSIDI 中的值依次压入栈中。

标志寄存器传送指令

标志寄存器传送指令用来传送标志寄存器 FLAGS 的内容,方便进行对各个标志位的直接操作。
在这里插入图片描述

  • 低 8 位传送
    • LAHFAHFLAGS 的低字节
      • LAHF 指令将标志寄存器的低字节传送给寄存器 AH
      • SF/ZF/AF/PF/CF 状态标志位分别送入 AH 的第 7/6/4/2/0 位,而 AH 的第 5/3/1 位任意。
    • SAHFFLAGS 的低字节 ← AH
      • SAHFAH 寄存器内容传送给 FLAGS 的低字节。
      • AH 的第 7/6/4/2/0 位相应设置 SF/ZF/AF/PF/CF 标志位。
  • 16 位传送
    • PUSHFPUSHF 指令将标志寄存器的内容压入堆栈,同时栈顶指针 SP 减 2 。
    • POPFPOPF 指令将栈顶字单元内容传送标给志寄存器,同时栈顶指针 SP 加 2 。
  • 32 位传送
    • PUSHFD:将 ELFAGS 压栈。
    • POPFD:将栈顶 32 字节出栈到 EFLAGS 中。

地址传送指令

地址传送指令将存储器单元的逻辑地址送至指定的寄存器

  • 有效地址传送指令 LEA(load EA):将存储器操作数的有效地址传送至指定的 16 位寄存器中。
  • LDS r16, mem:将主存中 mem 指定的字送至 r16 ,并将 mem 的下一字送 DS 寄存器。
  • LES r16, mem:将主存中 mem 指定的字送至 r16 ,并将 mem 的下一字送 ES 寄存器。

输入输出指令

8086 通过输入输出指令与外设进行数据交换;呈现给程序员的外设是端口(Port)即 I/O 地址。8086 用于寻址外设端口的地址线为 16 条,端口最多为 2 16 2^{16} 216=65536(64K)个,端口号为 0000H~FFFFH 。每个端口用于传送一个字节的外设数据。

8086 的端口有 64K 个,无需分段,设计有两种寻址方式:

  • 直接寻址:只用于寻址 00H~FFH 前 256个 端口,操作数 i8 表示端口号。
  • 间接寻址:可用于寻址全部 64K 个端口,DX 寄存器的值就是端口号。对大于 FFH 的端口只能采用间接寻址方式。

输入指令 IN:以将外设数据传送给 CPU 内的 AL/AX 为例

  • IN AL, i8:字节输入,AL ← I/O 端口(i8 直接寻址)
  • IN AL, DX:字节输入,AL ← I/O 端口(DX 间接寻址)
  • IN AX, i8:字输入,AX ← I/O 端口(i8 直接寻址)
  • IN AX, DX:字输入,AX ← I/O 端口(DX 间接寻址)

输出指令 OUT:以将 CPU 内的 AL/AX 数据传送给外设为例

  • OUT i8, AL:字节输出,I/O 端口 ← ALi8 直接寻址)
  • OUT DX, AL:字节输出,I/O 端口 ← ALDX 间接寻址)
  • OUT i8, AX:字输出,I/O 端口 ← AXi8 直接寻址)
  • OUT DX, AX:字输出,I/O 端口 ← AXDX 间接寻址)

这个指令的其中一个用途是检测虚拟机。在真机环境中由于输入输出指令为特权指令,在 3 环执行会触发异常。而在虚拟机中则不会。

算术运算类指令

加法

  • add:加法
    • ADD reg, imm/reg/memregreg + imm/reg/mem
    • ADD mem, imm/regmemmem + imm/reg
  • adc:带进位加法
    • ADC reg, imm/reg/memregreg + imm/reg/mem + CF
    • ADC mem, imm/regmemmem + imm/reg + CF
  • inc:加一,不影响 CF 标志位。
    • INC reg/memreg/memreg/mem + 1

减法

  • sub:减法
    • SUB reg, imm/reg/memregreg - imm/reg/mem
    • SUB mem, imm/regmemmem - imm/reg
  • sbb:带借位的减法
    • SBB reg, imm/reg/memregreg - imm/reg/mem - CF
    • SBB mem, imm/regmemmem - imm/reg - CF
  • dec:减一,不影响 CF 标志位。
    • DEC reg/memreg/memreg/mem - 1

求补指令 NEG(negative)

NEG 指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加 1 。

NEG reg/memreg/mem ← 0 - reg/mem 。如果操作数为 0 则 CF = 0 ,否则 CF = 1

x == 0 ? 0 : -1 为例,我们可以通过 neg 指令将其优化为无分支程序:

mov ax, x
sub ax, 0		; CF 标志位清零
neg ax			; 如果 ax 非 0 则 CF 置位 
sbb ax, ax		; ax = ax - ax - CF = - CF 

对于其他类似的三目运算我们可以通过加减偏移和乘除系数转换为上述的三目运算,因此都可以把分支优化掉。

比较指令 CMP(compare)

  • 格式:CMP OPD, OPS
  • 功能:(OPD) - (OPS)
  • 说明:目的操作数减去源操作数,然后根据结果设置标志位,但该结果不存入目的地址。
  • 影响标志位:AFCFOFPFSFZF
  • 作用:一般后面跟一条条件转移指令,根据比较结果跳转到不同的分支,用于处理 OPDOPS 大小比较不同的情况。

乘法指令

  • 无符号乘法
    位数隐含的被乘数乘积存放的位置举例
    8位ALAXMUL BL
    16位AXDX-AXMUL BX
    32位EAXEDX-EAXMUL ECX
    • 格式:MUL reg/mem
    • 功能:显式操作数*隐式操作数(看成无符号数)
    • 影响标志位:CFOF ,如果乘积的高一半位(AH/DX/EDX)包含有乘积的有效位,则 CF=1OF=1 ;否则 CF=0OF=0
  • 有符号乘法
    • 格式:
      • IMUL reg/mem
      • IMUL reg, imm(80286+)
      • IMUL reg, reg, imm(80286+)
      • IMUL reg, reg/mem(80386+)
    • 功能:有符号数相乘
    • 影响标志位:CFOF ,如果乘积的高一半位(AH/DX/EDX)不是低位的符号扩展,则 CF=1OF=1 ;否则 CF=0OF=0

除法指令

  • 无符号乘法
    位数隐含的被除数除数余数
    8位AX8位opsALAH
    16位DX-AX16位opsAXDX
    32位EDX-EAX32位opsEAXEDX
    • 格式:DIV reg/mem
    • 影响标志位:未定义,即指令执行后标志位是任意的,不可预测的。
    • 除法溢出:8 位除法运算结果大于 8 位,16 位除法运算结果大于 16 位。
  • 有符号除法
    位数隐含的被除数除数余数
    8位AX8位opsALAH
    16位DX-AX16位opsAXDX
    32位EDX-EAX32位opsEAXEDX
    • 格式:IDIV reg/mem
    • 影响标志位:AFCFOFPFSFZF
    • 除法溢出:字节除时商不在 -128~127 范围内或者在字除时商不在 -32768~32767 范围内。

符号扩展指令

  • CBW(Convert Byte to Word):将 AL 中的符号扩展至 AH 中,操作数是隐含且固定的。
    • XX040004
    • XXFEFFFE
  • CWD(Covert Word to Doubleword):将 AX 中的符号扩展至 DX 中,操作数是隐含且固定的。
  • CWDE(Covert Word to Extended Doubleworld,386+):将 AX 中的符号位扩展至 EAX 的高 16 位,操作数是隐含且固定的。
  • CDQ(Cover Doubleword to Quadword,386+):将 EAX 中的符号位扩展至 EDX 中,操作数是隐含且固定的。
  • CDQE(Convert Doubleword to Quadword Extended,x86-64)将 EAX 中的符号位扩展至 RAX 中,操作数是隐含且固定的。

位操作类指令

逻辑运算

  • 逻辑与:AND
    • 格式:AND reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
      • CF(进位标志):AND 指令总是将 CF 标志设置为 0 ,即不会影响进位标志。
      • OF(溢出标志):AND 指令总是将 OF 标志设置为 0 ,即不会影响溢出标志。
      • PF(奇偶标志):AND 指令根据结果中的位数 1 的个数来设置奇偶标志。如果结果中的位数 1 是偶数个,则PF被设置为 1 ,否则设置为 0 。
      • SF(符号标志):AND 指令将结果的最高位(符号位)复制到 SF 标志位中。如果结果的最高位为 1 ,则 SF 被设置为 1 ,表示结果为负数;如果结果的最高位为 0 ,则 SF 被设置为 0 ,表示结果为非负数。
      • ZF(零标志):AND 指令将结果的所有位进行按位与操作,并将零标志设置为1,如果结果为零;否则,将零标志设置为 0 。
      • AF(辅助进位标志):AND 指令不会定义或影响辅助进位标志,因此对该标志位没有任何影响。
  • 逻辑或:OR
    • 格式:OR reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
  • 逻辑非(按位取反):NOT
    • 格式:NOT reg/mem
    • 受影响的标志位:无
  • 异或:XOR
    • 格式:XOR reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
  • TEST 指令
    • 格式:TEST reg/mem, reg/mem/imm
    • 作用:执行 AND ,但不影响目标操作数。
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)

x >= 0 ? x : -x 为例,我们可以通过 cwd 指令和逻辑运算指令将其优化为无分支程序:

mov ax, x
cwd				; 如果 x < 0 则 dx = -1 ,否则 dx = 0
xor ax, dx		; 如果 x < 0 则将 ax 取反,否则 ax 不变
sub ax, dx		; 如果 x < 0 则将 ax 加一,否则 ax 不变

移位指令

  • 算术移位和逻辑移位
    • 格式:OP reg/mem, 1/cl
    • 影响标志:OFZFSFPFCF
    • 指令
      • SAL(Shift Arithmetic Left)/SHL(Shift Logical Left):算术左移/逻辑左移
        在这里插入图片描述
      • SAR(Shift Arithmetic Right):算术右移
        在这里插入图片描述
      • SHR(Shift Logical Right):逻辑右移
        在这里插入图片描述
  • 循环移位
    • 格式:OP reg/mem, 1/cl
    • 影响标志:OFCF,其他标志无定义。
    • 指令
      • ROL(Rotate Left):循环左移
        在这里插入图片描述
      • ROR(Rotate Right):循环右移
        在这里插入图片描述
      • RCL(Rotate through Carry Left):带进位循环左移
        在这里插入图片描述
      • RCR(Rotate through Carry Right):带进位循环右移
        在这里插入图片描述

串操作类指令

串操作指令

  • 源操作数使用 SI ,默认段为 DS ,可段超越。
  • 目的操作数使用 DI ,默认段为 ES ,不可段超越。
  • DF 寄存器决定串操作方向。
    • DF 值为 0(UP) 则执行完指令之后 SIDI 都加操作的数据长度。
    • DF 值为 1(DN) 则执行完指令之后 DIDI 都减操作的数据长度。

段超越(segment override)是指在指令中显式地指定要使用的段寄存器,而不是使用默认的段寄存器。

  • MOVS(Move String):串移动,把字节或字操作数从主存的源地址传送至目的地址。
    • MOVSB:字节串传送,ES:[DI] ← DS:[SI] (SI ← SI ± 1, DI ← DI ± 1)
    • MOVSW:字串传送, ES:[DI] ← DS:[SI] (SI ← SI ± 2, DI ← DI ± 2)
    • MOVSD:双字串传送, ES:[DI] ← DS:[SI] (SI ← SI ± 4, DI ← DI ± 4)
  • STOS(Store String):串存储,把 ALAX 数据传送至目的地址。
    • STOSB:字节串存储,ES:[DI] ← AL (DI ← DI ± 1)
    • STOSW:字串存储,ES:[DI] ← AX (DI ← DI ± 2)
    • STOSD:双字串存储,ES:[DI] ← EAX (DI ← DI ± 4)
  • LODS(Load String):串读取,把指定主存单元的数据传送给 ALAX
    • LODSB:字节读取,AL ← DS:[SI] (SI ← SI ± 1)
    • LODSW:字串读取,AX ← DS:[SI] (SI ← SI ± 2)
    • LODSD:双字串读取,EAX ← DS:[SI] (SI ← SI ± 4)
  • CMPS(Compare String):串比较,将主存中的源操作数减去至目的操作数,以便设置标志,进而比较两操作数之间的关系。
    • CMPSB:字节串比较,DS:[SI] - ES:[DI] (SI ← SI ± 1, DI ← DI ± 1)
    • CMPSW:字串比较,DS:[SI] - ES:[DI] (SI ← SI ± 2, DI ← DI ± 2)
    • CMPSD:双字串比较,DS:[SI] - ES:[DI] (SI ← SI ± 4, DI ← DI ± 4)
  • SCAS(Scan String):串扫描,将 AL/AX 减去至目的操作数,以便设置标志,进而比较 AL/AX 与操作数之间的关系。
    • SCASB:字节串扫描,AL - ES:[DI] (DI ← DI ± 1)
    • SCASW:字串扫描,AX - ES:[DI] (DI ← DI ± 2)
    • SCASD:双字串扫描,EAX - ES:[DI] (DI ← DI ± 4)

重复前缀指令

串操作指令执行一次,仅对数据串中的一个字节或字进行操作。

串操作指令前都可以加一个重复前缀,实现串操作的重复执行。重复次数隐含在 CX 寄存器中。

  • REP:每执行一次串指令,CX 减 1 ,直到 CX = 0 重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0),则继续传送。
    • 举例:
      • REP LODS/LODSB/LODSW/LODSD
      • REP STOS/STOSB/STOSW/STOSD
      • REP MOVS/MOVSB/MOVSW/MOVSD
  • REPZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 0 。只要 CX = 0ZF = 0 则重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0)并且串相等(ZF = 1)则继续比较。
    • 举例:
      • REPE/REPZ SCAS/SCASB/SCASW/SCASD
      • REPE/REPZ CMPS/CMPSB/CMPSW/CMPSD
  • REPNZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 1 。只要 CX = 0ZF = 1 则重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0)并且串不相等(ZF = 0)则继续比较。
    • 举例:
      • REPNE/REPNZ SCAS/SCASB/SCASW/SCASD
      • REPNE/REPNZ CMPS/CMPSB/CMPSW/CMPSD

流程转移类指令

无条件跳转

  • 直接转移
    名称修饰关键字格式功能指令长度示例
    短跳shortjmp short 标号ip ← 标号偏移20005:EB0B jmp 0012
    近跳near ptrjmp near 标号ip ← 标号偏移30007:E90A01 jmp 0114
    远跳far ptrjmp far ptr 标号
    jmp 段名:标号
    ip ← 标号偏移
    cs ← 段地址
    50000:EA00007C07 jmp 0077C:0000
  • 使用寄存器间接转移
    • 格式:jmp regreg 为通用寄存器)
    • 功能:ip ← reg(只能用于段内转移)
  • 使用 EA 的间接转移
    指令说明示例
    jmp 变量名
    jmp word ptr [EA]
    jmp near ptr [EA]
    从内存中取出两字节的段偏移,然后
    ip ← [EA]
    000b:ff260000 jmp[0000]
    000f:8d1e0000 lea bx, [0000]
    0013:ff27 jmp [bx]

    0000:cd 20
    jmp 变量名
    jmp dword ptr [EA]
    jmp far ptr [EA]
    从内存中取出两字节的段偏移和两字节段基址,然后
    ip ← [EA]cs ← [EA + 2]
    0021:ff260600 jmp[0002]
    0025:8d1e0400 lea bx, [0002]
    0029:ff2f jmp far [bx]

    0002:00 00 7d 07

条件跳转

根据标志位判断,条件成立则跳转,条件不成立则不跳。

  • 单条件跳转
    指令英文标志说明
    JZ/JEzero,equalZF = 1相等/等于零
    JNZ/JNEnot zero,not equalZF = 0不相等/不等于零
    JCXZCX is zeroCX = 0CX 为 0
    JSsignSF = 1结果为负
    JNSnot signSF = 0结果为正
    JP/JPEparity,parity evenPF = 11 为偶数个
    JNP/JPOnot parity,parity oddPF = 01 为奇数个
    JOoverflowOF = 1溢出
    JNOnot overflowOF = 0不溢出
    JCcarryCF = 1进位/小于
    JNCnot carryCF = 0不进位/大于等于
  • 无符号数判断
    指令英文标志说明
    JB/JNAEbelow,not above or equalCF = 1小于/不大于等于
    JAE/JNBabove or equal,not belowCF = 0大于等于/不小于
    JBE/JNAbelow or equal,not aboveCF = 1 || ZF = 1小于等于/不大于
    JA/JNBEabove,not below or equalCF = 0 && ZF = 0大于/不小于等于
  • 有符号判断
    指令英文标志说明
    JL/JNGEless,not geater or equalSF != OF小于/不大于等于
    JGE/JNLgreater or equal,not lessSF = OF大于等于/不小于
    JLE/JNGless or equal,not greaterSF != OF || ZF = 1小于等于/不大于
    JG/JNLEgreater,not less or equalSF = OF && ZF = 0大于/不小于等于

LOOP

格式:LOOP 标号 ,只能用于转移。

指令重复条件
LOOPCX != 0
LOOPZ/LOOPECX != 0 && ZF = 1
LOOPNZ/LOOPNECX != 0 && ZF = 0

函数调用相关指令

指令说明功能
call (near ptr) 标号段内直接调用push 返回地址
jmp 标号
call REG
call near ptr|word ptr [EA]
段内间接调用push 返回地址
jmp 函数地址
call far ptr 标号
call dword ptr [EA]
段间调用push cs
push 返回地址
jmp 标号
ret (n)段内返回pop ip
add sp, n
retf (n)段间返回pop ip
pop cs
add sp, n

处理器控制类指令

masm 基础

VSCode 开发环境配置

  • 配置 DOSBox 环境变量
  • 安装 VSCode
  • 安装 MASM/TASMVSCode DOSBox 插件
  • 设置 → 扩展 → MASM/TASM 配置插件

完成上述配置后打开 asm 文件右键会出现“打开DOS环境”等选项,这里使用的是 DOS 自带的 MASM 开发环境和 DOSBox 配置文件,不需要配置直接可以编译运行 asm 文件。

编译命令:

ml /c asm文件.asm
link asm文件.obj

编译+调试脚本(VSCode自带这个功能):

ml /c %1.asm
link %1.obj
debug %1.exe

如果多个 asm 文件编译则将命令中分别添加参与编译的 asm 文件和生成的 obj 文件即可。

函数和变量声明可以统一放在一个 inc 文件中,在使用声明的 asm 文件开头添加 include xxx.inc 即可。

如果想要调试的时候再特定的位置断下来可以在程序中添加 int3 指令或者 db 0cch

入口和段

入口

  • 入口点指定使用关键字 end ,后跟标号名。
  • 如果未指定入口点则默认入口点是整个程序的起始位置。
data_seg segment
mov cx,cx
mov cx,cx
ENTRY:
mov cx,cx
mov cx,cx
data_seg endsend  ENTRY

  • 一个程序必须至少有一个段
  • 一个程序中可以定义多个段
  • 段不能嵌套
  • 段可以重名,重名的段会被编译到同一块内存中。
  • 段的起始地址关于 0x10 对齐。
段名 segment段名 ends

注释

汇编中使用分号(;)来标注注释,汇编中只有行注释没有块注释。

; 这里是注释
mov ax, bx ; 这里是注释

常量

整数

  • 整数可以支持多个进制。
  • 数值必须以数字开头,如果非数字,前面必须加 0 。
  • 负数前面可以加负号(-)。
关键字说明示例
十进制mov ax, 1234
D十进制mov ax, 1234d
B二进制mov ax, 1011b
O八进制mov ax, 76o
H十六进制mov ax, 76o
mov ax, 0abh

字符

  • 字符可以用单引号(')或双引号("),例如 mov byte ptr [bx], '$'

变量

  • 变量可以支持多个类型
  • 变量可以有初始值,未初始化的值用问号(?)表示。
  • 变量一般定义在一个单独的段中。
变量名	类型	初始值
val		dd		5566h
关键字意义
db字节
dw
dd双字
dq8 字节
dt10 字节

变量使用前需要注意两点:

  • 首先要告诉编译器当前使用的是哪个段,这样编译器才能提供正确的段偏移。
  • 需要给段寄存器设置正确的值。
data_seg segmentg_btVal db 55h
data_seg endsuninitdata_seg segmentg_btVal1 db ?
uninitdata_seg endscode_seg segment
START:assume ds:data_seg	; 告诉编译器当前使用的是哪个段mov ax, data_segmov ds, ax			; 给段寄存器设置正确的值mov al, g_btVal		; 使用变量
code_seg endsend START

字符串

  • 字符串都可以用单引号(')或双引号(")。
  • 字符串一般以美元符($)结尾(在内存中 $ 是实际跟在字符串后面的,这么做是因为有些使用字符串的 API 有要求)。
g_szHello db "hello,word!$"

数组

格式:

名字 类型 值1[,值2][,值3][,值4][,值5]
名字 类型 数量 dup(初值)[,数量 dup(初值)][,值]

示例:

g_dbArray1 db 78h, 96h, 43h					; 后面跟初始化的值
g_dbArray2 db 256 dup(0), 128 dup(11h)		; 重复 256 个 0 ,再跟重复 128 个 1 。
g_dbArray3 db 256 dup(0), 78h, 96h, 43h		; 重复 256 个 0 ,再跟 78h 96h 43h 。
g_dbArray4 db 256 dup(?)					; 开辟 256 字节的空间,不做初始化(初始化为 0)。

属性

masm 提供了很多伪指令,可以获取变量的大小和地址,称之为变量的属性。这些属性在编译过程中会计算成具体的常量值。

关键字意义
seg取段基址
offset取段偏移
type取元素类型大小
length取元素个数
size取数据大小(length * type

注意:

  • seg 可以作用于段或者段内的变量,结果都是得到对应段的基址。
  • lengthsize 都是按定义的数组的第一个“,”前面的部分来计算的。例如前面的 g_dbArray3 计算的 length 是 0x100,g_dbArray1 计算的 length 是 1 。
  • 区分以下四种用法:
    • lea di, g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。
    • lea di, offset g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。
    • mov dl, g_dbArray:获取 g_dbArray 前 1 个字节到 DI 寄存器中。(这里不能用 DX ,因为寄存器应该与数组元素大小匹配。)
    • mov di, offset g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。

示例:

mov ax, seg g_dbArray1
mov ax, seg data_seg
mov ax, offset g_dbArray1
mov ax, type g_dbArray1
mov ax, length g_dbArray1
mov ax, size g_dbArray1

堆栈

  • stack 关键字让程序在被加载的时候指定 ssbpsp
  • 使用数组为栈设置大小。
stack_seg segment stackdb 256 dup(0cch)
stack_seg ends

调用 dos 功能号

  • DOS 系统提供的功能(API),通过 21 号中断来调用。
  • 每个功能都有一个编号,通过 AH 指定功能编号。
  • 每个功能的参数查看手册。

例如:

  • AH 为 0x4c 时为退出程序,退出码为 AL
  • AH 为 0x09 时为输出 $ 结尾的字符,字符串地址存放在 DX 中。

利用这两个功能号我们可以实现一个 Hello World 程序。

data_seg segmentg_szHello db "hello,word!$"
data_seg endsstack_seg segment stackdb 256 dup(0cch)
stack_seg endscode_seg segment
START:assume ds:data_segmov ax, data_segmov ds, axmov ah, 09mov dx, offset g_szHelloint 21hmov ax, 4c00hint 21h
code_seg endsend START

中断

  • 中断是 CPU 提供的流程跳转指令,类似函数调用。
  • 00:00 位置存储着一个双字数组,大小为 256 ,称作中断向量表。
  • 数组元素为逻辑地址段基址:段偏移(段偏移在低 2 字节)。
  • int n 的意思是从第 n 个元素获取地址,然后跳转执行。

在这里插入图片描述

函数

函数结构

函数执行流程:

  • 参数入栈
  • 返回地址入栈,跳转到函数
  • 保存栈帧
  • 申请局部变量空间
  • 保存寄存器环境
  • 执行函数功能
  • 恢复寄存器环境
  • 恢复栈帧
  • 弹出返回地址,返回[平栈]
  • [平栈]

函数定义

函数名 proc [距离][调用约定] [uses reg1 reg2..] [参数:word, 参数名:word..]local 变量:wordlocal 变量:wordret
函数名 endp

示例:

TestProc PROC far stdcall uses bx dx si di arg1:wordlocal btVal:byteret
TestProc ENDP
  • 距离:
    距离关键字说明
    near函数只能段内调用
    函数使用 ret 返回
    调用时 ip 入栈
    far段内段间都可调用
    函数使用 retf 返回
    调用时 ipcs 入栈
    • 如果是用 far 修饰且段内调用,汇编器也会手动压一个 cs 寄存器确保 retf 能正常返回。
  • 调用约定
    调用约定关键字说明
    c调用方平栈
    stdcall被调用方平栈
  • 局部变量
    类型局部变量类型备注
    dbbyte可以直接赋值使用
    dwword可以直接赋值使用
    dddword不可以直接赋值使用
    dqqword不可以直接赋值使用
    dttword不可以直接赋值使用
    • 一般习惯在局部变量前加 @ ,属于一种编程规范。
    • 数组局部变量定义:local @dwBuf[100h]:byte
  • 保存寄存器:uses reg1 reg2.. 表示函数中会使用相应的寄存器,因此在函数开始和结束位置会保存和恢复相应的寄存器。

invoke 伪指令

invoke 函数名, 参数1, 参数2, 参数3

说明:

  • 会生成参数入栈代码,参数可以是立即数,变量,寄存器等。注意立即数不能直接 push ,汇编器会使用 AX 寄存器中转一下,因此注意 AX 寄存器的使用。
  • 如果是 C 调用约定会生成平栈代码。
  • 如果是局部变量取地址需要使用 addr 伪指令。
  • 使用 addr 的时候会用 AX 临时存放指针值,因此注意 AX 的使用。
伪指令说明
offset获取段内偏移
addr获取局部变量地址,使用 LEA 指令。专用于 invoke

函数声明

如果调用另一个文件中的函数或者在定义函数之前调用函数需要进行函数声明。masm 的函数声明的语法如下:

函数名 proto 距离 调用约定 参数列表

示例:

Fun1 proto fat c pAddr:word

宏汇编

表达式

表达式中的求值是在程序链接时完成的,所以表达式中的各值必须是在汇编或链接期就能确定,也就是说不能将寄存器或者变量运用于表达式。

  • 算术表达式
    运算符意义例子
    +65 + 32
    -size val - 54
    *23h * 65h
    /98 / 45
    mod取模99 / 65
  • 逻辑运算
    逻辑运算即位运算,逻辑运算符与对应的指令助记符单词是相同的,当他们出现在操作码部分时是指令,出现在操作数时是逻辑运算符。
    运算符意义
    and位与
    or位或
    not按位取反
    xor异或
  • 关系运算符
    关系运算符的结果,如果结果为真,则所有位都置为 1 ,即 0xFFFF;否则所有位都置为 0 ,即 0x0000 。
    运算符英文例子
    EQequal等于 ==
    NEnot equal不等于 !=
    GTgreater than大于 >
    LTless than小于 <
    GEgreater than or equal大于等于 >=
    LEless than or equal小于等于 <=

标号

  • 匿名标号
    • @@ 是匿名标号。
    • @b 向上查找最近的 @@b 是 back 。
    • @f 向下查找最近的 @@f 是 front 。
    @@:mov ax, 5566h and 6655h
    @@:mov ax, 7788h or 8877hjmp @b	; 跳到第 3 行jmp @f	; 跳到第 8 行@@:mov ax, not 5566h@@:mov ax, 5566h xor 7788h
    
  • 调整偏移量指令 ORG
    • 指令:ORG 偏移值
    • 此指令后的下一个变量或指令从 偏移值 开始存放。
    • 两个内容放在同一偏移处,后一个内容会覆盖前一个内容。
    data_seg segmentg_buf dw 10h dup(0)org 20hg_w dw 65h		; 段偏移 20h 开始存放org 4g_w0 dw 6655h	; 会与 g_buf 的第四个字节开始的数据重复
    data_seg ends
    
  • 当前地址指令 $
    • $ 伪指令代表当前指令或变量的地址(段内偏移)。
    • 常用于计算缓冲区长度和获取当前 IP 值。
    • 可与 ORG 配合使用。

结构体

结构体名 struc; 这里定义结构体成员结构体名 ends
  • 结构体使用 <> 来初始化。
  • 结构体可以通过变量名和寄存器来访问成员。
Student strucm_sz db 64 dub(0)m_id dw 0
Student endsdata_seg segmentg_stu Students <"Hello", 5566h>	; 结构体全局变量
data_seg endsCODE segmentFunc1 PROClocal @stu:Students			; 结构体局部变量mov @stu.m_id, 6			; 使用结构体局部变量assume bx:ptr Studentslea bx, @stumov [bx].m_id, 6			; 使用结构体指针ret
Func1 ENDPCODE ends

equ 语句

  • 不可以重命名
  • 可用于常量和表达式
  • 可用于字符串
  • 可用于指令名,给指令取别名
  • 可用于类型,给类型取别名
  • 可用于操作数
COUNT equ 100h				; 后跟数值
SZHELLO equ "Hello,world!"
MOVE equ mov				; 后跟助记符
MYWORD equ dw				; 后跟类型
BX_CONE equ byte ptr [bx]	; 后跟表达式

= 语句

  • 可以被修改
  • 只能用于常数
COUNT2 = 100h	; 后跟数值
COUNT2 = 100h	; 可以再次赋值
mov ax, COUNT2

macro 语句

宏名 macro [参数1][,参数2]...宏体
endm
  • 宏会在使用的地方展开
  • 宏可以带参数
  • 字符串拼接使用 &
movm macro op1, op2push op2pop op1
endmshift macro n, reg, dmov cl, nro&d reg, cl
endm	

分支

.IF condition; 条件成立时所执行的指令序列
.ENDIF.IF condition; 条件成立时所执行的指令序列
.ELSE; 条件不成立时所执行的指令序列
.ENDIF.IF condition1; condition1 成立时所执行的指令序列
.ELSEIF condition2; condition2 成立时所执行的指令序列
.ENDIF

其中条件表达式 condition 的书写方式与 C 语言中条件表达式的书写方式相似,也可用括号来组成复杂的条件表达式。

循环

.WHILE condition循环体的指令序列    ; 条件"condition”成立时所执行的指令序列
.ENDW

多文件编译

源文件

  • 源文件后缀名为 asm
  • 每个源文件末尾都需要有 end

头文件

  • 汇编头文件后缀名为 inc
  • 头文件包含 include xxx.inc
  • 头文件防重复包含
ifndef SECOND_1
SECOND_1 equ 1Func1 proto far stdcall arg1:word, arg2:word
extern g_dw:wordendif

函数使用

函数在源文件定义,在头文件声明即可。

全局变量

  • 全局变量定义在文件中必须使用 public 指明此变量为全局 public 变量名
  • 全局变量在使用文件中必须使用 extern 指明此变量来自外部文件 extern 变量:类型
; 文件1
public g_wValdata_seg segmentg_wVal dw 5566h
data_seg ends; 文件2
extern g_wVal:word

这篇关于kr 第三阶段(一)16 位汇编的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/231391

相关文章

【JavaScript】LeetCode:16-20

文章目录 16 无重复字符的最长字串17 找到字符串中所有字母异位词18 和为K的子数组19 滑动窗口最大值20 最小覆盖字串 16 无重复字符的最长字串 滑动窗口 + 哈希表这里用哈希集合Set()实现。左指针i,右指针j,从头遍历数组,若j指针指向的元素不在set中,则加入该元素,否则更新结果res,删除集合中i指针指向的元素,进入下一轮循环。 /*** @param

笔记整理—内核!启动!—kernel部分(2)从汇编阶段到start_kernel

kernel起始与ENTRY(stext),和uboot一样,都是从汇编阶段开始的,因为对于kernel而言,还没进行栈的维护,所以无法使用c语言。_HEAD定义了后面代码属于段名为.head .text的段。         内核起始部分代码被解压代码调用,前面关于uboot的文章中有提到过(eg:zImage)。uboot启动是无条件的,只要代码的位置对,上电就工作,kern

16 子组件和父组件之间传值

划重点 子组件 / 父组件 定义组件中:props 的使用组件中:data 的使用(有 return 返回值) ; 区别:Vue中的data (没有返回值);组件方法中 emit 的使用:emit:英文原意是:触发、发射 的意思components :直接在Vue的方法中声明和绑定要使用的组件 小炒肉:温馨可口 <!DOCTYPE html><html lang="en"><head><

react笔记 8-16 JSX语法 定义数据 数据绑定

1、jsx语法 和vue一样  只能有一个根标签 一行代码写法 return <div>hello world</div> 多行代码返回必须加括号 return (<div><div>hello world</div><div>aaaaaaa</div></div>) 2、定义数据 数据绑定 constructor(){super()this.state={na

汇编:嵌入式软件架构学习资源

成为嵌入式软件架构设计师需要掌握多方面的知识,包括嵌入式系统、实时操作系统、硬件接口、软件设计模式等。 以下是一些推荐的博客和网站,可以帮助你深入学习嵌入式软件架构设计: ### 1. **Embedded.com**    - **网址**: [Embedded.com](https://www.embedded.com/)    - **简介**: 这是一个专注于嵌入式系统设计的专业网

打靶记录16——Momentum

靶机: https://download.vulnhub.com/momentum/Momentum.ova 下载后使用 VirtualBox 打开 难度:中 目标:取得 root 权限 + 2 Flag 攻击方法: 主机发现端口扫描信息收集Web 路径爆破XSS 漏洞JS 脚本分析AES 解密Redis 认证漏洞 主机发现 sudo arp-scan -l 端口扫描和服务发

GDB 查看汇编

查看汇编 x disassemble

C语言-程序环境 #预处理 #编译 #汇编 #链接 #执行环境

文章目录 前言 一、程序的环境翻译和执行环境 二、翻译环境 (一)、整体把握 (一)、编译 1、预处理(预编译) 2、编译 a、词法分析 b、语法分析 c、语义分析 d、符号汇总 3、汇编 (二)、链接 三、运行环境 总结​​​​​​​ 前言 路漫漫其修远兮,吾将上下而求索; PS:本文参考了《程序员的自我修养》,致敬大佬们! 一、程序的

Linux 技巧汇编

10个重要的Linux ps命令实战 显示所有当前进程 根据用户过滤进程 通过cpu和内存使用来过滤进程 通过进程名和PID过滤 根据线程来过滤进程 树形显示进程 显示安全信息 格式化输出root用户(真实的或有效的UID)创建的进程 使用PS实时监控进程状态 https://linux.cn/article-4743-1.html Python 云服务器应用 | Http

正点原子阿尔法ARM开发板-IMX6ULL(二)——介绍情况以及汇编

文章目录 一、裸机开发(21个)二、嵌入式Linux驱动例程三、汇编3.1 处理器内部数据传输指令3.2 存储器访问指令3.3 压栈和出栈指令3.4 跳转指令3.5 算术运算指令3.6 逻辑运算指令 一、裸机开发(21个) 二、嵌入式Linux驱动例程 三、汇编 我们在进行嵌入式 Linux 开发的时候是绝对要掌握基本的 ARM 汇编,因为 Cortex-A 芯片一