基本的数据处理指令

本章将介绍一些常用的基本的数据处理指令。

我们常见的数据处理指令包括加、减、乘、除、求余、与、或、非、异或等。大部分的数据处理指令都是二元运算,也就是说,需要将两个操作数进行计算,然后赋值给第三个操作数。因此,这些二元运算指令大都有如下的形式:

opcode    dest, source1, source2

加、减、与、或、非、异或

首先是一些常规的、比较简单的计算:

  • add    dest_reg, src_reg1, src_reg2/imm
    
  • sub    dest_reg, src_reg1, src_reg2/imm
    
  • and    dest_reg, src_reg1, src_reg2/imm
    
  • orr    dest_reg, src_reg1, src_reg2/imm
    
  • mvn    dest_reg, src_reg
    
  • 异或

    eor    dest_reg, src_reg1, src_reg2/imm
    

值得一提的是,这些操作都没有涉及到符号。在底层的整数一章中,我们提到,使用补码存储整数的好处就是,无论是有符号整数还是无符号整数,其加减运算都不需要区分有无符号。而对与、或、非、异或来说,这些操作都是逐位进行逻辑运算,因此在这些操作中,也不用区分有无符号。

乘、除、求余

  • mul    dest_reg, src_reg1, src_reg2
    umull  dest_reg, src_reg1, src_reg2
    smull  dest_reg, src_reg1, src_reg2
    

    其中,mul指令的三个操作数都是32位寄存器,umullsmull的源操作数是32位寄存器,目的操作数是64位寄存器。

    umull代表无符号乘法,smull代表有符号乘法。

  • sdiv   dest_reg, src_reg1, src_reg2
    udiv   dest_reg, src_reg1, src_reg2
    

    其中,sdiv代表有符号除法,udiv代表无符号除法。

    使用这种除法得到的结果,与C语言中的除法操作类似,都是向0取整的整数。因此,对5和2进行sdiv,得到的是2;对-5和2进行sdiv,得到的是-2。

  • 求余

    A64指令集不提供直接的求余计算。如果我们想求存储有符号整数的寄存器w1w2的余数,结果存储在w0中,那么,根据上面介绍的数据处理指令,我们可以这么做:

    sdiv   w0, w1, w2
    mul    w0, w2, w0
    sub    w0, w1, w0
    

    我们可以查看codes/9-div.s文件,编译并运行它,使用echo $?查看结果是否符合预期。

移位操作

由于计算机底层存储数是用二进制,所以还有一类操作非常常见:移位操作。其中最常用的莫过于逻辑左移:

lsl   w0, w1, #2

上述指令的意思是,将w1内的值逻辑左移2位赋值给w0。例如,w1的值是0x12345678,用二进制表示就是0001 0010 0011 0100 0101 0110 0111 1000。所谓的逻辑左移,就是将这个二进制数整体向左移动2位(舍弃开头2位),最后的2位用0填充。也就是说,最终w00100 1000 1101 0001 0101 1001 1110 0000,也就是0x48d159e0

逻辑左移操作有什么用呢?我们通过简单的数学知识就可以知道,对于宽度为\(n\)的寄存器来说,将其值\(x\)逻辑左移\(m\)位的运算\(lsl(x, m)\)满足

$$ lsl(x, m)\equiv 2^{m}\cdot x\pmod{2^{n}} $$

也就是说,在汇编层面,如果想将一个数乘2,可以直接逻辑左移1位;乘4就逻辑左移2位。

操作数的可选移位

在之后的章节里,我们会发现,将某个寄存器的值乘以2的倍数往往是一个常见的中间操作。因此,AArch64针对这种情况,对部分指令进行了优化。当我们使用部分指令的时候,可以附带一个移位。例如:

add    w0, w1, w2, lsl #2

就是指,将w2的值乘4,加上w1的值,赋值给w0

当然,并非所有指令的操作数都可以带上可选移位,可以使用可选移位的指令都会在官方文档中注明。目前我们还没有遇到什么可选移位很重要的地方。