LINUX-Linker scripts
Linker command scripts
下面範例將討論每個章節的細節
概括的說它定義了四個記憶體區塊( vect, rom, ram and cache)跟五個區段(vect, text, bss, init, and stack)
在採用段式記憶體管理的架構中,BSS段(bss segment)通常是指用來存放程序中未初始化的全局變數的一塊記憶體區域。
BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態記憶體分配。
在採用段式記憶體管理的架構中,數據段(data segment)通常是指用來存放程序中已初始化的全局變數的一塊記憶體區域。
數據段屬於靜態記憶體分配。
範例
/* a list of files to link (others may be supplied on the command line) */
INPUT(libc.a libg.a libgcc.a libc.a libgcc.a)
/* output format (can be overridden on command line) */
OUTPUT_formAT("coff-sh")
/* output filename (can be overridden on command line) */
OUTPUT_FILENAME("main.out")
/* our program’s entry point; not useful for much except to make sure the S7 record
is proper, because the reset vector actually defines the "entrypoint" in most embedded systems */
ENTRY(_start)
/* list of our memory sections */
MEMORY
{
vect : o = 0, l = 1k
rom : o = 0x400, l = 127k
ram : o = 0x400000, l = 128k
cache : o = 0xfffff000, l = 4k
}
/* how we’re organizing memory sections defined in each module */
SECTIONS
{
/* the interrupt vector table */
.vect :
{
__vect_start = .;
*(.vect);
__vect_end = .;
} > vect
/* code and constants */
.text :
{
__text_start = .;
*(.text)
*(.strings)
__text_end = .;
} > rom
/* uninitialized data */
.bss :
{
__bss_start = . ;
*(.bss)
*(COMMON)
__bss_end = . ;
} > ram
/* initialized data */
.init : AT (__text_end)
{
__data_start = .;
*(.data)
__data_end = .;
} > ram
/* application stack */
.stack :
{
__stack_start = .;
*(.stack)
__stack_end = .;
} > ram
}
注意!
ld將使用預設的命令檔,除非你告訴它使用其他方法
去指示ld去使用你的命令檔則給gcc -Wl,T OUTPUT_formAT命令
支援多種格式的輸出檔,包括S-records (srec), binary (binary), Intel Hex (ihex), 跟數個 debug-aware formats
像是COFF (coff-sh for SH-2 targets, coff-m68k for CPU32, 等等.)
使用objdump工具去找到你的linker版本有支援的格式
MEMORY命令
MEMORY命令描述目標系統的記憶體映射
典型簡單的語法如下
MEMORY {
name : o = origin, l = length
name : o = origin, l = length
...
}
SECTIONS命令
在SECTIONS命令的述語(Statements)是描述每一個輸出區段的配置而且詳細述語輸入區段
你可以只允許一個SECTIONS述語(Statements)在每個命令檔
但如果必要的話也可以有很多述語(Statements)在裡面
在這個例子,述語(statement)如下
/* code and constants */
.text :
/*一開始定義一個區段叫'.text'然後將述語(Statements)包含在{}裡*/
{
__text_start = .;
/*建立一個符號 叫__text_start 而且配置區段起始點*/
*(.strings)
*(.text)
/*合併全部輸入檔的.text 和 .strings 到這各個區段*/
__text_end = .;
/*建立一個符號 叫__text_end 而且配置區段結束點*/
/*最後結束述語*/
} > rom
/*告訴連結器(linker)定這個記憶體區段叫'rom' 根據MEMORY命令知道起始位址為0X400*/
輸入區段的列表也可以被歸成特定的檔案.如你增加一個像 foo.o (.specialsection)
到.text區段定義,然而連結器也將檔案foo.o的.specialsection區段合併到.text
bss/ data / code 相關概念 如下圖所示
AT指令
AT指令去告訴連結器(linker)去載入區段的資料到何處,
最好的了解AT指令的方法就是用例子解釋
所以考慮一個應用在只有一個初始全域變數
int a_global = 102;
在編譯的時候,gcc將宣告一個整數物件 a_global 跟 值=102 在模組的.data區段
但在連結.data區段時藉由一個AT指令,我們告訴連結器去分配a_global在一個位置(如RAM)的位址.但配置初始值到其他的地方(如 __text_end , 通常是 ROM)
跟隨著code初始a_global(跟其他初始的全域資料)
這個code使用符號_text_end, _data_start , 跟 _data_end 去找到初始值,決定它的大小跟配置它到RAM適當的位置
extern const char _text_start,
_text_end;
extern char _data_start,
_data_end;
memcpy( &_data_start, &_text_end,
&_data_end - &_data_start );
現在把它全部放在一起,跟隨命令列告訴gcc去編譯一個檔案 main.c 而且連結它使用 連結命令檔案 main.cmd gcc -g -Wl,-Tmain.cmd main.c
//-----------------------------------------------------------------------------------------
範例 u-boot lds
下面,結合u-boot.lds看看一個正式的連接腳本檔案。這個檔案的基本功能還能看明白,雖然上面分析了好多,但其中那些GNU風格的符號還是著實讓我感到迷惑,好菜啊,怪不得連被3家公司鄙視,自己鄙視自己。。。
OUTPUT_FORMAT("elf32littlearm", "elf32littlearm", "elf32littlearm")
;指定輸出可執行檔案是elf格式,32位ARM指令,小端
OUTPUT_ARCH(arm)
;指定輸出可執行檔案的平台為ARM
ENTRY(_start)
;指定輸出可執行檔案的起始代碼段為_start.
SECTIONS
{
. = 0x00000000 ; 從0x0位置開始
. = ALIGN(4) ; 代碼以4位元組對齊
.text : ;指定代碼段
{
cpu/arm920t/start.o (.text) ; 代碼的第一個代碼部分
*(.text) ;其它代碼部分
}
. = ALIGN(4)
.rodata : { *(.rodata) } ;指定只讀數據段
. = ALIGN(4);
.data : { *(.data) } ;指定讀/寫數據段
. = ALIGN(4);
.got : { *(.got) } ;指定got段, got段式是uboot自定義的一個段, 非標準段
__u_boot_cmd_start = . ;把__u_boot_cmd_start賦值為當前位置, 即起始位置
.u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在該段.
__u_boot_cmd_end = .;把__u_boot_cmd_end賦值為當前位置,即結束位置
. = ALIGN(4);
__bss_start = .; 把__bss_start賦值為當前位置,即bss段的開始位置
.bss : { *(.bss) }; 指定bss段
_end = .; 把_end賦值為當前位置,即bss段的結束位置
}
相關連結
http://www.delorie.com/gnu/docs/binutils/ld.html#SEC_Top
节:节名称,大小,节数据
输出节:输出文件中的节
加载节:节内数据,在运行时,被加载到内存
重加载节:节内无数据,在内存中要预留一个空间
调试节:含调试信息的节
符号名:带双引号, 不带双引号(不能同已有的关键字冲突)
在输入节中,未处理的孤儿节,被链接器放置在具有相同属性节的后面,若放不下,则放在文件尾部
ALIGN(exp,align) 按指定数据对齐 等同于ALIGN(.,align)
ORIGIN(ram) 计算内存区域的起址地址
LENGTH(ram) 计算内存区域的长度
. 表示定位计数器
VMA(virtual memory address):运行时的地址
LMA(load memory address):存储地址
输出节
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
SECTION输出节名
ADDRESS:运行的地址,若无,则用REGION设置它,若也无REGION,则用当前定位计数器
TYPE:默认即可, 也可用NOLOAD表示,这个节,运行时不加载
>REGION 将这个输入节放到哪个内存中
LMA_REGION
=FILLEXP 填充空白的数据
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
…
}
name 内存区名称
attr 属性 R W X A(Allocatable section) I(Initialized section) L(Same as 'I') !(插入以前的属性)
ORIGIN 开始地址
LENGTH 区域长度
程序头
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}

ld,即GNU的连接工具,用于将各目标文件合并在一起,并重新安排他们的数据以及符号的引用,常常是程序编译的最后一步。
ld scripts 即ld脚本。ld 脚本的主要目的是要描述怎样将输入文件的各段印象到输出文件中去。它控制输出文件在内存的布局情况。
--------------------------------------------------------------------------------
关于VMA ,LMA
每一个可装载的输出段都有两个地址:VMA(Virtual memory address) 和 LMA(Load mem
ory
address)VMA 是输出段运行时的地址,LMA
则是输出段被装载的地址。而这2个地址常常是相同的。在某些情况下二者是有区别的。比如,一个data段被装载到ROM中,然后在程序启动的时候被拷贝到
了RAM中去。(这种技术常常用在以ROM 为基础的系统中,用来初始化全局变量,而我们的系统的处理方法可能与此类似?)
--------------------------------------------------------------------------------
关于SECTIONS命令的使用
SECTIONS告诉LD怎样将输入的段印象到输出的段,以及怎样将输出的段装载到内存中去。这是我们在内存布局中常常要用到的命令。段的基本结构如下:
SECTIONS
{
sections- command
sections- command
...
}
其中的sections-command 可作如下选择:
* 程序入口点设置命令ENTRY。
* 符号赋值
* 输出段描述(下面会提到)
* 覆盖描述。(overlay description)
1.输出段描述(Output Section Description):
输出段描述的完整格式如下:
SECTION [ address] [( type)] : [AT( LMA)]
{
output-sections-command
output-sections-command
...
} [> region] [: phdr : phdr ...] [=fillexp]
一般对于以上的描述不会全部用到,对于这些描述的用法,后面将会讲述到。
(1).关于段名
对于不同的输出格式,段名应该满足相应的约定。比如a.out格式的文件就应该只能
使用这些段名:.text .data .bss
(3).输出段地址
输出段地址 address 是输出段的VMA地址。例:
.text [address] :{ *(.text)}
如果没有指定 address 则将按照 region 或者是当前地址记数值"."来分配地址。注意,这里分配的是VMA地址(见VMA的说明)。
另外,在分配地址的时候还有地址对齐操作,这里就不赘述了。
2.输入段描述:
输出段描述告诉连接器怎样在内存中安排你的程序布局,而输入段描述则告诉连接器怎样将各输入文件映射到你的内存布局中去。
(1).输入段的基本语法如下:
输入文件名(段名)
例: file1(.text)
表示将文件 file1的.text 段放于此处。
也支持通配符,如:
*(.text)
将所有的文件的.text段放置到此处。
又例:
a.out
直接将a.out的所有段放置此处。
(2).输入段的通配符使用,就是一般的通配符语法,这里就不赘述了。
3.COMMON 段的设置
在很多的目标文件格式中,对于common symbols(什么是common symbols?)都没有专门的段来存放。
所以在连接时,连接器专门指定了一个COMMON段来包含这些common symbols.
而COMMON段一般放于输出文件的.bss段中。
例:
.bss { *(.bss) *(COMMON) }
在.bss段中放置所有输入文件的.bss段 和 所有输入文件的 common symbols
4.输入段描述的示例:
SECTIONS {
outputa 0x10000 :
{
all.o all.o的所有段
foo.o (.input1) foo.o的所有.input1 段
}
outputb :
{
foo.o (.input2) foo.o的所有.input2 段
foo1.o (.input1) foo1.o的所有.input1段
}
outputc :
{
*(.input1) 所有文件的余下的.input1段
*(.input2) 所有文件的余下的.input2段
}
}
5.在输出段中装填数据。
(1). BYTE, SHORT, LONG, and QUAD
如:BYTE(1)
在当前位置装填1字节的1
(2). FILL 的使用,可以从当前位置开始装填本SECTION。
6.两个输出段关键字。
CREATE_OBJECT_SYMBOLS 和 CONSTRUCTORS
CREATE_OBJECT_SYMBOLS:
每一个输入的文件将对应一个同名的symbol.而这些symbol将被放置在CREATE_OBJECT_SYMBOLS
命令出现的段中。
CONSTRUCTORS:
在
此命令出现的地方放置C++全局构造和析构函数信息。对于a.out
格式,它使用其专有的方法来支持C++构造与析构函数。对于不能使用任意段名的文件格式如:XCOFF 和ECOFF
则需要一个CONSTRUCTORS 命令来在输出文件中记录C++全局构造函数和析构函数的信息。对于能使用任意段名的文件格式如COFF 和
ELF 。则连接器将自动生成名为.ctors 和 .dtors 的段来记录相应信息。我们可能暂时用不到这个东西。
7.丢弃段。
命名为 /DISCARD/ 的段的内容将不被包含到输出文件中。
8.输出段描述的其它特性,输出段描述的基本结构正如前边所提到的如下所示:
SECTION [ address] [( type)] : [AT( LMA)]
{
output-sections-command
output-sections-command
...
} [> region] [: phdr : phdr ...] [=fillexp]
下面对其各个特性做一个比较详细的说明:
(1). Output Section Type (即输出段的type属性)
此属性可以赋值为:
NOLOAD 此段的内容将不被装载到内存中去,比如直接在ROM中运行程序。
DSECT
COPY
INFO
OVERLAY
以上4个代表运行时不另外为改段分配内存空间,这样可以一定的节省程序
空间。
(2). 输出段LMA
使用AT 命令来设置段的LMA
这样的话可以使用这个特性很方便的建立ROM image.
例如:
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; } //.text 的VMA为0x1000
.mdata 0x2000 : //.mdata 的VMA为0x2000
AT ( ADDR (.text) + SIZEOF (.text) ) //.mdata 的LMA为紧随.text
//段之后。
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 : //.bss的VMA为0x3000
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
与之配合的初始化代码示例如下(可能是由ld自动生成):
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext; //从这里可以看出,"_etext=."的意思实际上是令_etext的地址
//等于当前的VMA
char *dst = &_data;
/* ROM has data at end of text; copy it. */
while (dst region 属性可以将某个段定位到预定义好的内存位置去。
例:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
(4). Output Section Segmentation
程序段的定义。
例:
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
后面将提到PHDRS
(5).输出段填充
例:采用如下的方法来填充段
SECTIONS { .text : { *(.text) } =0x9090 }
9.覆盖描述:(Overlay Description)
定义如下:
OVERLAY [ start] : [NOCROSSREFS] [AT ( ldaddr )]
{
secname1
{
output-section-command
output-section-command
...
} [:PHDR...] [=FILL]
secname2
{
output-section-command
output-section-command
...
} [: phdr...] [= fill]
...
} [> region] [: phdr...] [= fill]
OVERLAY 命令应该定义于SECTIONS 命令之内。
--------------------------------------------------------------------------------
------------
10.MEMORY 命令
分配内存区域
MEMORY
{
name [( attr)] : ORIGIN = origin, LENGTH = len
...
}
atrr 为段的匹配特性,如果一个段没有明确指定将其放置到某一个region中去,那么如果
其属性匹配该region的atrr属性,将被加入到该region中去。
有以下属性:
R 只读段
W 只写段
X 执行段
A 可分配段
I 已初始化段
L 与I相同。
! 非特性
例子:
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
一旦完成了region的定义,就可以使用 >region 的方法将某段指定添加到某个region中去