AVR g++的指针大小为16位。然而,我的特定芯片(ATMega 2560)具有256 KB的RAM。为了支持这一点,编译器会在与当前执行代码相同的ROM区域中自动生成trampoline区域,该区域包含跳转到高内存或跳转回高内存的扩展汇编代码。为了生成trampoline,必须获取位于高内存中的某个对象的地址。
在我的场景中,我编写了一个引导加载程序,它位于高内存中。应用程序代码需要能够调用引导加载程序中的一个函数。我知道这个函数的地址,并且需要能够通过在代码中硬编码地址来直接寻址它。
如何让编译器/链接器为任意地址生成适当的蹦床?
2条答案
按热度按时间c6ubokkw1#
编译器和链接器只会在远地址是一个 * 符号 * 地址而不是代码中已经存在的文字常数时生成trampoline代码。类似于(假设您要跳转到的地址是0x20000)。
肯定不会起作用,它不会导致链接器做任何事情,因为地址已经解析。
您应该能够在链接器命令行中注入符号地址,如下所示:
“正常”编译并链接
我认为很明显,你需要确保自己的东西明智的坐在
farfun
。您很可能还需要
--relax
。编辑
我自己从来没试过,但也许:
您可能会尝试将函数地址存储在高内存中的表中,并像下面这样声明它:
并使用完全相同的链接器命令解析外部符号(现在位于0x20000的表(在引导加载程序中)需要如下所示:
我建议将
func1() ... func10()
放在与farfunctab
不同的模块中,以便使链接器知道它必须生成trampolins。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”,那么您可以只
这将打印全局符号值,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
提供该符号,并且该符号将包含在装配模块中定义符号
定义符号的另一种方式是通过包含像
.global boot1
和boot1 = 0x00020102
这样的定义的汇编模块。所有这些方法都有一个共同点,即所有符号都必须定义,否则链接器将抛出未定义符号错误。这意味着
boot.elf
必须可用,并且不管是只有一个符号未定义还是有几十个符号未定义。让 Boot 提供分派表
直接使用
boot.elf
的问题是引入了一个直接依赖,这意味着如果 Boot 被改进或重构,那么你每次都必须重新编译App,即使接口没有改变。一个解决方案是让 Boot 提供一个调度表,调度表的位置和布局都是提前知道的,只有当界面本身发生变化时,App才需要重新构建,只是重构Boot就不需要重新构建App。
带跳转表的汇编模块
正如下面的“崩溃”部分所解释的,由于EIND的值错误,调度表中的地址(以及间接跳转)将不起作用。因此,让我们假设我们有一个指向所需 Boot 函数的JMP表,比如在汇编模块
boot-table.sx
中,该表如下所示:在这个例子中,我们将把跳转表定位在
.vectors
之后,这样就可以提前知道它的位置。如果 Boot 位于0x 20000,则可以在C/C++模块中定义符号
vectors_size
,此处通过滥用avr-gcc属性“address”:查找跳转表
为了找到输入部分
.boot.table
,我们需要一个自己的链接器描述文件,你可能已经在引导时使用了它。我们从avr-gcc安装在./avr/lib/ldscripts/avr6.xn
的链接器脚本开始,将其复制到boot.ld
,并在vectors后面添加以下两行:自动生成 Boot 跳转表模块和应用程序符号
最好有一个应用程序和 Boot 都使用的接口描述,比如
common.h
。此外,为了保持引导程序的boot-table.sx
和应用程序的syms.opt
与接口同步,从common.h
自动生成这两个文件是一个好主意。为此,假设common.h
为:为了简单起见,我们假设这是C代码或者接口是
extern "C"
,这样源代码中的符号就与程序集名称相匹配,而不需要使用损坏的名称。使用魔术注解从common.h
生成boot-table.sx
和syms.opt
非常容易。魔术注解紧跟在符号之后。因此正则表达式将检索magic注解左边的标记,类似于Python:syms.opt
的输出模板如下所示:将崩溃的代码
从应用程序使用 Boot 代码受到以下限制:
间接调用和跳转
由于应用程序和引导程序的起始地址位于闪存的不同128 KiB段中,因此这些程序会崩溃。当获取代码符号的地址时,编译器根据
gs(symbol)
执行此操作,如果目标地址在蹦床所在的128 KiB段之外,则gs(symbol)
指示链接器生成一个存根并将gs()
解析为.trampolines
中的该存根。gs()
的解释可以在this answer中找到,但是还有更多的内容:启动代码将有效地初始化请参阅gcrt1.S,启动代码
crt<device>.o
的AVR-LibC位。编译器假定EIND
在执行过程中从不更改,请参阅GCC文档中的EIND和超过128 KiB的闪存。这意味着 Boot 中的代码假定
EIND = 1
,但使用EIND = 0
调用,因此EICALL
和EIJMP
将指向错误的地址。这意味着公共代码必须避免间接调用和跳转,并且应该使用-fno-jump-tables
编译,这样switch/case就不会生成这样的表。这也意味着,如果上面描述的调度表只保存
gs(symbol)
条目,则它将不工作,因为应用程序和 Boot 将在EIND
上不一致。静态存储中的数据
如果常用的 Boot 代码使用静态存储器中的数据,这些数据可能会与App的静态存储器发生冲突。一种解决方法是避免Boot各部分的静态存储,并通过各函数的指针参数将地址传递给(比如)某个数据缓冲区。
可以具有完全分离的RAM区域;一个用于 Boot ,一个用于应用程序,但这会浪费RAM,因为应用程序永远不会同时运行。
静态构造函数
如果应用程序使用来自启动的代码,则将绕过 Boot 的静态构造函数。这包括:
__attribute__((__constructor__))
的C/C++代码或.initN
部分中应在main之前运行的代码。EIND
等的启动代码,也可通过在某些.initN
节中定位它来运行,但如果应用程序调用 Boot 代码,则将被绕过。