程序编译阶段

以C程序为例:

  1. 预处理(Preprocessing):包括宏展开、文件包含、条件编译等
  2. 编译(Compilation):将预处理后的代码转换为汇编或机器语言
  3. 汇编(Assembly):汇编器将目标代码转换为可重定位目标文件
  4. 链接(Linking):链接器将可重定位目标文件和库文件进行链接,生成最终的可执行文件
graph TB
    c["源文件(.c)(source file)"]
    i["预处理后的文件/中间文件(.i)(internal file)"]
    s["汇编文件(.s)(assembly file)"]
    o["目标文件(.o)(object file)"]
    exe["可执行文件(.exe/a.out)"]

    c -- 预处理 --> i
    i -- 编译 --> s
    s -- 汇编 --> o
    o -- 链接 --> exe

参考资料
爱编程的大丙——预处理

链接脚本

简介

链接阶段由链接脚本控制,链接脚本主要用于描述输入文件中的段映射到输出文件的方法,以及控制内存的布局

Basic Linker Script Concepts

文件

  • 每个输入文件称为目标文件(object file)
  • 输出文件通常被称为可执行文件(executable file),但在此也被称为目标文件

段(section)

  • 每个目标文件里有一个段列表,段根据文件的不同被称为输入段和输出段
  • 段内容(section contents)是一个相关的数据块
  • 当输出文件运行时,某一个段应该被载入内存中,此时称该段是可加载的(loadable)
  • 一个段没有内容也有可能是可分配的(allocatable),即内存中只留出特定区域,不该加载特定的内容(必要时需要对该区域做清零处理)
  • 既不可加载也不可分配的段通常包含某种调试信息

地址

前提:该段是loadable或allocatable

  1. 虚拟内存地址(virtual memory address, VMA):输出文件运行时该段拥有的地址
  2. 加载内存地址(load memory address, LMA):该段被加载进内存的地址
  • 两个地址可能不同,例如载入ROM,程序开始运行复制进RAM(通常用于初始化ROM系统的全局变量)

符号(symbol)

每个目标文件有一个符号列表,称为符号表。每个符号有一个命名,可能是有定义的或未定义,有定义的符号有对应的地址和其它信息。如果将C或C++程序编译到目标文件中,则将会将所有定义过的函数和全局变量以及静态变量作为已定义符号。输入文件中引用的每个未定义函数或全局变量都将成为未定义符号

简单例子分析

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

将代码段载入到0x10000中,将数据段载入到0x8000000中,bss紧跟在数据段后

  • .:位置计数器,设置当前地址
  • .text : {*(.text)}来看:.text为输出段,{*(.text)}为输入段
  • 链接器在必要的时候通过增加位置计数器来确保每个输出段按某种要求对齐

部分命令

Entry Point

1
ENTRY(symbol)

有几种方式可以设置Entry Point,链接器通过尝试下述方法寻找入口点,一旦找到则停止

  1. ‘-e’ 命令行选项
  2. ENTRY(symbol)
  3. 目标特定的已定义的符号值,大多为start,但例如基于PE和BeOS的系统检查可能的输入符号列表,并与找到的第一个符号匹配
  4. 代码段的第一个字节地址
  5. 地址0

处理文件的命令

1
INCLUDE filename
  • 用于包含链接文件,INCLUDE最多嵌套10层
1
2
INPUT(file, file, …)
INPUT(file file …)
  • 指示链接程序在链接中包含指定的文件
  • 使用 INPUT (-lfile)ld会将名称转换为libfile.a
1
2
GROUP(file, file, …)
GROUP(file file …)
  • INPUT类似,但指定的文件必须全为库(archives),并且会一直重复的搜索它们,直到没有新的未定义的引用被创建
1
2
AS_NEEDED(file, file, …)
AS_NEEDED(file file …)
  • 在其它的文件名内的INPUTGROUP命令内部出现,除了ELF共享库外被列出的文件会像在INPUTGROUP命令下进行直接处理(ELF共享库当需要时才加入),实质上为其中列出的所有文件启用了-as-needed选项,为了恢复以前编译环境,之后需设置 --no-as-needed
1
OUTPUT(filename)
  • 像使用命令 -o filename,默认a.out(linux环境)
1
SEARCH_DIR(path)
  • 像使用命令 -L path
1
STARTUP(filename)
  • 像使用INPUT命令,但filename会成为第一个链接的输入文件,可用于吧第一个文件当作入口点的场景

处理目标文件格式的命令

1
2
OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)
  • OUTPUT_FORMAT(bfdname) == --oformat bfdname
1
TARGET(bfdname)
  • 设置读取输入文件时的BFD格式,类似于 -b bfdname

内存区域创建别名

1
REGION_ALIAS(alias, region)
  • .text:代码块
  • .rodata:一般表示只读数据
  • .data:可读写的已初始化的数据
  • .bss:可读写的使用0初始化的数据

其它命令

1
ASSERT(exp, message)
  • 确保 exp 不为零。 如果为零,则退出链接并显示错误代码,并打印一些相关的信息
1
ASSERT(exp, message)
  • 强制将符号作为未定义符号输入到输出文件中

段命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SECTIONS
{
sections-command
sections-command

}
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command

} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp] [,]

Output Section Address

address是输出段VMA的表达式,提供该值则输出地址被精确设置为该值

  • 区分
    • .text . : { *(.text) }:将.text的地址设置为位置计数器的值
    • .text : { *(.text) }:将.text的地址设置为位置计数器的值,该值与任何.text输入段中最严格对齐方式对齐
    • 若要在0x10字节边界上对齐段,以使字节地址的最低四位为0:.text ALIGN(0x10) : { *(.text) }

Output Section LMA

  • 使用ATAT>指令

  • AT关键字将一个表达式作为参数,规定了段的明确的加载地址

  • AT>将一个内存区域的命名作为参数

  • 若没有使用ATAT>,链接器根据以下规则确定加载地址

    • 若该段有指定的VMA,将LMA设置为VMA
    • 若该段不可分配,LMA设置为VMA;否则,若可以找到与当前段兼容的内存区域,且该区域至少包含一个段,LMA被设置为使得VMA和LMA之间的差与所定位的区域中的最后的段的VMA和LMA之间的差相同
    • 若没有声明内存区域且默认区域覆盖了整个地址空间,则采用前面的步骤
    • 若找不到合适的区域或者没有前面存在的段,则LMA被设置为等于VMA
  • 例子解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SECTIONS
    {
    .text 0x1000 : { *(.text) _etext = . ; }
    .mdata 0x2000 :
    AT ( ADDR (.text) + SIZEOF (.text) )
    { _data = . ; *(.data); _edata = . ; }
    .bss 0x3000 :
    { _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
    }
    • 输出段.text:从0x1000开始,放置所有输入段的.text;定义符号_etext,赋值为.text的结束地址
    • 输出段.mdata:虚拟地址为0x2000,实际加载到.text之后;定义符号_data,赋值为.mdata的起始地址;定义符号_edata,赋值为.mdata的结束地址
    • 输出段.bss:虚拟地址为0x3000;放置所有输入段的.bss;定义符号_bstart,赋值为.bss的起始地址;定义符号_bend,赋值为.bss的结束地址;COMMON表示还未被分配位置的未初始化的的数据目标

    此链接脚本的运行时初始化代码应该类似于下面的形式,把初始化数据从ROM镜像复制到运行时地址。注意这些代码是如何利用链接器脚本定义的符号的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    extern char _etext, _data, _edata, _bstart, _bend;
    char *src = &_etext;
    char *dst = &_data;

    /* ROM has data at end of text; copy it. */
    while (dst < &_edata)
    *dst++ = *src++;

    /* Zero bss. */
    for (dst = &_bstart; dst< &_bend; dst++)
    *dst = 0;

参考文档

linker scripts
中文翻译