修复了C中的地址变量

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

// 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工具,我显然没有扩展名。 使用指针方法:

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

这是一个没有操作系统且没有高级内存管理的简单应用程序。 实质上:

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

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

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

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

 --defsym symbol=expression 

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

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

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

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

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

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

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

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

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

干杯!

获得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系统中,(通常)不支持动态加载,因此您不会这样做。 但是您仍然需要原始二进制映像(通常采用适合某种forms的PROM编程器的HEX格式),因此您需要使用binutil中的objcopy实用程序将链接器输出转换为合适的格式。

那么,回答你问的实际问题……

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

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

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

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

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

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

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

 symbolname1 = address; symbolname2 = address; ... 

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

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

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

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

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

我可能会做的(在现代的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