小猫爪:嵌入式小知识07-MCUXpresso GCC ld链接文件解析-链接代码至RAM

1 前言

我们已经简单学习完IAR和MDK链接文件的基本用法,接下来我们再简单的对ld链接文件做一下介绍。

在这里之前我们需要了解输入段和输出段,输入段就是我需要给链接器的信息,包括各种section,加载地址,链接地址,输出段就是链接器根据输入段的各种信息条件之后输出的东西。(section就是段,段就是section,有时候也叫它节,都可以。)

(注:不管是IAR的icf,还是MDK的scf,还有GNU的ld,大家的功能都是一样的,给链接器提供地址信息,让链接器按照这些地址信息将编译器编出来的section和地址一一对应生成最后MCU需要的可执行文件。所以很多东西都是想通的,唯一不同的是三者提供地址信息的方式不同而已。)

2 基本语法

ld链接文件的语法就相对比较复杂了,各种花里胡哨的符号,各种修饰符,反正就是很烦的玩意,建议大家不要深究,不值得,除非你想成为这一行的大佬级别的人物,关于ld链接文件的相关语法介绍大家可以参考文章《LD说明文档–3.LD链接脚本》,这写的很全了。

下面我就以土话介绍几个最常见的语句。

① ENTRY(Reset_Handler):这是一个设置入口函数的命令,也就是说启动后,PC最先指向Reset_Handler,然后运行它。

② MEMORY:定义地址空间,我们可在在里面定义一段段的空间,相当于IAR中的region,也就是待链接地址空间,可以将section链接到这些空间。

MEMORY
{
  my_mem        (RX)  : ORIGIN = 0x60000000, LENGTH = 0x00001000
}

上面语句的意思就是定义了一个region, 起始地址是0x60000000,大小为0x00001000。

③ SECTIONS:定义输出段,相当于是一个加载空间,我们会往这个加载空间里面慢慢的放section,同时给这些section链接地址空间,同时里面会放各种输出段描述和输入段描述。

SECTIONS
{
    .text : 
    { 
      *(.text) 
    }
}

上面语句的意思就是定义了一个加载空间,然后把.text段放进去了。其中外面的.text是输出段描述,意思就是这一部分输出的数据为text类型,大括号里面的.text为输入段描述,意思就是输入的数据类型为text, 其中*则是通配符。所以这句话的意思就是我要把所有文件中的.text类型的数据放在这里,链接完成后输出的数据类型也是text类型。

④ . :这个 . 代表的是位置计数器的值,我们可以把这个值取出来,也可以改变这个值,换句话说,这个值大有用处,我们可以通过它获取记录很多信息,后面碰到后在慢慢体会。

除了上面三个,还有很多零碎的,下面我们结合一个完整的实例给大家做分解。

3 实例解析

接下来就以一个RT1050芯片的工程链接文件做分析解释。大家自行看着解释对照着语法介绍,你就会有一个非常新的了解。

ENTRY(Reset_Handler)   //设置入口地址

HEAP_SIZE  = DEFINED(__heap_size__)  ? __heap_size__  : 0x0400; //设置堆大小
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400; //设置栈大小

MEMORY     //定义链接地址空间
{
  /*定义只读空间m_interrupts ,起始地址0x00000000, 大小0x00000400 */
  m_interrupts   (RX)  : ORIGIN = 0x00000000, LENGTH = 0x00000400 
  /*定义只读空间m_text,起始地址0x00000400, 大小0x0001FC00*/
  m_text         (RX)  : ORIGIN = 0x00000400, LENGTH = 0x0001FC00
  /*定义读写空间m_data,起始地址0x20000000, 大小0x00020000*/
  m_data         (RW)  : ORIGIN = 0x20000000, LENGTH = 0x00020000
  /*定义读写空间m_data2s ,起始地址0x20200000, 大小0x00000400*/
  m_data2        (RW)  : ORIGIN = 0x20200000, LENGTH = 0x00040000
}

SECTIONS //定义输出段
{
  __NCACHE_REGION_START = ORIGIN(m_data2); //将m_data2的起始地址赋值给__NCACHE_REGION_START 
  __NCACHE_REGION_SIZE  = 0;//__NCACHE_REGION_SIZE赋值0

  .interrupts : //输出段描述,表示这一段是中断向量表
  {
    __VECTOR_TABLE = .; //把当前位置计数器的值赋值给__VECTOR_TABLE ,位置计数器的值一开始默认为0
    __Vectors = .;//把当前位置计数器的值赋值给__VECTOR_TABLE ,位置计数器的值一开始默认为0
    . = ALIGN(4); //这句话不是再给位置计数器赋值,而是给它增加限制条件,表示其增加一次增加4,在内存中的表现即为4字节对齐
    KEEP(*(.isr_vector))//放置所有文件中的.isr_vector section,*是通配符,表示所有,就跟我们搜索文件使用*时一样的。使用KEEP的意思就是告诉编译器这段数据非常重要,不要把它当成垃圾优化掉了
    . = ALIGN(4);
  } > m_interrupts  //将这一输出段链接至m_interrupts区域处,所以中断向量表的链接地址就是m_interrupts的起始地址

  .text :   //定义输出段,就是一个名字,大家可以自己取
  {
    . = ALIGN(4); //4字节对齐
    *(.text)                 /*放置所有文件的.text段(code) */
    *(.text*)                /*放置所有文件的.text*段(code) */
    *(.rodata)               /*放置所有文件的.rodata段(constants, strings, etc.) */
    *(.rodata*)              /*放置所有文件的.rodata*段(constants, strings, etc.) */
    *(.glue_7)               /*同上*/
    *(.glue_7t)              /*同上*/
    *(.eh_frame)             /*同上*/
    KEEP (*(.init))          
    KEEP (*(.fini))
    . = ALIGN(4);
  } > m_text       //将这一输出段链接至m_text区域处

  .ARM.extab :   //定义输出段,就是一个名字,大家可以自己取
  {
    *(.ARM.extab* .gnu.linkonce.armextab.*) //
  } > m_text   //将这一输出段链接至m_text区域处

  .ARM :   //定义输出段,就是一个名字,大家可以自己取
  {
    __exidx_start = .;
    *(.ARM.exidx*)  
    __exidx_end = .;
  } > m_text   //将这一输出段链接至m_text区域处

 .ctors :    //定义输出段,就是一个名字,大家可以自己取
  {
    __CTOR_LIST__ = .;
    KEEP (*crtbegin.o(.ctors))  //放置*crtbegin.o中的.ctors段,并保证不被优化
    KEEP (*crtbegin?.o(.ctors)) //同上

    /*下面的语句中出现了EXCLUDE_FILE函数,这个函数的意思就是把括号里面的除外,
    意思就是说放置所有文件除了*crtend?.o *crtend.o文件的 .ctors段,
    因为在上面已经放置过了*/
    KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)) 
    /*下面的语句中出现了SPORT函数,SOPT是SORT_BY_NAME的别名,
    意思是放置.ctors.*段的时候,按照名字的排列顺序来放置*/
    KEEP (*(SORT(.ctors.*)))  
    KEEP (*(.ctors))
    __CTOR_END__ = .;
  } > m_text   //将这一输出段链接至m_text区域处

  .dtors :   //定义输出段,就是一个名字,大家可以自己取
  {
    __DTOR_LIST__ = .;
    KEEP (*crtbegin.o(.dtors))  
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
    __DTOR_END__ = .;
  } > m_text

  .preinit_array :   //定义输出段,就是一个名字,大家可以自己取
  {
    /*下面的出现了PROVIDE_HIDDEN, 意思就是后面的这个符号__preinit_array_start 只能在
    链接器中被使用,外部文件是不能调用的,与它相反的还有PROVIDE, PROVIDE表示这个符号可以
    被外部调用,而且如果外部文件也定义了同样的符号也不会发生冲突,优先使用外部定义值,
    后面会出现很多PROVIDE*/
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } > m_text

  .init_array :   //定义输出段,就是一个名字,大家可以自己取
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } > m_text

  .fini_array :   //定义输出段,就是一个名字,大家可以自己取
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } > m_text

  __etext = .;   
  __DATA_ROM = .; 

  __VECTOR_RAM = ORIGIN(m_interrupts);
  __RAM_VECTOR_TABLE_SIZE_BYTES = 0x0;

  .data : AT(__DATA_ROM)  //AT的作用就是给当前输出段指定加载地址
  {
    . = ALIGN(4);
    __DATA_RAM = .;
    __data_start__ = .;     
    *(m_usb_dma_init_data)
    *(.data)               
    *(.data*)              
    KEEP(*(.jcr*))
    . = ALIGN(4);
    __data_end__ = .;        
  } > m_data
  __NDATA_ROM = __DATA_ROM + (__data_end__ - __data_start__);
  .ncache.init : AT(__NDATA_ROM)
  {
    __noncachedata_start__ = .;  
    *(NonCacheable.init)
    . = ALIGN(4);
    __noncachedata_init_end__ = .;   
  } > m_data
  . = __noncachedata_init_end__;
  .ncache :
  {
    *(NonCacheable)
    . = ALIGN(4);
    __noncachedata_end__ = .;   
  } > m_data

  __DATA_END = __NDATA_ROM + (__noncachedata_init_end__ - __noncachedata_start__);
  text_end = ORIGIN(m_text) + LENGTH(m_text);
  ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")

  /* Uninitialized data section */
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss section */
    . = ALIGN(4);
    __START_BSS = .;
    __bss_start__ = .;
    *(m_usb_dma_noninit_data)
    *(.bss)
    *(.bss*)
    /*放置COMMON块,关于COMMON块是链接器为弱符号所制定的编译解决方案,
    本质上其实就是bass段,感兴趣的小伙伴可以自行去搜一搜*/
    *(COMMON)  
    . = ALIGN(4);
    __bss_end__ = .;
    __END_BSS = .;
  } > m_data

  .heap :
  {
    . = ALIGN(8);
    __end__ = .;
    PROVIDE(end = .);
    __HeapBase = .;
    . += HEAP_SIZE;
    __HeapLimit = .;
    __heap_limit = .;
  } > m_data

  .stack :
  {
    . = ALIGN(8);
    . += STACK_SIZE;
  } > m_data 

  __StackTop   = ORIGIN(m_data) + LENGTH(m_data);
  __StackLimit = __StackTop - STACK_SIZE;
  PROVIDE(__stack = __StackTop);

  
  .ARM.attributes 0 : { *(.ARM.attributes) }
  
  /*ASSERT表示断言,跟C中的assert功能是一摸一样的*/
  ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
}

好了,到这里,一些最常见的命令大家都搞清楚了。

4 链接代码至RAM

接下我们以RT1050来实战一下学习成果。IDE为NXP的MCUXpresso,我们可以通过Properties->C/C++ Build->Setting中指定我们自己的ld链接文件:
在这里插入图片描述

4.1 链接自定义section至RAM

使用下面语句自定义section。

__attribute__((section(".my_code_section")))
void my_code_fun(void)
{
	PRINTF("hello world.\r\n");
}

如果我们不指定链接地址,查看map文件,发现它的链接地址在FLASH中,如下:
在这里插入图片描述
接下来在ld链接文件中给这个section指定链接地址至RAM:

MEMORY
{
  ......
  ......
  /*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的OCRAM(RAM),定义自己的地址空间时,避免两个空间有重合的区域*/
  MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}

SECTIONS
{
  ......
  ......
  //自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
  .my_text : ALIGN(4)     
  {
    *(.my_code_section)		
     . = ALIGN(4);	
   } > MY_RAM 
}

链接后查看map文件,发现它的链接地址已经在我们指定的RAM中,如下:
在这里插入图片描述
运行代码查看跳转地址:
在这里插入图片描述

4.2 链接自定义.o文件至RAM

在ld链接文件中使用以下代码将该文件编译后的fsl_gpio.o文件链接至我们指定的RAM空间。

MEMORY
{
  ......
  ......
  /*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的DTCM(RAM)*/
  MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}

SECTIONS
{
  ......
  ......
  //自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
  .text : ALIGN(4)
  {
     *(EXCLUDE_FILE(*fsl_gpio.o) .text*)   //放置所有文件除了*my_code.c的text*段
     *( .rodata .rodata.* .constdata .constdata.*)
     . = ALIGN(4);
  } > BOARD_FLASH /*将所有文件除了*my_code.c的text*段链接至FLASH*/
	
	
  .my_text : ALIGN(4)
  {
     *fsl_gpio.o(.text*)  //放置*my_code.c的text*段
     . = ALIGN(4);
  } > MY_RAM    //将*my_code.c的text*段链接至MY_RAM
}

链接后查看map文件,发现fsl_gpio.c中的函数的链接地址已经在我们指定的RAM中,如下:

在这里插入图片描述

运行代码查看跳转地址:
在这里插入图片描述
注意:ld链接文件的语法非常的严格,所以每一个文件的section只能被链接一次,如果链接多次则会出现链接错误,但是编译器却不报错。所以一定要巧妙地应用EXCLUDE_FILE函数来隔离相关的section。)

4.3 链接全部代码至RAM

使用以下代码将所有代码链接至我们指定的RAM中:

MEMORY
{
  ......
  ......
  /*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的DTCM(RAM)*/
  MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}

SECTIONS
{
  ......
  ......
  //自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
  .text : ALIGN(4)
  {
     *(.text*)  //将所有代码链接至RAM
     *(.rodata .rodata.* .constdata .constdata.*)//将所有只读性质的数据链接至RAM
     . = ALIGN(4);
  } > MY_RAM    //将所有代码段链接至RAM
}

在这里我们可以使用 (.text)链接所有代码的原因是NXP的SDK已经在MCUXpresso中将启动相关的代码自定义了一个叫做.after_vectors*( * 表示通配符)的段,然后将其链接至了FLASH中来保证MCU的正常启动和代码搬运,如下图为.after_vectors*段的一部分:
在这里插入图片描述

链接后查看map文件,发现除了除了有关启动代码之外代码的链接地址已经在我们指定的RAM中,如下:
在这里插入图片描述
运行代码查看跳转地址:
在这里插入图片描述

注意1:对于MCU来说,我们不能将所有的代码的执行域都放入至RAM中,换句话说,其实entry入口地址必须得是FLASH,因为必须运行完MCUXpresso的初始化函数,该初始化函数将完成相关代码和数据的拷贝之后,RAM中才会有代码,之后再跳转进入RAM中运行, 所以我们至少需要将与MCUXpresso的初始化相关的section链接至FLASH,不然是无法正常启动的。如果想完全将代码放入RAM中运行,则是可以直接将代码下载至RAM中,或者像RT1050这种支持non-XIP启动的MCU,则需要修改头信息,让BootROM实现所有代码的拷贝,类似于non-XIP启动。)

注意2:如果想在正常启动过程中将中断向量表链接至RAM,则需要注意两个点,第一点就是将中断向量链接至RAM的同时还要保证芯片的正常启动,我们可以通过将与启动相关的部分中断向量表拷贝副本链接至FLASH保证正常启动,再将完整的中断向量链接至RAM。第二点则是需要通过修改SCB->VTOR寄存器改变中断向量映射地址。)

END

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
当出现"make: arm-linux-gnueabihf-ld: Command not found"的错误提示时,这表示系统无法找到arm-linux-gnueabihf-ld这个命令。这通常是由于缺少必要的编译工具链或者相关的库文件导致的。解决这个问题的方法有以下几种: 1. 确保安装了正确的编译工具链:arm-linux-gnueabihf-ld是GNU链接器的一部分,用于链接目标文件生成可执行文件。请确认已经正确安装了与arm-linux-gnueabihf-gcc相匹配的工具链,包括GNU链接器。 2. 检查系统的PATH环境变量:请确保arm-linux-gnueabihf-ld所在的目录已经添加到系统的PATH环境变量。可以通过在Terminal运行`echo $PATH`命令来查看当前的PATH设置,并确认是否包含了arm-linux-gnueabihf-ld所在的目录。 3. 检查编译工具链的安装位置:如果arm-linux-gnueabihf-ld确实存在于系统,但仍然无法找到,可能是因为它被安装在了非标准的位置。可以尝试使用完整路径来运行该命令,例如`/path/to/arm-linux-gnueabihf-ld`,其"/path/to"是arm-linux-gnueabihf-ld命令所在的实际路径。 4. 确保相关的库文件已安装:arm-linux-gnueabihf-ld可能依赖于某些库文件,如果缺少这些库文件,也会导致无法找到该命令。可以通过查看编译工具链的文档或官方网站,找到arm-linux-gnueabihf-ld所依赖的库文件,并确保这些库文件已正确安装在系统。 总结:当出现"make: arm-linux-gnueabihf-ld: Command not found"的错误提示时,需要检查是否正确安装了arm-linux-gnueabihf-ld所在的编译工具链,并确保相关的库文件已正确安装。另外,还需要确认系统的PATH环境变量是否包含arm-linux-gnueabihf-ld所在的目录。如果问题仍然存在,可以尝试使用完整路径来运行该命令。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [sudo: arm-linux-gnueabihf-gcc: command not found解决方法](https://blog.csdn.net/weixin_47715886/article/details/108963986)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [make: arm-linux-gnueabihf-gcc: Command not found](https://blog.csdn.net/HGGshiwo/article/details/120479087)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猫爪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值