18

背景:
我正在研究 ARM 目标,更具体地说是 ST 的 Cortex-M4F 微控制器。在这样的平台(通常是微控制器)上工作时,显然没有操作系统;为了获得一个有效的 C/C++“环境”(此外,在变量初始化方面符合标准),必须有某种启动代码在重置时运行,在显式调用main. 正如我所暗示的,此类启动代码必须初始化已初始化的全局和静态变量(例如int foo = 42;在全局范围内)并将其他全局变量(例如在全局范围内)清零int bar;。然后,如有必要,将调用全局“ctors”。

在微控制器上,这仅仅意味着启动代码必须为每个初始化的全局(全部在“.data”部分中)将数据从闪存复制到 ram,并清除其他(全部在“.bss”中)。因为我使用 GCC,所以我必须提供这样的启动代码,我很高兴地分析了几个启动代码(及其相关的链接器脚本!)捆绑了我在 Internet 上找到的大量示例,所有示例都使用我正在开发的同一个演示板.

问题:
如前所述,我见过许多启动代码,它们以不同的方式初始化全局变量,其中一些在空间和时间方面比其他的更有效。但它们都有一些奇怪的共同点:它们没有使用memsetnor memcpy,而是使用手写循环来完成这项工作。由于在可能的情况下使用标准函数对我来说似乎很自然(简单的“DRY 原则”),我尝试了以下方法来代替最初的手写循环:

/* Initialize .data section */
ldr r0, DATA_LOAD
ldr r1, DATA_START
ldr r2, DATA_SIZE
bl  memcpy       /* memcpy(DATA_LOAD, DATA_START, DATA_SIZE); */

/* Initialize .bss section */
ldr r0, BSS_START
mov r1, #0
ldr r2, BSS_SIZE
bl  memset       /* memset(BSS_START, 0, BSS_SIZE); */

...而且效果很好。节省的空间可以忽略不计,但现在显然很简单。

所以,我想了想,在这种情况下我认为没有理由做手写循环:

  • memcpy并且memset很可能无论如何都链接到可执行文件中,因为程序员会直接使用它,或者通过另一个库间接使用它;
  • 它更小;
  • 速度不是启动代码的一个非常重要的因素,但它可能更快;
  • 几乎不可能弄错。

知道为什么不依赖memcpymemset启动代码吗?

4

3 回答 3

21

我怀疑启动代码不想对memcpylibc 中的实现等做出假设。例如,实现memcpy可能使用 libc 初始化代码设置的全局变量来报告哪些 cpu 扩展可用,以便在支持此类操作的机器上提供优化的 SIMD 复制。在早期的“crt”启动代码运行时,这样一个全局的存储可能完全未初始化(包含随机垃圾),在这种情况下调用memcpy. 即使使调用对您有用,这也是实现的结果(或者甚至可能是 UB 的不可预测的结果......)使其工作;这可能不是 crt 代码想要依赖的东西。

于 2013-03-17T03:14:11.003 回答
15

标准库是否链接完全由应用程序开发者决定(--nostdlib例如可以使用),但需要启动代码,因此不能做任何假设。

此外,启动代码的目的是建立一个可以运行C代码的环境;在此之前,任何可能合理假设完整运行时环境的库代码都不会正确运行,这绝不是给定的。对于有问题的函数,在许多情况下这可能不是问题,但您无法知道这一点。

启动代码至少要建立一个堆栈并初始化静态数据,在 C++ 中它额外调用全局静态对象的构造函数。标准库可能会合理地假设这些已经建立,因此在此之前使用标准库可能会导致错误行为。

最后,您应该清楚 C 语言和 C 标准库是不同的实体。语言必须能够独立存在。

于 2013-03-17T08:38:00.187 回答
1

我认为这可能与“关于 memcy/memset 的内部状态的假设”没有任何关系,它们不太可能使用任何全局资源(尽管我认为它们确实存在一些奇怪的情况)。

微控制器上的所有启动代码通常都以这种方式编写“内联汇编程序”,这仅仅是因为它在代码的早期阶段运行,此时可能还没有堆栈,并且可能尚未执行 MMU 设置。因此,初始化代码不想冒险将任何东西放在堆栈上,就这么简单。函数调用把东西放在堆栈上。

因此,虽然这恰好是静态存储复制的初始化代码,但您也可能在其他此类初始化代码中找到相同的内联汇编程序。例如,您可能会在复制之前的某个地方找到一些用汇编器编写的基本寄存器设置代码,并且您还会在附近的某个地方找到汇编器中的 MMU 设置。

于 2013-03-18T07:57:07.527 回答