03-21-2026, 11:19 PM
(此帖子最后修改于: 03-21-2026, 11:38 PM 由 小鸟unsigned.)
AMX 汇编
AMX 版本: 3.0
AMX 文件版本: 8
本文档是关于抽象机执行器(Abstract Machine eXecutor)汇编的非官方文档。本文档的内容可能不准确且可能随时更改。术语部分中定义的部分术语在官方手册中并未出现。它们是为了帮助读者理解而引入的辅助术语。
本文档的目标读者是首次接触汇编语言的程序员。他们应根据需要点击超链接以收集背景信息。高级用户可跳过部分章节或将本文档用作快速参考指南。
目录
- 先决条件
- 术语
- 抽象机执行器 (AMX)
- AMX 汇编
- AMX 二进制
- AMX 实例
- 宿主程序
- 扩展模块
- 基本概念
- 单元格概念
- 内存地址
- 栈与堆
- 指令
- 存储程序概念与冯·诺依曼体系结构
- 头、数据段、代码段
- 寄存器
- 文件与内存布局
- 指令集
- Pawn 内联汇编语法
- 指令表
- 加载/存储指令
- 索引指令
- 算术指令
- 逻辑指令
- 关系指令
- 栈操作指令
- 堆指令
- 控制寄存器操作指令
- 控制流指令
- Switch-case 指令
- 调用栈与调用约定
- 局部变量与函数帧
- 调用约定
- 参数传递方法
- 系统请求
- 多维数组
先决条件
- Pawn 脚本语言
- 二进制数制
- 十六进制数制
- 内存地址(理解指针更佳)
- 栈(作为抽象数据类型)
术语
<a name="term-amx"></a>抽象机执行器 (AMX):AMX 是我们感兴趣的抽象机(或虚拟机)。
<a name="term-amx-assembly"></a>AMX 汇编:AMX 的汇编语言。
<a name="term-amx-binary"></a>AMX 二进制:AMX 二进制文件是编译后生成的可执行文件(通常扩展名为
.amx)。术语 AMX 程序在某些上下文中可能指代 AMX 二进制文件。<a name="term-amx-instance"></a>AMX 实例:每个加载的 AMX 二进制文件独立存在。加载后的程序称为 AMX 实例。术语 AMX 程序在某些上下文中可能指代 AMX 实例。
<a name="term-host-program"></a>宿主程序:嵌入 AMX 的程序称为宿主程序。以 San Andreas Multiplayer (SA-MP) 或 open.mp 为例,服务器就是宿主程序。
<a name="term-extension-module"></a>扩展模块:扩展模块是一个独立的模块,通常动态链接到宿主程序,提供额外的原生函数。扩展模块通常被称为插件。
基本概念
- ## <a name="concept-cell"></a> 单元格概念
Pawn 是一种无类型语言:没有数据类型。所有数据都作为原始二进制数据存储在一个单元格或一组单元格中。我们将使用有符号二进制补码表示法来传达一个或多个单元格的内容,而不是显式地用二进制写出来。
代码:
new a = 5, b = 'A';上述代码创建了两个独立的单元格,分别由
a 和 b 标识。单元格 a 将包含值 5,单元格 b 将包含值 65(字符 'A' 的 ASCII 等效值)。为了弥补数据类型的缺失,Pawn 提供了为单元格添加标签的能力。这些标签仅仅是编译时的辅助工具,不会直接<sup>1</sup>出现在 AMX 二进制文件中。
代码:
new Float:x = 5.0, Float:y = 10.0, Float:z;
z = x + y;上述代码创建了三个带有标签的单元格,由
x、y 和 z 标识。它们分别被初始化为值 5.0、10.0 和 0.0(默认初始化为二进制零,也是 0.0),其二进制表示符合浮点数的正确表示。编译器会记住遇到的变量的标签,并确保在变量被使用时生成正确的代码 <sup>2</sup>。在上述例子中,编译器在处理第二行时,会生成正确的代码来执行两个浮点数 x 和 y 的加法。请记住,整数加法和浮点数加法的执行方式不同。编译器使用与单元格关联的标签来确定应对操作数执行哪种加法操作。编译完成后,变量的标签信息会丢失。生成的 AMX 二进制文件将包含对
x 和 y 中存储的数据执行浮点数加法并将结果存储在 z 中的指令。在运行时,浮点数加法会在不考虑脚本中
x 和 y 的标签原本是否为 Float 的情况下执行。<a name="bc-cc-note-1"></a> [[1]](#bc-cc-note-1) Pawn 语言提供了
tagof 运算符,它返回与标签关联的编译时 id,该 id 可以被存储。编译器还会在 AMX 二进制文件的标签表中创建一个公开可访问的标签列表。<a name="bc-cc-note-2"></a> [[2]](#bc-cc-note-2) Pawn 语言允许基于标签的运算符重载。编译器知道所有存在的运算符重载,并根据操作数的标签调用正确的重载。如果给定的操作数没有重载存在,编译器将默认使用无标签单元格的运算符(即整数算术)。
- ## <a name="concept-memory-addresses"></a> 内存地址
(平坦内存),每个字节都有一个唯一的地址(字节可寻址)。
代码:
new a[5];上述代码创建了一个由五个单元格组成的数组,由
a 标识。这些单元格连续存储,即一个接一个地存储。假设单元格大小为 4 字节,如果存储 a[0] 的位置地址是 1000,那么存储 a[1] 的位置地址将是 1004。代码:
data: a[0] | a[1] | a[2] | a[3] | a[4]
address: 1000 1004 1008 1012 1016所需知识:基地址,相对地址
所需知识:数组数据结构,元素标识符和寻址公式
另请参阅:字节序
- ## <a name="concept-stack-heap"></a> 栈和堆
栈负责:
- 存储局部变量
- 存储函数参数
- 存储函数调用信息(例如参数个数)
- 提供临时存储
堆负责:
- 为动态分配提供内存
- 存储作为引用/数组传递的默认参数
- 存储不是左值且作为可变参数传递的常量和表达式
###
代码:
f(argc, ...) { }
main ()
{
new a, b, c[100];
f(1, 25);
}在上面的代码片段中,变量
a、b、c 和常量 1 在栈上分配空间。常量 25 在堆上分配空间。引用和可变参数作为地址传递给函数。由于字面量 25 是一个常量且没有地址,因此在堆上分配了一个临时单元格来存储值 25。该单元格的地址被传递给函数。- ## <a name="concept-instructions"></a> 指令
指令是离散的原子执行单元。大多数指令执行简单的任务,如基本算术。高级结构(如循环和条件语句)可以简化为一系列简单的指令。
例如,将值压入调用栈然后弹出的指令可能是:
代码:
push 100
pop汇编器为上述代码片段生成的二进制代码可能是:
push-opcode | 100 | pop-opcode0x0013 0x0064 0x0014 或者可能是 0000 0000 0000 0000 0000 0000 0001 0011 0000 0000 0000 0000 0000 0000 0110 0100 0000 0000 0000 0000 0000 0000 0001 0100是的,它只是一系列比特位,CPU(或者在我们这里是抽象机)知道如何解释这些比特位。当为物理硬件上的执行汇编时,这种表示被称为机器码(或原生码)。在为假想 CPU(如 AMX)上的执行汇编时,这种表示被称为 p-code。
另请参阅: 字节码
- ## <a name="concept-spc-vna"></a> 存储程序概念与冯·诺依曼体系结构
代码像数据一样驻留在内存中的想法被称为存储程序概念。这也意味着内存中的每条指令都有一个地址,就像数据一样。代码和数据都驻留在同一内存中的想法是冯·诺依曼体系结构的一个原则。这也意味着所有代码和数据共享一个地址空间。
AMX 使用一个公共内存来存储代码和数据。
- ## <a name="concept-hdc"></a> 头、数据段、代码段
可执行二进制文件及程序的内存结构根据其包含的内容被划分为逻辑上不同的区域。一个典型的二进制可执行文件至少包含一个头、一个代码段和一个数据段。头提供了程序本身的信息,例如程序入口点。
代码:
HEADER
-------
CODE
-------
DATA代码段以二进制形式(机器码/p-code)存储代码,而数据段存储数据,如全局变量和静态局部变量。
程序的内存结构通常不同。除了代码段和数据段外,它通常还包含程序栈和程序堆的区域。
代码:
HEADER
-------
CODE
-------
DATA
-------
HEAP
|
STACKAMX 二进制文件组织为三个部分:前缀、代码和数据(按顺序)。前缀部分包含有关 AMX 二进制文件的信息,例如代码和数据部分在二进制文件中的起始偏移量。代码部分存储 p-code,数据段存储全局变量和静态局部变量。
堆栈区域是在 AMX 二进制文件加载到内存后,使用前缀部分中的信息创建的。
另请参阅:内存分段,数据段,代码段,ELF 格式
- ## <a name="concept-registers"></a> 寄存器
代码:
|-------| |------------------|
| CPU | =============== | MEMORY |
|-------| BUS |------------------|内存存储数据,CPU 对数据进行操作。对于每次操作,处理器都必须将操作数从内存中取出并临时存储在 CPU 内部。这些临时存储位置被称为寄存器。这些寄存器可以容纳少量数据,范围在几个字节(通常为 2、4 或 8 字节)。
假设处理器必须将两个变量
x 和 y 相加,并将结果存储在变量 z 中。一个典型的处理器会执行以下操作:- 从内存中取出
x的值(即从x的地址读取)并将其存储在一个寄存器中,比如R1
- 从内存中取出
y的值(即从y的地址读取)并将其存储在一个寄存器中,比如R2
- 将
R1和R2的内容相加,并将结果存储在某寄存器中,比如R3
- 将
R3的内容存储到z中(写入z的地址)
处理器包含不同类型的寄存器,用于不同目的。一些寄存器专用于特定任务,一些寄存器可用于任何目的。上例中使用的那类寄存器属于通用寄存器。
AMX 的寄存器集包括以下寄存器:
- 主寄存器 (PRI):通用寄存器(常用作累加器寄存器)
- 辅助寄存器 (ALT):通用寄存器(常用作地址寄存器)
- 代码段寄存器 (COD):内存中代码段起始的绝对地址
- 数据段寄存器 (DAT):内存中数据段起始的绝对地址
- 当前指令指针 (CIP):下一条要执行指令的地址(相对于 COD 寄存器)
- 栈顶寄存器 (STP):栈顶的地址(相对于 DAT 寄存器)
- 栈指针寄存器 (STK):栈上当前位置的地址(相对于 DAT 寄存器)
- 帧指针寄存器 (FRM):栈中当前函数帧起始的地址(相对于 DAT 寄存器)(稍后解释)
- 堆指针 (HEA):堆顶的地址(相对于 DAT 寄存器)
在编写汇编代码时,地址通常不是绝对的;它们是相对于某个段或帧指针寄存器的。全局变量和字符串的地址相对于 DAT 寄存器。函数、指令和标签的地址相对于 COD 寄存器。这意味着变量或指令的绝对地址分别是相对地址加上 DAT 或 COD 寄存器的值。局部变量的地址相对于帧指针寄存器。这有点复杂,稍后会解释。
从现在开始,当我们讨论代码地址和数据地址时,除非明确说明,否则暗示这些地址分别相对于 COD 寄存器和 DAT 寄存器。
文件与内存布局
所有非调试 AMX 二进制文件按如下方式组织:
代码:
START OF BINARY FILE
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
END OF BINARY FILE- 前缀:包含程序的基本信息
- 代码:包含代码
- 数据:包含数据
注意:根据编译器的选项,前缀部分可能会被填充以使代码和数据部分对齐。
注意:启用调试编译的脚本的二进制映像将在末尾附加符号调试信息。更多详情请参阅 Pawn 实现者指南。
内存中的结构采用与二进制文件类似的结构,但额外包含一个栈和一个堆,这是使用前缀中的信息设置的。
代码:
LOW ADDRESS (0)
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
HEAP
| |
| |
FREE SPACE
| |
| |
STACK
| ---------------------- |
HIGH ADDRESS堆和栈共享一个公共的内存区域。它们从该区域的两端开始,并向相反方向增长。堆向高地址增长,栈向低地址增长。由于它们共享同一内存区域,因此可能会相互覆盖(当堆指针和栈指针碰撞时)。发生这种情况时,AMX 会中止并显示如下错误信息:
代码:
Script[gamemodes/TEST.amx]: Run time error 3: "Stack/heap collision (insufficient stack size)"根据编译器掌握的信息,它会估算并间接地在 AMX 二进制文件的前缀部分设置堆栈大小。然而,程序员可以使用
#pragma dynamic [estimated number of cells] 指令提供自己对所需堆栈区域大小的估计。前缀部分的字段
| 类型 | 大小 | 描述 |
|---|---|---|
| size | 4 | 内存映像的大小;不包括栈和堆 |
| magic | 2 | 指示格式和单元格大小 |
| file version | 1 | 格式版本 |
| amx version | 1 | 抽象机所需的最低版本 |
| flags | 2 | 是否存在符号调试信息、紧凑编码等。 |
| defsize | 2 | 表中记录的大小 |
| cod | 4 | 代码部分起始的偏移量 |
| dat | 4 | 数据部分起始的偏移量 |
| hea | 4 | HEA 寄存器的初始值(标记数据段结束) |
| stp | 4 | STP 寄存器的初始值(指示总内存需求) |
| cip | 4 | CIP 寄存器的初始值(main 函数的地址;如果不存在则为 -1) |
| publics | 4 | 公共函数表起始的偏移量 |
| natives | 4 | 原生函数表起始的偏移量 |
| libraries | 4 | 库表起始的偏移量 |
| pubvars | 4 | 公共变量表起始的偏移量 |
| tags | 4 | 标签表起始的偏移量 |
| name table | 4 | 名称表起始的偏移量 |
| overlays | 4 | 覆盖表起始的偏移量 |
| publics | 可变 | 公共函数列表 |
| natives | 可变 | 原生函数列表 |
| libraries | 可变 | 库列表 |
| pubvars | 可变 | 公共变量列表 |
| tags | 可变 | 公共标签列表 |
| overlays | 可变 | 覆盖列表 |
| name table | 可变 | 符号名称列表 |
注意:关于 magic、version 和 flags 字段中信息如何编码的详细信息,请参阅 Pawn 实现者指南。
注意:前缀中的所有多字节字段都以小端格式存储,无论 AMX 二进制文件是在哪个平台上生成或将在哪个平台上执行。
可以看出,前缀由一个固定部分组成,后面跟着一系列表。除了名称表之外,表中的每条记录都由两个字段组成,如下所示:
| 字段 | 大小 | 描述 |
|---|---|---|
| 变量 | 单元格大小 | 变量 |
| 名称字符串偏移量 | 4 字节 | 从前缀起始到名称表中字符串的偏移量 |
表中每条记录(除了名称表)的大小由前缀中的
defsize 字段给出:defsize = 4 + 单元格大小。表中的每条记录都被分配了一个索引号。每个表的第一个记录被分配索引 0,后续记录的索引号是前一个记录索引加 1。
名称表:
除了名称表本身之外,其他表中的记录包含一个指向字符串的指针。这些字符串作为以空字符结尾的 C 字符串存储在名称表中。此表中记录的大小是其包含的字符串的大小。
公共函数表:
为脚本中定义的每个公共函数创建一条记录。这些记录包含公共函数的地址(函数第一条指令的地址)和指向相应公共函数名称的偏移量。表中记录的索引就是相应公共函数的索引。
原生函数表:
为脚本中调用的每个原生函数创建一条记录。记录的第一个字段由编译器在二进制文件中设置为 0,但宿主程序在加载二进制文件时会将该字段初始化为函数的地址(位于宿主程序的地址空间中)。第二个字段包含指向原生函数名称的偏移量。表中记录的索引就是相应原生函数的索引。
库表:
Pawn 语言提供了一个 pragma 指令
#pragma directive [library name],用于告知编译器调用的某些原生函数需要扩展模块。对于在脚本中调用了其原生函数的库,会在库表中创建一条记录。目的是告知宿主程序 AMX 程序依赖于一个扩展模块。记录的第一个字段供内部使用,在 AMX 二进制文件中设置为 0。另一个字段包含从前缀起始到名称表中库名称的偏移量。公共变量表:
为脚本中声明的每个公共变量创建一条记录。该记录包含公共变量的地址和指向公共变量名称的偏移量。表中记录的索引就是公共变量的索引。
标签表:
与
sleep 和 exit 语句一起使用的标签以及与 tagof 运算符一起使用的标签会被导出。为每个这样的标签创建一条记录。第一个字段包含标签 id 号,第二个字段存储指向标签名称的偏移量。指令集
<a name="pawn-inline-assembly"></a> Pawn 编译器接受的大部分 AMX 汇编指令可以用以下格式表示:
代码:
mnemonic[.prefix][.register suffix] operand
SHL.C.pri 3
ZERO.alt
ADD.C 100
LIDX助记符给出了指令功能的提示。可选的前缀指示指令操作数的类型。可选的尾缀指示指令主要作用于哪个寄存器。
前缀列表:
- .C = 常量
- .S = 栈
- .I = 间接寻址
- .B = 无 B 变体的变体<sup>\[需要更好的描述\]</sup>
- .ADR = 地址
- .R = 重复
寄存器尾缀列表:
- .pri = 主寄存器
- .alt = 辅助寄存器
每条指令的二进制形式需要一个单元格来存储操作码,每个操作数还需要一个额外的单元格。绝大多数指令使用隐含寄存器作为操作数。这减少了所需显式操作数的数量,从而减小了代码段的大小并提高了性能。
<a name="instruction-table"></a> 阅读表格:
- [address] 指的是存储在位置
DAT + address的值
- 语义列中使用的运算符执行与 Pawn 中相同的操作
| 操作码 | 助记符 | 操作数 | 语义 |
|---|
| 1 | LOAD.pri | address | PRI = [address] |
|---|---|---|---|
| 2 | LOAD.alt | address | ALT = [address] |
| 3 | LOAD.S.pri | offset | PRI = [FRM + offset] |
|---|---|---|---|
| 4 | LOAD.S.alt | offset | ALT = [FRM + offset] |
| 5 | LREF.pri | address | PRI = [[address]] |
| 6 | LREF.alt | address | ALT = [[address]] |
| 7 | LREF.S.pri | offset | PRI = [[FRM + offset]] |
| 8 | LREF.S.alt | offset | ALT = [[FRM + offset]] |
| 9 | LOAD.I | PRI = [PRI] | |
| 10 | LODB.I | number | PRI = 'number' of bytes from [PRI] (read 1/2/4 bytes) |
| 11 | CONST.pri | value | PRI = value |
| 12 | CONST.alt | value | ALT = value |
| 13 | ADDR.pri | offset | PRI = FRM + offset |
| 14 | ADDR.alt | offset | ALT = FRM + offset |
| 15 | STOR.pri | address | [address] = PRI |
| 16 | STOR.alt | address | [address] = ALT |
| 17 | STOR.S.pri | offset | [FRM + offset] = PRI |
| 18 | STOR.S.alt | offset | [FRM + offset] = ALT |
| 19 | SREF.pri | address | [[address]] = PRI |
| 20 | SREF.alt | address | [[address]] = ALT |
| 21 | SREF.S.pri | offset | [[FRM + offset]] = PRI |
| 22 | SREF.S.alt | offset | [[FRM + offset]] = ALT |
| 23 | STOR.I | [ALT] = PRI (full cell) | |
| 24 | STRB.I | number | number of bytes at [ALT] = PRI (store 1/2/4 bytes) |
| 25 | LIDX | PRI = [ALT + (PRI x cell size)] | |
| 26 | LIDX.B | shift | PRI = [ALT + (PRI << shift)] |
| 27 | IDXADDR | PRI = ALT + (PRI x cell size) (calculate indexed address) | |
| 28 | IDXADDR.B | shift | PRI = ALT + (PRI << shift) (calculate indexed address) |
| 29 | ALIGN.pri | number | Little Endian: PRI ^= cell size - number |
| 30 | ALIGN.alt | number | Little Endian: ALT ^= cell size - number |
| 31 | LCTRL | index | PRI = value contained in the selected register; 1=COD, 1=DAT, 2=HEA,3=STP, 4=STK, 5=FRM, 6=CIP |
| 32 | SCTRL | index | selected register = PRI; 2=HEA, 4=STK, 5=FRM, 6=CIP |
| 33 | MOVE.pri | PRI = ALT | |
| 34 | MOVE.alt | ALT = PRI | |
| 35 | XCHG | Exchange contents of PRI and ALT | |
| 36 | PUSH.pri | STK = STK - cell size, [STK] = PRI | |
| 37 | PUSH.alt | STK = STK - cell size, [STK] = ALT | |
| 38 | PUSH.R | number | repeat (STK = STK - cell size, [STK] = PRI) 'number' times |
| 39 | PUSH.C | value | STK = STK - cell size, [STK] = value |
| 40 | PUSH | address | STK = STK - cell size, [STK] = [address] |
|---|---|---|---|
| 41 | PUSH.S | offset | STK = STK - cell size, [STK] = [FRM + offset] |
| 42 | POP.pri | PRI = [STK], STK = STK + cell size | |
| 43 | POP.alt | ALT = [STK], STK = STK + cell size | |
| 44 | STACK | value | ALT = STK, STK = STK + value |
| 45 | HEAP | value | ALT = HEA, HEA = HEA + value |
| 46 | PROC | STK = STK - cell size, [STK] = FRM, FRM = STK | |
| 47 | RET | FRM = [STK], STK = STK + cell size, CIP = [STK], STK = STK + cell size | |
| 48 | RETN | FRM = [STK], STK = STK + cell size, CIP = [STK], STK = STK + cell size, STK = STK + [STK] + cell size | |
| 49 | CALL | offset | STK = STK − cell size, [STK] = CIP, CIP = offset |
| 50 | CALL.pri | STK = STK − cell size, [STK] = CIP, CIP = PRI | |
| 51 | JUMP | offset | CIP = offset |
| 53 | JZER | offset | if PRI == 0 then CIP = offset |
| 54 | JNZ | offset | if PRI != 0 then CIP = offset |
| 55 | JEQ | offset | if PRI == ALT then CIP = offset |
| 56 | JNEQ | offset | if PRI != ALT then CIP = offset |
| 57 | JLESS | offset | if PRI < ALT (unsigned) then CIP = offset |
| 58 | JLEQ | offset | if PRI <= ALT (unsigned) then CIP = offset |
|---|---|---|---|
| 59 | JGRTR | offset | if PRI > ALT (unsigned) then CIP = offset |
| 60 | JGEQ | offset | if PRI >= ALT (unsigned) then CIP = offset |
| 61 | JSLESS | offset | if PRI < ALT (signed) then CIP = offset |
| 62 | JSLEQ | offset | if PRI <= ALT (signed) then CIP = offset |
| 63 | JSGRTR | offset | if PRI > ALT (signed) then CIP = offset |
|---|---|---|---|
| 64 | JSGEQ | offset | if PRI >= ALT (signed) then CIP = offset |
| 65 | SHL | PRI = PRI << ALT | |
| 66 | SHR | PRI = PRI >> ALT (without sign extension) | |
| 67 | SSHR | PRI = PRI >> ALT (with sign extension) | |
| 68 | SHL.C.pri | value | PRI = PRI << value |
| 69 | SHL.C.alt | value | ALT = ALT << value |
| 70 | SHR.C.pri | value | PRI = PRI >> value |
| 71 | SHR.C.alt | value | ALT = ALT >> value |
| 72 | SMUL | PRI = PRI * ALT (signed multiply) | |
| 73 | SDIV | PRI = PRI / ALT (signed divide), ALT = PRI mod ALT | |
| 74 | SDIV.alt | PRI = ALT / PRI (signed divide), ALT = ALT mod PRI | |
| 75 | UMUL | PRI = PRI * ALT (unsigned multiply) | |
| 76 | UDIV | PRI = PRI / ALT (unsigned divide), ALT = PRI mod ALT | |
| 77 | UDIV.alt | PRI = ALT / PRI (unsigned divide), ALT = ALT mod PRI | |
| 78 | ADD | PRI = PRI + ALT | |
| 79 | SUB | PRI = PRI - ALT | |
| 80 | SUB.alt | PRI = ALT - PRI | |
| 81 | AND | PRI = PRI & ALT | |
| 82 | OR | PRI = PRI | ALT | |
| 83 | XOR | PRI = PRI ^ ALT | |
| 84 | NOT | PRI = !PRI | |
| 85 | NEG | PRI = -PRI | |
| 86 | INVERT | PRI = ~PRI | |
| 87 | ADD.C | value | PRI = PRI + value |
| 88 | SMUL.C | value | PRI = PRI * value |
| 89 | ZERO.pri | PRI = 0 | |
| 90 | ZERO.alt | ALT = 0 | |
| 91 | ZERO | address | [address] = 0 |
| 92 | ZERO.S | offset | [FRM + offset] = 0 |
| 93 | SIGN.pri | sign extend the byte in PRI to a cell | |
| 94 | SIGN.alt | sign extend the byte in ALT to a cell | |
| 95 | EQ | PRI = PRI == ALT ? 1 : 0 | |
| 96 | NEQ | PRI = PRI != ALT ? 1 : 0 | |
| 97 | LESS | PRI = PRI < ALT ? 1 : 0 (unsigned) | |
| 98 | LEQ | PRI = PRI <= ALT ? 1 : 0 (unsigned) | |
| 99 | GRTR | PRI = PRI > ALT ? 1 : 0 (unsigned) | |
| 100 | GEQ | PRI = PRI >= ALT ? 1 : 0 (unsigned) | |
| 101 | SLESS | PRI = PRI < ALT ? 1 : 0 (signed) | |
| 102 | SLEQ | PRI = PRI <= ALT ? 1 : 0 (signed) | |
| 103 | SGRTR | PRI = PRI > ALT ? 1 : 0 (signed) | |
| 104 | SGEQ | PRI = PRI >= ALT ? 1 : 0 (signed) | |
| 105 | EQ.C.pri | value | PRI = PRI == value ? 1 : 0 |
| 106 | EQ.C.alt | value | PRI = ALT == value ? 1 : 0 |
| 107 | INC.pri | PRI = PRI + 1 | |
| 108 | INC.alt | ALT = ALT + 1 | |
| 109 | INC | address | [address] = [address] + 1 |
| 110 | INC.S | offset | [FRM + offset] = [FRM + offset] + 1 |
| 111 | INC.I | [PRI] = [PRI] + 1 | |
| 112 | DEC.pri | PRI = PRI - 1 | |
| 113 | DEC.alt | ALT = ALT - 1 | |
| 114 | DEC | address | [address] = [address] - 1 |
| 115 | DEC.S | offset | [FRM + offset] = [FRM + offset] - 1 |
| 116 | DEC.I | [PRI] = [PRI] - 1 | |
| 117 | MOVS | number | copy 'number' bytes of non-overlapping memory from [PRI] to [ALT] |
| 118 | CMPS | number | compare 'number' bytes of non-overlapping memory at [PRI] with [ALT] |
| 119 | FILL | number | fill 'number' bytes of memory from [ALT] with value in PRI (number must be multiple of cell size) |
| 120 | HALT | 0 | abort execution (exit value in PRI) |
| 121 | BOUNDS | value | abort execution if PRI > value or if PRI < 0 |
| 122 | SYSREQ.pri | call system service, service number in PRI | |
| 123 | SYSREQ.C | value | call system service |
| 128 | JUMP.pri | CIP = PRI | |
| 129 | SWITCH | offset | compare PRI to the values in the case table (whose address is passed in the 'offset' argument) and jump to the associated address in the matching record |
| 130 | CASETBL | ... | a variable number of case records follows this opcode, where each record takes two cells |
| 131 | SWAP.pri | [STK] = PRI, PRI = [STK] | |
| 132 | SWAP.alt | [STK] = ALT, ALT = [STK] | |
| 133 | PUSH.ADR | offset | STK = STK - cell size, [STK] = FRM + offset |
| 134 | NOP | no operation |
加载/存储指令
当使用全局变量作为操作数时,编译器会替换该变量的地址。下面代码中的第一条加载指令,如果
some_global 的地址是 1288,则实际上变成 #emit LOAD.pri 1288。请注意,编译器替换的地址是从数据段起始的偏移量;因此,该地址是相对于 DAT 寄存器的。代码:
new some_global = 10, another_global = 25;
main()
{
#emit LOAD.pri some_global // 将 'some_global' 的值加载到主寄存器
#emit LOAD.alt another_global // 将 'another_global' 的值加载到辅助寄存器
}当使用局部变量时,编译器会替换从函数帧起始的偏移量。(稍后解释)
代码:
main()
{
static s_local = 20; // 静态局部变量存储在数据段中
new some_local = 10, another_local = 25; // 局部变量存储在栈中
#emit LOAD.S.pri some_local // 将 'some_local' 的值加载到主寄存器
#emit LOAD.S.alt another_local // 将 'another_local' 的值加载到辅助寄存器
#emit LOAD.pri s_local // 注意静态局部变量的行为类似于全局变量
}由于编译器会为全局变量替换地址,因此可以使用
CONST.pri/CONST.alt 来获取这些变量的地址。代码:
new some_global;
main()
{
#emit CONST.pri 10 // 将 10 放入主寄存器
#emit CONST.alt 50 // 将 50 放入辅助寄存器
#emit CONST.pri some_global // 将 'some_global' 的地址存储在主寄存器中
#emit CONST.alt some_global // 将 'some_global' 的地址存储在辅助寄存器中
}代码:
new some_global;
main()
{
new some_local;
#emit ZERO.pri // 将零存储在主寄存器中
#emit STOR.pri some_global // 将主寄存器中存储的值(本例中为零)设置给 'some_global'
#emit CONST.alt 125
#emit STOR.S.alt some_local // 将辅助寄存器中存储的值(125)设置给 'some_local'
}索引指令
代码:
new global_arr[10];
main ()
{
#emit CONST.alt global_arr // 将 'global_arr' 的地址('global_arr' 第一个元素的地址)加载到辅助寄存器中
#emit CONST.pri 2 // 设置我们感兴趣的 'global_arr' 元素的索引
#emit LIDX // 现在主寄存器中存储的是 'global_arr[2]` 处存储的值
#emit CONST.alt global_arr // 将 'global_arr' 的地址(第一个元素的地址)加载到辅助寄存器中
#emit CONST.pri 2 // 设置我们感兴趣的 'global_arr' 元素的索引
#emit IDXADDR // 现在主寄存器中存储的是 'global_arr[2]` 的地址
}算术指令
代码:
main ()
{
#emit CONST.pri 4
#emit CONST.alt 5
#emit SMUL // 现在主寄存器中是 20(SMUL 执行有符号乘法;UMUL 执行无符号乘法)
#emit ADD // 将 5 加到 20(由于上一条指令,主寄存器中存储的是 20)
#emit ADD.C 10 // 将 10 加到主寄存器;现在主寄存器中是 35
#emit SUB.alt // 用 5 减去 35;现在主寄存器中是 -30
#emit SMUL.C 2 // -30 乘以 2;得到 -60
}逻辑指令
代码:
main ()
{
#emit CONST.pri 5 // .. 0000 0101
#emit CONST.alt 3 // ... 0000 0011
#emit AND // 主寄存器现在包含 ... 0000 0001
#emit XOR // 主寄存器现在包含 ... 0000 0110
#emit INVERT // 对主寄存器中存储的值取反码
#emit NEG // 对主寄存器中存储的值取补码(本质上是取负)
}关系指令
代码:
main ()
{
#emit CONST.pri 5
#emit CONST.alt 8
#emit EQ // 如果 5 == 8,则将主寄存器设置为 1,否则为 0
#emit LESS // 如果 0 < 8,则将主寄存器设置为 1,否则为 0
}栈操作指令
局部变量存储在栈上。详细说明将在后续章节中提供,但现在我们假设使用
CONST.pri some_local 会得到某个偏移量,该偏移量加上帧寄存器中存储的基地址即得数据地址。代码:
main ()
{
new some_local = 25;
#emit ADDR.alt some_local // 计算 'some_local' 的地址
#emit CONST.pri 100
#emit STOR.I // 将 100 存储到 'some_local'
#emit CONST.pri 100
#emit STOR.S.pri some_local // 实现上述代码功能的更好方法
#emit CONST.pri some_local // 神秘地等价于 CONST.pri -4(稍后解释)
}代码:
main ()
{
#emit PUSH.C 100 // 将值 100 压入调用栈
#emit POP.pri // 从栈中弹出一个值并将结果存储在主寄存器中
#emit PUSH.pri // 推送主寄存器的值
#emit PUSH.alt // 推送辅助寄存器的值
#emit POP.pri
#emit POP.alt // 最后 4 条指令有效地交换了主寄存器和辅助寄存器的内容
#emit XCHG // 交换主寄存器和辅助寄存器内容的更好方法
}局部数组位于栈上。因此,在使用索引指令之前,必须使用
ADDR.alt/ADDR.pri 来获取完整的地址。代码:
main ()
{
new local_array[10];
#emit ADDR.alt local_array // 加载存储在 'local_array' 中的值,即数组的地址
#emit CONST.pri 5
#emit LIDX // 有效地将 'local_array[5]' 的值存储在主寄存器中
}堆指令
堆指针(
HEA 寄存器)指向堆的顶部。将堆指针向前移动 x 字节,我们就能有效地在堆上预留
x 字节。代码:
main ()
{
#emit HEAP 16 // 为四个单元格预留空间(假设单元格为 4 字节)
// 注意 HEAP 指令也将辅助寄存器设置为我们预留内存的起始地址
// ALT = HEA, HEA += 16
#emit CONST.pri 50
#emit STOR.I // 有效地将值 50 存储在我们预留的堆区域的第一个单元格中
#emit HEAP -16 // 归还预留的内存
}控制寄存器操作指令
可以使用
LCTRL 和 SCTRL 指令直接读取和修改专用寄存器的内容。| 助记符 | 操作数 | 描述 |
|---|---|---|
| LCTRL | index | PRI = 所选寄存器中包含的值;0=COD, 1=DAT, 2=HEA, 3=STP, 4=STK, 5=FRM, 6=CIP |
| SCTRL | index | 所选寄存器 = PRI;2=HEA, 4=STK, 5=FRM, 6=CIP |
代码:
main ()
{
new cod, dat;
#emit LCTRL 0 // 将 COD 段寄存器的值存储在主寄存器中
#emit STOR.S.pri cod
#emit LCTRL 1 // 将 DAT 段寄存器的值存储在主寄存器中
#emit STOR.S.pri dat
printf("%d %d", cod, dat);
}当函数名用作操作数时,编译器会用函数的地址替换它。替换的地址是相对于 COD 寄存器的。
代码:
f()
{
print("f() was called.");
}
main ()
{
#emit PUSH.C 0 // 参数占用的字节数(稍后解释)
#emit LCTRL 6 // 获取 CIP 的值,即下一条指令(本例中的 ADD.C)的地址
#emit ADD.C 28 // 计算 'f' 之后要执行的指令的地址(注意每个操作码和操作数都需要一个单元格)
#emit PUSH.pri // 推送返回地址,以便 'f' 知道返回到哪里(稍后解释)
#emit CONST.pri f // 将函数 'f' 的地址存储在 pri 中
#emit SCTRL 6 // 将当前指令指针设置为存储在主寄存器中的值
// 函数 'f' 执行
// 函数 'f' 返回后,下一条指令(本例中的 NOP)将开始执行
#emit NOP // 不执行任何操作的指令
}控制流指令
代码:
main()
{
#emit JUMP check // 跳转到 check 标签
not_equal:
printf("1 is not equal to 2");
return 0;
equal:
print("1 is equal to 2");
return 0;
check:
#emit CONST.pri 1
#emit CONST.alt 2
#emit JEQ equal // 如果主寄存器的值等于辅助寄存器的值,则跳转到 'equal'
#emit JUMP not_equal // 如果执行到这里,意味着两个寄存器的值不相等;因此跳转到 'not_equal'
}Switch case 指令
switch-case 块使用分支表实现。分支表仅仅是由 case 值及其对应的跳转地址组成的元组列表。对于给定的 case 值,AMX 会搜索分支表并跳转到相应的跳转地址。分支表中的记录按 case 值的升序排列。这允许 AMX 对分支表执行二分搜索,但它也可能执行线性搜索。
SWITCH 指令标记 switch-case 块的开始。它接受一个指向分支表(也位于代码段中)的偏移量作为操作数。分支表正式以 CASETBL 操作码(仅作为标记,功能上未使用)开始,后跟一系列 CASE 记录,每条记录占用两个参数。第一条 case 记录的参数具有特殊含义:第一个参数是分支表中的 case 数量,第二个参数是默认 case 的偏移量。如果没有提供默认 case,则第二个参数包含 switch-case 块之后指令的偏移量。其余的 CASE 记录包含 case 值及其对应的跳转地址。代码:
switch(expression)
{
case 2: {}
case 4: {}
case 3: {}
case 7: {}
case 5: {}
}编译器会添加指令来计算给定的
expression,其结果存储在主寄存器中。紧接着是一条 SWITCH 指令,它使 AMX 搜索分支表,查找与主寄存器中存储的值匹配的 case 值。如果找到匹配项,则执行跳转到匹配记录指向的地址;否则,跳转到默认地址。代码:
; 编译器通过在汇编输出中使用标签而不是实际的偏移量/地址,使阅读更容易
; 每个标签都以前缀 "l." 开头
switch 0 ; 注意这里的零是一个标签
l.2 ; 标签 2
jump 1 ; 跳转到标签 1
l.3
jump 1
l.4
jump 1
l.5
jump 1
l.6
jump 1
l.0
casetbl
case 5 1 ; 记录数量,默认跳转地址(本例中为标签 1)
case 2 2 ; 真正的第一条记录
case 3 4 ; case 值: 3, 跳转标签: 4
case 4 3 ; 编译器在实际二进制文件中会用正确的地址替换标签
case 5 6 ;
case 7 5 ; 最后一条记录
l.1
; 其余代码
