3.2.5 X86-64指令集
很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的,而在同一种CPU中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素,但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进:首先Go汇编语言在相同CPU架构上是完全一致的,也就是屏蔽了操作系统的差异;同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令,从而减少不同CPU架构下汇编代码的差异(寄存器名字和数量的差异是一直存在的)。本节的目的也是找出一个较小的精简指令集,以简化Go汇编语言的学习。
X86是一个极其复杂的系统,有人统计x86-64中指令有将近一千个之多。不仅仅如此,X86中的很多单个指令的功能也非常强大,比如有论文证明了仅仅一个MOV指令就可以构成一个图灵完备的系统。以上这是两种极端情况,太多的指令和太少的指令都不利于汇编程序的编写,但是也从侧面体现了MOV指令的重要性。
通用的基础机器指令大概可以分为数据传输指令、算术运算和逻辑运算指令、控制流指令和其它指令等几类。因此我们可以尝试精简出一个X86-64指令集,以便于Go汇编语言的学习。
因此我们先看看重要的MOV指令。其中MOV指令可以用于将字面值移动到寄存器、字面值移到内存、寄存器之间的数据传输、寄存器和内存之间的数据传输。需要注意的是,MOV传输指令的内存操作数只能有一个,可以通过某个临时寄存器达到类似目的。最简单的是忽略符号位的数据传输操作,386和AMD64指令一样,不同的1、2、4和8字节宽度有不同的指令:
Data Type | 386/AMD64 | Comment |
---|---|---|
[1]byte | MOVB | B => Byte |
[2]byte | MOVW | W => Word |
[4]byte | MOVL | L => Long |
[8]byte | MOVQ | Q => Quadword |
MOV指令它不仅仅用于在寄存器和内存之间传输数据,而且还可以用于处理数据的扩展和截断操作。当数据宽度和寄存器的宽度不同又需要处理符号位时,386和AMD64有各自不同的指令:
Data Type | 386 | AMD64 | Comment |
---|---|---|---|
int8 | MOVBLSX | MOVBQSX | sign extend |
uint8 | MOVBLZX | MOVBQZX | zero extend |
int16 | MOVWLSX | MOVWQSX | sign extend |
uint16 | MOVWLZX | MOVWQZX | zero extend |
比如当需要将一个int64类型的数据转为bool类型时,则需要使用MOVBQZX指令处理。
基础算术指令有ADD、SUB、MUL、DIV等指令。其中ADD、SUB、MUL、DIV用于加、减、乘、除运算,最终结果存入目标寄存器。基础的逻辑运算指令有AND、OR和NOT等几个指令,对应逻辑与、或和取反等几个指令。
名称 | 解释 |
---|---|
ADD | 加法 |
SUB | 减法 |
MUL | 乘法 |
DIV | 除法 |
AND | 逻辑与 |
OR | 逻辑或 |
NOT | 逻辑取反 |
其中算术和逻辑指令是顺序编程的基础。通过逻辑比较影响状态寄存器,再结合有条件跳转指令就可以实现更复杂的分支或循环结构。需要注意的是MUL和DIV等乘除法指令可能隐含使用了某些寄存器,指令细节请查阅相关手册。
控制流指令有CMP、JMP-if-x、JMP、CALL、RET等指令。CMP指令用于两个操作数做减法,根据比较结果设置状态寄存器的符号位和零位,可以用于有条件跳转的跳转条件。JMP-if-x是一组有条件跳转指令,常用的有JL、JLZ、JE、JNE、JG、JGE等指令,对应小于、小于等于、等于、不等于、大于和大于等于等条件时跳转。JMP指令则对应无条件跳转,将要跳转的地址设置到IP指令寄存器就实现了跳转。而CALL和RET指令分别为调用函数和函数返回指令。
名称 | 解释 |
---|---|
JMP | 无条件跳转 |
JMP-if-x | 有条件跳转,JL、JLZ、JE、JNE、JG、JGE |
CALL | 调用函数 |
RET | 函数返回 |
无条件和有条件调整指令是实现分支和循环控制流的基础指令。理论上,我们也可以通过跳转指令实现函数的调用和返回功能。不过因为目前函数已经是现代计算机中的一个最基础的抽象,因此大部分的CPU都针对函数的调用和返回提供了专有的指令和寄存器。
其它比较重要的指令有LEA、PUSH、POP等几个。其中LEA指令将标准参数格式中的内存地址加载到寄存器(而不是加载内存位置的内容)。PUSH和POP分别是压栈和出栈指令,通用寄存器中的SP为栈指针,栈是向低地址方向增长的。
名称 | 解释 |
---|---|
LEA | 取地址 |
PUSH | 压栈 |
POP | 出栈 |
当需要通过间接索引的方式访问数组或结构体等某些成员对应的内存时,可以用LEA指令先对目前内存取地址,然后在操作对应内存的数据。而栈指令则可以用于函数调整自己的栈空间大小。
最后需要说明的是,Go汇编语言可能并没有支持全部的CPU指令。如果遇到没有支持的CPU指令,可以通过Go汇编语言提供的BYTE命令将真实的CPU指令对应的机器码填充到对应的位置。完整的X86指令在 https://github.com/golang/arch/blob/master/x86/x86.csv 文件定义。同时Go汇编还正对一些指令定义了别名,具体可以参考这里 https://golang.org/src/cmd/internal/obj/x86/anames.go 。
学员评价