鏈接腳本官方文檔:
https://sourceware.org/binutils/docs-2.39/ld.html
鏈接腳本實例:(STM32F407VG,RT-Thread Studio生成的工程所含)
/* * linker script for STM32F407ZG with GNU ld */ /* Program Entry, set to mark it as "used" and avoid gc */ MEMORY { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 1024k /* 1024K flash */ RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128k /* 128K sram */ } ENTRY(Reset_Handler) _system_stack_size = 0x400; SECTIONS { .text : { . = ALIGN(4); _stext = .; KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); *(.text) /* remaining code */ *(.text.*) /* remaining code */ *(.rodata) /* read-only data (constants) */ *(.rodata*) *(.glue_7) *(.glue_7t) *(.gnu.linkonce.t*) /* section information for finsh shell */ . = ALIGN(4); __fsymtab_start = .; KEEP(*(FSymTab)) __fsymtab_end = .; . = ALIGN(4); __vsymtab_start = .; KEEP(*(VSymTab)) __vsymtab_end = .; /* section information for utest */ . = ALIGN(4); __rt_utest_tc_tab_start = .; KEEP(*(UtestTcTab)) __rt_utest_tc_tab_end = .; /* section information for at server */ . = ALIGN(4); __rtatcmdtab_start = .; KEEP(*(RtAtCmdTab)) __rtatcmdtab_end = .; . = ALIGN(4); /* section information for initial. */ . = ALIGN(4); __rt_init_start = .; KEEP(*(SORT(.rti_fn*))) __rt_init_end = .; . = ALIGN(4); PROVIDE(__ctors_start__ = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array)) PROVIDE(__ctors_end__ = .); . = ALIGN(4); _etext = .; } > ROM = 0 /* .ARM.exidx is sorted, so has to go in its own output section. */ __exidx_start = .; .ARM.exidx : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) /* This is used by the startup in order to initialize the .data secion */ _sidata = .; } > ROM __exidx_end = .; /* .data section which is used for initialized data */ .data : AT (_sidata) // 設置加載首地址為 _sidata,位於ROM區 { . = ALIGN(4); /* This is used by the startup in order to initialize the .data secion */ _sdata = . ; *(.data) *(.data.*) *(.gnu.linkonce.d*) PROVIDE(__dtors_start__ = .); KEEP(*(SORT(.dtors.*))) KEEP(*(.dtors)) PROVIDE(__dtors_end__ = .); . = ALIGN(4); /* This is used by the startup in order to initialize the .data secion */ _edata = . ; } >RAM // 設置運行地址位於RAM區 .stack : { . = ALIGN(4); _sstack = .; . = . + _system_stack_size; . = ALIGN(4); _estack = .; } >RAM __bss_start = .; .bss : { . = ALIGN(4); /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; *(.bss) *(.bss.*) *(COMMON) . = ALIGN(4); /* This is used by the startup in order to initialize the .bss secion */ _ebss = . ; *(.bss.init) } > RAM // bss段只設置運行地址位於RAM區,不占用ROM區 __bss_end = .; _end = .; /* Stabs debugging sections. */ .stab 0 : { *(.stab) } .stabstr 0 : { *(.stabstr) } .stab.excl 0 : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 0 : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 0 : { *(.comment) } /* DWARF debug sections. * Symbols in the DWARF debugging sections are relative to the beginning * of the section so we begin them at 0. */ /* DWARF 1 */ .debug 0 : { *(.debug) } .line 0 : { *(.line) } /* GNU DWARF 1 extensions */ .debug_srcinfo 0 : { *(.debug_srcinfo) } .debug_sfnames 0 : { *(.debug_sfnames) } /* DWARF 1.1 and DWARF 2 */ .debug_aranges 0 : { *(.debug_aranges) } .debug_pubnames 0 : { *(.debug_pubnames) } /* DWARF 2 */ .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_line 0 : { *(.debug_line) } .debug_frame 0 : { *(.debug_frame) } .debug_str 0 : { *(.debug_str) } .debug_loc 0 : { *(.debug_loc) } .debug_macinfo 0 : { *(.debug_macinfo) } /* SGI/MIPS DWARF 2 extensions */ .debug_weaknames 0 : { *(.debug_weaknames) } .debug_funcnames 0 : { *(.debug_funcnames) } .debug_typenames 0 : { *(.debug_typenames) } .debug_varnames 0 : { *(.debug_varnames) } }
特別注意:
1 .text :{} .stack :{} 表示輸出文件包含的 section
2 {}里面的 section,是輸入文件的 section,比如 *(.isr_vector) *(.text) *(.rodata) 這些 .isr_vector section .text section .rodata section,都有指定輸入文件,*表示所有的輸入文件;所以 *(.isr_vector) 表示從所有的輸入文件中獲取所有 .isr_vector section 放在一塊連續的地址空間;main.o(.data) 表示從 main.o文件中獲取所有的 .data section 放在一塊連續的地址空間
3 鏈接腳本從上往下,如果輸入文件 A 已經被取出 .text section,此后輸入文件 A 就沒有 .text section,不能再被獲取
4 關於 section 的命名,名字前可以包含 . ,也可以不包含,大多取名會包含 . 。
概述:
鏈接器:把一個或多個輸入文件合並成一個輸出文件,輸入文件是目標文件或者鏈接腳本文件,輸出文件是目標文件(庫文件)或者可執行文件,鏈接器從鏈接腳本讀完一個 section 后,將定位器符號的值增加該 section 的大小
鏈接腳本:控制輸出文件內各部分在程序地址空間內的布局,地址空間包括 ROM 和 RAM
-T 選項用於指定自己的鏈接腳本,否則使用默認的鏈接腳本
語法:
定位符 .
. 是定位器符號,可以對定位器符號賦值指定接下來內容的存儲位置,如“ .= 0x10000”,也可以通過定位器符獲取此位置的地址,比如 ”_stext = .;”,此后就可以用變量 _stext 表示此位置地址
入口地址
ENTRY(SYMBOL):將符號 SYMBOL 的值設置為入口地址,入口地址是進程執行的第一條指令在進程地址空間的地址(比如 ENTRY(Reset_Handler) 表示進程最開始從復位中斷服務函數處執行)
有多種方法設置進程入口地址,以下編號越小,優先級越高
1、ld 命令行的 -e 選項
2、鏈接腳本的 ENTRY(SYMBOL) 命令
3、在匯編程序中定義了 start 符號,使用 start 符號值
4、如果存在 .text section,使用 .text section 首地址的值
5、使用地址 0 的值
注:對於使用BIN文件的STM32,芯片決定了固定從0x0000_0004地址取值初始化PC,然后運行,因為0x0000_0004存的是復位中斷地址,所以程序從復位中斷開始運行;對於可執行文件是ELF,可以通過ENTRY()設置入口地址。
內存布局
腳本中以MEMORY命令定義了存儲空間,其中以ORIGIN定義地址空間的起始地址,LENGTH定義地址空間的長度。
結構:
MEMORY {
NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
…
}
NAME :存儲區域的名字。(自己可以隨意命名)
ATTR :定義該存儲區域的屬性。ATTR屬性內可以出現以下7 個字符:
R 只讀section
W 讀/寫section
X 可執行section
A 可分配的section
I 初始化了的section
L 同 I
! 不滿足該字符之后的任何一個屬性的section
ORIGIN :關鍵字,區域的開始地址,可簡寫成 org 或 o
LENGTH :關鍵字,區域的大小,可簡寫成 len 或 l
例子:
MEMORY { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 1024k /* 1024K flash */ RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128k /* 128K sram */ }
在鏈接文件中定義的變量(符號)可以在目標文件中使用
在鏈接文件中定義變量
_init_start = .; .application_init : { *(.application_init) } _init_end = .;
在源文件中使用變量 _init_start
#include <stdio.h> #include <string.h> struct _s_application_init { int(*function)(void); }; extern struct _s_application_init _init_start;//段".application_init"的起始地址,在*.lds文件中定義 extern struct _s_application_init _init_end;//段".application_init"的末尾地址,在*.lds文件中定義 #define __app_init_section __attribute__((section(".application_init"))) #define __application_init(function) \ struct _s_application_init _s_a_init_##function __app_init_section = {function} static int application_init_a(void) { printf("execute funtion : %s\n", __FUNCTION__); return 0; } __application_init(application_init_a); // 函數地址存在指定的段
int main(int argc, char **argv) { /* * 從段的起始地址開始獲取數據,直到段的末尾地址 */ struct _s_application_init *pf_init = &_init_start; do { printf("Load init function from address %p\n", pf_init); pf_init->function(); ++pf_init; } while (pf_init < &_init_end); return 0; }
PROVIDE 關鍵字(感覺有無 PROVIDE 修飾都可以被輸入文件引用,看上面的例子)
該關鍵字定義一個(輸入文件內被引用但沒定義)符號。相當於定義一個全局變量的符號表,其他C文件可以通過該符號來操作對應的存儲內存。
SECTIONS
{
.text :
{
*(.text) PROVIDE(_etext = .);
} }
如上,在鏈接腳本中聲明了_etext 符號。特別注意的是_etext 只是一個符號,沒有存儲內存,並不是一個變量,該符號對應(映射)的是一個地址,其地址為 .text section 之后的第一個地址。C文件中引用用法如下:
int main()
{
//引用該變量
extern char _etext;
char *p = &_etext;
//... }
若在鏈接腳本中 " _etext = 0x100; ",即表示符號_etext對應的地址為0x100, 此時 & _etext的值為 0x100, char a= *p;表示為 從0x100地址取存儲的值賦值給變量a
在目標文件內定義的符號可以在鏈接腳本內被賦值
此時該符號被定義為全局的. 每個符號都對應了一個地址, 此處的賦值是更改這個符號對應的地址.
e.g. 通過下面的程序查看變量a的地址:
/* a.c */ #include <stdio.h> #include <stdlib.h> int a = 100; int main(void) { printf( “&a=0x%p“, &a ); return 0; }
文件a.lds
/* a.lds :注意格式有要求*/ a = 3;
編譯命令:$gcc -Wall -o a-without-lds a.c,執行./a-without-lds輸出&a = 0×8049598.
編譯命令:$gcc -Wall -o a-with-lds a.c a.lds,執行a-with-lds,輸出&a = 0×3
對符號的賦值只對全局變量起作用! 一些簡單的賦值語句,能使用任何c語言內的賦值
除了可以在 C源文件中指定函數屬於某個 section,匯編文件也可以,比如 startup.s
.section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr sp, =_estack /* set stack pointer */ bl entry bx lr .size Reset_Handler, .-Reset_Handler
.section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler
section 結構
SECTIONS { ... secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)] { contents } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP] ... }
[ ]內的內容是可選選項
secname: 表示輸出文件的 section 名,即輸出文件中有哪些 section。而contents就是描述輸出文件的這個 section 內容從哪些輸入文件的哪些 section 里抽取而來。
輸出section名字必須符合輸出文件格式要求,比如:a.out格式的文件只允許存在.text、.data和.bss section名。而有的格式只允許存在數字名字,那么此時應該用引號將所有名字內的數字組合在一起;另外,還有一些格式允許任何序列的字符存在於 section名字內,此時如果名字內包含特殊字符(比如空格、逗號等),那么需要
用引號將其組合在一起。
如下,將輸入文件的數據段存放在輸出文件的數據段(section 名自己定義,section 名前后必須要有空格)
SECTIONS
{
...
.data :
{
main.o(.data)
*(.data)
}
...
}
其中 *(.data) 表示將所有的輸入文件的 .data section 鏈接到輸出文件 .data section 中, 特別注意的是,之前鏈接的就不會再鏈接,這樣做的目的是可以將某些特殊的輸入文件鏈接到地址前面。
start_addr :表示將某個段強制鏈接到的地址( VMA ),start_addr 改變定位符的值。
SECTIONS { .bss : { . = ALIGN(4); /* Align the start of the section */ _sbss = .; /* Provide the name for the start of this section */ *(.bss) *(.bss.*) . = ALIGN(512); USB_RAM_START = .; . += USB_RAM_GAP; . = ALIGN(4); /* Align the end of the section */ } > RAM _ebss = .; /* Provide the name for the end of this section */ m_usb_bdt USB_RAM_START (NOLOAD) : { *(m_usb_bdt) USB_RAM_BDT_END = .; } m_usb_global USB_RAM_BDT_END (NOLOAD) : { *(m_usb_global) } }
TYPE:每個輸出section都有一個類型,如果沒有指定TYPE類型,那么鏈接器根據輸出section引用的輸入section的類型設置該輸出section的類型。它可以為以下五種值
- NOLOAD 該section在程序運行時,不被載入內存。
- DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向后兼容才被保留下來。這種類型的section必須被標記為“不可加載的”,以便在程序運行不為它們分配內存。
AT( LAM_ADDR ):輸出 section 的 LMA,默認情況下 LMA 等於 VMA,但可以通過關鍵字 AT() 指定 LMA。
用關鍵字 AT()
指定,括號內包含表達式,表達式的值用於設置LMA。如果不用AT()
關鍵字,那么可用AT>LMA_REGION
表達式設置指定該section加載地址的范圍。這個屬性主要用於構建ROM鏡像。
[>REGION]:這個region就是前面說的MEMORY命令定義的位置信息,用於指定section在哪個memory執行,也就是VMA。如果不指定LMA,LMA = VMA。
VMA 和 LMA
section包含兩個地址:VMA
(virtual memory address虛擬內存地址)和LMA
(load memory address加載內存地址)。通常VMA和LMA是相同的。
- VMA是執行輸出文件時section所在的地址
- LMA是加載輸出文件時section所在的地址
要作為運行地址,首先PC指針要能在這個地址空間內跑動,所以這段地址空間必須是可隨機尋址的,也就是說可以訪問想要訪問的地址,如RAM,NorFlash(Norflash帶有SRAM接口,有足夠的地址引腳來尋址,可以很容易地存取其內部的每一個字節);如果是NandFlash,則不行,因為NandFlash只能通過一個塊一個塊的讀/寫,不能做到隨機尋址。
VMA和LMA大多數情況下是相等的,但也可以不相等。通常,當LMA地址空間不支持隨機尋址,或者是嫌棄LMA地址空間的訪問速度比較慢時(比如NorFlash速度比SDRAM慢),則會將VMA設置到RAM中,這時,VMA與LMA就不相等了。
例:NandFlash因為不能隨機訪問想要訪問的每個地址,不能作為運行地址,所以這里想要把在NandFlash的代碼復制到SDRAM中。這里除了代碼中要加入復制模塊外,還要在鏈接腳本中使NandFlash部分的VMA設為SDRAM地址。大多數情況下,是在起始代碼中初始化時就將需要復制的部分復制的到VMA地址空間中。
還有一種情況就是當嵌入式系統中先都將代碼和數據加載到了ROM中,此時的地址就是LMA,但是當開始運行之后,需要將data數據部分拷貝到RAM中,此時數據的地址就是VMA,本文最開始的鏈接腳本,就是把.data的LMA設置在ROM,VMA設置在RAM。
KEEP 關鍵字
在鏈接命令行內使用了選項 -gc-sections 后,鏈接器可能將某些它認為沒用的 section 過濾掉,此時就有必要強制讓鏈接器保留一些特定的 section,可用 KEEP() 關鍵字達此目的。如 KEEP(* (.text)) 或 KEEP(SORT(*)(.text))。說的通俗易懂就是:防止被優化。
ALIGN 關鍵字
表示字節對齊, 如 “ . = ALIGN(4);”表示從該地址開始后面的存儲進行4字節對齊。
實例詳解:
SECTIONS
{
.= 0x10000;
.text : { *(.text) } .= 0×8000000; .data : { *(.data) } .bss : { *(.bss) } }
解釋一下上訴的例子:
.= 0x10000:把定位器符號置為 0x10000(若不指定,則該符號的初始值為0)
.text : { *(.text) }:*符號代表所有的輸入文件,此句表示獲取所有輸入文件的 .text section放在一塊連續的地址空間,首地址由上一句的定位器符號確定,即 0x10000
.= 0x8000000:把定位器符號置為 0x8000000
.data : { *(.data) }:獲取所有輸入文件的 .data section 放在一塊連續的地址空間,該 section 的首地址為 0x8000000
.bss : { *(.bss) }:獲取所有輸入文件的 .bss section 放在一塊連續的地址空間,該 section 的首地址為 0x8000000 + .data section 的大小
輸出文件包含 .text section .data section .bss section