ARM汇编:存储时自动增加寄存器

6
能否在使用[Rn]!进行STR操作时自动递增寄存器的基地址?我已经阅读了文档,但没有找到明确的答案,主要是因为该命令语法适用于LDR和STR两种情况。理论上来说,它们都能使用递增操作,但我没有找到任何关于存储(LDR操作正常)的自动递增示例。我编写了一个小程序,在向量中存储了两个数字。当执行完毕后,out 的内容应该是 {1, 2},但实际情况是存储覆盖了第一个字节,似乎自动递增没有起作用。
#include <stdio.h>

int main()
{
        int out[]={0, 0};
        asm volatile (
        "mov    r0, #1          \n\t"
        "str    r0, [%0]!       \n\t"
        "add    r0, r0, #1      \n\t"
        "str    r0, [%0]        \n\t"
        :: "r"(out)
        : "r0" );
        printf("%d %d\n", out[0], out[1]);
        return 0;
}

编辑: 虽然答案对于常规的加载和存储是正确的,但我发现优化器会混乱向量指令(如vldm/vstm)上的自动增量。例如,以下程序:

#include <stdio.h>

int main()
{
        volatile int *in = new int[16];
        volatile int *out = new int[16];

        for (int i=0;i<16;i++) in[i] = i;

        asm volatile (
        "vldm   %0!, {d0-d3}            \n\t"
        "vldm   %0,  {d4-d7}            \n\t"
        "vstm   %1!, {d0-d3}            \n\t"
        "vstm   %1,  {d4-d7}            \n\t"
        :: "r"(in), "r"(out)
        : "memory" );

        for (int i=0;i<16;i++) printf("%d\n", out[i]);
        return 0;
}

编译器使用
g++ -O2 -march=armv7-a -mfpu=neon main.cpp -o main

在最后8个变量的输出中会产生无意义的字符,因为优化器保留了递增的变量并将其用于printf。换句话说,out[i]实际上是out[i+8],所以打印出来的前8个值是向量的最后8个值,其余的是超出边界的内存位置。

我尝试过在代码中使用不同组合的volatile关键字,但只有在使用-O0标志编译或者使用一个易失性向量而不是指针和new时行为才会改变,例如:

volatile int out[16];

第44页,我认为它可以,但现在没有测试的方法... http://simplemachines.it/doc/arm_inst.pdf 使用!运算符 - L7ColWinters
3个回答

6
存储和加载时,您应执行以下操作:
ldr r0,[r1],#4
str r0,[r2],#4

无论你在结尾处添加什么,比如这个例子中的4,都会在寄存器被用作地址之后但指令完成之前加到基本寄存器(ldr示例中的r1和str示例中的r2)中,这非常类似于。
unsigned int a,*b,*c;
...
a = *b++;
*c++ = a;

编辑,您需要查看反汇编以了解正在发生的情况(如果有)。我正在使用最新的Sourcery CodeBench Lite工具链,来自Mentor Graphics。

arm-none-linux-gnueabi-gcc(Sourcery CodeBench Lite 2011.09-70)4.6.1

#include <stdio.h>
int main ()
{
        int out[]={0, 0};
        asm volatile (
        "mov    r0, #1          \n\t"
        "str    r0, [%0], #4       \n\t"
        "add    r0, r0, #1      \n\t"
        "str    r0, [%0]        \n\t"
        :: "r"(out)
        : "r0" );
        printf("%d %d\n", out[0], out[1]);
        return 0;
}


arm-none-linux-gnueabi-gcc str.c -O2  -o str.elf

arm-none-linux-gnueabi-objdump -D str.elf > str.list


00008380 <main>:
    8380:   e92d4010    push    {r4, lr}
    8384:   e3a04000    mov r4, #0
    8388:   e24dd008    sub sp, sp, #8
    838c:   e58d4000    str r4, [sp]
    8390:   e58d4004    str r4, [sp, #4]
    8394:   e1a0300d    mov r3, sp
    8398:   e3a00001    mov r0, #1
    839c:   e4830004    str r0, [r3], #4
    83a0:   e2800001    add r0, r0, #1
    83a4:   e5830000    str r0, [r3]
    83a8:   e59f0014    ldr r0, [pc, #20]   ; 83c4 <main+0x44>
    83ac:   e1a01004    mov r1, r4
    83b0:   e1a02004    mov r2, r4
    83b4:   ebffffe5    bl  8350 <_init+0x20>
    83b8:   e1a00004    mov r0, r4
    83bc:   e28dd008    add sp, sp, #8
    83c0:   e8bd8010    pop {r4, pc}
    83c4:   0000854c    andeq   r8, r0, ip, asr #10

所以,这个……
sub sp, sp, #8

需要分配两个本地整数 out[0] 和 out[1]

mov r4,#0
str r4,[sp]
str r4,[sp,#4]

之所以这样做是因为它们被初始化为零,然后是内联汇编

8398:   e3a00001    mov r0, #1
839c:   e4830004    str r0, [r3], #4
83a0:   e2800001    add r0, r0, #1
83a4:   e5830000    str r0, [r3]

然后是printf:

83a8:   e59f0014    ldr r0, [pc, #20]   ; 83c4 <main+0x44>
83ac:   e1a01004    mov r1, r4
83b0:   e1a02004    mov r2, r4
83b4:   ebffffe5    bl  8350 <_init+0x20>

现在很明显为什么它不起作用了。你没有将out声明为volatile。你没有让代码回到RAM中获取out[0]和out[1]的值来进行printf,编译器知道r4同时包含out[0]和out[1]的值,这个函数中的代码非常简短,所以它不需要清空r4并重新使用它,因此它将r4用于printf。
如果你更改为volatile。
    volatile int out[]={0, 0};

那么你应该能够得到预期的结果:
83a8:   e59f0014    ldr r0, [pc, #20]   ; 83c4 <main+0x44>
83ac:   e59d1000    ldr r1, [sp]
83b0:   e59d2004    ldr r2, [sp, #4]
83b4:   ebffffe5    bl  8350 <_init+0x20>

printf的准备工作从内存中读取。


使用!符号是为了ldm/stm指令,增加或减少的数量由寄存器列表中的寄存器数量确定(指令中没有足够的空间用于常数)。ldr/str有常数的空间。 - old_timer
就此而言,您可以使用stm,stmia r0!,{r1}将r1存储在地址r0处,然后“增量后”(ia)存储。 stmia增量后,stmdb减量前,stmda减量后,stmib增量前。 - old_timer
虽然这个解决方案能够工作,但是我发现在使用 -O2 编译器标志时程序的输出是 {0, 0}。你对此有什么想法吗?我在 printf 函数中使用了这些值,在编译时也没有收到任何警告信息。 - John S
好的,我刚刚想到我可以将“memory”添加到clobber列表中,并获得与使用volatile关键字相同的结果。现在一切都好了 :) - John S
1
从我的汇编之道的根源和一点常识来看,你确定你正在针对正确的性能问题吗?问题可能是数据移动,而不是计算速度有多快。赛车队可能能够在几秒钟内更换轮胎和加油,但如果汽车最高只能达到20英里/小时,那就无所谓了,你不会注意到停车时间有多快。是否有可能将数据通过未经处理的方式传递,并查看是否是复制/I/O而不是处理的问题? - old_timer
显示剩余13条评论

0

GCC内联汇编要求所有修改的寄存器和非易失性变量都列在输出或破坏项中。在第二个示例中,GCC可能会并且确实假定分配给inout的寄存器不会改变。

正确的方法是:

out_temp = out;
asm volatile ("..." : "+r"(in), "+r"(out_temp) :: "memory" );

0

我在寻找类似问题的答案时发现了这个问题:如何绑定输入/输出寄存器。GCC内联汇编器约束的文档说,输入寄存器列表中的+前缀表示输入/输出寄存器。

在这个例子中,我认为您希望保留变量out的原始值。然而,如果您想要使用后增量(!)指令的变体,我认为您应该将参数声明为读/写。以下代码在我的树莓派2上运行良好:

#include <stdio.h>

int main()
{
  int* in = new int(16);
  volatile int* out = new int(16);

  for (int i=0; i<16; i++) in[i]=i;

  asm volatile(
    "vldm %0!, {d0-d3}\n\t"
    "vldm %0, {d4-d7}\n\t"
    "vstm %1!, {d0-d3}\n\t"
    "vstm %1, {d4-d7}\n\t"
    :"+r"(in), "+r"(out) :: "memory");

  for (int i=0; i<16; i++) printf("%d\n", out[i-8]);
  return 0;
}

这样做,代码的语义对编译器来说是清晰的: inout 指针都会被改变(增加8个元素)。

免责声明:我不知道ARM ABI是否允许函数自由破坏NEON寄存器d0到d7。在这个简单的例子中,这可能无关紧要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接