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



AVR g++ 的指针大小为 16 位。但是,我的特定芯片(ATMega2560)具有256 KB的RAM。为了支持这一点,编译器在与当前执行代码相同的ROM部分中自动生成蹦床部分,然后包含扩展汇编代码以跳转到高内存或返回。为了生成蹦床,您必须获取位于高内存中的内容的地址。

在我的场景中,我有一个我编写的引导加载程序位于高内存中。应用程序代码需要能够在引导加载程序中调用函数。我知道这个函数的地址,需要能够通过在我的代码中硬编码地址来直接寻址它。

如何让编译器/链接器为任意地址生成适当的蹦床?

编译器和链接器仅在远地址是符号地址而不是代码中已有的文字常量时生成蹦床代码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不同的模块中,以使链接器知道它必须生成蹦床。

我计划放置一个调度结构(即,具有指向所有各种函数的函数指针的结构)。您的解决方案运行良好,但需要提前了解所有功能的所有位置。有没有办法对编译时未知的远地址执行函数调用?
[...]我的目标是将带有指向函数的指针的结构放在固定位置。这样,它将是一个需要固定地址而不是每个外部函数的单一事物。

所以你有两个应用程序,我们称它们为App和Boot,其中Boot提供了App想要使用的一些功能。 必须解决以下问题:

  1. 如何从启动到应用程序获取地址。
  2. 如何为引导构建跳转表。
  3. 避免在应用尝试使用启动中的代码时会崩溃的构造,例如:使用间接调用或跳转、使用静态构造函数或在 Boot 中使用静态存储。

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

-Wl,-R,boot.elf链接

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

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

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

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

这将打印全局符号值,grep 过滤掉文本部分中的符号。 然后,awk 将类似00020102 T boot1的行转换为类似 写入选项文件syms.opt--defsym boot1=0x00020102。 然后,可以通过-Wl,@syms.opt将选项文件提供给链接器。

选项文件的优点是,在像make这样的构建环境中,它比普通选项更容易提供:app.elf将取决于(除其他外)syms.opt,而又取决于boot.elf

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

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

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

在组件模块中定义符号

定义符号的另一种方法是通过一个组装模块,该模块包含.global boot1boot1 = 0x00020102等定义。

所有这些方法的共同点是必须定义所有符号,否则链接器将引发未定义的符号错误。 这意味着boot.elf必须可用,并且是否只有一个符号未定义或符号的 dozends 是否未定义并不重要。

让引导提供调度表

直接使用boot.elf的问题(如上一节所述)在于它引入了直接依赖关系。 这意味着,如果 Boot 得到改进或重构,那么即使界面没有更改,您每次也必须重新编译 App。

一种解决方案是让 Boot 提供一个调度表,其位置和布局是提前知道的。只有当界面本身发生变化时,才必须重建应用程序。 只需重构启动就不需要重新构建应用。

带有跳转台的装配模块

如下面的"崩溃"部分所述,调度表中的地址(以及间接跳转)将不起作用,因为 EIND 的值错误。 因此,假设我们有一个所需引导函数的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之后立即定位跳转表,以便提前知道它的位置。 然后,应用程序syms.opt中的相应符号定义将读取

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

前提是引导位于0x20000。 符号vectors_size可以在 C/C++ 模块中定义,这里通过滥用 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,并在向量后添加以下 2 行:

...
.text   :
{
*(.vectors)
KEEP(*(.vectors))
*(.boot.table)
KEEP(*(.boot.table))
/* For data that needs to reside in the lower 64k of progmem.  */
*(.progmem.gcc*)
...

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

强烈建议应用程序和启动都使用接口描述,例如common.h。此外,为了使Boot的boot-table.sx和App的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非常简单。魔术注释紧跟在符号之后,因此正则表达式将检索魔术注释留下的标记,类似于 Python:

# ... symbol /* @boot_table:index */...
pat = re.compile (r".*(bw+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"

将崩溃的代码

使用来自应用程序的启动代码受到以下几个限制:

间接呼叫和跳转

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

EIND = __vectors >> 17;

参见GCRT1。S,启动代码的AVR-LibC位crt<device>.o。 编译器假设EIND执行过程中永远不会更改,请参阅 GCC 文档中的 EIND 和超过 128KiB 的闪存。

这意味着 Boot 中的代码假定EIND = 1但调用EIND = 0,因此EICALLresp。EIJMP将定位错误的地址。 这意味着公共代码必须避免间接调用和跳转,并且应该使用-fno-jump-tables进行编译,以便 switch/case 不会生成此类表。

这也意味着,如果上面描述的调度表只保存gs(symbol)条目,它将不起作用,因为应用程序和启动将在EIND上存在分歧。

静态存储中的数据

如果常见的启动代码使用静态存储中的数据,则数据可能会与应用的静态存储冲突。 一种出路是避免在 Boot 的各个部分进行静态存储,并通过相应函数的指针将地址传递给一些数据缓冲区。

一个可以有完全独立的RAM区域;一个用于启动,一个用于App,但这将是对RAM的浪费,因为应用程序永远不会同时运行。

静态构造函数

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

  • C++ Boot 中显式或隐式生成此类构造函数的代码。

  • 引导中的 C/C++ 代码依赖于__attribute__((__constructor__))或第.initN节中的代码,这些代码应该在 main 之前运行。

  • 初始化静态存储、EIND等的启动代码,也通过在某些.initN部分中定位来运行,但如果 App 调用启动代码,则会被绕过。