如何在 ARM64 中初始化 16 位整数数组?

问题描述

我正在尝试学习一些汇编,特别是 ARM64。

我正在尝试将一个 16 位整数数组初始化为某个固定值 (123)。

这是我所拥有的:

.global _main
.align 2

_main:
  mov x0,#0               ; start with a 0-byte offset
  mov x1,#123             ; the value to set each 16-bit element to
  lsl x1,x1,#48          ; shift this value to the upper 16-bits of the register

  loop:
    str x1,[sp,x0]       ; store the full 64-bit register at some byte offset
    add x0,x0,#2         ; advance by two bytes (16-bits)
    cmp x0,#10            ; loop until we've written five 16-bit integers
    b.ne loop

  ldr x2,[sp]             ; load the first four 16-bit integers into x2
  ubfm x0,x2,#48,#63    ; set the exit status to the leftmost 16-bit integer
  mov x16,#1              ; system exit
  svc 0                    ; supervisor call

我期望退出状态为 123,但它是 0。我不明白为什么。

如果我注释掉循环的最后两行,退出状态是 123,这是正确的。

有人能解释一下这是怎么回事吗?是对齐问题吗?

谢谢

解决方法

假设您在 little-endian Aaarch64 system 上运行您的程序,在给定的循环迭代中,您将覆盖您在前一个中修改的字节:

您实际上是在写入字节: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7b
来自:sp + x0 + 0
到:每次迭代时sp + x0 + 7

初始条件:

(gdb) p/x $sp
$3 = 0x40010000
(gdb) x/12xh 0x40010000
0x40010000:     0x0000  0x0000  0x0000  0x0000  0x0000  0x0000  0x0000  0x0000
0x40010010:     0x0000  0x0000  0x0000  0x0000

# initializing some memory to 0xff so that we may see what is going on
(gdb) set {unsigned long }(0x40010000) = 0xffffffffffffffff
(gdb) set {unsigned long }(0x40010008) = 0xffffffffffffffff
(gdb) set {unsigned long }(0x40010010) = 0xffffffffffffffff

(gdb) x/12xh 0x40010000
0x40010000:     0xffff  0xffff  0xffff  0xffff  0xffff  0xffff  0xffff  0xffff
0x40010010:     0xffff  0xffff  0xffff  0xffff

在第一次循环之前:

(gdb) p/x$x1
$4 = 0x7b000000000000

循环:

# pass #1,after str x1,[sp,x0]
(gdb) x/12xh 0x40010000
0x40010000:     0x0000  0x0000  0x0000  0x007b  0xffff  0xffff  0xffff  0xffff
0x40010010:     0xffff  0xffff  0xffff  0xffff

# pass #2,x0]
(gdb) x/12xh 0x40010000
0x40010000:     0x0000  0x0000  0x0000  0x0000  0x007b  0xffff  0xffff  0xffff
0x40010010:     0xffff  0xffff  0xffff  0xffff

# pass #3,x0]
(gdb) x/12xh 0x40010000
0x40010000:     0x0000  0x0000  0x0000  0x0000  0x0000  0x007b  0xffff  0xffff
0x40010010:     0xffff  0xffff  0xffff  0xffff

# pass #4,x0]
0x40010000:     0x0000  0x0000  0x0000  0x0000  0x0000  0x0000  0x007b  0xffff
0x40010010:     0xffff  0xffff  0xffff  0xffff

# pass #5,x0]
(gdb) x/12xh 0x40010000
0x40010000:     0x0000  0x0000  0x0000  0x0000  0x0000  0x0000  0x0000  0x007b
0x40010010:     0xffff  0xffff  0xffff  0xffff

# after ldr x2,[sp]:
(gdb) p/x $sp
$2 = 0x40010000
(gdb) p/x $x2
$3 = 0x0
(gdb) 

如果您没有通过注释掉 x1 来移动 lsl x1,x1,#48 中的值,您的程序将可以运行:

# after ldr x2,[sp]
(gdb) x/12xh 0x40010000
0x40010000:     0x007b  0x007b  0x007b  0x007b  0x007b  0x0000  0x0000  0x0000
0x40010010:     0x0000  0x0000  0x0000  0x0000
(gdb) p/x $x2
$1 = 0x7b007b007b007b
(gdb) 

话虽如此,使用 strh 指令可能会更好,这样您就可以避免在循环的每次迭代中写入比应有的更多字节,即 16 而不是 2。

底线,在小端系统上,常量0x0000000000007b将在内存中(升序地址)存储为7b 00 00 00 00 00 00 00,常量0x7b00000000000000将存储为{{ 1}}。

由于您所做的移位,您将 00 00 00 00 00 00 00 7b 而不是 0x7b00000000000000 存储到内存中。

,

“有趣”的方式:

AArch64 可以有效地将重复模式(任何 2 的幂长度)放入 64 位寄存器中,如果所有设置位在每个重复中都是连续的。 (这就是它如何为 orr x1,xzr,#0x0303030303030303 = mov x1,#... 等按位布尔指令编码立即数)。这几乎适用于您的 123 = 0x7b = 0b1111011

或者,ldr x1,=0x007B007B007B007B 会要求汇编器为您完成;在这种情况下,GAS 选择将常量放在附近的内存中,并以相对于 PC 的寻址模式加载它。

您可以在将数组存储到堆栈的同时为数组保留空间,方法是使用具有回写寻址模式的存储来更新基址寄存器 (sp在这种情况下)与您减去的偏移量。这就是 AArch64 如何有效地实现堆栈“推送”操作。例如在需要保存一些寄存器的函数中,GCC 在函数入口使用 stp x29,x30,-32]! 从 SP 中减去 32 个字节,并将该对寄存器存储在该空间的底部。 (Godbolt example)

所以我认为这应该有效。这确实组装,但我还没有尝试运行它。 AArch64 的标准调用约定保持 16 字节堆栈对齐,因此此存储对 16 字节存储对齐。

mov     x0,#0x7f007f007f007f
and     x0,x0,#~0x0004000400040004    // construct 0x007B repeating
stp     x0,-16]!              // push x0 twice

// SP now points at 8 copies of (uint16_t)123,below whatever was on the stack before

带有 strh 的循环(存储 16 位半字)适用于无聊的编译器;手写时,尽量用尽可能少的指令完成尽可能多的工作。 (这是一个一般经验法则,并不总是与性能相关!例如,如果存储是最近的,那么仅与先前存储部分重叠的宽负载可能会导致存储转发停顿。