汇编指令
计算机在执行过程时只识别代表0或者1的电信号。因此为了让计算机能够执行则须向计算机输入一系列01构成的指令。
例如在x64平台下,0x53
,二进制为01010011
,表示将rbx寄存器中的值压栈。
但是,对于程序员而言,如果每个操作都用二进制数来表示(早期的程序员确实是这样)则会带来2个问题。
- 容易出错
- 不方便记忆
为了避免上述的错误,各厂商定义了一系列由英文关键字用来表示二进制指令,这些关键字构成的指令称为汇编指令,或者汇编语言。
例如,上述0x53
在x64下的汇编指令为
push %rbx
如果将上述汇编指令写入汇编文件(.s),经过编译器(gcc)编译后,形成了可被执行的二进制文件,该文件中的对应位置将push %rbx
转换为0x53
。
此时,如果CPU中执行上述指令时,则会将rbx的值压栈。
总而言之,push %rbx
一定会转为唯一的二进制指令让CPU执行。
指令编码
上述的汇编指令由英文构成,其中开始部分的关键字称为助记符(mnemonic),后面的被操作对象称为操作数。编译器将根据助记符和操作数按照一定的规则将其翻译成唯一的二进制数供CPU执行。
每个厂商由于其指令集的区别(CISC:复杂指令集;RISC:精简指令集)而有各自的编码规则。
根据x64的手册,其push %rbx
的编码规则如下
push
代表0x50
reg64由%rbx代替,而rbx寄存器中x64架构下的编码为0x3,因此,将二者相加后最终的编码结果为0x53
。
汇编转换规则
根据上述描述可发现,汇编指令转二进制指令根据的是助记符和操作码的值进行编制。首先需确定助记符的值,该值在二进制指令中被称为操作码,后面紧跟的值是操作数。每个厂商编制了各自的编码规则。
例如,假设一个寄存器加法指令。将一个寄存器(源寄存器)内的值与另一个寄存器(源寄存器)相加,将结果放入寄存器(目的寄存器)中。我们看看在x64指令下是如何编码的。
假设加法指令如下
add %rbx, %rax
则翻译的机器码为
48 01 D8
其翻译规则如下
Byte(s) | Meaning |
---|---|
48 | REX prefix: 64-bit operand size (REX.W = 1)| |
01 | Opcode: ADD r/m64, r64 (adds second register to the first) |
D8 | ModR/M byte: specifies registers → rbx to rax |
关于x86的编码规则,可参见我另一篇文章
AArch64
是ARM v8
架构。其指令属于RISC
指令集。嵌入式系统,苹果M系列芯片以及国产飞腾,鲲鹏等服务器芯片等采用该架构的指令集。如果是在AArch64
下实现上述加法, 其写法如下
add x0, x1, x2
由于AArch64
指令与x86
不是同一指令集,因此寄存器定义也不一致,只是厂商在定义加法指令时都采用了add
助记符。上述指令的意思是将x1
寄存器内的值加上x2
寄存器内的值之后的结果放入x0
寄存器。
AArch64
针对上述指令的编码如下
由于AArch64
的编码规则较x86
简单,这里以AArch64
为例进行说明。
根据手册上的描述可以看出,对于64位机器,31位是1,Rm是x2,Rn是x1, Rd是x0的编码。即2,1和0,其他位置按照规范不变,option和imm3规定了其他的行为,这里是单纯的寄存器赋值,按照手册全部为0
因此机器码为
#二进制
10001011000 00010 000000 00001 00000#十六进制
0x8B020020
#小端编码需要反转
0x2000028B