如何从 AVR C 中的内存地址调用函数?
Posted
技术标签:
【中文标题】如何从 AVR C 中的内存地址调用函数?【英文标题】:How does one call a function from it's memory address in AVR C? 【发布时间】:2020-02-06 05:14:02 【问题描述】:我正在写一个函数:
void callFunctionAt(uint32_t address)
//There is a void at address, how do I run it?
这是在 Atmel Studio 的 C++ 中。如果要相信前面的问题,简单的答案是写“address();”行。这不可能是正确的。如果不改变这个函数的头部,如何调用位于给定地址的函数?
对于所有支持标准 c++ 编译的微控制器,答案应该与系统无关。
【问题讨论】:
我不明白你想做什么。当我阅读主题时,我坚持你问“函数存储在哪里”,但当我阅读其余部分时,我感到困惑。 @dunajski 我知道函数的存储位置。如何仅基于该位置运行它? optiboot.h 在引导加载程序中包装了对 do_spm 函数的调用。看看能不能从中提取出你想要的东西github.com/Optiboot/optiboot/blob/master/optiboot/examples/… 这是否是基于 AVR 的 MCU?请修复您的标签,或者更好地在问题中简单地说明目标 MCU,正如答案中所指出的那样,AVR 是一种特殊的野兽,充其量很难做到这一点,其他的则什么都不是。 对于所有支持标准 c++ 编译的微控制器,答案不能与系统无关,您需要删除该要求。这不是嵌入式世界的运作方式。 【参考方案1】:执行此操作的常用方法是为参数提供正确的类型。然后你可以马上调用它:
void callFunctionAt(void (*address)())
address();
然而,既然你写了“不改变这个函数的头[...]”,你需要把无符号整数转换成一个函数指针:
void callFunctionAt(uint32_t address)
void (*f)() = reinterpret_cast<void (*f)()>(address);
f();
但这不安全也不可移植,因为它假定uint32_t
可以转换为函数指针。这不一定是真的:“[...] 与所有微控制器的系统无关 [...]”。函数指针可以具有 32 位以外的其他宽度。通常,指针可能包含的不仅仅是纯地址,例如包括用于内存空间的选择器,具体取决于系统的架构。
如果您从链接描述文件中获得地址,您可能会这样声明它:
extern const uint32_t ext_func;
并且喜欢这样使用它:
callFunctionAt(ext_func);
但是你可以把声明改成:
extern void ext_func();
并直接或间接调用它:
ext_func();
callFunctionAt(&ext_func);
链接器中的定义可以保持原样,因为链接器对类型一无所知。
【讨论】:
您只是将问题移出 callFuctionAt。问题是如何使用包含地址的 uint32_t 类型的参数来调用 callFuctionAt。所以向 OP 展示如何将地址转换为函数 谢谢@Juraj,我补充了一些想法。 -- 顺便说一句,您在问题评论中的示例并没有 解决问题。它将地址 a 作为整数再向下移动一级。 这是一个非常有趣的。谢谢你的回答。很有用【参考方案2】:没有通用的方法。这取决于您使用的编译器。下面我假设avr-g++
,因为它很常见并且免费提供。
剧透:在 AVR 上,它比在大多数其他机器上更复杂。
假设您实际上有一个uint32_t
地址,它是一个字节地址。 avr-g++
中的函数指针实际上是字地址,其中一个字有 16 位。因此,您必须先将字节地址除以 2 才能获得字地址;然后将其转换为函数指针并调用它:
#include <stdint.h>
typedef void (*func_t)(void);
void callFunctionAt (uint32_t byte_address)
func_t func = (func_t) (byte_address >> 1);
func();
如果你从单词地址开始,那么你可以毫不费力地调用它:
void callFunctionAt (uint32_t address)
((func_t) word_address)();
这仅适用于闪存高达 128KiB 的设备!
原因是avr-g++
中的地址是 16 位长,参见。 void*
的布局与 avr-gcc ABI 相同。 这意味着在闪存 > 128KiB 的设备上使用标量地址通常不起作用,例如当您在 ATmega2560 上发出 callFunctionAt (0x30000)
时。
在此类设备上,EICALL
指令使用的Z
寄存器中的 16 位地址由EIND
特殊功能寄存器中保存的值扩展,您不得更改EIND
输入main
后。 avr-g++ documentation is clear about that。
这里的关键点是您如何获取地址。首先,为了正确调用和传递它,请使用函数指针:
typedef void (*func_t)(void);
void callFunctionAt (func_t address)
address();
void func (void);
void call_func()
func_t addr = func;
callFunctionAt (addr);
我在声明中使用void
参数,因为这就是你在 C 中的做法。
或者,如果你不喜欢 typedef:
void callFunctionAt (void (*address)(void))
address();
void func (void);
void call_func ()
void (*addr)(void) = func;
callFunctionAt (addr);
如果您想在特定字地址调用函数,例如 0x0
以“重置”1 µC,您可以
void call_0x0()
callFunctionAt ((func_t) 0x0);
但这是否有效取决于您的向量表所在的位置,或者更具体地说,取决于启动代码如何初始化EIND
。始终有效的是使用符号并在与以下代码链接时使用 -Wl,--defsym,func=0
定义它:
extern "C" void func();
void call_func ()
void (*addr)(void) = func;
callFunctionAt (addr);
与直接使用0x0
相比,最大的区别在于编译器会将符号func
与符号修饰符gs
包装起来,而直接使用0x0
时不会这样做:
_Z9call_funcv:
ldi r24,lo8(gs(func))
ldi r25,hi8(gs(func))
jmp _Z14callFunctionAtPFvvE
如果地址超出EIJMP
的范围,则需要这样做,以建议链接器生成存根。
1 这不会重置硬件。强制复位的最佳方法是让看门狗定时器 (WDT) 为您发出复位。
方法
还有一种情况是当你想要一个类的非静态方法的地址时,因为在这种情况下你还需要一个this
指针:
class A
int a = 1;
public:
int method1 () return a += 1;
int method2 () return a += 2;
;
void callFunctionAt (A *b, int (A::*f)())
A a;
(a.*f)();
(b->*f)();
void call_method ()
A a;
callFunctionAt (&a, &A::method1);
callFunctionAt (&a, &A::method2);
callFunctionAt
的第二个参数指定了你想要的(给定原型的)方法,但你还需要一个对象(或指向一个对象的指针)来应用它。 avr-g++
将在获取方法的地址时使用gs
(前提是不能内联以下调用),因此它也适用于所有 AVR 设备。
【讨论】:
OP 用“sam”标记了这个问题,意思是 Atmel 的 SAM 系列,它使用 ARM 内核。 我很困惑……它同时被标记为avr
和sam
,所以问题同时针对两者?【参考方案3】:
基于 cmets,我认为您是在询问微控制器如何调用函数。
你能编译你的程序来查看汇编文件吗?
我建议您阅读其中之一。
编译后的每个函数都被转换为 CPU 可以执行的指令(加载到寄存器、添加到寄存器等)。
因此,您的 void foo(int x) statements;
编译为简单的 CPU 指令,并且每当您在程序中调用 foo(x)
时,您将转向与 foo
相关的指令 - 您正在调用子程序。
据我所知,AVR 中有一个CALL 函数来调用子程序,子程序的名称是执行程序跳转和在地址调用下一条指令的标签。
我想你可以通过阅读一些 AVR 汇编教程来澄清你的疑惑。
看到 CPU 在调用我编写的函数时究竟做了什么很有趣(至少对我而言),但它需要知道指令是做什么的。您使用 AVR 进行开发,因此您可以在此 PDF 中阅读 set of instructions 并与您的程序集文件进行比较。
【讨论】:
以上是关于如何从 AVR C 中的内存地址调用函数?的主要内容,如果未能解决你的问题,请参考以下文章