链接脚本

链接脚本

程序编译的几个阶段

一般程序编译都会经过下面几个阶段,不管是PC上还是单片机上

graph TD 源码 -- *.c --> 预处理器&#40cpp&#41 -- *.i --> 编译器&#40ccl&#41 -- *.s --> 汇编器&#40as&#41 -- *.o -->链接器&#40ld&#41 -->可执行程序

目标文件结构

常见段 存放内容
text 代码
rodata 全局常量、字符串常量
bss 未初始化的全局变量和局部静态变量
data 已初始化的全局变量和局部静态变量
heap 动态分配的数据,new、malloc
stack 局部变量

LMA和VMA

  • VMA( Virtual Memory Address,虚拟内存地址): 程序在运行时访问内存的地址,即变量和函数在程序执行期间所在的内存地址。对于 MCU(微控制器),VMA 通常就是程序运行时的物理地址,因为 MCU 通常没有复杂的内存管理单元(MMU)。
  • LMA( Load Memory Address,加载内存地址): 程序在加载到内存时的实际存放地址,即程序或数据最初被存储的位置。对于 MCU,通常是指代码或初始化数据在 Flash 等非易失性存储器中的存放地址。

[!NOTE]

在MCU开发中,使用链接脚本指定每个section的LMA和VMA,以控制代码和数在存储中的布局:

  • 将初始化数据从FLASH加载到SRAM:data段的初始化值存储在FLASH(LMA),程序运行时需要复制到SRAM(VMA)
  • 设置未初始化数据的位置:bss段的变量在程序启动时被清零,直接放在SRAM,其LMA和VMA都在SRAM

MCU与现代CPU(如PC或MPU)的区别

  • MCU:
    • 缺少MMU: 大多数 MCU 没有内存管理单元,物理地址和逻辑地址是一致的
    • 直接物理寻址: 程序访问的地址就是实际的物理地址
    • 启动代码负责初始化: 复制 .data 段,清零 .bss 段。
    • 固定的内存布局: 程序和数据的物理地址在编译时确定。
    • 开发者需要明确指定 LMA 和 VMA,因为程序加载和运行在相同的物理地址空间
    • 有限的内存保护: 一些高级 MCU 提供 MPU(Memory Protection Unit),但功能有限
    • 缺乏权限控制: 程序通常可以访问所有内存区域
  • 现代CPU:
    • 拥有 MMU: 具备复杂的内存管理单元,可以进行虚拟内存地址到物理内存地址的转换
    • 支持虚拟内存: 进程运行在各自的虚拟地址空间中,地址由 MMU 映射到物理内存
    • 操作系统和 MMU 负责地址映射,开发者通常不需要直接处理物理地址
    • 动态加载: 操作系统的加载器根据可执行文件的元数据加载程序。
    • 地址重定位: 支持动态链接库和地址重定位,物理地址在运行时确定。
    • 支持虚拟内存和分页: 提供内存保护、进程隔离和内存扩展功能 , 每个进程有独立的虚拟地址空间
    • 严格的内存保护: 操作系统和 MMU 管理内存访问权限,防止非法访问
    • 用户态和内核态: 进程运行在受限的用户态,系统资源受保护

链接脚本

链接过程是将各式各样的.o文件链接为一个文件的过程。链接脚本描述连接器如何将这些输入文件(.o)文件映射为一个输出文件的,并且定义了输出文件的memory layout。几乎所有的链接脚本都是在做这些事情。

在使用ld的时候,通过-T选项,可以使用自己写的链接脚本完成链接过程,否则会使用默认的链接脚本。

链接器,它是编译过程中的最后一步,负责将各种.o文件链接为一个可执行文件或库文件。链接器需要解决两个问题:一是如何将输入文件中的section映射到输出文件中的section,并确定它们在内存中的位置和大小;二是如何解析输入文件中引用的未定义的symbol,并将它们与输出文件中定义的symbol进行匹配

section和symbol,它们是object file format中最重要的两个概念。每个object file都包含一个section列表和一个symbol列表。section是object file中存放代码或数据的区域,每个section都有一个名字、一个大小、一个数据块(除了.bss section)和一些属性(如可读、可写、可执行等)。symbol是object file中定义或引用的标识符,每个symbol都有一个名字、一个地址、一个类型(如函数、变量等)和一些属性(如全局、局部、弱等)。链接器需要根据section和symbol的信息来组合输入文件和生成输出文件

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

第一行,使用'.'给memory map定位地址。如果不适用 "."来指定开始地址,那么将会从0开始分配地址。

第二行定义了一个output scetion,名字叫'.text',后面花括号里面的内容是其他.o文件里面作为输入的section名称,输入的section会被存放到output section中。是通配符的含义,可以匹配任意文件名 ,*(.text) 意味着所有输入文件中的.text段。

因为location counter 被配置为0x10000,因此连接器会把text的内容存放到0x10000中。

后面的几行与text段同理,.data段和.bss段都是从0x8000000开始的,并且是紧紧挨在一起的。

连接器会保证每个段的对齐方式,以此作为依据来增加loaction counter。

实践链接器过程

准备三个c文件

main.c
----------------
extern int add(int a , int b);
extern int data1;
extern int data2;

//使用汇编调用系统调用结束程序,否则将运行到意外的内存地址,导致段错误
void exit()
{
    asm( "movq $66,%rdi \n\t"
         "movq $60,%rax \n\t"
         "syscall \n\t");
}

int main(void){
        add(data1,data2);
        exit();
}

data.c
-----------------
int data1=10;
int data2=20;

add.c
-----------------
int add(int a , int b){
    return a+b;
}

编译每一个c文件生成.o文件

#gcc -fno-builtin -fno-stack-protector -c *.c
gcc -c *.c

观察.o文件内容

main.o

使用objdump查看main.o文件

objdump -h main.o
main.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000049  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000089  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000089  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000089  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000b5  2**0
                  CONTENTS, READONLY
  5 .note.gnu.property 00000020  0000000000000000  0000000000000000  000000b8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

可以看到.data和.bss都为0,.text代码段存在数据

查看反汇编

objdump -s -d main.o
main.o:     file format elf64-x86-64

Contents of section .text:
 0000 f30f1efa 554889e5 48c7c742 00000048  ....UH..H..B...H
 0010 c7c03c00 00000f05 905dc3f3 0f1efa55  ..<......].....U
 0020 4889e58b 15000000 008b0500 00000089  H...............
 0030 d689c7e8 00000000 b8000000 00e80000  ................
 0040 0000b800 0000005d c3                 .......].
Contents of section .comment:
 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11
 0010 2e342e30 2d317562 756e7475 317e3232  .4.0-1ubuntu1~22
 0020 2e303429 2031312e 342e3000           .04) 11.4.0.
Contents of section .note.gnu.property:
 0000 04000000 10000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 1b000000 00450e10 8602430d  .........E....C.
 0030 06520c07 08000000 1c000000 3c000000  .R..........<...
 0040 00000000 2e000000 00450e10 8602430d  .........E....C.
 0050 06650c07 08000000                    .e......

Disassembly of section .text:

0000000000000000 <exit>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 c7 c7 42 00 00 00    mov    $0x42,%rdi
   f:   48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
  16:   0f 05                   syscall
  18:   90                      nop
  19:   5d                      pop    %rbp
  1a:   c3                      ret

000000000000001b <main>:
  1b:   f3 0f 1e fa             endbr64
  1f:   55                      push   %rbp
  20:   48 89 e5                mov    %rsp,%rbp
  23:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 29 <main+0xe>
  29:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 2f <main+0x14>
  2f:   89 d6                   mov    %edx,%esi
  31:   89 c7                   mov    %eax,%edi
  33:   e8 00 00 00 00          call   38 <main+0x1d>
  38:   b8 00 00 00 00          mov    $0x0,%eax
  3d:   e8 00 00 00 00          call   42 <main+0x27>
  42:   b8 00 00 00 00          mov    $0x0,%eax
  47:   5d                      pop    %rbp
  48:   c3                      ret

可以看到一个需要链接的地方

  18:   e8 00 00 00 00          call   1d <main+0x1d>

data.o

objdumo -h data.o
data.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000000  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000048  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000048  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000074  2**0
                  CONTENTS, READONLY
  5 .note.gnu.property 00000020  0000000000000000  0000000000000000  00000078  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

可以看到.data段存在数据,.text代码段为空

objdump -s -d data.o
data.o:     file format elf64-x86-64

Contents of section .data:
 0000 0a000000 14000000                    ........
Contents of section .comment:
 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11
 0010 2e342e30 2d317562 756e7475 317e3232  .4.0-1ubuntu1~22
 0020 2e303429 2031312e 342e3000           .04) 11.4.0.
Contents of section .note.gnu.property:
 0000 04000000 10000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000 00000000  ................

反汇编后不存在代码

add.o

objdump -h add.o
add.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000018  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000058  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000058  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000058  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000084  2**0
                  CONTENTS, READONLY
  5 .note.gnu.property 00000020  0000000000000000  0000000000000000  00000088  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .eh_frame     00000038  0000000000000000  0000000000000000  000000a8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

和main.o一样,.data和.bss无数据,.text存在数据

objdump -s -d add.o
add.o:     file format elf64-x86-64

Contents of section .text:
 0000 f30f1efa 554889e5 897dfc89 75f88b55  ....UH...}..u..U
 0010 fc8b45f8 01d05dc3                    ..E...].
Contents of section .comment:
 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11
 0010 2e342e30 2d317562 756e7475 317e3232  .4.0-1ubuntu1~22
 0020 2e303429 2031312e 342e3000           .04) 11.4.0.
Contents of section .note.gnu.property:
 0000 04000000 10000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 18000000 00450e10 8602430d  .........E....C.
 0030 064f0c07 08000000                    .O......

Disassembly of section .text:

0000000000000000 <add>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   89 75 f8                mov    %esi,-0x8(%rbp)
   e:   8b 55 fc                mov    -0x4(%rbp),%edx
  11:   8b 45 f8                mov    -0x8(%rbp),%eax
  14:   01 d0                   add    %edx,%eax
  16:   5d                      pop    %rbp
  17:   c3                      ret

可以看到在汇编中存在

  14:   01 d0                   add    %edx,%eax

链接脚本

假定将代码段放在0x10000,数据段放在0x80000

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

链接

ld -e main add.o data.o main.o -T my.ld -o hello.out

-e main:指定从main开始执行,这里将main函数的名字改成其它名字也可以,从main开始执行是glibc规定的,但是这里并没有使用glibc,故可以随意指定开始函数

观察输出文件

objdump -h hello.out
hello.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000061  0000000000010000  0000000000010000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000078  0000000000010068  0000000000010068  00001068  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.property 00000020  00000000000100e0  00000000000100e0  000010e0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data         00000008  0000000000080000  0000000000080000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .comment      0000002b  0000000000000000  0000000000000000  00002008  2**0
                  CONTENTS, READONLY

可以看到数据段和代码段都存在数据

反汇编

objdump -s -d hello.out
hello.out:     file format elf64-x86-64

Contents of section .text:
 10000 f30f1efa 554889e5 897dfc89 75f88b55  ....UH...}..u..U
 10010 fc8b45f8 01d05dc3 f30f1efa 554889e5  ..E...].....UH..
 10020 48c7c742 00000048 c7c03c00 00000f05  H..B...H..<.....
 10030 905dc3f3 0f1efa55 4889e58b 15c3ff06  .].....UH.......
 10040 008b05b9 ff060089 d689c7e8 b0ffffff  ................
 10050 b8000000 00e8beff ffffb800 0000005d  ...............]
 10060 c3                                   .
Contents of section .eh_frame:
 10068 14000000 00000000 017a5200 01781001  .........zR..x..
 10078 1b0c0708 90010000 1c000000 1c000000  ................
 10088 78ffffff 18000000 00450e10 8602430d  x........E....C.
 10098 064f0c07 08000000 1c000000 3c000000  .O..........<...
 100a8 70ffffff 1b000000 00450e10 8602430d  p........E....C.
 100b8 06520c07 08000000 1c000000 5c000000  .R..........\...
 100c8 6bffffff 2e000000 00450e10 8602430d  k........E....C.
 100d8 06650c07 08000000                    .e......
Contents of section .note.gnu.property:
 100e0 04000000 10000000 05000000 474e5500  ............GNU.
 100f0 020000c0 04000000 03000000 00000000  ................
Contents of section .data:
 80000 0a000000 14000000                    ........
Contents of section .comment:
 0000 4743433a 20285562 756e7475 2031312e  GCC: (Ubuntu 11.
 0010 342e302d 31756275 6e747531 7e32322e  4.0-1ubuntu1~22.
 0020 30342920 31312e34 2e3000             04) 11.4.0.

Disassembly of section .text:

0000000000010000 <add>:
   10000:       f3 0f 1e fa             endbr64
   10004:       55                      push   %rbp
   10005:       48 89 e5                mov    %rsp,%rbp
   10008:       89 7d fc                mov    %edi,-0x4(%rbp)
   1000b:       89 75 f8                mov    %esi,-0x8(%rbp)
   1000e:       8b 55 fc                mov    -0x4(%rbp),%edx
   10011:       8b 45 f8                mov    -0x8(%rbp),%eax
   10014:       01 d0                   add    %edx,%eax
   10016:       5d                      pop    %rbp
   10017:       c3                      ret

0000000000010018 <exit>:
   10018:       f3 0f 1e fa             endbr64
   1001c:       55                      push   %rbp
   1001d:       48 89 e5                mov    %rsp,%rbp
   10020:       48 c7 c7 42 00 00 00    mov    $0x42,%rdi
   10027:       48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
   1002e:       0f 05                   syscall
   10030:       90                      nop
   10031:       5d                      pop    %rbp
   10032:       c3                      ret

0000000000010033 <main>:
   10033:       f3 0f 1e fa             endbr64
   10037:       55                      push   %rbp
   10038:       48 89 e5                mov    %rsp,%rbp
   1003b:       8b 15 c3 ff 06 00       mov    0x6ffc3(%rip),%edx        # 80004 <data2>
   10041:       8b 05 b9 ff 06 00       mov    0x6ffb9(%rip),%eax        # 80000 <data1>
   10047:       89 d6                   mov    %edx,%esi
   10049:       89 c7                   mov    %eax,%edi
   1004b:       e8 b0 ff ff ff          call   10000 <add>
   10050:       b8 00 00 00 00          mov    $0x0,%eax
   10055:       e8 be ff ff ff          call   10018 <exit>
   1005a:       b8 00 00 00 00          mov    $0x0,%eax
   1005f:       5d                      pop    %rbp
   10060:       c3                      ret

MCU上使用链接脚本

MEMORY {
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
    .text : {
        *(.text)
        *(.rodata)
    } > FLASH
    __text_end = .;
     .data  :  AT(__text_end){
        *(.data)
    } > RAM
    .bss (NOLOAD):
    {
        *(.bss)
    } > RAM AT>RAM
}

MEMORY 块

MEMORY 块定义了链接器可用来放置段的内存区域

  • FLASH区域:
    • 属性:rx(可读、可执行)
    • 起始地址:0x08000000
    • 长度:512K
  • RAM区域:
    • 属性:rwx(可读、可写、可执行)
    • 起始地址:0x20000000
    • 长度:128K

SECTIONS 块

SECTIONS 块定义了程序中不同段在内存中的布局

.text段

.text : {
    *(.text)
} > FLASH

内容: 包含所有输入文件中的 .text 段(*(.text)) 和.rodata段(*(.rodata)

VMA分配:

  • .text 段被放置在 FLASH 区域。这意味着它的 VMA(运行时地址)从 FLASH 中的当前地址计数器(.)开始

LMA:

  • 由于未指定 AT 属性,LMA 默认为 VMA。因此,.text 段的 LMA 和 VMA 都在 FLASH
  • 代码不需要复制到 RAM;它直接在 FLASH 中运行

.data段

.data  :  AT(__text_end){
    *(.data)
} > RAM

内容: 包含所有输入文件中的 .data 段(*(.data)

VMA分配:

  • .data 段被放置在 RAM 区域。这意味着它的 VMA(运行时地址)从 RAM 中的当前地址计数器(.)开始

LMA分配:

  • .data 段的 LMA 被设置为 __text_end,即 .text 段在 FLASH 中的结束地址
  • .data 段的初始化数据紧接在 .text 段之后存储在 FLASH

:::color1
在运行时,这些变量需要位于 RAM 中,以便被读取和修改,但是初始值配存储与PFLASH中,故在MCU启动时, 初始值从其在 FLASH 中的 LMA 复制到其在 RAM 中的 VMA

:::

.bss段

.bss (NOLOAD):
{
    *(.bss)
} > RAM AT>RAM

内容: 包含所有输入文件中的 .bss 段(*(.bss)),.bss 段包含未初始化的全局和静态变量,这些变量被初始化为零

NOLOAD 属性告诉链接器,该段不应在输出文件中占用空间

VMA分配:

  • .bss 段被放置在 RAM 区域。其 VMARAM 中的当前地址计数器开始

LMA分配(AT>RAM):

  • LMA 也被分配到 RAM 区域。由于 LMA 区域与 VMA 区域相同,LMA 和 VMA 相等

MEMORY块

Using LD, the GNU linker - Command Language

MEMORY
  {
    name [(attr)] : ORIGIN = origin, LENGTH = len
    …
  }
  • attr 字符是一个可选的属性列表,用于指定是否对链接器脚本中未显式映射的输入段使用特定的内存区域
    • R只读段
    • W可写段
    • X可执行段
    • A可分配段
    • I已初始化段
    • L类似于I
    • !反转后面所有属性
  • origin是内存区域起始地址的数值表达式。表达式的计算结果必须为常量,并且不能包含任何符号
  • len是内存区域的字节大小的表达式。与原始表达式一样,表达式必须仅为数值,并且必须计算为常量

output section描述

Using LD, the GNU linker - Command Language

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 Type: 输出段类型
  • Output Section LMA: 输出段LMA —加载地址
  • Forced Output Alignment: 强制输出对齐
  • Forced Input Alignment: 强制输入对齐
  • Output Section Constraint: 输出段限制
  • Output Section Region: 输出段区域
  • Output Section Phdr: 输出段phdr
  • Output Section Fill: 输出段填充

Output Section LMA

每个段有一个虚拟地址(VMA)和一个加载地址(LMA),加载地址由 ATAT>关键字指定

AT关键字把一个表达式当作自己的参数。这将指定段的实际加载地址

关键字 AT>使用内存区域的名字作为参数

AT(lma) 用于明确指定输出段的加载地址(LMA)

AT>lma_region 用于将输出段的加载地址分配到**指定的内存区域, 由链接器根据内存区域自动计算实际的加载地址 **

[!NOTE]

如果没有为可分配段使用 ATAT>,链接器会使用下面的方式尝试来决定加载地址:

  • 如果段有一个特定的VMA地址,则LMA也使用该地址。
  • 如果段为不可分配的则LMA被设置为它的VMA
posted @ 2025-04-18 11:15  Midraos  阅读(49)  评论(0)    收藏  举报