了解 ARM Cortex-M 微控制器的链接描述文件

Posted

技术标签:

【中文标题】了解 ARM Cortex-M 微控制器的链接描述文件【英文标题】:Understanding the linkerscript for an ARM Cortex-M microcontroller 【发布时间】:2017-03-24 18:02:06 【问题描述】:

我正在使用 STMicroelectronics 的 STM32F746NG 微控制器。该设备基于 ARM Cortex-M7 架构。我花了很多时间从示例项目中理解链接脚本。我弄清楚了基础知识,但我仍然无法掌握其中的大部分内容。请帮助我理解这些部分。

链接脚本的开始

链接描述文件开始如下:

/* Entry Point */
ENTRY(Reset_Handler) /* The function named 'Reset_Handler' is defined */
                     /* in the 'startup.s' assembly file.             */

/* Highest address of the user mode stack */
/* Remember: the stack points downwards */
_estack = 0x20050000;    /* End of RAM */

/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;  /* Required amount of heap  */
_Min_Stack_Size = 0x400; /* Required amount of stack */

/* --------------------------------------------------------------------*/
/*                    MEMORY AREAS                                     */
/* --------------------------------------------------------------------*/
MEMORY

    /* FLASH MEMORY */
    /* ------------ */
    /* Remember: the flash memory on this device can   */
    /* get accessed through either the AXIM bus or the */
    /* ITCM bus. Accesses on the ITCM bus start at     */
    /* address 0x0020 0000. Accesses on the AXIM bus   */
    /* at address 0x0800 0000.                         */
    FLASH (rx)     : ORIGIN = 0x08000000, LENGTH = 1024K
    /* FLASH (rx)     : ORIGIN = 0x00200000, LENGTH = 1024K */

    /* RAM MEMORY */
    /* ---------- */
    RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 320K

向量表及程序代码

定义内存区域后,链接描述文件继续定义节。链接描述文件中定义的第一部分是向量表。它必须在闪存的第一个字节中结束。

/* --------------------------------------------------------------------*/
/*                    OUTPUT SECTIONS                                  */
/* --------------------------------------------------------------------*/
SECTIONS

    /****************************/
    /*      VECTOR TABLE        */
    /****************************/
    .isr_vector :
    
        . = ALIGN(4);
        KEEP(*(.isr_vector)) /* Vector Table */
        . = ALIGN(4);
     >FLASH

向量表插入之后,就到了程序代码的时间了:

    /****************************/
    /*      PROGRAM CODE        */
    /****************************/
    .text :
    
        . = ALIGN(4);
        *(.text)           /* .text sections (code) */
        *(.text*)          /* .text* sections (code) */
        *(.glue_7)         /* Glue ARM to Thumb code */
        *(.glue_7t)        /* Glue Thumb to ARM code */
        *(.eh_frame)


        /* Note: The function ‘.text.Reset_Handler’ is one of the *(.text*) sections,      */
        /* such that it gets linked into the output .text section somewhere here.          */
        /* We can verify the exact spot where the Reset_Handler section is positioned, by  */
        /* examining the second entry of the vector table.                                 */
        /* A test has given the following results:
        /*    FLASH (rx) : ORIGIN = 0x0800 0000    ==>  Reset_Handler = 0x0800 1C91        */
        /*    FLASH (rx) : ORIGIN = 0x0020 0000    ==>  Reset_Handler = 0x0020 1CB9        */
        /*
        /* In both cases, the Reset_Handler section ends up a few hundred bytes after the  */
        /* vector table in Flash. But in the first case, the “Reset_Handler” symbol points */
        /* to the Reset-code through AXIM-interface, whereas in the latter case it points  */
        /* to the Reset-code through the ITCM-interface.                                   */


        KEEP (*(.init))
        KEEP (*(.fini))

        . = ALIGN(4);
        _etext = .;        /* Define a global symbol at end of code */

     >FLASH

链接描述文件定义了e_text全局符号,表示闪存中的程序代码结束的地址。

常量数据

只读数据也最终存储在闪存中(将其放入易失性 RAM 中是没有意义的)。链接描述文件定义.rodata 部分应该在闪存中:

    /****************************/
    /*      CONSTANT DATA       */
    /****************************/
    .rodata :
    
        . = ALIGN(4);
        *(.rodata)         /* .rodata sections (constants, strings, etc.) */
        *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
        . = ALIGN(4);
     >FLASH

flash 中的神秘部分

在定义了常量只读数据应该去哪里之后,链接脚本定义了一些“神秘”的部分也应该在闪存中结束:

    .ARM.extab :
    
        *(.ARM.extab* .gnu.linkonce.armextab.*)
     >FLASH
    .ARM :
    
        __exidx_start = .;
        *(.ARM.exidx*)
        __exidx_end = .;
     >FLASH
    .preinit_array :
    
        PROVIDE_HIDDEN (__preinit_array_start = .);
        KEEP (*(.preinit_array*))
        PROVIDE_HIDDEN (__preinit_array_end = .);
     >FLASH
    .init_array :
    
        PROVIDE_HIDDEN (__init_array_start = .);
        KEEP (*(SORT(.init_array.*)))
        KEEP (*(.init_array*))
        PROVIDE_HIDDEN (__init_array_end = .);
     >FLASH
    .fini_array :
    
        PROVIDE_HIDDEN (__fini_array_start = .);
        KEEP (*(SORT(.fini_array.*)))
        KEEP (*(.fini_array*))
        PROVIDE_HIDDEN (__fini_array_end = .);
     >FLASH

我不知道这些部分是什么。所以让这是第一个问题。这些部分是什么,它们出现在哪些目标文件中?如您所知,链接描述文件需要将一些目标文件链接在一起。我不知道这些神秘部分存在于哪些目标文件中:

.ARM.extab .ARM .preinit_array .init_array .fini_array

这是对闪存分配的结束。链接描述文件继续定义最终在 RAM 中的部分。

RAM 中的部分

.data.bss 部分对我来说很清楚。对此没有任何疑问。

    /****************************/
    /*    INITIALIZED DATA      */
    /****************************/
    _sidata = LOADADDR(.data);
    .data :
    
        . = ALIGN(4);
        _sdata = .;        /* create a global symbol at data start */
        *(.data)           /* .data sections */
        *(.data*)          /* .data* sections */

        . = ALIGN(4);
        _edata = .;        /* define a global symbol at data end */

     >RAM AT> FLASH


    /****************************/
    /*    UNINITIALIZED DATA    */
    /****************************/
    . = ALIGN(4);
    .bss :
    
        _sbss = .;         /* define a global symbol at bss start */
        __bss_start__ = _sbss;
        *(.bss)
        *(.bss*)
        *(COMMON)

        . = ALIGN(4);
        _ebss = .;         /* define a global symbol at bss end */
        __bss_end__ = _ebss;

     >RAM

链接脚本还定义了一个._user_heap_stack 部分:

    /****************************/
    /* USER_HEAP_STACK SECTION  */
    /****************************/
    /* User_heap_stack section, used to check that there is enough RAM left */
    ._user_heap_stack :
    
        . = ALIGN(8);
        PROVIDE ( end = . );
        PROVIDE ( _end = . );
        . = . + _Min_Heap_Size;
        . = . + _Min_Stack_Size;
        . = ALIGN(8);
     >RAM

显然这部分没有立即使用。它仅用于检查 RAM 是否还有足够的空间用于堆栈和堆。如果不是这种情况(. 超出顶部 RAM 地址),则会引发链接器错误。

链接脚本结束

这就是链接描述文件的结束方式。老实说,我不知道它做了什么。所以这是第二个问题:下面是什么意思?

    /* Remove information from the standard libraries */
    /DISCARD/ :
    
        libc.a ( * )
        libm.a ( * )
        libgcc.a ( * )
    

    .ARM.attributes 0 :  *(.ARM.attributes) 

/* END OF LINKERSCRIPT */

【问题讨论】:

【参考方案1】:

    .ARM.extab 和 .ARM.exidx 与展开有关。你可以在这里找到更多信息http://infocenter.arm.com/help/topic/com.arm.doc.ihi0044e/index.html。如果您不关心展开,则不需要它们(展开对于 C++ 异常和调试很有用)。

    这些符号与在 main() 之前/之后调用的 C/C++ 构造函数和析构函数启动和拆除代码有关。名为 .init、.ctors、.preinit_array 和 .init_array 的部分用于初始化 C/C++ 对象,而 .fini、.fini_array 和 .dtors 部分用于拆卸。开始和结束符号定义了与此类操作相关的代码段的开始和结束,并且可能从运行时支持代码的其他部分引用。

.preinit_array 和 .init_array 部分包含指向将在初始化时调用的函数的指针数组。 .fini_array 是一个函数数组,将在销毁时调用。大概开始和结束标签用于遍历这些列表。

    heap:不是真的,那部分允许为堆保留一些空间,为堆栈保留一些空间。显然,如果保留区域的总和超出 RAM 边界,则会出现错误。这是一个例子:

    _Min_Heap_Size = 0; /* 所需的堆数量 / _Min_Stack_Size = 0x400; / 所需的堆栈数量 */

    ._user_heap_stack : . =对齐(4); 提供(结束=。); 提供(_end = .); . = 。 + _Min_Heap_Size; . = 。 + _Min_Stack_Size; . =对齐(4); >内存

    链接库我更喜欢不同的表示法,这只是裸机无 RTOS C++ 项目的示例: GROUP(libgcc.a libc_nano.a libstdc++_nano.a libm.a libcr_newlib_nohost.a crti.o crtn.o crtbegin.o crtend.o)

【讨论】:

谢谢先生。你的回答真的很有帮助! :-) @massimo 您应该添加以下内容:一个未提及的有趣细节:.data : . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ >RAM AT> FLASH 数据部分已放入闪存。为什么这很有趣?因为 .data 部分是读/写的。它们被放入闪光灯中,以便在打开电源时它们就在那里。启动代码将它们复制到 RAM。不幸的是,我不知道如何添加换行符。 答案似乎提供了很好的信息,但是,我发现它很复杂并且非常交错。我可以请您重新格式化并尝试重新组合信息吗?或者如果提问者(@K.Mulier)在这个问题之后得到了很好的理解,他可以这样做吗?第二个问题(关于链接器脚本的最后一部分)仍未得到解答。【参考方案2】:

首先,你理解这个概念的方法是错误的;这就是我所相信的。在理解这个概念的过程中,我也遇到过类似的问题。

用简单的语言,我可以向您解释,链接器脚本为我们提供了三个主要功能:

    入口点 主内存中的运行时地址。 复制过程

让我们考虑一个例子,

假设我们的项目中有 N 个 .c 文件。现在编译后每个文件都包含自己的翻译单元,称为目标文件。

每个目标文件都包含.text 部分/段,其中包含实际代码。和 .data 部分/数据段。

为了组合每个翻译单元的所有.text 部分,链接描述文件提供了一些相同的特定命令。 .data 部分也是如此。

组合所有目标文件后,最终的可执行文件就可以使用了。

现在来到一些悖论......

Cortex-M 系列的入口点就是 ResetISR。 执行完ResetISR函数并初始化SoC中的其他可屏蔽中断后,下一步就是copy-down过程。

复制过程只不过是将.data 部分复制到RAM 中(甚至包括有趣的.bss 部分,但我现在不考虑这部分)。

从 ROM 复制到 RAM 是必不可少的,因为实际的 ELF 文件始终存储在 ROM 中。

在执行完所有这些启动相关的事情后,我们现在可以调用我们的main() 函数了。

【讨论】:

这个答案实际上与问题无关。它从非常高的级别谈论(糟糕,我可能会补充)链接器脚本,OP 显然已经很好地理解了。

以上是关于了解 ARM Cortex-M 微控制器的链接描述文件的主要内容,如果未能解决你的问题,请参考以下文章

启用外部中断 ARM Cortex-M0+ (STM32G070)

Arm Cortex-M4 LDRD 指令导致硬故障

ARM Cortex-M底层技术—启动代码详解

ARM探索之旅 | 一带你认识ARM Cortex-M阵营

ARM探索之旅 | 一带你认识ARM Cortex-M阵营

ARM Cortex-M0权威指南高清中文版pdf免费分享下载