如何从 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 内核。 我很困惑……它同时被标记为avrsam,所以问题同时针对两者?【参考方案3】:

基于 cmets,我认为您是在询问微控制器如何调用函数。 你能编译你的程序来查看汇编文件吗? 我建议您阅读其中之一。 编译后的每个函数都被转换为 CPU 可以执行的指令(加载到寄存器、添加到寄存器等)。 因此,您的 void foo(int x) statements; 编译为简单的 CPU 指令,并且每当您在程序中调用 foo(x) 时,您将转向与 foo 相关的指令 - 您正在调用子程序。 据我所知,AVR 中有一个CALL 函数来调用子程序,子程序的名称是执行程序跳转和在地址调用下一条指令的标签。 我想你可以通过阅读一些 AVR 汇编教程来澄清你的疑惑。 看到 CPU 在调用我编写的函数时究竟做了什么很有趣(至少对我而言),但它需要知道指令是做什么的。您使用 AVR 进行开发,因此您可以在此 PDF 中阅读 set of instructions 并与您的程序集文件进行比较。

【讨论】:

以上是关于如何从 AVR C 中的内存地址调用函数?的主要内容,如果未能解决你的问题,请参考以下文章

AVR 内存和英特尔十六进制

在C语言中,如何给函数分配内存?

C语言函数指针,敲黑白,讲重点,如何定义函数指针?

C语言 调用函数完后释放内存吗?

扫描调用进程的内存

Go 如何查看一个变量的内存地址 理解指针问题