将内联汇编代码插入 C 函数 - I/O 问题

us3*_*und 1 c arm bare-metal inline-assembly cortex-m

我正在使用 GNU arm-none-eabi 工具链为我的 Cortex M3 微控制器开发嵌入式 C 应用程序。

我计划采用供应商在我的 C 应用程序中实现的汇编子例程。我计划创建一个新的 C 函数,然后在其中使用扩展内联汇编协议编写内联汇编块。在这篇文章中,我计划将此汇编子例程视为黑匣子,并计划向此论坛询问如何构建输入和破坏列表;该例程没有输出。

汇编子例程期望在调用之前预先设置 r0、r1 和 r2。此外,子例程使用寄存器 r4、r5、r6、r7、r8、r9 作为临时寄存器来执行其功能。它写入设备上的一系列内存,由 r0 和 r1 指定,分别是起始地址和停止地址。

所以,我正在检查我的假设是否正确。我的问题如下。

  1. 我认为我应该写的函数是这样的吗?:
void my_asm_ported_func(int reg_r0, int reg_r1, int reg_r2 {
    __asm__ __volatile__ (
        "ldr r0, %0        \n\t",
        "ldr r1, %1        \n\t",
        "ldr r2, %2        \n\t",
        "<vendor code...>  ",
        :  /* no outputs */
        : "r" (reg_r0), "r" (reg_r1), "r" (reg_r2) /* inputs */
        : "r0", "r1", "r2", "r4", "r5", "r6", 
          "r7", "r8", "r9", "memory" /* clobbers */
    );
}
Run Code Online (Sandbox Code Playgroud)
  1. 由于此 asm 子例程写入设备上的一系列其他内存,因此将“内存”添加到 clobber 列表中是否足够?看起来太简单了。

  2. 是否有更优雅的方法从周围 C 函数的输入参数中输入 r0 - r2 ?我从 AAPCS 了解到,寄存器 r0-r3 是输入参数 1-4,因此像我在输入列表中那样手动提供 r0-r2 输入似乎是多余的。我是否应该以某种方式将其作为单独的 .S 文件中的纯汇编函数?

先感谢您。

我尝试了上述方法,但使用基本的内联汇编协议却产生了可怕的结果 - 它崩溃了。我这样做是因为我认为汇编块自然会通过函数序言获取 r0-r2 ,它显然是这样做的,因为它正确地写入了内存,但是一旦我在 asm 块开头的断点被启动(我的vs 代码扩展没有分步反汇编视图,因此它只是将其作为块框运行,然后崩溃了)。我还没有尝试过扩展,我已经做了很多阅读,所以我只是想确保我的黑匣子方法应该有效,并且我不会错过任何太大的东西。

Pet*_*des 5

是的,volatile asm带有"memory"clobber 的语句对于 MMIO(或者几乎任何支持的东西)来说都很好:编译器将确保它生成的 asm 的内存内容在语句之前与 C 抽象机同步asm,并且会假设任何全局的- 可达内存已更改。请参阅如何指示可以使用内联 ASM 参数*指向*的内存?更深入地解释为什么当指向的内存是 C 变量时这很重要,您还可以在内联汇编之外访问这些变量,而不仅仅是 MMIO 寄存器。


寄存器

为了避免浪费指令,请告诉编译器您想要输入哪些寄存器,或者更好地让编译器选择并更改要使用的“供应商代码”%0而不是硬寄存器r0

ldr r0, r0填写ldr r0, %0模板字符串要么无效,要么将源r0视为符号名称。无论哪种方式都不会将函数 arg 放入r0,因为您强制编译器将其放在不同的寄存器中(通过在 上声明一个 clobber "r0")。如果您确实想在寄存器之间进行复制,则 ARM 指令是mov。但如果这是模板字符串的第一条指令asm,通常意味着您做错了,应该使用更好的约束来告诉编译器您想要什么。

// Worse way, but can use a template string with hard-coded registers unchanged

void my_asm_ported_func(int a, int b, int c)
{
    register int reg_r0 asm ("r0") = a;  // forces "r" to pick r0 for an asm template
    register int reg_r1 asm ("r1") = b;  // no other *guaranteed* effects.
    register int reg_r2 asm ("r2") = c;

    __asm__ __volatile__ (
                 // no extra mov or load instructions
        "<vendor code...>  "   // still unchanged

        : "+r" (reg_r0), "+r" (reg_r1), "+r" (reg_r2) // read-write outputs
        : // no pure inputs
        : "r4", "r5", "r6", 
          "r7", "r8", "r9", "memory" // clobbers
    );
}
Run Code Online (Sandbox Code Playgroud)

最好的办法

void my_asm_ported_func(int reg_r0, int reg_r1, int reg_r2) {

    __asm__ __volatile__ (
         // no extra mov or load instructions.
        "<vendor code changed to use %0 instead of r0, etc...>  "

        : "+r" (reg_r0), "+r" (reg_r1), "+r" (reg_r2) // read-write outputs
        : // no pure inputs
        : "r4", "r5", "r6", 
          "r7", "r8", "r9", "memory" // clobbers.  Not including r3??
    );

    // the C variables reg_r0 and so on have modified values here
    // but they're local to this function so no effect outside of this
}
Run Code Online (Sandbox Code Playgroud)

"r4"实际上,进一步的改进是将像through这样的寄存器破坏者替换"r9""=r"(dummy1)输出操作数,让编译器选择要破坏的寄存器。

我很惊讶模板字符串不使用r3. 如果确实如此,您忘记告诉编译器这一点,这是未定义的行为,当该函数内联时,它会咬住您。你提到了崩溃;ldr如果你不是的话,这可能就是原因。

在“供应商代码”中使用%0notr0将使编译器填写它选择的寄存器名称。通常,它会选择r0值已经存在的 C 变量,除非函数内联且该值位于不同的寄存器中。

我假设 asm 模板修改了该寄存器,这就是为什么我将它作为输入/输出操作数"+r"(reg_r0),输出端基本上是一个虚拟的,让编译器知道寄存器已更改。您不能在寄存器上声明一个同时也是操作数的破坏者,如果您让编译器选择寄存器,您甚至不知道是哪一个。

如果模板未修改任何输入寄存器asm,请将它们设为纯输入。您可以[name] "r"(c_var)在操作数和%[name]模板字符串中使用名称而不是数字,从而可以轻松移动它们,而无需重新编号和跟踪哪个操作数是哪个数字。

也可以看看


单独的.S文件:

我是否应该以某种方式将其作为单独的 .S 文件中的纯汇编函数?

这是 100% 有效的选项,特别是当 call/ret 开销与所需时间相比较小,或者不是一直被调用时。

gcc -S如果您不确定声明函数的语法(.globl foofoo:要定义符号,请将其机器代码放在它后面。),请查看编译器生成的 asm ( )。当然,push还有pop您的函数使用的任何调用保留寄存器。

(GNU C 内联汇编要求您向编译器精确描述汇编;函数调用约定是无关紧要的,因为它是内联的 asm。您正在与编译器共舞,不需要踩到它的脚趾,而不是仅仅遵循标准调用习俗。)