-2

我正在写一个函数:

void callFunctionAt(uint32_t address){
  //There is a void at address, how do I run it?
}

这是在 Atmel Studio 的 C++ 中。如果要相信前面的问题,简单的答案是写“address();”行。这不可能是正确的。如果不更改此函数的标头,如何调用位于给定地址的函数?

对于所有支持标准 c++ 编译的微控制器,答案应该与系统无关。

4

3 回答 3

4

执行此操作的常用方法是为参数提供正确的类型。然后您可以立即调用它:

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);

链接器中的定义可以保持原样,因为链接器对类型一无所知。

于 2020-02-06T06:59:35.667 回答
3

没有通用的方法。这取决于您使用的编译器。在下文中,我将假设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 的设备上使用标量地址通常不起作用,例如当您callFunctionAt (0x30000)在 ATmega2560 上发布时。

Z在此类设备上,指令使用的寄存器中的 16 位地址由特殊功能寄存器EICALL中保存的值扩展,输入后不得更改。avr-g++ 文档对此很清楚EINDEINDmain

这里的关键点是您如何获取地址。首先,为了正确调用和传递它,请使用函数指针:

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 设备。

于 2020-02-06T10:14:33.640 回答
0

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

于 2020-02-06T06:18:04.927 回答