调用传递给gcc内联汇编程序(avr-gcc)的常量函数地址

vnjpjtjt  于 2022-11-12  发布在  其他
关注(0)|答案(3)|浏览(185)

我正在为AVR编写一个RPC库,需要将一个函数地址传递给一些内联汇编代码,并从汇编代码中调用该函数。然而,当我试图直接调用该函数时,汇编程序会抱怨。
这个最小的例子test.cpp说明了这个问题(在实际情况中,我传递的是args,函数是模板化类的静态成员的示例化):

void bar () {
    return;
}

void foo() {
    asm volatile (
        "call %0" "\n"
        :
        : "p" (bar)
    );
}

使用avr-gcc -S test.cpp -o test.S -mmcu=atmega328p编译可以正常工作,但是当我尝试使用avr-gcc -c test.S -o test.o -mmcu=atmega328p avr-as进行汇编时,它会抱怨:

test.c: Assembler messages:
test.c:38: Error: garbage at end of line

我不知道它为什么写“test. c”,它引用的文件是test.S,它在第38行包含以下内容:

call gs(_Z3barv)

我已经尝试了所有甚至是远程合理的约束参数,以内联汇编程序,我可以找到这里,但没有一个我尝试的工作。
我想如果移除gs()部分,一切都应该正常,但是所有的约束似乎都添加了它。我不知道它做了什么。
奇怪的是,像这样执行间接调用可以很好地进行组装:

void bar () {
    return;
}

void foo() {
    asm volatile (
        "ldi r30, lo8(%0)" "\n"
        "ldi r31, hi8(%0)" "\n"
        "icall" "\n"
        :
        : "p" (bar)
    );
}

产生的汇编器如下所示:

ldi r30, lo8(gs(_Z3barv))
ldi r31, hi8(gs(_Z3barv))
icall

而且avr-as不抱怨任何垃圾。

zf9nrax1

zf9nrax11#

该代码存在以下几个问题:

问题1:错误的约束

调用目标的正确约束是"i",因此在链接时已知。

问题2:%打印修改量错误

为了打印一个适合调用的地址,使用%x,它将打印一个没有gs()的普通符号。通过gs()在这个地方生成一个链接器存根是无效的语法,因此 “行末垃圾”。除此之外,因为你是 * 直接 * 调用bar,不需要链接器存根(至少对于这种符号用法来说不需要)。

问题3:call指令可能不可用

为了区分设备是支持call还是只支持rcall,如果只有rcall可用,则存在%~,它打印单个r,如果call可用,则不打印任何内容。

问题4:调用可能会损坏寄存器或产生其他副作用

调用不可能对寄存器或内存没有任何影响。如果您对内联asm的描述与代码的某些副作用不匹配,那么您很可能迟早会得到错误的代码。
"把所有的一切都放在一起"
我们假设你有一个用汇编语言编写的函数bar,它在R22和R26中接受两个16位操作数,并在R22中计算结果。这个函数不遵守avr-gcc C/C++调用约定,所以内联汇编是一种与这样的函数接口的方法。对于bar,我们无论如何都不能编写正确的原型。因此,我们只提供一个原型,以便使用符号bar。寄存器X具有约束"x",但R22没有自己的寄存器约束,因此,我们必须使用本地asm寄存器:

extern "C" void bar (...);

int call_bar (int x, int y)
{
    register int r22 __asm ("r22") = x;
    __asm ("%~call %x2"
           : "+r" (r22)
           : "x" (y), "i" (bar));
    return r22;
}

生成的ATmega 32+优化代码:

_Z8call_barii:
    movw r26,r22
    movw r22,r24
    call bar
    movw r24,r22
    ret

那么,“生成存根”gs()是什么?

假设C/C++代码正在获取一个函数的地址。唯一明智的做法是调用该函数,通常是间接调用。现在,间接调用最多可以指向64 KiW = 128 KiB,因此在代码内存大于128 KiB的设备上,必须采用特殊的方法间接调用128 KiB边界之外的函数。AVR硬件具有一个名为EIND的SFR,用于此目的。但是使用它的问题是显而易见的,您必须在调用之前设置它,然后在某个地方以某种方式重新设置它;所有邪恶的东西都是必要的。
AVR-GCC采用不同的方法:对于每个这样的地址,编译器生成gs(func)。如果地址在128 KiB范围内,则将其解析为func。如果不在128 KiB范围内,则将gs()解析为.trampolines部分中的地址,该部分位于闪存的开头附近。.trampolines包含指向128 KiB范围以外的目标的直接JMP列表。
以下面的C代码为例:

extern int far_func (void);

int main (void)
{
    int (*pfunc)(void) = far_func;
    __asm ("" : "+r" (pfunc)); /* Forget content of pfunc. */
    return pfunc();
}

__asm用于防止编译器将间接调用优化为直接调用。

> avr-gcc main.c -o main.elf -mmcu=atmega2560 -save-temps -Os -Wl,--defsym,far_func=0x24680
> avr-objdump -d main.elf > main.lst

为了简洁起见,我们只在每个命令行定义了符号far_funcmain.s中的程序集转储显示far_func可能需要一个链接器存根:

main:
    ldi r30,lo8(gs(far_func))
    ldi r31,hi8(gs(far_func))
    eijmp

main.lst中的最终可执行文件清单显示存根实际上已生成并使用:

main.elf:     file format elf32-avr

Disassembly of section .text:
...

000000e4 <__trampolines_start>:
  e4:   0d 94 40 23     jmp 0x24680 ; 0x24680 <far_func>

...

00000104 <main>:
 104:   e2 e7           ldi r30, 0x72   ; 114
 106:   f0 e0           ldi r31, 0x00   ; 0
 108:   19 94           eijmp

主加载Z= 0x 0072,其是字节地址0x 00 e4的字地址,即代码间接跳到0x 00 e4,并且从那里直接跳到0x 24680。

9cbw7uwe

9cbw7uwe2#

注意call需要一个常量,在链接时已知的值。它还允许来自变量的指针(例如char* x),而call不能处理。(我记得有时gcc很聪明,可以通过这种方式进行优化,使“p”在这里可以工作--但这基本上是未记录的行为,并且是不确定的,所以最好不要指望它。)
如果你调用的函数实际上是编译时常量,你可以使用"i" (bar);如果不是,你就别无选择,只能使用icall,就像你已经知道的那样。
顺便说一句,www.example.com的AVR部分https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html#Machine-Constraints文档更多,AVR特定的约束。

gojuced7

gojuced73#

我尝试过各种方法将C函数名传递给内联ASM代码,但都没有成功。不过我确实找到了一个解决方法,* 似乎 * 可以提供所需的结果。

问题答案:

https://www.nongnu.org/avr-libc/user-manual/inline_asm.html中所述,您可以在原型声明中将ASM名称分配给C函数:

void bar (void) asm ("ASM_BAR");    // any name possible here
void bar (void)
{
    return;
}

然后,您可以轻松地从ASM代码中调用该函数:

asm volatile("call ASM_BAR");

用于库函数:

这种方法不适用于库函数,因为它们有自己的原型声明。要从ISR中更有效地调用time.h库中的system_tick()这样的函数,您可以声明一个帮助函数。不幸的是,GCC不将内联设置应用于ASM代码的调用。

inline void asm_system_tick(void) asm ("ASM_SYSTEM_TICK") __attribute__((always_inline));
void asm_system_tick(void)
{
    system_tick();
}

在下面的例子中,GCC只为周围的代码生成push/ pop指令,而不为函数调用生成push/ pop指令!注意,system_tick()是专门为ISR_NAKED设计的,它自己完成所有需要的堆栈操作。

volatile uint8_t tick = 0;
ISR(TIMER2_OVF_vect)
{
    tick++;
    if (tick > 127)
    {
        tick = 0;
        asm volatile ("call ASM_SYSTEM_TICK");
    }
}

因为inline属性不起作用,所以每个函数调用需要额外的8个CPU周期。与使用普通函数调用进行推/拉操作所需的5632个CPU周期(每次运行ISR需要44个CPU周期)相比,这仍然是一个非常令人印象深刻的改进。

相关问题