gcc AVR G++:执行超过128 Kb ROM边界的函数

disho6za  于 2022-11-12  发布在  其他
关注(0)|答案(2)|浏览(161)

AVR g++的指针大小为16位。然而,我的特定芯片(ATMega 2560)具有256 KB的RAM。为了支持这一点,编译器会在与当前执行代码相同的ROM区域中自动生成trampoline区域,该区域包含跳转到高内存或跳转回高内存的扩展汇编代码。为了生成trampoline,必须获取位于高内存中的某个对象的地址。
在我的场景中,我编写了一个引导加载程序,它位于高内存中。应用程序代码需要能够调用引导加载程序中的一个函数。我知道这个函数的地址,并且需要能够通过在代码中硬编码地址来直接寻址它。
如何让编译器/链接器为任意地址生成适当的蹦床?

c6ubokkw

c6ubokkw1#

编译器和链接器只会在远地址是一个 * 符号 * 地址而不是代码中已经存在的文字常数时生成trampoline代码。类似于(假设您要跳转到的地址是0x20000)。

extern void (*farfun)() = 0x20000;

farfun ();

肯定不会起作用,它不会导致链接器做任何事情,因为地址已经解析。
您应该能够在链接器命令行中注入符号地址,如下所示:

extern void farfun ();

farfun ();

“正常”编译并链接

-Wl,--defsym,farfun=0x20000

我认为很明显,你需要确保自己的东西明智的坐在farfun
您很可能还需要--relax

编辑

我自己从来没试过,但也许:
您可能会尝试将函数地址存储在高内存中的表中,并像下面这样声明它:

extern void (*farfunctable [10])();

(farfunctable [0])();

并使用完全相同的链接器命令解析外部符号(现在位于0x20000的表(在引导加载程序中)需要如下所示:

extern void func1();
extern void func2();

void ((*farfunctab [10])() = {
   func1,
   func2,....
};

我建议将func1() ... func10()放在与farfunctab不同的模块中,以便使链接器知道它必须生成trampolins。

btxsgosb

btxsgosb2#

我计划放置一个调度结构体(也就是说,一个带有指向所有不同函数的函数指针的结构体)。您的解决方案运行良好,但需要提前知道所有函数的所有位置。有没有办法执行对编译时未知的远地址的函数调用?
[...]我的目标是把结构体和指向函数的指针放在一个固定的位置,这样,它将是一个需要固定地址的东西,而不是每个外部函数。
因此,您有两个应用程序,我们将其命名为 App 和 * Boot *,其中Boot提供了一些App希望使用的功能。必须解决以下问题:
1.如何从 Boot into App获取地址。
1.如何为 Boot 建立跳转表。
1.避免在应用程序尝试使用 Boot 中的代码时会崩溃的构造,例如:使用间接调用或跳转、使用静态构造函数或使用 Boot 中的静态存储。

应用程序直接使用 Boot .elf的地址

正在与-Wl,-R,boot.elf链接

一个简单的方法是通过-Wl,-R,boot.elf链接app.elf和 Boot .elf。选项-R指示链接器使用指定文件中的符号值,而不拖动任何代码。问题是没有办法指定要使用的符号,例如,这可能会导致应用程序使用引导程序中的libgcc函数。

通过-Wl,--defsym,symbol=value定义符号

通过遵循特定的命名约定,可以对定义哪些符号进行更多的控制。假设Boot中所有名称中包含“boot”的符号都应该“exported”,那么您可以只

> avr-nm -g boot.elf | grep ' T ' | awk '/boot/ { printf("--defsym %s=0x%s\n",$3,$1) }' > syms.opt

这将打印全局符号值,grep过滤掉文本部分中的符号。awk然后将类似00020102 T boot1的行转换为类似--defsym boot1=0x00020102的行,这些行被写入一个选项文件syms.opt。然后,该选项文件可以通过-Wl,@syms.opt提供给链接器。
选项文件的优点是,它比构建环境(如make)中的普通选项更容易提供:app.elf将依赖于syms.opt,而syms.opt又将依赖于boot.elf

在链接器脚本代码段中定义符号

另一种方法是在链接器脚本扩充中定义符号,您可以在链接期间通过-T syms.ld提供该符号,并且该符号将包含

"boot1"=ABSOLUTE(0x00020102);
"boot2"=...
...
INSERT AFTER .text

在装配模块中定义符号

定义符号的另一种方式是通过包含像.global boot1boot1 = 0x00020102这样的定义的汇编模块。
所有这些方法都有一个共同点,即所有符号都必须定义,否则链接器将抛出未定义符号错误。这意味着boot.elf必须可用,并且不管是只有一个符号未定义还是有几十个符号未定义。

让 Boot 提供分派表

直接使用boot.elf的问题是引入了一个直接依赖,这意味着如果 Boot 被改进或重构,那么你每次都必须重新编译App,即使接口没有改变。
一个解决方案是让 Boot 提供一个调度表,调度表的位置和布局都是提前知道的,只有当界面本身发生变化时,App才需要重新构建,只是重构Boot就不需要重新构建App。

带跳转表的汇编模块

正如下面的“崩溃”部分所解释的,由于EIND的值错误,调度表中的地址(以及间接跳转)将不起作用。因此,让我们假设我们有一个指向所需 Boot 函数的JMP表,比如在汇编模块boot-table.sx中,该表如下所示:

;;; Linker description file boot.ld locates input section .boot.table
;;; right after .vectors, hence the address of .boot_table will be
;;; text-section-start + _VECTORS_SIZE, where the latter is
;;; #define'd in <avr/io.h>.

;;; No "x" section flag so that the linker won't relax JMPs to RJMPs.
.section .boot.table,"a",@progbits

.global .boot_table
.type .boot_table,@object
boot_table: 
    jmp boot1
    jmp boot2
.size boot_table, .-boot_table

在这个例子中,我们将把跳转表定位在.vectors之后,这样就可以提前知道它的位置。

--defsym boot1=0x20000+vectors_size+0*4
--defsym boot2=0x20000+vectors_size+1*4

如果 Boot 位于0x 20000,则可以在C/C++模块中定义符号vectors_size,此处通过滥用avr-gcc属性“address”:

#include <avr/io.h>

__attribute__((__address__(_VECTORS_SIZE)))
char vectors_size;

查找跳转表

为了找到输入部分.boot.table,我们需要一个自己的链接器描述文件,你可能已经在引导时使用了它。我们从avr-gcc安装在./avr/lib/ldscripts/avr6.xn的链接器脚本开始,将其复制到boot.ld,并在vectors后面添加以下两行:

...
  .text   :
  {
    *(.vectors)
    KEEP(*(.vectors))

    *(.boot.table)
    KEEP(*(.boot.table))

    /* For data that needs to reside in the lower 64k of progmem.  */
    *(.progmem.gcc*)
    ...

自动生成 Boot 跳转表模块和应用程序符号

最好有一个应用程序和 Boot 都使用的接口描述,比如common.h。此外,为了保持引导程序的boot-table.sx和应用程序的syms.opt与接口同步,从common.h自动生成这两个文件是一个好主意。为此,假设common.h为:

#ifndef COMMON_H
#define COMMON_H

#define EX __attribute__((__used__,__externally_visible__))

EX int boot1 /* @boot_table:0 */ (int);
EX int boot2 /* @boot_table:1 */ (void);

#endif /* COMMON_H */

为了简单起见,我们假设这是C代码或者接口是extern "C",这样源代码中的符号就与程序集名称相匹配,而不需要使用损坏的名称。使用魔术注解从common.h生成boot-table.sxsyms.opt非常容易。魔术注解紧跟在符号之后。因此正则表达式将检索magic注解左边的标记,类似于Python:

# ... symbol /* @boot_table:index */...
pat = re.compile (r".*(\b\w+\b)\s*/\* @boot_table:(\d+) \*/.*")

for line in sys.stdin.readlines():
    match = re.match (pat, line)
    if match:
        index = int (match.group(2))
        symbol = match.group(1)

syms.opt的输出模板如下所示:

asm_line = "--defsym {symbol}=0x20000+vectors_size+4*{index}\n"

将崩溃的代码

从应用程序使用 Boot 代码受到以下限制:

间接调用和跳转

由于应用程序和引导程序的起始地址位于闪存的不同128 KiB段中,因此这些程序会崩溃。当获取代码符号的地址时,编译器根据gs(symbol)执行此操作,如果目标地址在蹦床所在的128 KiB段之外,则gs(symbol)指示链接器生成一个存根并将gs()解析为.trampolines中的该存根。gs()的解释可以在this answer中找到,但是还有更多的内容:启动代码将有效地初始化

EIND = __vectors >> 17;

请参阅gcrt1.S,启动代码crt<device>.o的AVR-LibC位。编译器假定EIND在执行过程中从不更改,请参阅GCC文档中的EIND和超过128 KiB的闪存。
这意味着 Boot 中的代码假定EIND = 1,但使用EIND = 0调用,因此EICALLEIJMP将指向错误的地址。这意味着公共代码必须避免间接调用和跳转,并且应该使用-fno-jump-tables编译,这样switch/case就不会生成这样的表。
这也意味着,如果上面描述的调度表只保存gs(symbol)条目,则它将不工作,因为应用程序和 Boot 将在EIND上不一致。

静态存储中的数据

如果常用的 Boot 代码使用静态存储器中的数据,这些数据可能会与App的静态存储器发生冲突。一种解决方法是避免Boot各部分的静态存储,并通过各函数的指针参数将地址传递给(比如)某个数据缓冲区。
可以具有完全分离的RAM区域;一个用于 Boot ,一个用于应用程序,但这会浪费RAM,因为应用程序永远不会同时运行。

静态构造函数

如果应用程序使用来自启动的代码,则将绕过 Boot 的静态构造函数。这包括:

  • Boot 中显式或隐式生成此类构造函数的C++代码。
  • Boot 中依赖于__attribute__((__constructor__))的C/C++代码或.initN部分中应在main之前运行的代码。
  • 初始化静态存储、EIND等的启动代码,也可通过在某些.initN节中定位它来运行,但如果应用程序调用 Boot 代码,则将被绕过。

相关问题