Kika's
Blog
图片简介 | CC BY 4.0 | 换一张

链接器脚本(Linker Scripts)入门

2024-02-07

基础概念

连接器将多个输入的object文件合并成一个输出的object文件(又叫executable), 每个object文件都含有许多sections, 即段, 每个段都有名字和大小, 大多数段含有数据, 段可以被标记为:

  • loadable: 运行时内容可被载入到内存
  • allocatable: 内存中留出的区域, 此区域不应该加载任何特定的内容
  • 其余的, 一般都是某种调试信息

每个loadableallocatable类型的段都有两个地址:

  • VMA(virtual memory address): 虚拟地址, 这是运行时的地址
  • LMA(load memory address): 加载地址, 这是段被加载的地址

大多时候这两个地址是一样的, 但比如当一个在ROM里面的数据段在程序启动时被复制到RAM, 这种情况下两者不同, LMA是这个数据段在ROM中的地址, 而VMA是这个数据段在RAM中的地址. 下面第二个例子就是这样的.

# 查看object文件中的段
objdump -h a_obj_file

每个object文件也会有一个symbol table, 即符号表, 每个符号可以是definedundefined, 符号都有名字, 有定义的符号会有地址以及其他的一些信息. 如果编译C/C++程序到object文件, 每个有定义的函数,全局变量或静态变量都会有对应定义的符号.

# 查看object文件中的符号
objdump -t a_obj_file
# 或者
nm a_obj_file

例子

简单的例子

下面是一个简单的例子:

SECTIONS
{
  . = 0x10000;
  .text : { *(.text) }
  . = 0x8000000;
  .data : { *(.data) }
  .bss : { *(.bss) }
}

其中.location counter, 指示当前位置的地址, 对.赋值即可使得位置移动, 但不能回退. text段一般存放代码, data段一般存放已初始化的数据, bss段存放未初始化的数据. *(.text)代表通配符匹配所有文件的text段(使用EXCLUDE_FILE还可以排除某些文件, 更多详见Input-Section-Wildcards).

更复杂的例子

下面是一个更复杂的例子: 将text,rodata,data加载到ROM, bss段加载到RAM, 另外程序启动时, 将data从ROM复制到RAM.

其中> REGION_TEXT 指定段分配到自定义的REGION_TEXT内存区域内, 即指定了text段的Output Section Address, 同时也即指定了上文提到的段的VMA(即虚拟地址).

AT (rodata_end)则指定了LMA(即加载地址), 如果没有AT命令指定LMA地址, 则LMA和VMA地址一样(更详细见Output-Section-LMA).

/* linker script */
INCLUDE linkcmds.memory

SECTIONS
{
    .text :
    {
        *(.text)
    } > REGION_TEXT
    .rodata :
    {
        *(.rodata)
        rodata_end = .;
    } > REGION_RODATA
    .data : AT (rodata_end)
    {
        data_start = .;
        *(.data)
    } > REGION_DATA
    data_size = SIZEOF(.data);
    data_load_start = LOADADDR(.data);
    .bss :
    {
        *(.bss)
    } > REGION_BSS
}

下面的linkcmds.memory文件就定义了REGION_TEXT等内存区域.

其中ORIGIN指定了内存区域的起始地址, LENGTH指定了内存区域的字节大小(还可以设置权限属性, 详见MEMORY).

/* linkcmds.memory */
MEMORY
  {
    ROM : ORIGIN = 0, LENGTH = 3M
    RAM : ORIGIN = 0x10000000, LENGTH = 1M
  }

REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);

下面的启动程序就将ROM中的data内容写入到RAM去.

其中特别注意程序引用data_start等符号的用法, 更多可见下文关于程序引用符号的说明.

#include <string.h>

extern char data_start [];
extern char data_size [];
extern char data_load_start [];

void copy_data(void)
{
  if (data_start != data_load_start)
    {
      memcpy(data_start, data_load_start, (size_t) data_size);
    }
}

实用工具

Linux下面一些可以用来查看相关信息的工具

# 查看所有段
objdump -h xxx.elf

# 查看符号表
objdump -t xxx.elf
nm xxx.elf
readelf -s xxx.elf

# 查看文件头
readelf -h xxx.elf

一个例子

我们想要查找一个存放字符串的变量mainargs里面的内容

objdump -t xxx.elf | grep "mainargs"

得到输出:

a0003c0c l     O .rodata        00000002 mainargs

可知mainargs是一个局部符号(l)并且是一个对象(O), 即数据, 放在了.rodata段中, 其内容在a0003c0c处. 于是我们查找a0003c0c处的内容(注意是搜索地址a0003c0), -s指定显示段的所有内容, -j .rodata指定只显示.rodata段:

objdump -s -j .rodata amtest-riscv32e-ysyxsoc.elf | grep "a0003c0"

得到输出:

 a0003c04 646c6962 2e630000 6800               dlib.c..h.

对照地址a0003c0c, 可知其内容是h.(0x6800), 即字符串"h"

其他细节

下面就是一些细节内容的概要, 具体的内容可以参考官方的文档或者中文翻译

Entry Point

指示第一条被执行的程序指令, 按下面的先后顺序确定:

  1. the -e entry command-line option;
  2. the ENTRY(symbol) command in a linker script;
  3. the value of a target-specific symbol, if it is defined; For many targets this is start, but PE- and BeOS-based systems for example check a list of possible entry symbols, matching the first one found.
  4. the address of the first byte of the code section, if present and an executable is being created - the code section is usually .text, but can be something else;
  5. The address 0.

include 文件

File-Command

  • INCLUDE filename
  • INPUT(file file …)
  • GROUP(file, file, …)
  • AS_NEEDED(file, file, …)
  • OUTPUT(filename)
  • SEARCH_DIR(path)
  • STARTUP(filename)

Object文件格式

Format-Commands

  • OUTPUT_FORMAT
  • TARGET

其他命令

  • ASSERT 等等

赋值

类同C语言, +=等都是可以的

程序引用符号

对于在链接脚本中定义的符号(Symbol), 程序应当只使用其地址, 而不应该尝试访问其值, 因为符号不同于C语言中的一个变量, 不会分配到任何内存, 符号只有地址而没有值.

例如:

start_of_ROM   = .ROM;
end_of_ROM     = .ROM + sizeof (.ROM);
start_of_FLASH = .FLASH;

在程序中应当& start_of_FLASH取其指向的地址

extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);

或者使用char[], 这样就不必&取址了

extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);

一些内建函数

Builtin-Functions

  • ABSOLUTE
  • ADDR 返回段的VMA地址
  • ALIGN 对齐
  • LENGTH 返回memory内存区域的长度
  • SIZEOF 返回段的字节数
  • LOADADDR 返回段的LMA地址
  • LOG2CEIL