修复了C中的地址变量

时间:2009-03-25 17:48:13

标签: c embedded

对于嵌入式应用,通常需要访问外设寄存器的固定存储器位置。我发现这样做的标准方法如下:

// access register 'foo_reg', which is located at address 0x100
#define foo_reg *(int *)0x100

foo_reg = 1;      // write to foo_reg
int x = foo_reg;  // read from foo_reg

我理解它是如何工作的,但是我不明白的是foo_reg的空间是如何分配的(即什么使链接器不能将另一个变量放在0x100?)。可以在C级别保留空间,还是必须有一个链接器选项,指定不应将任何内容放在0x100。我正在使用GNU工具(gcc,ld等),因此我最感兴趣的是该工具集的具体细节。

有关我的架构的一些其他信息,以澄清问题:

我的处理器通过一组映射到FPGA的接口,这些寄存器映射到处理器的常规数据空间(其中存在变量)。所以我需要指向那些寄存器并阻止相关的地址空间。在过去,我使用了一个编译器,它具有从C代码中定位变量的扩展。我将寄存器分组为一个结构,然后将结构放在适当的位置:

typedef struct
{ 
   BYTE reg1;
   BYTE reg2;
   ...
} Registers;

Registers regs _at_ 0x100;

regs.reg1 = 0;

实际上创建'Registers'结构会保留编译器/链接器的空间。

现在,使用GNU工具,我显然没有 at 扩展名。使用指针方法:

#define reg1 *(BYTE*)0x100;
#define reg2 *(BYTE*)0x101;
reg1 = 0

// or
#define regs *(Registers*)0x100
regs->reg1 = 0;

这是一个简单的应用程序,没有操作系统,也没有高级内存管理。基本上:

void main()
{
    while(1){
        do_stuff();
    }
}

10 个答案:

答案 0 :(得分:12)

你的链接器和编译器不知道这一点(当然没有你告诉它)。您的平台的ABI设计者可以指定他们不在这些地址分配对象。

因此,有时(我工作的平台有)虚拟地址空间中的一个范围直接映射到物理地址,另一个范围可供用户空间进程用来增长堆栈或分配堆记忆。

您可以使用GNU ld的defsym选项在固定地址分配一些符号:

--defsym symbol=expression

或者,如果表达式比简单算法更复杂,请使用自定义链接描述文件。在这里您可以定义内存区域并告诉链接器应该为哪些区域/对象指定哪些区域。有关说明,请参阅here。虽然这通常完全是您使用的工具链的作者的工作。他们采用ABI的规范,然后编写满足平台要求的链接器脚本和汇编器/编译器后端。

顺便提一下,GCC有attribute section可用于将结构放入特定部分。然后,您可以告诉链接器将该部分放入寄存器所在的区域。

Registers regs __attribute__((section("REGS")));

答案 1 :(得分:9)

链接器通常使用链接描述文件来确定将分配变量的位置。这称为“数据”部分,当然应该指向RAM位置。因此,不能在不在RAM中的地址分配变量。

您可以在GCC here中阅读有关链接描述文件的更多信息。

答案 2 :(得分:5)

您的链接器处理数据和变量的放置。它通过链接描述文件了解您的目标系统。链接描述文件定义memory layout中的区域,例如.text(用于常量数据和代码)和.bss(用于全局变量和堆),还可以创建虚拟关联和物理地址(如果需要)。链接器脚本的维护者的工作是确保链接器可用的部分不会覆盖您的IO地址。

答案 3 :(得分:3)

当嵌入式操作系统将应用程序加载到内存中时,它通常会在某个指定位置加载它,假设为0x5000。您使用的所有本地内存都将相对于该地址,即int x将在某处,如0x5000 +代码大小+ 4 ...假设这是一个全局变量。如果它是局部变量,则它位于堆栈上。当您引用0x100时,您将引用系统内存空间,操作系统负责管理的相同空间,以及它可能监视的非常特定的位置。

链接器不会将代码放在特定的内存位置,它的工作方式是“相对于我的程序代码所在的位置”。

当你进入虚拟内存时,这会分解一点,但对于嵌入式系统,这往往是正确的。

干杯!

答案 4 :(得分:3)

让GCC工具链为您提供一个适合直接在硬件上使用的图像,而无需加载操作系统,但可能涉及正常程序通常不需要的几个步骤。

  1. 您几乎肯定需要自定义C运行时启动模块。这是一个汇编模块(通常命名为crt0.s),它负责初始化初始化数据,清除BSS,如果包含带有全局对象的C ++模块,则调用全局对象的构造函数等。典型的自定义包括需要设置硬件以实际寻址RAM(可能还包括设置DRAM控制器),以便有一个放置数据和堆栈的地方。某些CPU需要按特定顺序完成这些操作:例如ColdFire MCF5307具有一个芯片选择,可在启动后响应每个地址,最终必须配置为仅覆盖为连接芯片规划的存储器映射区域。

  2. 您的硬件团队(或者您可能还有其他帽子)应该有一张记忆地图,记录各种地址的内容。 ROM位于0x00000000,RAM位于0x10000000,设备寄存器位于0xD0000000等。在某些处理器中,硬件组可能只将CPU的芯片选择连接到设备,并由您自行决定触发选择引脚的地址

  3. GNU ld支持非常灵活的链接描述文件,允许将可执行映像的各个部分放在特定的地址空间中。对于正常编程,您永远不会看到链接器脚本,因为gcc提供了一个库存,它根据您的操作系统对正常应用程序的假设进行了调整。

  4. 链接器的输出采用可重定位格式,旨在由OS加载到RAM中。它可能具有需要完成的重定位修正,甚至可能动态加载一些库。在ROM系统中,(通常)不支持动态加载,因此您不会这样做。但是你仍然需要一个原始二进制映像(通常是适合某种形式的PROM程序员的HEX格式),因此你需要使用binutil中的objcopy实用程序将链接器输出转换为合适的格式。

    < / LI>

    所以,回答你问的实际问题......

    使用链接描述文件指定程序图像每个部分的目标地址。在该脚本中,您有多种处理设备寄存器的选项,但所有这些选项都涉及将文本,数据,bss堆栈和堆段放在避免硬件寄存器的地址范围内。还有一些机制可以确保如果你的ROM或RAM溢出,ld会抛出错误,你也应该使用它们。

    实际上,可以使用#define将设备地址放入C代码中,或者通过在链接器脚本中直接声明符号来解析寄存器的基址,并使用匹配C头文件中的extern声明。

    虽然可以使用GCC的section属性将未初始化的struct的实例定义为位于特定部分(例如FPGA_REGS),但我发现不是在真实系统中运行良好。它可能会产生维护问题,并且它成为描述片上器件的完整寄存器映射的昂贵方式。如果您使用该技术,则链接描述文件将负责将FPGA_REGS映射到其正确的地址。

    在任何情况下,您都需要很好地理解对象文件概念,例如“部分”(特别是文本,数据和bss部分),并且可能需要追踪桥接硬件和软件之间的差距,例如中断向量表,中断优先级,管理程序与用户模式(或x86变体上的0到3环)等。

答案 5 :(得分:1)

通常,这些地址超出了您的流程范围。所以,你的链接器不敢把东西放在那里。

答案 6 :(得分:1)

如果内存位置在您的体系结构中具有特殊含义,编译器应该知道并且不在其中放置任何变量。这与大多数架构上的IO映射空间类似。它不知道你用它来存储值,它只知道正常变量不应该去那里。许多嵌入式编译器支持语言扩展,允许您在特定位置声明变量和函数,通常使用#pragma。此外,通常我看到人们实现您尝试执行的内存映射的方式是在所需的内存位置声明一个int,然后将其视为全局变量。或者,您可以声明一个指向int的指针并将其初始化为该地址。这两者都提供了比宏更多的类型安全性。

答案 7 :(得分:1)

要扩展litb的答案,您还可以使用--just-symbols= {symbolfile}选项定义多个符号,以防您拥有多个内存映射设备。符号文件必须采用

格式
symbolname1 = address;
symbolname2 = address;
...

(等号周围的空格似乎是必需的。)

答案 8 :(得分:1)

通常,对于嵌入式软件,您可以在链接器文件中为链接器指定的变量定义一个RAM区域,并在绝对位置定义一个单独的区域,链接器不会触及这些区域。

如果不这样做会导致链接器错误,因为它应该发现它正在尝试将变量放在已经被具有绝对地址的变量使用的位置。

答案 9 :(得分:0)

这取决于您使用的操作系统。我猜你正在使用像DOS或vxWorks这样的东西。通常,系统将为硬件保留存储空间的certian区域,并且该平台的编译器总是足够智能以避免这些区域用于自己的分配。否则,当您打算访问变量时,您将不断地将随机垃圾写入磁盘或行式打印机。

如果其他东西让你困惑,我还应该指出#define是一个预处理器指令。没有为此生成代码。它只是告诉编译器用foo_reg以文本方式替换它在源文件中看到的任何*(int *)0x100。与您*(int *)0x100处的任何地方都输入foo_reg没有什么不同,除了它看起来更清晰。

我可能会做的(在现代的C编译器中)是:

// access register 'foo_reg', which is located at address 0x100
const int* foo_reg = (int *)0x100;
*foo_reg = 1;  // write to foo_regint 
x = *foo_reg;  // read from foo_reg