文章目录
Overview
gcc 编译然后链接之后生成 elf 文件,然后通过 objcopy
将elf 转成 binary文件,在链接的时候会使用链接脚本,链接脚本中会设定各个段的开始地址与结束地址,比如 text 段 bss 段,这些段最终在 bin文件中的偏移地址与链接脚本中设定的地址关系,比如当前将 text 段的开始地址设置为 0xc0000800
最后却发现text 段生成位于 bin 文件的开始处,所以又需要在系统启动的时候将 bin开始属于text 段的部分拷贝到 0xc00000800
处,这时什么原因呢?
一、关键概念
-
VMA(Virtual/Run Address):段在运行时被加载到的地址(linker 把它写进 ELF 的
Addr
列)。 -
LMA(Load/Physical Address):段在镜像(ELF 文件)中的实际存放地址/文件偏移所对应的物理地址(linker 可通过
AT(...)
指定)。 -
ELF -> raw .bin 转换(objcopy):
objcopy -O binary
会把段写入二进制,根据 段的 LMA(load address / file offset) 决定文件中该段的偏移。-
如果 LMA 是 0(或较小),该段会出现在 bin 文件开头。
-
如果 LMA = 0xC0000800,那么 objcopy 会在生成的 bin 中填充(pad)到该偏移位置,段数据就会出现在 bin 的对应偏移处(bin 会很大)。
-
结论:ELF 中段的 VMA ≠ LMA 的情况下,objcopy 使用 LMA(或文件偏移)来决定 bin 中的位置。如果你只把段的 VMA 设置为
0xC0000800
,但没有把段的 LMA 设置成相应值(或 linker 把 LMA 设为 0),最终 bin 中该段会在文件起始处 —— 这正是你遇到的问题。
二、如何检查 ELF(确认 VMA / LMA / file offset / size)
用 readelf
/ llvm-readelf
查看段表与节在文件中的偏移:
# 查看 Section Headers:Name, Type, Address(VMA), Off(file offset), Size
llvm-readelf -S cpu_rv0.elf
# 或
readelf -S cpu_rv0.elf
示例输出中关注列:
-
Name(段名)
-
Address(VMA)
-
Off(file offset)
-
Size(Size)
也可用:
readelf -sW cpu_rv0.elf # 看符号
llvm-nm -S cpu_rv0.elf # 看大小、类型
三、方案与示例
方案 A — 在链接脚本里把段的 LMA 设为你要的文件偏移(让 objcopy 直接生成带偏移的 bin)
适用场景:你想直接得到一个 bin,bin 中 .text
就位于你指定的偏移(例如 0xC0000800
),免得在启动时再拷贝。
做法:在 linker script 中为该段明确指定 AT(LOADADDR)
,或直接把段放到某个地址(这同时会设置 VMA 与 LMA)。示例:
/* example.ld */
SECTIONS
{
/* 让 .text 的 VMA 和 LMA 都是 0xC0000800(注意:这会生成一个很大的 bin) */
.text 0xC0000800 : AT(0xC0000800) {
*(.text*)
*(.rodata*) /* 若需要可合并 */
}
/* data 放内存其它地方... */
.data 0x80000000 : AT(0x80000000) {
*(.data*)
}
.bss 0x80001000 : { *(.bss*) }
}
然后链接并生成 bin:
# 链接生成 ELF
riscv64-ld -T example.ld -o app.elf objs/*.o
# 直接生成 binary(注意:生成的 binary 会在文件开头填充到 0xC0000800,文件会很大)
llvm-objcopy -O binary app.elf app.bin
要点/风险:
-
bin 文件会被填充到你指定的地址,会使文件非常大(占用从 0 到 0xC0000800 的空间),通常不可取,除非你需要一个“扁平映像”并且有空间/工具处理大文件。
-
如果你只想把某个段放在 bin 中某个偏移而不想让文件超大,需要更小心地设定 LMA(例如把 LMA 设为某个相对基址,而不是直接 0xC0000800)。
方案 B — 保持 ELF LMA 紧凑(bin 起始处为 0),在启动代码把数据拷贝到期望 VMA(常见方案)
适用场景:常见做法:把镜像尽量紧凑生成(.text 在 bin 开头),启动时 bootloader/ROM 把 .text
/.data
等从存储(flash)拷贝到其运行地址(RAM/flash映射地址)。这是你当前正在做的方式。
做法:
-
链接时通常把
.text
的 LMA 放在 flash 的 load address(比如 0),而 VMA 设置为0xC0000800
。在启动代码中读取载入地址(linker 提供的符号如__data_load_start
、__data_start
)并拷贝。 -
启动步骤(pseudo code):
extern uint8_t __text_load_start[]; // LMA in file/flash extern uint8_t __text_start[]; // VMA runtime (0xC0000800) extern uint8_t __text_end[]; // copy .text (if needed), or for position-independent you might not copy .text memcpy(__text_start, __text_load_start, __text_end - __text_start);
-
这种方式常在 bootloader/ROM 完成。
优点:
-
bin 文件保持紧凑(没有巨大空洞)。
-
通用且常用,不需要修改 objcopy/linker 复杂参数。
方案 C — 用 objcopy/后处理在 bin 中增加 padding(更灵活的文件制作)
如果你不想修改链接脚本,但仍想把段放到 bin 的特定偏移,可以后处理生成的 bin(将段文件拼接到带偏移的空洞文件):
步骤举例(把 text.bin 放到 offset 0xC0000800):
- 先把 ELF 转成单段 bin(只包含 .text):
llvm-objcopy -j .text -O binary app.elf text.bin
- 创建一个目标二进制文件(用
dd
/truncate
或fallocate
),在 offset 0xC0000800 写入 text.bin:
# 创建空文件并设置大小(快速)
truncate -s 0 output.bin
# 使用 dd 将 text.bin 写入到指定偏移
dd if=/dev/zero bs=1 count=0 seek=$((0xC0000800)) of=output.bin
dd if=text.bin bs=1 seek=0 conv=notrunc of=output.bin
# 或直接使用:
python3 - <<'PY'
data = open('text.bin','rb').read()
with open('output.bin','wb') as f:
f.seek(0xC0000800)
f.write(data)
PY
- 若还有其它段(.rodata/.data)按类似方式放置(注意不会覆盖及对齐)。
优点:可精确控制文件中每段的偏移,而不依赖 linker-script LMA。
缺点:手工拼接麻烦,容易错,文件可能很大。
四、如何选择
-
推荐(常见):方案 B —— 让 ELF/bin 紧凑,启动时把需要的段从存储拷贝到运行地址(bootloader 完成)。这种方式最普遍、安全,也不产生很大二进制文件。
-
若必须直接得到带偏移的扁平镜像(flash 镜像):使用方案 A(设置段 LMA = 期望文件偏移),或方案 C(后处理拼接)。但要注意文件大小和磁盘/传输问题。
五、具体排查步骤(帮你定位问题并修正)
-
查看 ELF 段的 VMA / file offset / size:
llvm-readelf -S app.elf
记录
.text
的 Address(VMA)、Off(file offset)和 Size。 -
判断 objcopy 生成 bin 时的行为:
llvm-objcopy -O binary app.elf app.bin # 然后查找 app.bin 中 .text 的位置(例如:用 hexdump 找前几个非0) hexdump -C app.bin | head
或者检查 ELF 的 LMA 信息(如果 linker script 指定了 AT,Off 列通常反映 LMA)。
-
如果你希望 bin 中 .text 位于 0xC0000800:
-
简易:在启动时拷贝(方案 B)。实现方法:在 linker 脚本用
AT
把.text
的 LOADADDR 指向文件偏移处(通常是文件中该段的 Off),同时设置.text
的 VMA 为 0xC0000800;然后启动代码从 LOADADDR 拷贝到 VMA。常见 linker variables:__text_load_start
、__text_start
、__text_end
。 -
直接将 .text 写入 bin 偏移:把
.text
的 LMA 设置为 0xC0000800(在 linker script 中使用AT(0xC0000800)
),然后objcopy -O binary
会把数据放到该偏移(会生成很大文件)。
-
六、示例:linker script 常见写法(便于启动时拷贝)
SECTIONS
{
/* 出现在 FLASH 的位置(LMA) */
. = 0x00000000; /* file/flash base */
__text_load_start = .;
.text : {
*(.text)
*(.rodata)
} = 0x00000000 + SIZEOF(.text) /* linker 管理(示意) */
/* 在运行时地址(VMA) */
/* 设定 .data 在 RAM,但 LOAD 到 FLASH */
__data_load_start = LOADADDR(.data);
__data_start = ADDR(.data);
.data : AT( __data_load_start ) {
*(.data)
} > RAM
/* 定义运行时 .text 的 VMA(example) */
/* More typically: set VMA separately above: */
/* .text 0xC0000800 : { *(.text) } AT(0x00000000) */
}
在 C 启动代码:
extern uint8_t __data_load_start[], __data_start[], __data_end[];
memcpy(__data_start, __data_load_start, __data_end - __data_start);