我正在为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不抱怨任何垃圾。
3条答案
按热度按时间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寄存器:生成的ATmega 32+优化代码:
那么,“生成存根”
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代码为例:
__asm用于防止编译器将间接调用优化为直接调用。
为了简洁起见,我们只在每个命令行定义了符号
far_func
。main.s
中的程序集转储显示far_func
可能需要一个链接器存根:main.lst
中的最终可执行文件清单显示存根实际上已生成并使用:主加载Z= 0x 0072,其是字节地址0x 00 e4的字地址,即代码间接跳到0x 00 e4,并且从那里直接跳到0x 24680。
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特定的约束。
gojuced73#
我尝试过各种方法将C函数名传递给内联ASM代码,但都没有成功。不过我确实找到了一个解决方法,* 似乎 * 可以提供所需的结果。
问题答案:
如https://www.nongnu.org/avr-libc/user-manual/inline_asm.html中所述,您可以在原型声明中将ASM名称分配给C函数:
然后,您可以轻松地从ASM代码中调用该函数:
用于库函数:
这种方法不适用于库函数,因为它们有自己的原型声明。要从ISR中更有效地调用
time.h
库中的system_tick()
这样的函数,您可以声明一个帮助函数。不幸的是,GCC不将内联设置应用于ASM代码的调用。在下面的例子中,GCC只为周围的代码生成push/ pop指令,而不为函数调用生成push/ pop指令!注意,
system_tick()
是专门为ISR_NAKED
设计的,它自己完成所有需要的堆栈操作。因为inline属性不起作用,所以每个函数调用需要额外的8个CPU周期。与使用普通函数调用进行推/拉操作所需的5632个CPU周期(每次运行ISR需要44个CPU周期)相比,这仍然是一个非常令人印象深刻的改进。