【问题标题】:How does the linker decide where the code execution will start from? [Embedded]链接器如何决定代码执行从哪里开始? [嵌入式]
【发布时间】:2016-01-07 20:21:30
【问题描述】:

作为嵌入式 C 编程的初学者,我很好奇每个(根据我的经验)程序执行是如何从 main() 函数开始的?就像链接器识别出main() 并将那个"special" 函数的地址放入复位向量指向的地址中。

【问题讨论】:

  • 否 - crt 初始化需要在 main() 之前运行。只有一点 - 在许多环境中,如果没有先设置堆栈指针,则无法调用 main()。
  • 您说的是嵌入式 C 还是 C++? C++ 语言的初始化规则与 C 语言不同。请酌情调整您的标签。
  • 此链接中的讨论有一些相关的信息 - stackoverflow.com/questions/3379190/…

标签: c++ c linker embedded


【解决方案1】:

通常,链接描述文件会创建一个特殊的部分,该部分映射到复位向量,并包含指向 C 启动代码的跳转/goto 指令,该指令反过来调用 main()。

【讨论】:

  • 我使用的是映射到 SRAM 的代码,而不是复位向量。我使用 JTAG 将代码下载到 SRAM 并执行代码。不涉及重置向量。
  • @ThomasMatthews - JTAG 不是通常用于调试或项目板吗?如果嵌入式程序是独立的,那么它将从设备中的某种形式的本地存储器启动,而不是通过 JTAG,因此会涉及到复位序列。
  • @ThomasMatthews。 JTAG 有点不同。您正在控制处理器并将状态强制到一个已知点。通常这个点模拟尽可能接近复位点。对于带有引导加载程序的处理器,调试器会在离开引导加载程序后多次创建状态。
  • 我下载到内存中的程序是否独立?我相信它是,它不是托管环境。
  • 借助 CPU 支持,您可以将向量表移动到代码空间中,这可能是 Thomas 下载脚本的一部分。此后,不再需要 JTAG 参与。在 SRAM 中裸露宇宙射线翻转位,您可以重新启动并继续前进,直到奶牛回家。
【解决方案2】:

C 为将在“托管”环境中运行的代码和将在“独立”环境中运行的代码定义了不同的规范。大多数程序员在他们的整个职业生涯中都不必面对独立的环境,但大多数例外是那些从事嵌入式编程、内核编程、引导加载程序和其他在裸机上运行的软件的人。

在托管环境中,C 指定程序执行从调用main() 开始。这并不排除系统在该调用之前执行的初步设置,但这超出了规范的范围。 C 编译器和/或链接器负责安排这种情况发生;细节取决于实现。

另一方面,在独立实现中,程序入口点是由实现选择的方式确定的。可能没有main() 函数,如果有,那么它的签名不需要匹配那些允许在托管环境中运行的程序。

【讨论】:

  • 是的,@Olaf。不知道我是怎么弄错的,但我已经修好了。
【解决方案3】:

决定的不是链接器,而是处理器。上电时,指令指针设置为预定义的内存地址,通常与复位中断向量相同。然后链接器通过将分支指令放置到该地址的启动代码来启动。

【讨论】:

  • 并非总是如此。我可以使用 JTAG 将程序下载到内存中并执行它。这里没有预定义的地址。没有复位中断向量。
  • @ThomasMatthews 在这种情况下,PC 使用 JTAG 工具“手动”设置为代码的起始地址。对于以这种方式运行的代码,程序的链接方式通常与发布版本不同
【解决方案4】:

链接器链接一个用于处理器和运行时环境初始化的模块。该模块是从复位向量进入的。在 gcc 工具链中,该模块通常称为 crt0.o,是从源 crt0.s(汇编代码)构建的。您的工具链可能会有所不同,但会链接某种类型的启动代码,并且源代码应该可用于自定义。

启动代码通常会执行硬件初始化,例如将 PLL 配置为所需的时钟速度,以及在使用外部存储器时初始化存储器控制器。 C 运行时初始化需要设置堆栈指针,初始化全局静态数据,可能还需要初始化运行时库——例如堆和 stdio 初始化。对于 C++,它还调用任何全局静态对象的构造函数。最后main() 被调用。

请注意,不是链接器专门知道main();这只是运行时启动模块中未解析的链接。如果您的程序没有main(),它将无法链接。

您当然可以修改启动代码以使用除main() 之外的其他符号,但语言标准将main() 定义为入口点。

某些应用程序框架或环境可能看起来没有main();例如在 RTOS VxWorks 中,应用程序从 usrAppInit() 开始,但实际上这仅仅是因为在 VxWorks 库中定义了 main()

链接器根据汇编源代码或链接器脚本中的指令定位启动代码;工具链可能不同。

在 ARM Cortex-M 设备上,初始堆栈指针在向量表中定义并自动加载;因此,这些设备可以直接从 reset 运行 C 代码(尽管在一些有限的环境中),并允许使用 C 而不是汇编程序编写大部分运行时环境初始化。

【讨论】:

    【解决方案5】:

    每个处理器和工具链都不同。但是,通常,它们设置在从重置向量到达运行时库的入口点(很多时候 _start)的位置。运行时库准备处理器状态,清除 .bss 内存,初始化 .data 内存,可能设置堆,并调用一些调用以允许自定义启动,然后调用所有全局构造函数(如果 c++),最后跳转到 main()。

    它混合了硬件要求、工具链假设、运行时库和系统代码。你可以删减很多,因为对 C 的唯一真正要求是你有一个堆栈。其余的是您可能会或可能不会使用的库代码。

    【讨论】:

      【解决方案6】:

      为了满足程序员的标准或至少期望,在main之前你需要清除bss,编译时初始化变量(例如带=的全局变量),一个c库和其他有趣的东西。因此,您遇到了先有鸡还是先有蛋的问题,您如何拥有具有此类假设或要求的 C 代码,并拥有满足这些要求的 C 代码。你没有。还有其他代码,汇编并不少见,但可能来自已知假设不正确的 C。有时称为引导代码。这是嵌入式系统还是在操作系统上运行的应用程序都没有关系。该“程序”中的第一条指令与主指令之间存在一些胶水。如果你反汇编 gnu 工具创建的东西,你可以在名为 _start 和 main 的标签之间看到这个执行路径。其他工具链可能会也可能不会以不同的方式命名它们的入口点。

      在微控制器或您可能是裸机的情况下(PC 上的 bios,启动 rtos/os 的启动代码),如果您不关心 C 的某些要求/假设,则加载堆栈指针和分支到 main 就是您所需要的。将 bss 清零并将 .data 从闪存复制到其在 ram 中的正确位置,是您需要更接近 C 语言要求的接下来的两件事,您会发现这些都是您在某些嵌入式系统中获得的所有步骤。

      可能还有其他处理器,但是 arm cortex-m 硬件能够加载堆栈指针并跳转到一个地址(重置总是跳转到一个地址或从某个已知地址运行代码),进一步的中断系统保存状态为您服务,因此您不需要将 asm 包装在用 C 编写的中断服务例程周围(或做一些编译器特定的声明来做同样的事情)(这是您无论如何都需要问的下一个问题,1)重置为 C 代码 2)中断到 C 代码),因此中断向量表可以直接具有 C 函数的地址。该产品线的一个不错的功能。

      使用工具链反汇编程序并检查从入口点到 main() 的代码...过去肯定有一些工具链,当它专门看到 main() 时会做出假设并添加额外的代码。所以有时你会看到一些其他 C 函数名称用作第一个 C 函数,以避免工具链链接到其他东西。

      虽然链接器只是在寻找未解析的符号,一个是 main,一个 gnu 工具链,另一个是 _start,但 Clifford 还是一针见血。它会链接它已经知道的内容或您在命令行上提供的内容,直到所有标签都被解析为止。

      【讨论】:

        猜你喜欢
        • 2018-02-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多