本章,我们将看看在aarch64中如何访问内存
随机存储器,或者简单来说,内存是任一架构的必需部分。内存能够被看作由一系列连续的被称为地址的编号组成的数组,每一个元素都是一个字节。在AArch64中,地址是一个64位(这也并不意味着所有的位对地址都是有意义的)。
假定地址是一堆我们可以操作的数字。然而,并不是所有的算术操作都在地址上能进行操作。一个高位地址能够被减去称为一个低位地址。其结果不是一个地址,而是一个偏移。偏移能够被添加到一个地址从而形成一个新的地址。很多时候,我们需要在内存中访问一个连续的b大小的元素集合,因此,它们的地址也是连续的。这也就意味着计算一个形如A+b*i的地址是很普遍的,即i-th操作。
这些普通的地址操作影响了架构的指令,如下所述。
因为RISC的继承关系,AArch64指令不能直接操作内存。只有两个特殊的指令能够操作,即load和store。这两个指令有最基本的两个操作数,一个寄存器和一个地址。地址是基于寻址模式进行计算,接下来我们将会看到。一个load会从一个计算好的地址中得到一个以字节为单位的数字,并将其放入到寄存器中。一个store从寄存器中拿到一些字节,并将其放入到一个地址中。
AArch64支持一系列的load/store指令,但是本章的目的,我们只考虑ldr(load)和str(store)。它们的语法如下
ldr Xn, address-mode // Xn ← 8 bytes of memory at address computed by address-mode
ldr Wn, address-mode // Wn ← 4 bytes of memory at address computed by address-mode
str Xn, address-mode // 8 bytes of memory at address computed by address-mode ← Xn
str Wn, address-mode // 4 bytes of memory at address computed by address-mode ← Wn
AArch64能够配置为小端或者大端。这使得它们的有一点不同,但是它决定了哪8/4个字节我们将在寄存器中操作。我们假定一个小端设定(这一般是非常普通的)。这也意味着当做一个8字节的load/store,寄存器的最低位对应着第一个字节,接下来就往高位走。一个大端的机器将会在相反的工作模式下工作,第一个字节对应的是最高位。
寻址模式是一个过程,通过该过程,load/store指令会计算它将访问的地址值。指令在AArch64上可以被编码为32位但是我们已经说了,地址是64位的。这也就移位这大多数使用立即数的寻址模式是不可行的。一些架构能够在它们的指令中编码位全地址模式。在这些架构中的程序很少做这些,因为它可能会占据很多空间。
我们考虑一个最简单的模式,当然,这种模式在一些环境下还未被讨论。我们在Xn
寄存器中已经有了一个地址。在这种情况下,地址的计算只是使用寄存器中的值。这种模式称为基地址寄存器,并且它的语法是[Xn]。只有一个64位的寄存器能够被用作基地址。
ldr W2, [X1] // W2 ← *X1 (32-bit load)
ldr X2, [X1] // X2 ← *X1 (64-bit load)
如上所述的地址计算方法,我们能够给地址加上一个偏移使之称为另一个地址。其语法是[Xn, #offset]
。这个偏移的范围使-256到255。更大的偏移则会收到一些限制。对于32位立即数而言,其值必须是4的倍数,从0到16380,对于64位而言,它必须是8的倍数,从0到32760。
ldr W2, [X1, #4] // W2 ← *(X1 + 4) [32-bit load]
ldr W2, [X1, #-4] // W2 ← *(X1 - 4) [32-bit load]
ldr X2, [X1, #240] // X2 ← *(X1 + 240) [64-bit load]
ldr X2, [X1, #400] // X2 ← *(X1 + 400) [64-bit load]
// ldr X2, [X1, #404] // Invalid offset, not multiple of 8!
// ldr X2, [X1, #-400] // Invalid offset, must be positive!
// ldr X2, [X1, #32768] // Invalid offset, out of the range!
尽管已经能够使用立即数偏移了,但是有时,偏移不能被编码为立即数或者可能在程序运行之前并不知道。在这些情况下,一个寄存器就会被使用了。
ldr W1, [X2, X3] // W1 ← *(X2 + X3) [32-bit load]
ldr X1, [X2, X3] // X1 ← *(X2 + X3) [64-bit load]
改变偏移寄存器的值是可能的,使用lsl
移位指令。通过lsl #n
使偏移乘以2。
ldr W1, [X2, X3, lsl #3] // W1 ← *(X2 + (X3 << 3)) [32-bit load]
// this is the same as
// W1 ← *(X2 + X3*8) [32-bit load]
ldr X1, [X2, X3, lsl #3] // X1 ← *(X2 + (X3 << 3)) [64-bit load]
// this is the same as
// X1 ← *(X2 + X3*8) [64-bit load]
相对于基地址寄存器,偏移寄存器能够是32位,但是我们有时被迫指定32位到64位。这时,我们必须使用第三章中的扩展操作符。假设源是一个32位值,那么只有sxtw
和uxtw
是被允许的。
ldr W1, [X2, W3, sxtw] // W1 ← *(X2 + ExtendSigned32To64(W3)) [32-bit load]
ldr W1, [X2, W3, uxtw] // W1 ← *(X2 + ExtendUnsigned32To64(W3)) [64-bit load]
正如我们已经知道的,组合扩展操作符和移位操作符是可行的。
ldr W1, [X2, W3, sxtw #3] // W1 ← *(X2 + ExtendSigned32To64(W3 << 3)) [32-bit-load]
有时,我们正在读取连续的内存为止,在这种场景下,我们只关心当前被读的元素。最坏的情况,我们总是通过在一个寄存器总通过算术运算得到地址值并且使用基地址索引的模式。或者,好一点,把第一个地址放在寄存器中,那么这个地址就会被当作基地址,然后计算其偏移。如果我们使用后面的方法,大部分时间,我们的偏移都会随着相对简单的计算更新,例如加法或者加法和移位的组合(乘法就是2的n次方)。我们能够得到一个事实,那就是大部分的时间,地址计算都会有峰值(shape)。在这些情况下,我们可能想要一个索引模式。
在AArch64中有2中索引模式:预先索引(pre-indexing)和事后索引(post-indexing)模式。在预先索引模式下,其基地址寄存器添加偏移计算地址,并且这个地址会写回基地址寄存器。在事后索引模式中,基地址被用于计算地址,但是在地址访问基地址寄存器后会更新地址的值,该值是添加了偏移的。
这两种方式看起来有点相似,都是更新用偏移基地址寄存器。它们不同之处在于偏移的计算时机:预先索引模式会在访问地址之前计算,事后索引模式会在访问之后计算。而我们能够使用的偏移值必须在-256到255之间。
预算索引的访问模式是[Xn, #offset]!
,考虑!
符号,否则你可能会描述一个基地址加偏移,而没有索引。实际操作过程中,更可能是基地址加偏移,但是!
提醒我们更新基地址寄存器的副作用。
ldr X1, [X2, #4]! // X1 ← *(X2 + 4)
// X2 ← X2 + 4
语法是[Xn], #offset
。如果有一个!
在#offset
之后,其语法是获得一个可见的线索,与预先模式类似。
ldr X1, [X2], #4 // X1 ← *X2
// X2 ← X2 + 4
全局对象,如全局变量或者函数,有常数地址。这意味着它应该能把它们作为字面值加载。但是就像我们在AArch64上所知的,不能直接从literal加载。所以,我们必须使用两步走的方法(而这在RSIC架构中非常普遍)。首先,我们需要告诉汇编器把在当前指令附件的全局变量地址放入。然后我们加载地址到一个寄存器,该寄存器使用特殊的加载指令形式(称为加载立即数)。
在我们大多数的例子中,它可能看起来像这样
ldr Xn, addr_of_var // Xn ← &var
...
addr_of_var : .dword variable // This tells the assembler that
// we want here the address of var
// (This is not to be executed!)
一旦我们有了变量的地址,该地址被加载在寄存器中,我们能够做二次加载,使之加载我们想要的位数的个数。
ldr Xm, [Xn] // Xm ← *Xn [64-bit load]
ldr Wm, [Xn] // Wm ← *Xn [32-bit load]
使用64位地址是正确的,但是有一些浪费。其原因是我们的程序大多数不会需要超过32位的值去编码所有的代码和数据的地址。我们全局变量的地址总是让高32位位0.所以我们可能只想用32位的地址。
ldr Wn, addr_of_var // Wn ← &var
...
addr_of_var : .word variable // This tells the assembler that
// we want here the address of var
// (This is not to be executed!)
// 32-bit address here
回忆一下,当写一个32位的值到寄存器,它的高32位被清空了。所以,在ldr
之后,我们能在load或者store中用[Xn]
而没有任何的问题。
作为今天主题的例子,我们将load和store一些全局变量。这个问题不会有任何的用途。
全局变量被定义在.data
节。为了实现这个方法,我们只要简单地定义它们的初始值。如果我们想定义一个32位的变量,我们使用.word
。如果我们想顶一个64位的变量,我们使用.dword
。
// globalvar.s
.data
.balign 8 // Align to 8 bytes
.byte 1
global_var64 : .dword 0x1234 // a 64-bit value of 0x1234
// alternatively: .word 0x1234, 0x0
.balign 4 // Align to 4 bytes
.byte 1
global_var32 : .word 0x5678 // a 32-bit value of 0
在Linux中AArch64不需要内存访问对其。但是如果它们对齐了,则它们在硬件中会执行得快一点。所以我们使用.balign
指令去按照数据得尺寸(以字节)对齐每个变量。
限制我们能够加载变量。例如,我们将给每个变量加1。
.text
.globl main
main :
ldr X0, address_of_global_var64 // X0 ← &global_var64
ldr X1, [X0] // X1 ← *X0
add X1, X1, #1 // X1 ← X1 + 1
str X1, [X0] // *X0 ← X1
ldr X0, address_of_global_var32 // X0 ← &global_var32
ldr W1, [X0] // W1 ← *X0
add W1, W1, #1 // W1 ← W1 + 1
str W1, [X0] // *X0 ← W1
mov W0, #0 // W0 ← 0
ret // exit program
address_of_global_var64 : .dword global_var64
address_of_global_var32 : .dword global_var32
如上所述,保存64位的地址到我们的变量通常是有点浪费的。下面是一些改变,这些改变被要求去使用32位地址。
.text
.globl main
main :
ldr W0, address_of_global_var64 // W0 ← &global_var64
ldr X1, [X0] // X1 ← *X0
add X1, X1, #1 // X1 ← X1 + 1
str X1, [X0] // *X0 ← X1
ldr W0, address_of_global_var32 // W0 ← &global_var32
ldr W1, [X0] // W1 ← *X0
add W1, W1, #1 // W1 ← W1 + 1
str W1, [X0] // *X0 ← W1
mov W0, #0 // W0 ← 0
ret // exit program
address_of_global_var64 : .word global_var64 // note the usage of .word here
address_of_global_var32 : .word global_var32 // note the usage of .word here
注意,有必要在最后的连接阶段使用-static
标记。这将创建一个static文件,这个文件被直接加载到内存。默认地,程序运行的时候,链接器是创建动态文件,这些动态文件被动态链接器加载。动态链接器会在一个地址上加载程序,超过232个提交这些地址非法。当使用.dword
,静态链接器保证了对动态链接器的声明是发射的,所以后者能够在运行时修复64位地址。
还有一些更好的方法去获得全局变量,但是目前这些已经足够了。可能在后面的章节,我们还会重新回顾这里的知识。
今天就到这里。
译者补充:
GDB调试
首先用qemu-arm运行arm文件,设置调试端口为12345,然后启动gdb,设置架构,大端序或者小端序,设置远程目标即可开始调试。
$ qemu-arm -g 12345 ./a.out &
$ gdb-multiarch ./a.out
(gdb) set arch arm
The target architecture is assumed to be mips
(gdb) set endian little
The target is assumed to be little endian
(gdb) target remote localhost:12345
Remote debugging using localhost:12345
0x00400280 in _ftext ()
(gdb) x/i $pc
=> 0x767cb880 move $t9, $ra
————————————————
版权声明:本文为CSDN博主「搓雪小怪兽」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33892117/article/details/89500363