GCC编译四步
1、预处理:宏定义替换之类的工作
2、编译非汇编:将源代码经过词法分析、语法分析、语义分析转为汇编代码的过程
3、汇编:将汇编代码转为具体二进制机器码的过程(此时由于还没有进行链接,所以虽然是二进制代码也不可直接执行)
4、链接:将目标文件中链接到的其他目标二进制文件进行整合成最终可执行二进制文件
这里说整合是因为有两种链接方式
动态链接:最终形成的可执行二进制文件只包含需要链接的目标二进制文件的位置,本身自己的二进制文件并不包含目标二进制文件,最终形成的二进制文件较小,利于目标二进制库进行更新,不用二次编译(只要位置和接口都没变),但是自己单独不可运行。
静态链接:将需要的目标二进制文件打包进自己的二进制文件中,其实就是位置替换,将出现目标二进制文件的位置用目标二进制文件进行位置替换。可独立执行,生成的二进制文件较大,每次目标二进制库更新后,都需要重新进行编译(如果要使用新功能的话)。
text段、data段和bss段
一个程序的3个基本段:text段,data段,bss段。
text段在内存中被映射为只读,但.data和.bss是可写的。
text段:就是放程序代码的,编译时确定,只读;
data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存放在这个区域;
bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域;
bss(可读可写)
bss是英文Block Started by Symbol的简称,通常是指用来存放程序中未初始化的全局变量的一块内存区域,在程序载入时由内核清0。BSS段属于静态内存分配。它的初始值也是由用户自己定义的连接定位文件所确定,用户应该将它定义在可读写的RAM区内,源程序中使用malloc分配的内存就是这一块,它不是根据data大小确定,主要由程序中同时分配内存最大值所确定,不过如果超出了范围,也就是分配失败,可以等空间释放之后再分配。
text(只读)
text段是程序代码段,在AT91库中是表示程序段的大小,它是由编译器在编译连接时自动计算的,当你在链接定位文件中将该符号放置在代码段后,那么该符号表示的值就是代码段大小,编译连接时,该符号所代表的值会自动代入到源程序中。
data(可读可写)
data包含静态初始化的数据,所以有初值的全局变量和static变量在data区。段的起始位置也是由连接定位文件所确定,大小在编译连接时自动分配,它和你的程序大小没有关系,但和程序使用到的全局变量,常量数量相关。
栈(stack):保存函数的局部变量和参数。是一种“后进先出”(Last In First Out,LIFO)的数据结构,这意味着最后放到栈上的数据,将会是第一个从栈上移走的数据。对于哪些暂时存贮的信息,和不需要长时间保存的信息来说,LIFO这种数据结构非常理想。在调用函数或过程后,系统通常会清除栈上保存的局部变量、函数调用信息及其它的信息。栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。栈(stack)的顶部在可读写的RAM区的最后。
堆(heap)保存函数内部动态分配内存,是另外一种用来保存程序信息的数据结构,更准确的说是保存程序的动态变量。堆是“先进先出”(First In first Out,FIFO)数据结构。它只允许在堆的一端插入数据,在另一端移走数据。堆的地址空间“向上增加”,即当堆上保存的数据越多,堆的地址就越高。
Lds
介绍
链接器:把一个或多个输入文件合并成一个输出文件,输入文件是目标文件或者链接脚本文件,输出文件是目标文件(库文件)或者可执行文件,链接器从链接脚本读完一个 section 后,将定位器符号的值增加该 section 的大小。
链接脚本:链接脚本的一个主要目的是描述输入文件中的各个段(数据段,代码段,堆,栈,bss)如何被映射到输出文件中,并控制输出文件的各部分在程序地址空间内的布局,地址空间包括 ROM 和 RAM。
链接器总是使用链接脚本的,如果你不提供,则链接器会使用一个缺省的脚本,这个脚本是被编译进链接器可执行文件的。
关键字详解
ENTRY(进入)
ENTRY(main)
ENTRY(MultibootEntry)
ENTRY关键字用于定义应用程序的入口点,即输出文件中的第一条可执行指令。该关键字接受链接程序/内核入口点的符号名作为单个参数。所提供的符号名指向的代码将是。ELF和PE二进制文件中的文本部分。
ENTRY(SYMBOL) :将符号SYMBOL的值设置成入口地址。
入口地址(entry point)是指进程执行的第一条用户空间的指令在进程地址空间的地址
ld有多种方法设置进程入口地址, 按一下顺序: (编号越前, 优先级越高)
1、ld命令行的-e选项
2、链接脚本的ENTRY(SYMBOL)命令
3、如果定义了start符号, 使用start符号值
4、如果存在.text section, 使用.text section的第一字节的位置值
5、使用值0
OUTPUT_FORMAT(输出格式)
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_FORMAT("pe-i386")
OUTPUT_FORMAT指令只接受一个参数。它指定可执行文件的输出格式。要了解系统binutils和GCC支持哪些输出格式,可以使用objdump-i命令。
STARTUP (启动)
STARTUP(Boot.o)
STARTUP(crt0.o)
启动需要一个参数。它是要链接到可执行文件开头的文件。对于userland项目,这通常是crt0。o或crtbegin。o、 对于内核,通常是包含程序集样板的文件启动堆栈,在某些情况下是GDT之类的,然后调用kmain()。
SEARCH_DIR(搜索目录)
SEARCH_DIR(Directory)
这将为您的库搜索目录添加路径。-nostlib标志将导致在该路径中找到的任何库被有效忽略。我不知道为什么,这似乎就是ld的工作原理。它将链接器脚本指定的搜索目录视为标准目录,因此会忽略它们,而不使用默认的libs和此类标志
INPUT(输入)
INPUT(File1.o File2.o File3.o ...)
INPUT
(
File1.o
File2.o
File3.o
...
)
INPUT是一个“链接器脚本中”替换项,用于将对象文件添加到命令行。您通常会指定类似于ld File1的内容。o文件2。o、 可以使用输入部分在链接器脚本中执行此操作。
OUTPUT (输出)
OUTPUT(Kernel.bin)
OUTPUT命令指定要生成的文件作为链接过程的输出。这是最终创建的二进制文件的名称。此命令的效果与-o filename命令行标志的效果相同,后者会覆盖它。
MEMORY (记忆存储)
MEMORY
{
ROM (rx) : ORIGIN = 0, LENGTH = 256k
RAM (wx) : org = 0x00100000, len = 1M
}
MEMORY声明一个或多个内存区域,其属性指定该区域是否可以写入、读取或执行。这主要用于不同地址空间区域可能包含不同访问权限的嵌入式系统。
上面的示例脚本告诉链接器有两个内存区域:
a) “ROM”从地址0x00000000开始,长度为256kB,可以读取和执行。
b) “RAM”从地址0x00100000开始,长度为1MB,可以写入、读取和执行。
SECTIONS命令
SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section: 如何将输入section合为输出section; 如何把输出section放入程序地址空间(VMA)和进程地址空间(LMA).
该命令格式如下:
SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
…
}
SECTION-COMMAND有四种:
(1) ENTRY命令
(2) 符号赋值语句
(3) 一个输出section的描述(output section description)
(4) 一个section叠加描述(overlay description)
如果整个连接脚本内没有SECTIONS命令, 那么ld将所有同名输入section合成为一个输出section内, 各输入section的顺序为它们被连接器发现的顺序.如果某输入section没有在SECTIONS命令中提到, 那么该section将被直接拷贝成输出section。
KEEP (保持)
链接器脚本中的KEEP语句将指示链接器保留指定的节,即使其中没有引用任何符号。此语句在链接器脚本的节中使用。当在链接时执行垃圾收集时,这一点就变得很重要,在链接命令行内使用了选项 -gc-sections 后,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,可用 KEEP() 关键字达此目的。说的通俗易懂就是:防止被优化。
该语句常见于针对ARM体系结构的链接器脚本中,用于将中断向量表放置在偏移量0x00000000处。如果没有这个指令,代码中可能不会显式引用的表将被删除。
SECTIONS
{
.text :
{
KEEP(*(.text.ivt))
*(.text.boot)
*(.text*)
} > ROM
/** ... **/
}
PROVIDE
为在任何链接目标中没有定义但是被引用的一个符号,而在链接脚本定义一个符号。 PROVIDE(symbol = expression)。提供定义‘_exfun’的例子:
SECTIONS
{
.text :
{
*(.text)
_exfun = .;
PROVIDE(_exfun = .);
}
}
如果程序定义了’ _exfun ‘(带有前导下划线),链接器将给出重复定义错误。另一方面,如果程序定义了’ exfun‘(没有前导下划线),链接器会默认使用程序中的定义。如果程序引用了’ exfun '但没有定义它,链接器将使用链接器脚本中的定义。
PROVIDE指令将考虑定义一个普通符号,即使这样的符号可以与PROVIDE将创建的符号组合在一起。当考虑构造函数和析构函数列表符号时,这一点尤其重要,因为它们通常被定义为普通符号。
TYPE
每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为以下五种值
NOLOAD 该section在程序运行时,不被载入内存。
DSECT,COPY,INFO,OVERLAY :这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为“不可加载的”,以便在程序运行不为它们分配内存。
AT( LAM_ADDR )
section包含两个地址:VMA(virtual memory address虚拟内存地址或程序地址空间地址)和LMA(load memory address加载内存地址或进程地址空间地址)。默认情况下 LMA 等于 VMA,但可以通过关键字 AT() 指定 LMA。
用关键字 AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。这个属性主要用于构件ROM境象。
一般而言, 某section的VMA == LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定)。
例子,
SECTIONS
{
.text 0×1000 : {_etext = . ;*(.text); }
.mdata 0×2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0×3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
c程序如下:
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;
while (dst rom }
REGION
这个region就是前面说的MEMORY命令定义的位置信息。
ALIGN
表示字节对齐, 如 “ . = ALIGN(4);”表示从该地址开始后面的存储进行4字节对齐。
输入section描述
输入section描述基本语法:
FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)
FILENAME文件名,可以是一个特定的文件的名字,也可以是一个字符串模式。
SECTION名字,可以是一个特定的section名字,也可以是一个字符串模式。
具体示例解析:
1、*(.text) :表示所有输入文件的.text section
2、(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有输入文件的.ctors section。
3、data.o(.data) :表示data.o文件的.data section
4、data.o :表示data.o文件的所有section
5、*(.text .data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第一个文件的.data section,第二个文件的.text section,第二个文件的.data section,…
6、*(.text) *(.data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第二个文件的.text section,…,最后一个文件的.text section,第一个文件的.data section,第二个文件的.data section,…,最后一个文件的.data section
下面看连接器是如何找到对应的文件的。
当FILENAME是一个特定的文件名时,连接器会查看它是否在连接命令行内出现或在INPUT命令中出现。
当FILENAME是一个字符串模式时,连接器仅仅只查看它是否在连接命令行内出现。
注意:如果连接器发现某文件在INPUT命令内出现,那么它会在-L指定的路径内搜寻该文件。
字符串模式内可存在以下通配符:
* :表示任意多个字符
? :表示任意一个字符
[CHARS] :表示任意一个CHARS内的字符,可用-号表示范围,如:a-z
:表示引用下一个紧跟的字符
在文件名内,通配符不匹配文件夹分隔符/,但当字符串模式仅包含通配符*时除外。
任何一个文件的任意section只能在SECTIONS命令内出现一次。
看如下例子
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}
data.o文件的.data section在第一个OUTPUT-SECTION-COMMAND命令内被使用了,那么在第二个OUTPUT-SECTION-COMMAND命令内将不会再被使用,也就是说即使连接器不报错,输出文件的.data1 section的内容也是空的。
ASSERT(断言)
ASSERT(exp, message)
内建函数
lds中有以下一些内建函数:
ABSOLUTE(EXP) :转换成绝对值
ADDR(SECTION) :返回某section的VMA值。
ALIGN(EXP) :返回定位符’.'的按照EXP进行对齐后的修调值,对齐后的修调值算法为:(. + EXP – 1) & ~(EXP – 1)
BLOCK(EXP) :如同ALIGN(EXP),为了向前兼容。
DEFINED(SYMBOL) :如果符号SYMBOL在全局符号表内,且被定义了,那么返回1,否则返回0
LOADADDR(SECTION) :返回三SECTION的LMA
MAX(EXP1,EXP2) :返回大者
MIN(EXP1,EXP2) :返回小者
NEXT(EXP) :返回下一个能被使用的地址,该地址是EXP的倍数,类似于ALIGN(EXP)。除非使用了MEMORY命令定义了一些非连续的内存块,否则NEXT(EXP)与ALIGH(EXP)一定相同
SIZEOF(SECTION) :返回SECTION的大小。当SECTION没有被分配时,即此时SECTION的大小还不能确定时,连接器会报错
SIZEOF_HEADERS :返回输出文件头部的字节数。这些信息出现在输出文件的开始处。当设置第一个段的开始地址时,你可以使用这个数字。如果你选择了加速分页,当产生一个ELF输出文件时,如果链接器脚本使用SIZEOF_HEADERS内建函数,连接器必须在它算出所有段地址和长度之前计算程序头部的数值。如果连接器后来发现它需要附加程序头,它将报告一个“not enough room for program headers”错误。为了避免这样的错误,你必须避免使用SIZEOF_HEADERS函数,或者你必须修改你的连接器脚本去避免强制连接器去使用附加程序头,或者你必须使用PHDRS命令去定义你自己的程序头
Symbols (象征)
可以在链接器脚本中定义任意符号。这些符号被添加到程序的符号表中。表中的每个符号都有一个名称和一个关联的地址。链接器脚本中已赋值的符号将被赋予外部链接,并可在程序代码中作为指针访问。
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
在上面的示例中,符号浮点被定义为零。符号_etext被定义为最后一个字符后面的地址。文本输入部分。符号_bdata被定义为以下地址:。文本输出部分向上对齐到4字节边界。