# 汇编语言 - [汇编语言](#汇编语言) - [3.3 X86 汇编基础](#33-x86-汇编基础) - [3.3.2 寄存器 Registers](#332-寄存器-registers) - [3.3.3 内存和寻址模式 Memory and Addressing Modes](#333-内存和寻址模式-memory-and-addressing-modes) - [3.3.3.1 声明静态数据区域](#3331-声明静态数据区域) - [3.3.3.2 内存寻址](#3332-内存寻址) - [3.3.3.3 操作后缀](#3333-操作后缀) - [3.3.4 指令 Instructions](#334-指令-instructions) - [3.3.4.1 数据移动指令](#3341-数据移动指令) - [3.3.4.2 逻辑运算指令](#3342-逻辑运算指令) - [3.3.4.3 流程控制指令](#3343-流程控制指令) - [3.3.5 调用约定 Calling Convention](#335-调用约定-calling-convention) - [3.3.5.1 调用者约定 Caller Rules](#3351-调用者约定-caller-rules) - [3.3.5.2 被调用者约定 Callee Rules](#3352-被调用者约定-callee-rules) - [3.4 x64 汇编基础](#34-x64-汇编基础) - [3.4.1 导语](#341-导语) - [3.4.2 寄存器 Registers](#342-寄存器-registers) - [3.4.3 寻址模式 Addressing modes](#343-寻址模式-addressing-modes) - [3.4.4 通用指令 Common instructions](#344-通用指令-common-instructions) - [`mov` 和 `lea` 指令](#mov-和-lea-指令) - [算术和位运算](#算术和位运算) - [流程控制指令](#流程控制指令) - [`setx` 和 `movx`](#setx-和-movx) - [函数调用与栈](#函数调用与栈) - [3.4.5 汇编和 gdb](#345-汇编和-gdb) - [3.5 ARM汇编基础](#35-arm汇编基础) - [3.5.1 引言](#351-引言) - [3.5.2 ARM 的 GNU 汇编程序指令表](#352-arm-的-gnu-汇编程序指令表) - [3.5.3 寄存器名称](#353-寄存器名称) - [3.5.4 汇编程序特殊字符/语法](#354-汇编程序特殊字符语法) - [3.5.5 arm程序调用标准](#355-arm程序调用标准) - [3.5.6 寻址模式](#356-寻址模式) - [3.5.7 机器相关指令](#357-机器相关指令) - [3.6 MIPS汇编基础](#36-mips汇编基础) - [数据类型和常量](#数据类型和常量) - [寄存器](#寄存器) - [程序结构](#程序结构) - [数据声明](#数据声明) - [代码](#代码) - [注释](#注释) - [变量声明](#变量声明) - [读取/写入 ( Load/Store )指令](#读取写入--loadstore-指令) - [间接和立即寻址](#间接和立即寻址) - [算术指令](#算术指令) - [流程控制](#流程控制) - [系统调用和 I / O ( 针对 SPIM 模拟器 )](#系统调用和-i--o--针对-spim-模拟器-) ## 3.3 X86 汇编基础 ------ ### 3.3.2 寄存器 Registers ​现代 ( 386及以上的机器 )x86 处理器有 8 个 32 位通用寄存器, 如图 1 所示. ![x86-registers.png](https://github.com/MXYLR/A-note-from-a-weeb/blob/master/x86-registers.png?raw=true) ​这些寄存器的名字都是有点历史的, 例如 EAX 过去被称为 *累加器*, 因为它被用来作很多算术运算, 还有 `ECX` 被称为 *计数器* , 因为它被用来保存循环的索引 ( 就是循环次数 ). 尽管大多是寄存器在现代指令集中已经失去了它们的特殊用途, 但是按照惯例, 其中有两个寄存器还是有它们的特殊用途 ---`ESP` 和 `EBP`. ​对于 `EAX`, `EBX`, `ECX` 还有 `EDX` 寄存器, 它们可以被分段开来使用. 例如, 可以将 `EAX` 的最低的 2 位字节视为 16 位寄存器 ( `AX` ). 还可以将 `AX` 的最低位的 1 个字节看成 8 位寄存器来用 ( `AL` ), 当然 `AX` 的高位的 1 个字节也可以看成是一个 8 位寄存器 ( `AH` ). 这些名称有它们相对应的物理寄存器. 当两个字节大小的数据被放到 `DX` 的时候, 原本 `DH`, `DL` 和 `EDX` 的数据会受到影响 ( 被覆盖之类的 ). 这些 " 子寄存器 " 主要来自于比较久远的 16 位版本指令集. 然而, 姜还是老的辣, 在处理小于 32 位的数据的时候, 比如 1 个字节的 ASCII 字符, 它们有时会很方便. ### 3.3.3 内存和寻址模式 Memory and Addressing Modes #### 3.3.3.1 声明静态数据区域 ​你可以用特殊的 x86 汇编指令在内存中声明静态数据区域 ( 类似于全局变量 ). `.data`指令用来声明数据. 根据这条指令, `.byte`, `.short` 和 `.long` 可以分别用来声明 1 个字节, 2 个字节和 4 个字节的数据. 我们可以给它们打个标签, 用来引用创建的数据的地址. 标签在汇编语言中是非常有用的, 它们给内存地址命名, 然后 *编译器* 和 *链接器* 将其 " 翻译 " 成计算机理解的机器代码. 这个跟用名称来声明变量很类似, 但是它遵守一些较低级别的规则. 例如, 按顺序声明的位置将彼此相邻地存储在内存中. 这话也许有点绕, 就是按照顺序打的标签, 这些标签对应的数据也会按照顺序被放到内存中. 一些例子 : ```masm .data var : .byte 64 #声明一个字节型变量 var, 其所对应的数据是64 .byte 10 #声明一个数据 10, 这个数据没有所谓的 " 标签 ", 它的内存地址就是 var+1. x : .short 42 #声明一个大小为 2 个字节的数据, 这个数据有个标签 " x " y : .long 30000 #声明一个大小为 4 个字节的数据, 这个数据标签是 " y ", y 的值被初始化为 30000 ``` ​与高级语言不同, 高级语言的数组可以具有多个维度并且可以通过索引来访问, x86 汇编语言的数组只是在内存中连续的" 单元格 ". 你只需要把数值列出来就可以声明一个数组, 比如下面的第一个例子. 对于一些字节型数组的特殊情况, 我们可以使用字符串. 如果要在大多数的内存填充 0, 你可以使用`.zero`指令. ​例子 : ```masm s : .long 1, 2, 3 #声明 3 个大小为 4 字节的数据 1, 2, 3. 内存中 s+8 这个标签所对应的数据就是 3. barr: .zero 10 #从 barr 这个标签的位置开始, 声明 10 个字节的数据, 这些数据被初始化为 0. str : .string "hello" #从 str 这个标签的位置开始, 声明 6 个字节的数据, 即 hello 对应的 ASCII 值, 这最后还跟有一个 nul(0) 字节. ``` ![label_s](https://github.com/MXYLR/A-note-from-a-weeb/blob/master/x86image/label_s.png) ![label_barr](https://github.com/MXYLR/A-note-from-a-weeb/blob/master/x86image/label_barr.png) ![label_str](https://github.com/MXYLR/A-note-from-a-weeb/blob/master/x86image/label_str.png) #### 3.3.3.2 内存寻址 ​现代 x86 兼容处理器能够寻址高达 2^32 字节的内存 : 内存地址为 32 位宽. 在上面的示例中, 我们使用标签来引用内存区域, 这些标签实际上被 32 位数据的汇编程序替换, 这些数据指定了内存中的地址. 除了支持通过标签 (即常数值)引用存储区域之外, x86 提供了一种灵活的计算和引用内存地址的方案 : 最多可将两个 32 位寄存器和一个32位有符号常量相加, 以计算存储器地址. 其中一个寄存器可以选择预先乘以 2, 4 或 8. ​寻址模式可以和许多 x86 指令一起使用 ( 我们将在下一节对它们进行讲解 ). 这里我们用 `mov` 指令在寄存器和内存中移动数据当作例子. 这个指令有两个参数, 第一个是数据的来源, 第二个是数据的去向. ​一些`mov`的例子 : ```asm mov (%ebx), %eax #从 EBX 中的内存地址加载 4 个字节的数据到 EAX, 就是把 EBX 中的内容当作标签, 这个标签在内存中对应的数据放到 EAX 中 #后面如果没有说明的话, (%ebx)就表示寄存器ebx中存储的内容 mov %ebx, var(,1) # 将 EBX 中的 4 个字节大小的数据移动的内存中标签为 var 的地方去.( var 是一个 32 位常数). mov (%esi, %ebx, 4), %edx #将内存中标签为 ESI + 4 * EBX 所对应的 4 个字节大小的数据移动到 EDX 中. ``` ​一些**错误**的例子: ```masm mov (%ebx, %ecx, -1), %eax #这个只能把寄存器中的值加上一遍. mov %ebx,(%eax, %esi, %edi, 1) #在地址计算中, 最多只能出现 2 个寄存器, 这里却有 3 个寄存器. ``` #### 3.3.3.3 操作后缀 ​通常, 给定内存地址的数据类型可以从引用它的汇编指令推断出来. 例如, 在上面的指令中, 你可以从寄存器操作数的大小来推出其所占的内存大小. 当我们加载一个 32 位的寄存器的时候, 编译器就可以推断出我们用到的内存大小是 4 个字节宽. 当我们将 1 个字节宽的寄存器的值保存到内存中时, 编译器可以推断出我们想要在内存中弄个 1 字节大小的 " 坑 " 来保存我们的数据. ​然而在某些情况下, 我们用到的内存中 " 坑 " 的大小是不明确的. 比如说这条指令 `mov $2,(%ebx)`. 这条指令是否应该将 " 2 " 这个值移动到 EBX 中的值所代表的地址 " 坑 " 的单个字节中 ? 也许它表示的是将 32 位整数表示的 2 移动到从地址 EBX 开始的 4 字节. 既然这两个解释都有道理, 但计算机汇编程序必须明确哪个解释才是正确的, 计算机很单纯的, 要么是错的要么是对的. 前缀 b, w, 和 l 就是来解决这个问题的, 它们分别表示 1, 2 和 4 个字节的大小. ​举几个例子 : ```masm movb $2, (%ebx) #将 2 移入到 ebx 中的值所表示的地址单元中. movw $2, (%ebx) #将 16 位整数 2 移动到 从 ebx 中的值所表示的地址单元 开始的 2 个字节中#这话有点绕, 所以我故意在里面加了点空格, 方便大家理解. movl $2,(%ebx) #将 32 位整数 2 移动到 从 ebx中的值表示的地址单元 开始的 4 个字节中. ``` ### 3.3.4 指令 Instructions ​机器指令通常分为 3 类 : 数据移动指令, 逻辑运算指令和流程控制指令. 在本节中, 我们将讲解每一种类型的 x86 指令以及它们的重要示例. 当然, 我们不可能把 x86 所有指令讲得特别详细, 毕竟篇幅和水平有限. 完整的指令列表, 请参阅 intel 的指令集参考手册. ​我们将使用以下符号 : ```masm 重点内容 : 栈 > > **栈** 是一种特殊的存储空间, 特殊在它的访问形式上, 它的访问形式就是最后进入这个空间的数据, 最先出去, 也就是 "先进后出, 后进先出". - `lea` 加载有效地址 ​`lea`指令将其第一个参数指定的内存单元 放入到 第二个参数指定的寄存器中. 注意, 该指令不加载内存单元中的内容, 只是计算有效地址并将其放入寄存器. 这对于获得指向存储器区域的指针或者执行简单的算术运算非常有用. 也许这里你会看得一头雾水, 不过你不必担心, 这里有更为通俗易懂的解释. - **汇编语言中 lea 指令和 mov 指令的区别 ?** 这里的代码是 16 位 MASM 汇编的格式, 和我们现在用的 AT&T 汇编有一些细微区别, 不过不影响我们的理解. `MOV` 指令的功能是传送数据, 例如 `MOV AX,[1000H]`, 作用是将 1000H 作为偏移地址, 寻址找到内存单元, 将该内存单元中的数据送至 AX; `LEA` 指令的功能是取偏移地址, 例如 `LEA AX,[1000H]`, 作用是将源操作数 [1000H] 的偏移地址 1000H 送至 AX.理解时, 可直接将[ ]去掉, 等同于 `MOV AX,1000H`. 再如: `LEA BX,[AX]`, 等同于 `MOV BX,AX`; `LEA BX,TABLE` 等同于 `MOV BX,OFFSET TABLE`. 但有时不能直接使用 `MOV` 代替: 比如: `LEA AX,[SI+6]` 不能直接替换成: `MOV AX,SI+6`; 但可替换为: `MOV AX,SI` `ADD AX,6` 两步完成. [参考链接](https://zhidao.baidu.com/question/427095565.html) - *语法* `lea "` | 将字符串作为数据插入到程序中 | | `.asciz ""` | 与 .ascii 类似, 但跟随字符串的零字节 | | `.balign {,{,} }` | 将地址与 `` 字节对齐. 汇编程序通过添加值 `` 的字节或合适的默认值来对齐. 如果需要超过 `` 这个数字来填充字节, 则不会发生对齐 ( 类似于armasm 中的 ALIGN ) | | `.byte {, } …` | 将一个字节值列表作为数据插入到程序中 | | `.code ` | 以位为单位设置指令宽度. 使用 16 表示 Thumb, 32 表示 ARM 程序 ( 类似于 armasm 中的 CODE16 和 CODE32 ) | | `.else` | 与.if和 .endif 一起使用 ( 类似于 armasm 中的 ELSE ) | | `.end` | 标记程序文件的结尾 ( 通常省略 ) | | `.endif` | 结束条件编译代码块 - 参见.if, .ifdef, .ifndef ( 类似于 armasm 中的 ENDIF ) | | `.endm` | 结束宏定义 - 请参阅 .macro ( 类似于 armasm 中的 MEND ) | | `.endr` | 结束重复循环 - 参见 .rept 和 .irp (类似于 armasm 中的 WEND ) | | `.equ , ` | 该指令设置符号的值 ( 类似于 armasm 中的 EQU ) | | `.err` | 这个会导致程序停止并出现错误 | | `.exitm` | 中途退出一个宏 - 参见 .macro ( 类似于 armasm 中的 MEXIT ) | | `.global ` | 该指令给出符号外部链接 ( 类似于 armasm 中的 MEXIT ). | | `.hword {, }...` | 将16位值列表作为数据插入到程序中 ( 类似于 armasm 中的 DCW ) | | `.if ` | 把一段代码变成前提条件. 使用 .endif 结束代码块 ( 类似于 armasm中的 IF ). 另见 .else | | `.ifdef ` | 如果定义了 ``, 则包含一段代码. 结束代码块用 .endif, 这就是个条件判断嘛, 很简单的. | | `.ifndef ` | 如果未定义 ``, 则包含一段代码. 结束代码块用 .endif, 同上. | | `.include ""` | 包括指定的源文件, 类似于 armasm 中的 INCLUDE 或 C 中的#include | | `.irp {,} {,} ...` | 为值列表中的每个值重复一次代码块. 使用 .endr 指令标记块的结尾. 在里面重复代码块, 使用 `\` 替换关联的代码块值列表中的值. | | `.macro {} {,< arg_2>} ... {,}` | 使用 N 个参数定义名为``的汇编程序宏.宏定义必须以 `.endm` 结尾. 要在较早的时候从宏中逃脱, 请使用 `.exitm`. 这些指令是类似于 armasm 中的 MACRO, MEND 和MEXIT. 你必须在虚拟宏参数前面加 `\`. | | `.rept ` | 重复给定次数的代码块. 以`.endr`结束. | | ` .req ` | 该指令命名一个寄存器. 它与 armasm 中的 `RN` 指令类似, 不同之处在于您必须在右侧提供名称而不是数字 (例如, `acc .req r0`) | | `.section {," "}` | 启动新的代码或数据部分. GNU 中有这些部分:`.text`代码部分#`.data`初始化数据部分和`.bss`未初始化数据部分. 这些部分有默认值flags和链接器理解默认名称 (与armasm指令AREA类似的指令). 以下是 ELF 格式文件允许的 .section标志:
a 表示 allowable section
w 表示 writable section
x 表示 executable section | | `.set , ` | 该指令设置变量的值. 它类似于 SETA. | | `.space {, }` | 保留给定的字节数. 如果指定了字节, 则填充零或 `` (类似于 armasm 中的 SPACE) | | `.word {,}...` | 将 32 位字值列表作为数据插入到程序集中 (类似于 armasm 中的 DCD). | ### 3.5.3 寄存器名称 通用寄存器: %r0 - %r15 fp 寄存器: %f0 - %f7 临时寄存器: %r0 - %r3, %r12 保存寄存器: %r4 - %r10 堆栈 ptr 寄存器: %sp 帧 ptr 寄存器: %fp 链接寄存器: %lr 程序计数器: %ip 状态寄存器: $psw 状态标志寄存器: xPSR xPSR_all xPSR_f xPSR_x xPSR_ctl xPSR_fs xPSR_fx xPSR_fc xPSR_cs xPSR_cf xPSR_cx ### 3.5.4 汇编程序特殊字符/语法 内联评论字符: '@' ​行评论字符: '#' 语句分隔符: '#' 立即操作数前缀: '#' 或 '$' ### 3.5.5 arm程序调用标准 参数寄存器 : %a0 - %a4 (别名为%r0 - %r4) 返回值regs : %v1 - %v6 (别名为%r4 - %r9) ### 3.5.6 寻址模式 `addr` 绝对寻址模式 `%rn` 寄存器直接寻址 `[%rn]` 寄存器间接寻址或索引 `[%rn, #n]` 基于寄存器的偏移量 上述 "rn" 指任意寄存器, 但不包括控制寄存器. ### 3.5.7 机器相关指令 | 指令 | 描述 | | ------------------ | ------------------------------------| | .arm | 使用arm模式进行装配 | | .thumb | 使用thumb模式进行装配 | | .code16 | 使用thumb模式进行装配 | | .code32 | 使用arm模式进行组装 | | .force_thumb Force | thumb模式 (即使不支持) | | .thumb_func | 将输入点标记为thumb编码 (强制bx条目) | | .ltorg | 启动一个新的文字池 | ## 3.6 MIPS汇编基础 ### 数据类型和常量 - 数据类型: - 指令全是32位 - 字节 (8位), 半字 (2字节), 字 (4字节) - 一个字符需要1个字节的存储空间 - 整数需要1个字 (4个字节)的存储空间 - 常量: - 按原样输入的数字.例如 4 - 用单引号括起来的字符.例如 'b' - 用双引号括起来的字符串.例如 "A string" ### 寄存器 - 32个通用寄存器 - 寄存器前面有 $ 两种格式用于寻址: - 使用寄存器号码, 例如 `$ 0` 到 `$ 31` - 使用别名, 例如 `$ t1`, `$ sp` - 特殊寄存器 Lo 和 Hi 用于存储乘法和除法的结果 - 不能直接寻址# 使用特殊指令 `mfhi` ( " 从 Hi 移动 " )和 `mflo` ( " 从 Lo 移动 " )访问的内容 - 栈从高到低增长 | 寄存器 | 别名 | 用途| | ------- | ------- | ------------------------------------------------------------ | | `$0` | `$zero` | 常量0(constant value 0) | | `$1` | `$at` | 保留给汇编器(Reserved for assembler) | | `$2-$3` | `$v0-$v1` | 函数调用返回值(values for results and expression evaluation) | | `$4-$7` | `$a0-$a3` | 函数调用参数(arguments) | | `$8-$15` | `$t0-$t7` | 暂时的(或随便用的) | | `$16-$23` | `$s0-$s7` | 保存的(或如果用, 需要SAVE/RESTORE的)(saved) | | `$24-$25` | `$t8-$t9` | 暂时的(或随便用的) | | `$26~$27` | `$k0~$k1` | 保留供中断/陷阱处理程序使用 | | `$28` | `$gp` | 全局指针(Global Pointer) | | `$29` | `$sp` | 堆栈指针(Stack Pointer) | | `$30` | `$fp` | 帧指针(Frame Pointer) | | `$31` | `$ra` | 返回地址(return address) | 再来说一说这些寄存器 : - zero 它一般作为源寄存器, 读它永远返回 0, 也可以将它作为目的寄存器写数据, 但效果等于白写.为什么单独拉一个寄存器出来返回一个数字呢?答案是为了效率, MIPS 的设计者只允许在寄存器内执行算术操作, 而不允许直接操作立即数.所以对最常用的数字 0 单独留了一个寄存器, 以提高效率 - at 该寄存器为给编译器保留, 用于处理在加载 16 位以上的大常数时使用, 编译器或汇编程序需要把大常数拆开, 然后重新组合到寄存器里.系统程序员也可以显式的使用这个寄存器, 有一个汇编 directive 可被用来禁止汇编器在 directive 之后再使用 at 寄存器. - v0, v1.这两个很简单, 用做函数的返回值, 大部分时候, 使用 v0 就够了.如果返回值的大小超过 8 字节, 那就需要分配使用堆栈, 调用者在堆栈里分配一个匿名的结构, 设置一个指向该参数的指针, 返回时 v0 指向这个对应的结构, 这些都是由编译器自动完成. - a0-a3. 用来传递函数入参给子函数.看一下这个例子: `ret = strncmp("bear","bearer",4)` 参数少于 16 字节, 可以放入寄存器中, 在 strncmp 的函数里, a0 存放的是 "bear" 这个字符串所在的只读区地址, a1 是 "bearer" 的地址, a2 是 4. - t0-t9 临时寄存器 s0-s8 保留寄存器 这两种寄存器需要放在一起说, 它们是 mips 汇编里面代码里见到的最多的两种寄存器, 它们的作用都是存取数据, 做计算、移位、比较、加载、存储等等, 区别在于, t0-t9 在子程序中可以使用其中的值, 并不必存储它们, 它们很适合用来存放计算表达式时使用的"临时"变量.如果这些变量的使用要要跳转到子函数之前完成, 因为子函数里很可能会使用相同的寄存器, 而且不会有任何保护.如果子程序里不会调用其它函数那么建议尽量多的使用t0-t9, 这样可以避免函数入口处的保存和结束时的恢复. 相反的, s0-s8 在子程序的执行过程中, 需要将它们存储在堆栈里, 并在子程序结束前恢复.从而在调用函数看来这些寄存器的值没有变化. - k0, k1. 这两个寄存器是专门预留给异常处理流程中使用.异常处理流程中有什么特别的地方吗?当然.当 MIPS CPU 在任务里运行的时候, 一旦有外部中断或者异常发生, CPU 就会立刻跳转到一个固定地址的异常 handler 函数执行, 并同时将异常结束后返回到任务的指令地址记录在 EPC 寄存器 (Exception Program Counter)里.习惯性的, 异常 handler 函数开头总是会保持现场即 MIPS 寄存器到中断栈空间里, 而在异常返回前, 再把这些寄存器的值恢复回去.那就存在一个问题, 这个 EPC 里的值存放在哪里?异常 handler 函数的最后肯定是一句 `jr x`, X 是一个 MIPS 寄存器, 如果存放在前面提到的 t0,s0 等等, 那么 PC 跳回任务执行现场时, 这个寄存器里的值就不再是异常发生之前的值.所以必须要有时就可以一句 `jr k0`指令返回了. k1 是另外一个专为异常而生的寄存器, 它可以用来记录中断嵌套的深度.CPU 在执行任务空间的代码时, k1 就可以置为 0, 进入到中断空间, 每进入一次就加 1, 退出一次相应减 1, 这样就可以记录中断嵌套的深度.这个深度在调试问题的时候经常会用到, 同时应用程序在做一次事情的时候可能会需要知道当前是在任务还是中断上下文, 这时, 也可以通过 k1 寄存器是否为 0 来判断. - sp 指向当前正在操作的堆栈顶部, 它指向堆栈中的下一个可写入的单元, 如果从栈顶获取一个字节是 sp-1 地址的内容.在有 RTOS 的系统里, 每个 task 都有自己的一个堆栈空间和实时 sp 副本, 中断也有自己的堆栈空间和 sp 副本, 它们会在上下文切换的过程中进行保存和恢复. - gp 这是一个辅助型的寄存器, 其含义较为模糊, MIPS 官方为该寄存器提供了两个用法建议, 一种是指向 Linux 应用中位置无关代码之外的数据引用的全局偏移量表; 在运行 RTOS 的小型嵌入式系统中, 它可以指向一块访问较为频繁的全局数据区域, 由于MIPS 汇编指令长度都是 32bit, 指令内部的 offset 为 16bit, 且为有符号数, 所以能用一条指令以 gp 为基地址访问正负 15bit 的地址空间, 提高效率.那么编译器怎么知道gp初始化的值呢?只要在 link 文件中添加 `_gp` 符号, 连接器就会认为这是 gp 的值.我们在上电时, 将 `_gp` 的值赋给 gp 寄存器就行了. 话说回来, 这都是 MIPS 设计者的建议, 不是强制, 楼主还见过一种 gp 寄存器的用法, 来在中断和任务切换时做 sp 的存储过渡, 也是可以的. - fp 这个寄存器不同的编译器对其解释不同, GNU MIPS C 编译器使用其作为帧指针, 指向堆栈里的过程帧 (一个子函数) 的第一个字, 子函数可以用其做一个偏移访问栈帧里的局部变量, sp 也可以较为灵活的移动, 因为在函数退出之前使用 fp 来恢复; 还要一种而 SGI 的 C 编译器会将这个寄存器直接作为 s8,扩展了一个保留寄存器给编译器使用. - ra 在函数调用过程中, 保持子函数返回后的指令地址.汇编语句里函数调用的形式为: `jal function_X` 这条指令 jal(jump-and-link,跳转并链接) 指令会将当期执行运行指令的地址 +4 存储到 ra 寄存器里, 然后跳转到 function_X 的地址处.相应的, 子函数返回时, 最常见的一条指令就是 `jr ra` ra 是一个对于调试很有用的寄存器, 系统的运行的任何时刻都可以查看它的值以获取 CPU 的运行轨迹. ​最后, 如果纯写汇编语句的话, 这些寄存器当中除了 zero 之外, 其它的基本上都可以做普通寄存器存取数据使用 (这也是它们为什么会定义为"通用寄存器", 而不像其它的协处理器、或者外设的都是专用寄存器, 其在出厂时所有的功能都是定死的), 那为什么有这么多规则呢 ?MIPS 开发者们为了让自己的处理器可以运行像 C、Java 这样的高级语言, 以及让汇编语言和高级语言可以安全的混合编程而设计的一套 ABI (应用编程接口), 不同的编译器的设计者们就会有据可依, 系统程序员们在阅读、修改汇编程序的时候也能根据这些约定而更为顺畅地理解汇编代码的含义. ### 程序结构 - 本质上只是带有数据声明的纯文本文件, 程序代码 ( 文件名应以后缀 .s 结尾, 或者.asm ) - 数据声明部分后跟程序代码部分 #### 数据声明 - 数据以 `.data` 为标识 - 声明变量后, 即在内存中分配空间 #### 代码 - 放在用汇编指令 `.text` 标识的文本部分中 - 包含程序代码 ( 指令 ) - 给定标签 `main` 代码执行的起点 ( 和 C 语言一样 ) - 程序结束标志 (见下面的系统调用) #### 注释 - `#` 表示单行注释 `#` 后面的任何内容都会被视为注释 - MIPS 汇编语言程序的模板: ```masm #给出程序名称和功能描述的注释 #Template.s #MIPS汇编语言程序的Bare-bones概述 .data #变量声明遵循这一行 #... .text#指令跟随这一行 main: #表示代码的开始 (执行的第一条指令) #... #程序结束, 之后留空, 让SPIM满意. ``` #### 变量声明 声明格式: ```masm name: storage_type value (s) ``` 使用给定名称和指定值为指定类型的变量创建空间 value (s) 通常给出初始值# 对于.space, 给出要分配的空格数 注意: 标签后面跟冒号 (:) - **例如** ```arm var1: .word 3 #创建一个初始值为 3 的整数变量 array1: .byte'a', 'b' #创建一个元素初始化的 2 元素字符数组到 a 和 b array2: .space 40 #分配 40 个连续字节, 未初始化的空间可以用作 40 个元素的字符数组, 或者是 #10 个元素的整数数组. ``` #### 读取/写入 ( Load/Store )指令 - 对 RAM 的访问, 仅允许使用加载和存储指令 ( 即 `load` 或者 `store`) - 所有其他指令都使用寄存器参数 `load`: ```masm lw register_destination, RAM_source #将源内存地址的字 ( 4 个字节 ) 复制到目标寄存器, (lw中的'w'意为'word',即该数据大小为4个字节) lb register_destination, RAM_source #将源内存地址的字节复制到目标寄存器的低位字节, 并将符号映射到高位字节 ( 同上, lb 意为 load byte ) ``` `store`: ```masm sw register_source, RAM_destination #将源寄存器的字存储到目标内存RAM中 sb register_source, RAM_destination #将源寄存器中的低位字节存储到目标内存RAM中 ``` 立即加载: ```masm li register_destination, value #把立即值加载到目标寄存器中,顾名思义, 这里的 li 意为 load immediate, 即立即加载. ``` - *例子* ```masm .data var1: .word 23 # 给变量 var1 在内存中开辟空间, 变量初始值为 23 .text __start: lw $t0, var1 # 将内存单元中的内容加载到寄存器中 $t0: $t0 = var1 li $t1, 5 # $t1 = 5 ("立即加载") sw $t1, var1 # 把寄存器$t1的内容存到内存中 : var1 = $t1 done ``` #### 间接和立即寻址 - 仅用于读取和写入指令 ***直接给地址: *** ```masm la $t0, var1 ``` - 将 var1 的内存地址 (可能是程序中定义的标签)复制到寄存器 `$t0` 中 ***间接寻址, 地址是寄存器的内容, 类似指针: *** ```masm lw $t2, ($t0) ``` - 将 `$t0` 中包含的 RAM 地址加载到 `$t2` ```masm sw $t2, ($t0) ``` - 将 `$t2` 寄存器中的字存储到 `$t0` 中包含的地址的 RAM 中 ***基于偏移量的寻址: *** ```masm lw $t2, 4 ($t0) ``` - 将内存地址 ( `$t0 + 4` ) 的字加载到寄存器 `$t2` 中 - " 4 " 给出了寄存器 `$t0` 中地址的偏移量 ```masm sw $t2, -12 ($t0) ``` - 将寄存器 `$t2` 中的字放到内存地址 ( `$t0 - 12` ) - 负偏移也是可以的, 反向漂移方不方 ? 注意: 基于*偏移量* 的寻址特别适用于: - 数组# 访问元素作为与基址的偏移量 - 栈# 易于访问偏离栈指针或帧指针的元素 - *例子* ```masm .data array1: .space 12 # 定义一个 12字节 长度的数组 array1, 容纳 3个整型 .text __start: la $t0, array1 # 让 $t0 = 数组首地址 li $t1, 5 # $t1 = 5 ("load immediate") sw $t1, ($t0) # 数组第一个元素设置为 5# 用的间接寻址# array[0] = $1 = 5 li $t1, 13 # $t1 = 13 sw $t1, 4($t0) # 数组第二个元素设置为 13# array[1] = $1 = 13 #该数组中每个元素地址相距长度就是自身数据类型长度, 即4字节, 所以对于array+4就是array[1] li $t1, -7 # $t1 = -7 sw $t1, 8($t0) # 第三个元素设置为 -7# #array+8 = (address[array[0])+4)+ 4 = address(array[1]) + 4 = address(array[2]) done ``` #### 算术指令 - 最多使用3个参数 - 所有操作数都是寄存器# 不能有内存地址的存在 - 操作数大小是字 ( 4个字节 ), 32位 = 4 * 8 bit = 4bytes = 1 word ```masm add $t0,$t1,$t2 # $t0 = $t1 + $t2#添加为带符号 (2 的补码)整数 sub $t2,$t3,$t4 # $t2 = $t3 Ð $t4 addi $t2,$t3, 5 # $t2 = $t3 + 5# addu $t1,$t6,$t7 # $t1 = $t6 + $t7#跟无符号数那样相加 subu $t1,$t6,$t7 # $t1 = $t6 - $t7#跟无符号数那样相减 mult $t3,$t4 # 运算结果存储在hi,lo (hi高位数据, lo地位数据) div $t5,$t6 # Lo = $t5 / $t6 (整数商) # Hi = $t5 mod $t6 (求余数) #商数存放在 lo, 余数存放在 hi mfhi $t0 # 把特殊寄存器 Hi 的值移动到 $t0 : $t0 = Hi mflo $t1 # 把特殊寄存器 Lo 的值移动到 $t1: $t1 = Lo #不能直接获取 hi 或 lo中的值, 需要mfhi, mflo指令传值给寄存器 move $t2,$t3 # $t2 = $t3 ``` #### 流程控制 分支 ( if-else ) - 条件分支的比较内置于指令中 ```masm b target #无条件分支,直接到程序标签目标 beq $t0, $t1, target #if $t0 = $ t1, 就跳到目标 blt $t0, $t1, target #if $t0 <$ t1, 就跳到目标 ble $t0, $t1, target #if $t0 <= $ t1, 就跳到目标 bgt $t0, $t1, target #if $t0 $ t1, 就跳到目标 bge $t0, $t1, target #if $t0 = $ t1, 就跳到目标 bne $t0, $t1, target #if $t0 < $t1, 就跳到目标 ``` 跳转 ( while, for, goto ) ```masm j target #看到就跳, 不用考虑任何条件 jr $t3 #类似相对寻址, 跳到该寄存器给出的地址处 ``` 子程序调用 子程序调用: "跳转和链接" 指令 ```masm jal sub_label # "跳转和链接" ``` - 将当前的程序计数器保存到 `$ra` 中 - 跳转到 `sub_label` 的程序语句 子程序返回: "跳转寄存器"指令 ```masm jr $ra #"跳转寄存器" ``` - 跳转到$ ra中的地址 (由jal指令存储) 注意: 寄存地址存储在寄存器 `$ra` 中# 如果子程序将调用其他子程序, 或者是递归的, 则返回地址应该从 `$ra` 复制到栈以保留它, 因为 `jal` 总是将返回地址放在该寄存器中, 因此将覆盖之前的值 #### 系统调用和 I / O ( 针对 SPIM 模拟器 ) - 通过系统调用实现从输入/输出窗口读取或打印值或字符串, 并指示程序结束 - `syscall` - 首先在寄存器 `$v0` 和 `$a0 - $a1`中提供适当的值 - 寄存器 `$v0` 中存储返回的结果值 ( 如果有的话 ) 下表列出了可能的 **系统调用** 服务. | Service 服务 | Code in `$v0` 对应功能的调用码 | Arguments **所需参数** | Results 返回值 | | ----------------------------------- | ---------------------------- | ---------------------------------------------------------- | -------------------------- | | print 一个整型数 | `$v0` = 1 | `$a0` = 要打印的整型数 | | | print 一个浮点数 | `$v0` = 2 | `$f12` = 要打印的浮点数 | | | print 双精度数 | `$v0` = 3 | `$f12` = 要打印的双精度数 | | | print 字符串 | `$v0` = 4 | `$a0` = 要打印的字符串的地址 | | | 读取 ( read ) 整型数 | `$v0` = 5 | | `$v0` = 读取的整型数 | | 读取 ( read ) 浮点数 | `$v0` = 6 | | `$v0` = 读取的浮点数 | | 读取 ( read ) 双精度数 | `$v0`= 7 | | `$v0` = 读取的双精度 | | 读取 ( read ) 字符串 | `$v0` = 8 | 将读取的字符串地址赋值给 `$a0`# 将读取的字符串长度赋值给 `$a1` | | | 这个应该和 C 语言的 `sbrk()` 函数一样 | `$v0` = 9 | 需要分配的空间大小 (单位目测是字节 bytes) | 将分配好的空间首地址给 `$v0` | | exit | `$v0` =10 | 这个还要说吗.....= _ = | | - `print_string` 即 `print 字符串` 服务期望启动以 null 结尾的字符串.指令`.asciiz` 创建一个以 null 结尾的字符串. - `read_int`, `read_float` 和 `read_double` 服务读取整行输入, 包括换行符`\n`. - `read_string` 服务与 UNIX 库例程 fgets 具有相同的语义. - 它将最多 n-1 个字符读入缓冲区, 并以空字符终止字符串. - 如果当前行中少于 n-1 个字符, 则它会读取并包含换行符, 并使用空字符终止该字符串. - 就是输入过长就截取, 过短就这样, 最后都要加一个终止符. - `sbrk` 服务将地址返回到包含 n 个附加字节的内存块.这将用于动态内存分配. - 退出服务使程序停止运行 - *例子 : 打印一个存储在 $2 的整型数* ```masm li $v0, 1 #声明需要调用的操作代码为 1 ( print_int ), 然后赋值给 $v0 move $a0, $t2 #把这个要打印的整型数赋值给 $a0 syscall #让操作系统执行我们的操作 ``` - *例子 : 读取一个数, 并且存储到内存中的 int_value 变量中* ```masm li $v0, 5 #声明需要调用的操作代码为 5 ( read_int ), 然后赋值给 $v0 syscall #让操作系统执行我们的操作, 然后 $v0 = 5 sw $v0, int_value #通过写入 (store_word)指令 将 $v0 的值 (5)存入内存中 ``` - *例子 : 打印一个字符串 ( 这是完整的, 其实上面例子都可以直接替换 main: 部分, 都能直接运行 )* ```masm .data string1 .asciiz "Print this.\n" # 字符串变量声明 # .asciiz 指令使字符串 null 终止 .text main: li $v0, 4 # 将适当的系统调用代码加载到寄存器 $v0 中 # 打印字符串, 赋值对应的操作代码 $v0 = 4 la $a0, string1 # 将要打印的字符串地址赋值 $a0 = address(string1) syscall # 让操作系统执行打印操作 # 要指示程序结束, 应该退出系统调用, 所以最后一行代码应该是这个 : li $v0, 10    #对着上面的表, 不用说了吧 syscall # 让操作系统结束这一切吧 ! ``` #### 补充 : MIPS 指令格式 - **R 格式** | 6 | 5 | 5 | 5 | 5 | 6 | | ---- | ---- | ---- | ---- | ----- | ----- | | op | rs | rt | rd | shamt | funct | 用处: 寄存器 - 寄存器 ALU 操作 读写专用寄存器 - **I 格式** | 6 | 5 | 5 | 16 | | ---- | ---- | ---- | ---------- | | op | rs | rt | 立即数操作 | 用处: 加载/存储 字节, 半字, 字, 双字 条件分支, 跳转, 跳转并链接寄存器 - **J 格式** | 6 | 26 | | ---- | -------- | | op | 跳转地址 | 用处: 跳转, 跳转并链接 陷阱和从异常中返回 各字段含义: op : 指令基本操作, 称为操作码. rs : 第一个源操作数寄存器. rt : 第二个源操作数寄存器. rd : 存放操作结果的目的操作数. shamt : 位移量; funct : 函数, 这个字段选择 op 操作的某个特定变体. 例: ```masm add $t0,$s0,$s1 ``` 表示 `$t0=$s0+$s1`, 即 16 号寄存器 ( s0 ) 的内容和 17 号寄存器 ( s1 ) 的内容相加, 结果放到 8 号寄存器 ( t0 ). 指令各字段的十进制表示为: | 0 | 16 | 17 | 8 | 0 | 32 | | ---- | ---- | ---- | ---- | ---- | ---- | op = 0 和 funct = 32 表示这是加法, 16 = `$s0` 表示第一个源操作数 ( rs ) 在 16 号寄存器里, 17 = `$s1` 表示第二个源操作数 ( rt ) 在 17 号寄存器里, 8 = `$t0` 表示目的操作数 ( rd ) 在 8 号寄存器里. 把各字段写成二进制, 为: | 000000 | 10000 | 10001 | 01000 | 00000 | 100000 | | ------ | ----- | ----- | ----- | ----- | ------ | 这就是上述指令的机器码 ( machine code ), 可以看出是很有规则性的. #### 补充 : MIPS 常用指令集 - **lb/lh/lw** : 从存储器中读取一个 byte / half word / word 的数据到寄存器中. 如`lb $1, 0($2)` - **sb/sh/sw** : 把一个 byte / half word / word 的数据从寄存器存储到存储器中. 如 `sb $1, 0($2)` - **add/addu** : 把两个定点寄存器的内容相加 `add $1,$2,$3($1=$2+$3)`# u 为不带符号加 - **addi/addiu** : 把一个寄存器的内容加上一个立即数 `add $1,$2,#3($1=$2+3)`# u 为不带符号加 - **sub/subu** : 把两个定点寄存器的内容相减 - **div/divu** : 两个定点寄存器的内容相除 - **mul/mulu** : 两个定点寄存器的内容相乘 - **and/andi** : 与运算, 两个寄存器中的内容相与 `and $1,$2,$3($1=$2 & $3)`#i为立即数. - **or/ori** : 或运算. - **xor/xori** : 异或运算. - **beq/beqz/benz/bne** : 条件转移 eq 相等, z 零, ne 不等 - **j/jr/jal/jalr** : j 直接跳转; jr 使用寄存器跳转 - **lui** : 把一个 16 位的立即数填入到寄存器的高 16 位, 低 16 位补零 - **sll/srl** : 逻辑 左移 / 右移 `sll $1,$2,#2` - **slt/slti/sltui** : 如果 `$2` 的值小于 `​$3`, 那么设置 `$1` 的值为 1,否则设置 ​`$1` 的值为 0 `slt $1,$2,$3` - **mov/movz/movn** : 复制, n 为负, z 为零 `mov $1,$2# movz $1,$2,$3` ( `$3` 为零则复制 `$2` 到 `$1` ) - **trap** : 根据地址向量转入管态 - **eret** : 从异常中返回到用户态 ### 参考资料 [参考资料](http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm) ## 更多资料 - [汇编语言](https://onetale.xyz/category/%e7%bc%96%e7%a8%8b%e8%af%ad%e8%a8%80%e7%b1%bb%e7%ac%94%e8%ae%b0/%e6%b1%87%e7%bc%96%e8%af%ad%e8%a8%80/)