读懂CCS链接命令文件(.cmd)
链接器的核心工作就是符号表解析和重定位,链接命令文件则使得编程者可以给链接器提供必要的指导和辅助信息。多数时候,由于集成开发环境的存在,开发者无需了解链接命令文件的编写,使用默认配置即可。但若需要对计算机系统存储空间实行更精细化的管理,读懂链接命令文件并能稍作修改则显得很有必要。
段(section)
编译器生成可重分配地址的代码块和数据块,这些块被叫做“段”。通过段名对代码块和数据块的标识,连接器就能在链接的时候根据链接规则(默认规则或链接命令文件制定)将代码块和数据块分配到指定的存储空间中。
图1 将段组合到可执行文件中
汇编器有5个伪指令支持标识汇编语言程序各个部分应归属的段:.text .data .sect .bss .usect。编程者也可以创建任一种段的子段,得以更精细地控制存储器区域。
初始化段:.text .data .sect创建初始化段。
.text:代码区间。
.data:已初始化的全局和静态变量。
.sect:创建类似于.text .data 的命名段,同时用于创建子段。
未初始化段:.bss .usect指令创建未初始化段。
.bss:未初始化的全局和静态变量,以及被初始化为0的全局和静态变量。
.usect:创建类似于.bss的命名段,同时用于创建子段。
含有原始数据的段,归类为初始化的,意味着目标文件含有该段的存储器实际内容映像;默认情况下,.bss段和.usect伪指令定义的段没有原始数据,它们在存储器映射图里占据空间,但没有实际内容。每次使用bss和usect伪指指令时,汇编器就在.bss或该命名段内预留增补的空间。在目标文件里,一个未初始化段有正常的段头,也可以含有定义在其内的符号,但没有存于段内的存储器映像。
命名段是用户创建的,可以像.text .data .bss段一样使用它们。
子段是较大段内一些比较小的段,子段使用户更细致地控制存储器空间,一个子段可以单独分配地址,也可以与同一基段的其它子段分配在一起。子段用基段名加冒号加子段名来标识,对子段命名的语法如下:
symbol .usect "section name:subsection name",size in bytes [,alignment [,bank offset]]
.sect "section name:subsection name"
-mo选项使得编译器把一个文件中的每一个函数放入它自己的子段中。这样,只有被应用程序调用的函数,才被连接到最后的可执行函数中,这可以导致整个代码的尺寸减小。但是,如果一个文件中几乎所有的函数都被引用,使用-mo编译器选项,也能导致整个代码尺寸的增加。
加载时(load-time)和运行时(run-time)
“加载时”和“运行时”是两个容易让人产生迷惑的概念,理解它们需要了解代码在硬件层面的详细被执行过程。
在掉电时,包含运行代码的可执行文件一般都存放于非易失的ROM或磁盘中,但由于这些存储器的访问速率受到限制,在实际上电运行时,还需要专门的一段加载代码(或称加载器),将需要的代码和数据段复制到主存中,然后通过跳转到程序的第一条指令或入口点,来运行该代码。这个将程序复制到内存,并开始运行的过程叫做加载。
加载地址确定了加载器把段的原始数据放置的位置,任何对这个段的引用涉及的是它的运行地址,运行时必须把这个段从加载地址复制到运行地址。这也就能理解为什么计算机术语上把诸如stdio、string等库文件称作运行时库(run-time lib),因为它们在运行时才被加载到和调用代码一起运行。
链接命令文件语法
连接器命令文件是ASCII码文件,包括一个或多个下述信息:
输入文件。如目标文件、文档库、其它命令文件(如果一个命令文件调用另一个命令文件作为输入,这个语句必须是调用文件最后的语句,连接器不从被调用的命令文件返回)。
连接器选项。它能以与命令行同样的方式应用于命令文件。
MEMORY和SECTION连接器伪指令(只能在命令文件里使用这些伪指令,不能在命令行上使用它们)。
赋值语句。它定义全局符号并给它们赋值。
下列名字作为连接器伪指令的关键字被保留,在命令文件里不要使用它们作为符号或段名:
一个完整的简单链接命令文件示例如下:
a.obj b.obj c.obj /* Input filenames */
--output_file=prog.out /* Options */
--map_file=prog.map
MEMORY /* MEMORY directive */
{
FAST_MEM: origin = 0x0100 length = 0x0100
SLOW_MEM: origin = 0x7000 length = 0x1000
}
SECTIONS /* SECTIONS directive */
{
.text: > SLOW_MEM
.data: > SLOW_MEM
.bss: > FAST_MEM
}
3.1 MEMORY伪指令
MEMORY定义一个目标系统的存储器映像图。用户给存储器各部分命名,制定他们的起始地址和长度。它的通用语法如下:
MEMORY
{
name 1 [( attr )] : origin = expression , length = expression [, fill = constant]
..
name n [( attr )] : origin = expression , length = expression [, fill = constant]
}
其中name为一段存储区的名字;attr定义该存储区的属性,如可读、可写、可执行、可初始化;origin为该存储区的起始地址;length为存储区长度,以字节为单位;fill选项指定该存储区的空闲区域用什么constant来填充。一个使用例子如下:
MEMORY
{
FAST_MEM (RW) : o = 0x00000020, l = 0x00001000, f = 0xFFFFFFFF
}
由MEMORY伪指令定义的存储器是已配置的,没有用MEMORY伪指令显式地计入的存储器是未配置的。连接器不把程序任何一段放到未配置的存储器里。
如果不使用MEMORY伪指令,连接器则使用一个默认的基于处理器结构的存储器模型。这个模型假定系统内全部地址空间存在且可使用。
3.2 SECTIONS伪指令
SECTIONS告诉连接器怎样把输入段组合成输出段,以及把输出段放在存储器的什么位置。
SECTIONS
{
name : [property [, property] [, property] . . . ]
name : [property [, property] [, property] . . . ]
name : [property [, property] [, property] . . . ]
}
其中name为输出段名,SECTIONS伪指令的作用就是将输入段(基段或子段)重新组合到一个输出段(基段或子段),并指定该输出段的加载地址、运行地址、填充值等属性。
property则是可选的命令选项,这些命令选项有:
链接器给每个输出段在目标存储器内分配两个地址:加载时地址和运行时地址。一般情况下它们是同一个,可以认为每个段仅有单一的地址。如果加载和运行的地址是分离的,跟随关键字load后的所有参数,应用于加载定位;跟随关键字run后的所有参数,应用于运行定位。
未初始化段不加载,因此有意义的仅仅是运行地址。如果对未初始化段的加载地址和运行地址二者都指定,连接器发出警告并忽略加载地址。如果只指定一个地址,连接器把它作为运行地址对待,而不管称它是加载或是运行。
用户可以为输出段提供一个指定的起始地址,但这种地址绑定与边界对齐(alignment)和指定存储器(named memory)不兼容,如果使用了边界对齐(alignment)和指定存储器(named memory),将不能绑定段地址。如果试图这样做,连接器将发出错误信息。
输出段能以两种方法组成:
作为SECTIONS伪指令定义的结果;
把SECTIONS伪指令未定义的同名输入段组合到一个输出段。如果对子段没有显式地指定,子段将被组合到具有同一基段名的段内。
连接器允许在SECTIONS伪指令内任意嵌套GROUP和UNION语句。
3.3 SECTIONS伪指令内的UNION语句
UNION语句嵌套在SECTIONS指令内使用,它提供一种方法,把几个段定位到同一运行地址。UNION占据与它最大成员一样大的空间。UNION的成员保持为独立段,它们只是简单地作为一个单位定位在一起。
未初始化段不加载,不需要加载地址。
UNION: run = FAST_MEM
{
.bss:part1: { file1.obj(.bss) }
.bss:part2: { file2.obj(.bss) }
}
但如果初始化段是UNION的成员,它的加载定位必须分别指定,也即UNION共享地址只是对于运行地址而言,加载地址不能共享。
UNION run = FAST_MEM
{
.text:part1: load = SLOW_MEM, { file1.obj(.text) }
.text:part2: load = SLOW_MEM, { file2.obj(.text) }
}
3.4 SECTIONS伪指令内的GROUP 语句
GROUP语句嵌套在SECTIONS指令内使用,用于强制几个输出段连续定位。例如下面的语句,使用GROUP强制连接器将.data段和term_rec段相邻定位,其中.data定位到地址0x1000,term_rec紧随其后:
SECTIONS
{
.text /* Normal output section */
.bss /* Normal output section */
GROUP 0x00001000 : /* Specify a group of sections */
{
.data /* First section in the group */
term_rec /* Allocated immediately after .data */
}
}
3.5 原点“.”符号
一个用原点“.”标记的特殊符号,代表在地址分配期间的段程序计数器(SPC)的当前值,SPC保持跟踪段内当前地址。符号“.”指的是段的当前运行地址,而不是当前加载地址。
3.6 一个SECTIONS伪指令分配存储的例子
/**************************************************/
/* Sample command file with SECTIONS directive */
/**************************************************/
file1.obj file2.obj /* Input files */
--output_file=prog.out /* Options */
SECTIONS
{
.text: load = EXT_MEM, run = 0x00000800
.const: load = FAST_MEM
.bss: load = SLOW_MEM
.vectors: load = 0x00000000
{
t1.obj(.intvec1)
t2.obj(.intvec2)
endvec = .;
}
.data:alpha: align = 16
.data:beta: align = 16
}
链接器并不是一定需要连接器伪指令,如果没有使用它们,连接器将使用目标处理器默认的分配代码方案。
内存区域不够时的解决办法
在链接过程中经常会出现由于存储区空间不足导致段分配失败的提示,同时连接器会给出未使用空间的大小和需要空间的大小。这一现象可表现出两种情况:一是未使用 空间>需要空间;二是需要空间>未使用 空间。
第二种情况其实很好理解,第一种情况是怎么回事呢?实际上,段的空间的分配是并不是我们想象中的连续的一个紧挨一个,由于数据对齐的需要以及内存页的适配,都会在内存中产生一些空隙(hole),使得实际所需要的内存空间超过了根据变量大小计算出来的理论值。这样做的目的是为了优化数据页(DP)寄存器的加载,达到减小代码尺寸和优化程序性能的目的。
那么,一旦出现存储区空间不足的提示,我们该如何重新调整段的分配来解决这个问题呢?于一个单一的段而言,有三个办法可以尝试:
1. 查看编译后生成的.map文件,其中显示了每一个存储区的空间使用情况,另寻找一个空间大小足够,且内存属性相似的存储区,将该段分配到该区;
2. 标注多个备选存储区。操作符“| ”用来为段指定多个存储器区域,如果输出段不能成功地分配到任一个所指定的存储器区域,连接器发出一个错误信息。
.text : > FLASHA | FLASHC | FLASHD
这个例子中连接器将首先尝试将.text段分配给FLASHA,如果不成功,则依次尝试FLASHC和FLASHD,直到分配成功,否则报错误提示。
3. 将段分割分配到多个存储区。操作符“>> ”标明输出段能被分裂装到指定的存储区域内,前提是几个内存区域的总长度要满足要求。
.text : >> FLASHA | FLASHC | FLASHD
这个例子中,如果.text段不能完整地分配到FLASHA,则连接器会将剩余的部分继续分配到FLASHC,甚至分配到FLASHD中。
参考文献
【1】田黎育,何佩琨,朱梦宇. TMS320C6000系列DSP编程工具与指南[M].北京:清华大学出版社 2006.
【2】TMS320C6000 Assembly Language Tools v7.4--SPRU186W,2012.
【3】Beginner's Guide to Linkers.
【4】Linkers and Loaders,John Levine.
·END·
欢迎来我的微信公众号做客:信号君
专注于信号处理知识、高性能计算、现代处理器&计算机体系
技术成长 | 读书笔记 | 认知升级
幸会~
读懂CCS链接命令文件(.cmd)的更多相关文章
- pyi文件是干嘛的?(一文读懂Python的存根文件和类型检查)
参考资料: https://blog.csdn.net/weixin_40908748/article/details/106252884 https://www.python.org/dev/pep ...
- 使用 10046 查看执行计划并读懂 trace 文件
查看 sql 执行计划的方法有许多种, 10046 事件就是其中的一种. 与其他查看 sql 执行计划不同, 当我们遇到比较复杂的 sql 语句, 我们可以通过 10046 跟踪 sql 得到执行计划 ...
- 解析.DBC文件, 读懂CAN通信矩阵,实现车内信号仿真
通常我们拿到某个ECU的通信矩阵数据库文件,.dbc后缀名的文件. 直接使用CANdb++ Editor打开,可以很直观的读懂信号矩阵的信息,例如下图: 现在要把上图呈现的信号从.dbc文件中解析出来 ...
- [转帖] 读懂YML文件.. 书买了还没看完...
Copy From https://www.cnblogs.com/CloudMan6/p/8370501.html 读懂 Deployment YAML - 每天5分钟玩转 Docker 容器技 ...
- 目录处理文件&链接命令
一.目录处理文件 1.删除文件或目录 rm -rf [文件或目录] //remove:删除文件或目录 -r:删除目录 -f:强制 2.复制文件或目录 cp [选项] [原文件或 ...
- 【Windows删除指定后缀文件cmd命令】
如果我想删除指定目录下的"*.mp4"后缀文件 在命令行中,进入指定目录,输入 del [/q] "*.mp4" del 命令是删除文件cmd(命令行)命令. ...
- Linux学习笔记(三)Linux常用命令:链接命令和文件查找命令
一.链接命令 ln -s [原文件] [目标文件] (link) -s意为创建软连接 硬链接和软连接 硬链接的特点: (1)拥有相同的 i 结点和block块,可以看作是同一个文件 (2)可以通过 i ...
- 一篇文章教你读懂Makefile
makefile很重要 什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professiona ...
- 一篇文章,读懂Netty的高性能架构之道
一篇文章,读懂Netty的高性能架构之道 Netty是由JBOSS提供的一个java开源框架,是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架, ...
随机推荐
- CSS命名规范(规则)
常用的CSS命名规则 头:header内容:content/container尾:footer导航:nav侧栏:sidebar栏目:column页面外围控制整体佈局宽度:wrapper左右中:left ...
- IDEA中git的配置与使用
IDEA中git的配置与使用 1.介绍 git是目前非常流行的版本管理管理软件,因其具有分布式特点,越来越受到企业的欢迎.IDEA作为一款优秀的开发软件,其内部也提供了对git的支持. 2.下载并安装 ...
- User Agent字符串列表
User Agent字符串列表 --之心 User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA.它是一个特殊字符串头,是一种向访问网站提供 ...
- April 27 2017 Week 17 Thursday
Had I not seen the sun, I could have borne the shade. 我本可以忍受黑暗,如果我不曾见过阳光. A poem by Emily Dickinson, ...
- IOS 拼接按钮文字
NSMutableString *tempAnswerTitle=[[NSMutableString alloc]init]; for(UIButton *answerBtn in self.answ ...
- 问题 B: 矩形类中运算符重载【C++】
题目描述 定义一个矩形类,数据成员包括左下角和右上角坐标,定义的成员函数包括必要的构造函数.输入坐标的函数,实现矩形加法,以及计算并输出矩形面积的函数.要求使用提示中给出的测试函数并不得改动. 两个矩 ...
- 2017.10.29 C/C++/C#程序如何打成DLL动态库
C/C++程序如何打成DLL动态库: **1.在VS中新建main.h,添加如下内容:** extern "C" _declspec(dllexport) int onLoad() ...
- 20145238-荆玉茗 《Java程序设计》第4周学习总结
20145238 <Java程序设计>第4周学习总结 教材学习内容总结第六章 继承与多态 6.1.1 ·继承基本上就是避免多个类间重复定义共同行为. 在游戏中会有很多程序代码重复的片段,这 ...
- 剑指offer17 合并两个排序的链表
错误代码: 最后两个if语句的目的是,最后一次迭代,两个链表中剩下的直接连接最后一次比较的数值,同时也是迭代停止的标志.虽然大if语句中比较大小得到的Node是正确的值,但每次迭代只要pHead2不为 ...
- C#中类的成员
一.C#中类的成员 1. 类的成员 类中的数据和函数都称为类的成员.类的成员可以分为两类: ?类本身所声明的. ?从基类中继承来的. 如果在类声明中没有指定基类,则该类将继承System.Object ...