• GNU LD脚本命令语言(一)


    一、目的

            很多小伙伴并不是计算机专业出生,对编译链接原理并不是很熟悉(其实我也不是计算机专业的),很多时候我们并不一定要知道链接脚本的原理和使用,但是当我们需要执行一些复杂的链接过程时(例如将某个代码段放置在特定的位置),这个时候就需要知道一些链接脚本相关的知识点。

            首先让我们看一下链接脚本到底是啥?(GCC的链接脚本文件名称一般以.ld或者.lds结尾)

    1. /*
    2. * linker script for STM32H750XBHx with GNU ld
    3. */
    4. /* Program Entry, set to mark it as "used" and avoid gc */
    5. MEMORY
    6. {
    7. ROM (rx) : ORIGIN =0x08000000,LENGTH =128k
    8. RAM (rw) : ORIGIN =0x24000000,LENGTH =512k
    9. }
    10. ENTRY(Reset_Handler)
    11. _system_stack_size = 0x200;
    12. SECTIONS
    13. {
    14. .text :
    15. {
    16. . = ALIGN(4);
    17. _stext = .;
    18. KEEP(*(.isr_vector)) /* Startup code */
    19. . = ALIGN(4);
    20. *(.text) /* remaining code */
    21. *(.text.*) /* remaining code */
    22. *(.rodata) /* read-only data (constants) */
    23. *(.rodata*)
    24. *(.glue_7)
    25. *(.glue_7t)
    26. *(.gnu.linkonce.t*)
    27. /* section information for finsh shell */
    28. . = ALIGN(4);
    29. __fsymtab_start = .;
    30. KEEP(*(FSymTab))
    31. __fsymtab_end = .;
    32. . = ALIGN(4);
    33. __vsymtab_start = .;
    34. KEEP(*(VSymTab))
    35. __vsymtab_end = .;
    36. /* section information for utest */
    37. . = ALIGN(4);
    38. __rt_utest_tc_tab_start = .;
    39. KEEP(*(UtestTcTab))
    40. __rt_utest_tc_tab_end = .;
    41. /* section information for at server */
    42. . = ALIGN(4);
    43. __rtatcmdtab_start = .;
    44. KEEP(*(RtAtCmdTab))
    45. __rtatcmdtab_end = .;
    46. . = ALIGN(4);
    47. /* section information for initial. */
    48. . = ALIGN(4);
    49. __rt_init_start = .;
    50. KEEP(*(SORT(.rti_fn*)))
    51. __rt_init_end = .;
    52. . = ALIGN(4);
    53. PROVIDE(__ctors_start__ = .);
    54. KEEP (*(SORT(.init_array.*)))
    55. KEEP (*(.init_array))
    56. PROVIDE(__ctors_end__ = .);
    57. . = ALIGN(4);
    58. _etext = .;
    59. } > ROM = 0
    60. /* .ARM.exidx is sorted, so has to go in its own output section. */
    61. __exidx_start = .;
    62. .ARM.exidx :
    63. {
    64. *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    65. /* This is used by the startup in order to initialize the .data secion */
    66. _sidata = .;
    67. } > ROM
    68. __exidx_end = .;
    69. /* .data section which is used for initialized data */
    70. .data : AT (_sidata)
    71. {
    72. . = ALIGN(4);
    73. /* This is used by the startup in order to initialize the .data secion */
    74. _sdata = . ;
    75. *(.data)
    76. *(.data.*)
    77. *(.gnu.linkonce.d*)
    78. PROVIDE(__dtors_start__ = .);
    79. KEEP(*(SORT(.dtors.*)))
    80. KEEP(*(.dtors))
    81. PROVIDE(__dtors_end__ = .);
    82. . = ALIGN(4);
    83. /* This is used by the startup in order to initialize the .data secion */
    84. _edata = . ;
    85. } >RAM
    86. .stack :
    87. {
    88. . = ALIGN(4);
    89. _sstack = .;
    90. . = . + _system_stack_size;
    91. . = ALIGN(4);
    92. _estack = .;
    93. } >RAM
    94. __bss_start = .;
    95. .bss :
    96. {
    97. . = ALIGN(4);
    98. /* This is used by the startup in order to initialize the .bss secion */
    99. _sbss = .;
    100. *(.bss)
    101. *(.bss.*)
    102. *(COMMON)
    103. . = ALIGN(4);
    104. /* This is used by the startup in order to initialize the .bss secion */
    105. _ebss = . ;
    106. *(.bss.init)
    107. } > RAM
    108. __bss_end = .;
    109. _end = .;
    110. /* Stabs debugging sections. */
    111. .stab 0 : { *(.stab) }
    112. .stabstr 0 : { *(.stabstr) }
    113. .stab.excl 0 : { *(.stab.excl) }
    114. .stab.exclstr 0 : { *(.stab.exclstr) }
    115. .stab.index 0 : { *(.stab.index) }
    116. .stab.indexstr 0 : { *(.stab.indexstr) }
    117. .comment 0 : { *(.comment) }
    118. /* DWARF debug sections.
    119. * Symbols in the DWARF debugging sections are relative to the beginning
    120. * of the section so we begin them at 0. */
    121. /* DWARF 1 */
    122. .debug 0 : { *(.debug) }
    123. .line 0 : { *(.line) }
    124. /* GNU DWARF 1 extensions */
    125. .debug_srcinfo 0 : { *(.debug_srcinfo) }
    126. .debug_sfnames 0 : { *(.debug_sfnames) }
    127. /* DWARF 1.1 and DWARF 2 */
    128. .debug_aranges 0 : { *(.debug_aranges) }
    129. .debug_pubnames 0 : { *(.debug_pubnames) }
    130. /* DWARF 2 */
    131. .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
    132. .debug_abbrev 0 : { *(.debug_abbrev) }
    133. .debug_line 0 : { *(.debug_line) }
    134. .debug_frame 0 : { *(.debug_frame) }
    135. .debug_str 0 : { *(.debug_str) }
    136. .debug_loc 0 : { *(.debug_loc) }
    137. .debug_macinfo 0 : { *(.debug_macinfo) }
    138. /* SGI/MIPS DWARF 2 extensions */
    139. .debug_weaknames 0 : { *(.debug_weaknames) }
    140. .debug_funcnames 0 : { *(.debug_funcnames) }
    141. .debug_typenames 0 : { *(.debug_typenames) }
    142. .debug_varnames 0 : { *(.debug_varnames) }
    143. }

            上图是RT-Studio里面使用的一段链接脚本,后面会详细说明其组成。

            下面就让我们从头开始讲解LD脚本的语法以及使用。

    二、介绍

            按照惯例,官方参考资料如下:

            Linker scripts (haw-hamburg.de)https://users.informatik.haw-hamburg.de/~krabat/FH-Labor/gnupro/5_GNUPro_Utilities/c_Using_LD/ldLinker_scripts.html#Concepts

            英文比较好的小伙伴就直接去看这个链接,本文接下来的所有讲解都是基于这个文档。

    基本概念

            在介绍链接脚本之前,我们需要回顾一下代码编译的整个过程

            代码在编译成目标文件后,可以直接链接成可执行文件a.out或者打包成静态库或者链接成动态库。 

            链接器脚本控制着每一次链接过程,链接脚本是用链接器命令语言编写的(linker command language)。链接脚本的主要目的是描述输入文件中的段(section)应该如何映射到输出文件中的段(section),并且控制输出文件的内存布局(地址分配)。有些时候链接器脚本还可以使用链接器命令指示链接器执行许多其他操作。

            链接器总是使用一个链接脚本,如果你没有指定,那么会使用编译在链接器内部的默认链接脚本。通过命令"ld --verbose"可以查看默认的链接器脚本,某些命令行选项例如“-r”或者“-N”会影响默认的链接脚本。通过“-T”选项可以指定自己的链接脚本,这个时候就会替换默认的链接脚本。如果没有使用“-T”选项,而是直接将链接脚本文件作为命令行输入参数(不会替换默认的链接脚本),那么这种情况下链接脚本里面只能包含“INPUT”、“GROUP”、“VERSION”命令(这种方式也叫做隐式链接脚本方式)。

            链接器打开文件,如果此文件不是目标文件或者存档文件(静态库),则会认为其是链接脚本文件,如果不能解析其文件格式,链接器就会报错。

    链接脚本语言

            链接器将输入文件组合成一个单一的输出文件,输出文件和输入文件一般都是有特定的数据格式(叫做目标文件格式),每个文件被叫做目标文件,输出文件也可以被叫做可执行文件。每个目标文件都有一些段(SECTION)。输入文件中的段我们叫做input section,同理输出文件中的段我们叫做output section。目标文件中的每个段都有一个名字和大小,大部分段也有一个与之关联的数据块,叫做段内容(section content)。

            段可以被标记为可加载的(loadable),说明其内容在输出文件运行时需要加载到内存中(在STM32类型的MCU中就是说需要将段内容放到FLASH中);

            没有内容的段是可以被分配的(allocatable),这意味着内存中应该留出一个区域,但是不加载任何内容在里面(某些情况下需要清零)。段如果既不是可加载的又不是可分配的,一般是包含一些调试信息。

            每个可加载或者可分配的输出段都包含两个地址,一个是VMA(virtual memory address),输出文件运行时的段的地址;一个是LMA(load memory address),输出文件中的段加载时的地址;大部分情况下这两个地址是相同的。

            所以一个image就会有两个视图,一个叫做加载视图,一个叫做执行视图。后面再根据具体实例给大家讲解。

            一个典型的VMA和LMA不同的情形

             在嵌入式平台中,代码一般是放在Flash中,如果是XIP执行方式,此时VMA和LMA相同;但是如果代码在RAM中执行,那么VMA就是代码在RAM中的地址,LMA就是代码在Flash中的地址。

            RW段VMA和LMA一定不同,因为RW段在程序执行时是要搬运到RAM中。RW段一般存放初始化的全局变量,所以必须在程序执行时拷贝到RAM中。

            每个目标文件中也有也有一些符号,叫做符号表。符号可能是定义的或者未定义的,每个符号都有名字,每个定义的符号都有一个地址。如果我们编一个C/C++程序,那么目标文件中就会有每一个定义的函数、全局变量或者静态变量的符号;在输入文件中引用的每个未定义的函数或全局变量都将成为一个未定义的符号。

    链接脚本格式

            链接脚本文件是文本文件,由一系列命令(command)组成,每个命令要么是一个关键字紧跟着参数,要么是一个符号赋值操作,通过分号分隔不同的命令,空白字符被忽略;可以直接输入诸如文件名称或格式名称之类的字符串,如果文件名中有逗号,那么这个文件名就必须用双引号包围(因为逗号用于分隔文件名)。文件名中禁止有双引号。

            链接脚本文件中使用C语言的注释方式,即/**/;注释在语法上会被看做空白字符。

    简单的链接脚本示例

    1. SECTIONS
    2. {
    3. . = 0x10000;
    4. .text : { *(.text) }
    5. . = 0x8000000;
    6. .data : { *(.data) }
    7. .bss : { *(.bss) }
    8. }

            大部分情况下,链接脚本文件都是很简单的,最简单的脚本文件中只需要包含一个SECTIONS命令(其后的花括号是必须的,用于描述输出文件的内存布局。正如上面的示例,我们假设程序里面只有代码已经初始化的数据以及未初始化的数据,它们分别在.text、.data、.bss段中;同时我们也假设输出文件中也仅有这些段名。

            花括号内部可以包含符号赋值和输出段描述。

            上面的链接脚本指示链接器将所有输入文件的代码段放到输出文件的代码段中,并且输出文件的代码段首地址是0x10000;所有的输入文件中的数据段放到输出文件的数据段段中,其首地址为0x80000000;所有输入文件的bss段放到输出文件的bss段,其地址紧跟data段之后。

            语句说明

    . = 0x10000;

            注意示例中的,其代表location counter,我们可以对其赋值,也可以对其取值,上面的示例中我们是对其赋值。

            赋值语句或者取值语句必须以;结尾。

            如果我们没有使用其他方式指定输出段的地址,那么输出段的地址就是location counter的值,默认值为0。location counter的值会自动增加输出段的大小。

            那么还有哪些其他方式指定输出段的地址呢?这个我们后面会进一步讲解。

     .text : { *(.text) }

            这一行的中开头的.text前后都有一个空格(必须),其中.text是输出段的名称,具体有哪些名称要看编译的平台对输出文件的要求;冒号是必须的,花括号中就是输入段的描述,其中*是通配符,代表匹配所有字符,即匹配所有的输入文件,(.text)指输入文件中的.text段。

    .bss : { *(.bss) }

             所有输入文件的bss段放到输出文件的bss段,其地址为data段之后。也就是说bss段开始位置是location counter加上.data段的大小就是.bss段的开始位置。

    以上就是链接脚本的基本内容。


    设置程序入口函数

    ENTRY ( symbol )

             程序中执行的第一条指令叫做entry point(程序入口点),我们可以通过ENTRY命令来设置,也可以通过下面的方式(优先级由高到低)

    1. ld命令行选项“-e”指定的
    2. 链接脚本中ENTRY(symbol)指定的
    3. start符号,如果定义了此符号
    4. .text段中的第一个字节,如果存在此输出段
    5. 地址0

    处理文件的命令

            一些链接器脚本命令处理文件

    INCLUDE命令

    INCLUDE filename

            在当前脚本文件中调用此命令的位置包含其他脚本文件,在当前目录和在命令行选项-L指定的路径下搜索,INCLUDE命令最多可以嵌套10层

    INPUT命令

    1. INPUT (file , file , ...)
    2. INPUT (file file ...)

             指示链接器包含指定的这些文件,就像在命令行中输入这些文件名一样。

            例如你总是要包含“subr.o”这个目标文件,但是不想每次敲LD命令时都敲写一遍,就可以在链接脚本文件中添加“INPUT (subr.o)”命令;设置你可以将所有的输入文件通过INPUT命令包含进来。文件的搜索路径首先是搜索当前文件夹;如果没有找到,链接器将会从“-L”选项指定的路径中搜索。如果使用”INPUT (-l file)“命令,ld会将file转换成lib file .a,也就是指定库名称。

    GROUP命令

    GROUP (file file ...)

             类似INPUT,不同的是此处的file是静态库的文件名。

    OUTPUT命令

    OUTPUT (filename)

            指定输出文件的名字,效果等同于命令行选项"-o filename",如果两种方式都使用了,命令行选项优先级更高。

    SEARCH_DIR命令

    SEARCH_DIR (path)

            增加搜索静态库的路径;类似命令行选项"-L path";如果同时使用,链接器会搜索这些路径,但是命令行选项指定的路径优先搜索。

    STARTUP命令

    STARTUP ( filename )

            类似INPUT命令,区别在于此处指定filename是第一个被链接的文件,就像ld命令行中指定的第一个文件一样。

    符号赋值

            在链接脚本中可以进行符号赋值(使用和C语言一样的语法),这同时定义了一个全局符号。

    1. symbol = expression ;
    2. symbol += expression ;
    3. symbol -= expression ;
    4. symbol *= expression ;
    5. symbol /= expression ;
    6. symbol <<= expression ;
    7. symbol >>= expression ;
    8. symbol &= expression ;
    9. symbol |= expression ;

            第一条定义了一个符号并且赋值,其他的表达式则必须先有符号才能操作。

            特殊的符号名.代表location counter。一般只用在SECTIONS命令中。

            表达式后面的分号是必须的。

            你可以把符号赋值作为命令本身,或者作为' SECTIONS '命令中的语句,或者作为' SECTIONS '命令中输出段描述的一部分

    符号赋值可以使用的位置举例

    1. floating_point = 0;
    2. SECTIONS
    3. {
    4. .text :
    5. {
    6. *(.text)
    7. _etext = .;
    8. }
    9. _bdata = (. + 3) & ~ 4;
    10. .data : { *(.data) }
    11. }

             上面的例子中,floating_point符号被定义为0;_etext符号被定义为最后一个' .text '输入段之后的地址;符号' _bdata '将被定义为' .text '输出段后向上对齐的4字节边界的地址。

       PROVIDE命令(重要) 

            某些情况下我们希望链接脚本定义一个只是在目标文件内被引用,但没有在任何目标文件内被定义的符号。

    1. SECTIONS
    2. {
    3. .text :
    4. {
    5. *(.text)
    6. _etext = .;
    7. PROVIDE(etext = .);
    8. }
    9. }

            上面的示例中我们定义了_etext符号,如果程序(输入文件)中也定义了此符号,则链接器会报错(重复定义);

            通过PROVIDE命令我们定义了etext符号,如果 程序中也定义了此符号,那么链接器使用程序中的定义;如果程序中没有定义但是引用了etext,那么就会使用链接脚本中PROVIDE命令定义的这个符号。

            好,本篇就先介绍这么多,关于SECTION命令和MEMORY命令的说明见下篇。

  • 相关阅读:
    【ELFK】之zookeeper
    Java-Gui编程
    Docker开放远程安全访问(开启2376端口和CA认证)
    细说从0开始挖掘cms-
    Breakpad Windows 集成概述
    c++代码如何实现在win/linux下跨线程间事务触发实现(含示例代码)
    【Python】数据处理:SQLite操作
    参与开源社区还有证书拿?
    taro 支付宝/微信小程序的chooseImage真机和开发工具上的区别
    Kivy打包apk教程(含kivydev64)2022最新可用
  • 原文地址:https://blog.csdn.net/tianizimark/article/details/125865933