关于汇编的一些浅见
最近简要地了解了x86汇编、RISC-V精简指令集和龙芯(LoongArch64)汇编相关的一些内容,在学习以上这些内容的过程中,得到了一些心得体会,在此简要地将我的一些感悟和想法总结梳理一下,供自己回顾,也顾其他人学习和批评。
加载一个数据
如果我们希望使用数据来进行运算,那么我们一定需要某种途径来将数据载入到设备中,这一点是符合逻辑与常理的,那么,一个很直接的问题就是我们如何在计算机中载入一个具体的数据。最简单的想法有两种,一是将数据直接存放到代码中,这样的方法被称为“立即寻址”,使用到的数据也相应地被称为“立即数”。第二种方法就是将需要使用到的数据提前存放到内存中,接着使用访存指令从内存中加载需要的数据。
在定长指令集中通过立即数加载数据
如果我们使用的是32位CPU,那么很显然,寄存器和算术运算单元都是32位宽的,所以如果传入立即数,我们最多传入一个32位立即数来进行我们的运算。
以上这样一个观点很符合我们的直觉,换言之,我们如果需要进行计算,可以这样写,在我们熟知的x86汇编中也的确是这样工作的:
1 | MOV EAX, 1h |
不过此时我们发现了一个新问题,如果我们使用的是定长指令集呢?例如RISC-V或者LoongArch。在这种情况下,如果一个指令的总长度是32bit(RISC-V/LoongArch),那么操作数OPCode自身一定会占用一定长度,我们不可能在一个被占用过一定长度的指令中存放下一个完整的32bit立即数。
对于以上问题,解决方案其实很简单,那就是将一句立即数加载指令分为两句,在习惯上,我们将32位立即数分两次加载,先加载高20位,再加载低12位。举例来说,比如我们要将0x12345678加载到t0寄存器,那么我们可以通过以下的汇编代码来实现:
1 | lui t0, 0x12345 # t0 = 0x12345000 |
其中lui指令是Load Upper Immediate的简称,意思就是加载高20位立即数,其作用相当于传入一个20位立即数后将寄存器左移12位。
通过以上的方法,我们就实现了在定长指令集中加载和指令长度一致的立即数。
通过访存指令加载内存中的数据
其实关于这一点,没有什么特殊到必须讲一讲的,但是这里有个小细节可以分享一下。
对于32位CPU,其寻址上限是2^32 Byte,也就是4 GiB,对于64位CPU,其寻址上限是2^VALEN Byte,不过在理论计算的时候,我们可以当作2^64 Byte来计算。这当然很多,但是引发了和上文一个相似的问题,如果使用直接寻址的话,一条指令里面放不下目标地址的位置。
在讲这个的解决方案之前,这里简要地摘录一下常见的寻址方式,可能会对下文有所启发。
立即寻址、直接寻址、隐含寻址、间接寻址、寄存器寻址、寄存器间接寻址、基址寻址、变址寻址、相对寻址、堆栈寻址
那么其实就很明确了,最简单的办法就是可以通过间接寻址或者寄存器间接寻址来找到我们要加载的内存的地址。
在细节上,就是先将要读取的内存地址载入寄存器中,接着通过访存指令从内存中加载需要的数据进入寄存器。
举例来说,在LoongArch汇编中,有常用的访存指令LD/ST,其中LD是从内存中读取,
1 | addi.d r1, r0, 0x12345 # r1 = r0(恒为0) + 0x12345 |
通过以上的代码,即可完成从内存中指定位置(0x12345678)的读取。