基础概念
连接器将多个输入的object
文件合并成一个输出的object
文件(又叫executable
), 每个object
文件都含有许多sections
, 即段, 每个段都有名字和大小, 大多数段含有数据, 段可以被标记为:
loadable
: 运行时内容可被载入到内存allocatable
: 内存中留出的区域, 此区域不应该加载任何特定的内容- 其余的, 一般都是某种调试信息
每个loadable
或allocatable
类型的段都有两个地址:
- VMA(virtual memory address): 虚拟地址, 这是运行时的地址
- LMA(load memory address): 加载地址, 这是段被加载的地址
大多时候这两个地址是一样的, 但比如当一个在ROM里面的数据段在程序启动时被复制到RAM, 这种情况下两者不同, LMA是这个数据段在ROM中的地址, 而VMA是这个数据段在RAM中的地址. 下面第二个例子就是这样的.
# 查看object文件中的段
objdump -h a_obj_file
每个object
文件也会有一个symbol table
, 即符号表, 每个符号可以是defined
或undefined
, 符号都有名字, 有定义的符号会有地址以及其他的一些信息. 如果编译C/C++程序到object
文件, 每个有定义的函数,全局变量或静态变量都会有对应定义的符号.
# 查看object文件中的符号
objdump -t a_obj_file
# 或者
nm a_obj_file
例子
简单的例子
下面是一个简单的例子:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
其中.
是location counter
, 指示当前位置的地址, 对.
赋值即可使得位置移动, 但不能回退. text
段一般存放代码, data
段一般存放已初始化的数据, bss
段存放未初始化的数据. *(.text)
代表通配符匹配所有文件的text
段(使用EXCLUDE_FILE
还可以排除某些文件, 更多详见Input-Section-Wildcards).
更复杂的例子
下面是一个更复杂的例子: 将text
,rodata
,data
加载到ROM, bss
段加载到RAM, 另外程序启动时, 将data
从ROM复制到RAM.
其中> REGION_TEXT
指定段分配到自定义的REGION_TEXT
内存区域内, 即指定了text
段的Output Section Address, 同时也即指定了上文提到的段的VMA(即虚拟地址).
而AT (rodata_end)
则指定了LMA(即加载地址), 如果没有AT命令指定LMA地址, 则LMA和VMA地址一样(更详细见Output-Section-LMA).
/* linker script */
INCLUDE linkcmds.memory
SECTIONS
{
.text :
{
*(.text)
} > REGION_TEXT
.rodata :
{
*(.rodata)
rodata_end = .;
} > REGION_RODATA
.data : AT (rodata_end)
{
data_start = .;
*(.data)
} > REGION_DATA
data_size = SIZEOF(.data);
data_load_start = LOADADDR(.data);
.bss :
{
*(.bss)
} > REGION_BSS
}
下面的linkcmds.memory
文件就定义了REGION_TEXT
等内存区域.
其中ORIGIN
指定了内存区域的起始地址, LENGTH
指定了内存区域的字节大小(还可以设置权限属性, 详见MEMORY).
/* linkcmds.memory */
MEMORY
{
ROM : ORIGIN = 0, LENGTH = 3M
RAM : ORIGIN = 0x10000000, LENGTH = 1M
}
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
下面的启动程序就将ROM中的data
内容写入到RAM去.
其中特别注意程序引用data_start
等符号的用法, 更多可见下文关于程序引用符号的说明.
#include <string.h>
extern char data_start [];
extern char data_size [];
extern char data_load_start [];
void copy_data(void)
{
if (data_start != data_load_start)
{
memcpy(data_start, data_load_start, (size_t) data_size);
}
}
实用工具
Linux下面一些可以用来查看相关信息的工具
# 查看所有段
objdump -h xxx.elf
# 查看符号表
objdump -t xxx.elf
nm xxx.elf
readelf -s xxx.elf
# 查看文件头
readelf -h xxx.elf
一个例子
我们想要查找一个存放字符串的变量mainargs
里面的内容
objdump -t xxx.elf | grep "mainargs"
得到输出:
a0003c0c l O .rodata 00000002 mainargs
可知mainargs
是一个局部符号(l)并且是一个对象(O), 即数据, 放在了.rodata
段中, 其内容在a0003c0c
处. 于是我们查找a0003c0c
处的内容(注意是搜索地址a0003c0
), -s
指定显示段的所有内容, -j .rodata
指定只显示.rodata
段:
objdump -s -j .rodata amtest-riscv32e-ysyxsoc.elf | grep "a0003c0"
得到输出:
a0003c04 646c6962 2e630000 6800 dlib.c..h.
对照地址a0003c0c
, 可知其内容是h.
(0x6800), 即字符串"h"
其他细节
下面就是一些细节内容的概要, 具体的内容可以参考官方的文档或者中文翻译
Entry Point
指示第一条被执行的程序指令, 按下面的先后顺序确定:
- the
-e
entry command-line option; - the
ENTRY(symbol)
command in a linker script; - the value of a target-specific symbol, if it is defined; For many targets this is start, but PE- and BeOS-based systems for example check a list of possible entry symbols, matching the first one found.
- the address of the first byte of the code section, if present and an executable is being created - the code section is usually
.text
, but can be something else; - The address 0.
include 文件
- INCLUDE filename
- INPUT(file file …)
- GROUP(file, file, …)
- AS_NEEDED(file, file, …)
- OUTPUT(filename)
- SEARCH_DIR(path)
- STARTUP(filename)
Object文件格式
- OUTPUT_FORMAT
- TARGET
其他命令
- ASSERT 等等
赋值
类同C语言, +=
等都是可以的
程序引用符号
对于在链接脚本中定义的符号(Symbol), 程序应当只使用其地址, 而不应该尝试访问其值, 因为符号不同于C语言中的一个变量, 不会分配到任何内存, 符号只有地址而没有值.
例如:
start_of_ROM = .ROM;
end_of_ROM = .ROM + sizeof (.ROM);
start_of_FLASH = .FLASH;
在程序中应当& start_of_FLASH
取其指向的地址
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);
或者使用char[]
, 这样就不必&
取址了
extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);
一些内建函数
- ABSOLUTE
- ADDR 返回段的VMA地址
- ALIGN 对齐
- LENGTH 返回memory内存区域的长度
- SIZEOF 返回段的字节数
- LOADADDR 返回段的LMA地址
- LOG2CEIL