ちょっと話が逸れるが、今回は組み込みシステムにおけるVMAとLMAの考え方についてまとめておく。
例えばリンカスクリプトなどを組み上げるとき、「CPUから見たアドレス」と「外部からデータをRAMに配置するときにみる領域」が違うことがある。
例えば、こんな場合はどうだ。まずは0x0000_0000のROMにCPUが実行するべき命令とデータを置き、CPUがまずはそれらの情報を0x3000_0000から始まるRAMにコピーし、そこからフェッチして実行するとしたら?
プログラムのアドレスはどこに設定して置けばよいのだろう?
- マイコンの外部からデータを流し込む時:流し込みアドレスを0x0000_0000から始めたい(外部バスから見た場合、RAMが0x0000_0000にあるので)
- プログラムをCPUが実行するとき:RAMにコピーされているので0x3000_0000から始まるアドレスで進めたい
- VMA(Virtual Memory Address) : プログラムが実際に実行するときに参照されるアドレス。通常はリンカスクリプトを記述するときは、このアドレスをベースに記述する。上記の例だと0x3000_0000として配置される。
- LMA(Load Memory Address) : プログラムをロードするときに参照されるアドレス。外部からプログラムをロードするときに参照されるアドレス。上記の例だと0x0000_0000に相当する。
実際にはどのようなことが起きるのか(RISC-Vプログラムの例)
例えば、RISC-Vのプログラムを考えてみよう。RISC-Vはリセット時のフェッチアドレスが0x8000_0000に設定されている。このため、普通にプログラムをコンパイルしてobjdumpすると、0x8000_0000から配置されるが、
外部にRAMを配置している場合、0x8000_0000(つまり2GB空間から!)のRAMなんてものは存在しない。だいたいは0x8000_0000を先頭にRAMが置かれており、プログラムをロードするバスからは0x0000_0000から見えていたりする。
今自分が作っているRISC-V自作CPUだってそうだ。最初にフェッチリクエストがくるのは0x8000_0000だけれども、そんな高い領域まで存在するRAMは置けないので、0x8000_0000にマップする形で0x0000_0000からRAMを配置している。
riscv_cpu (
.fetch_addr(w_fetch_addr),
...
);
assign ram_addr = w_fetch_addr - 0x8000_0000;
ram_16k (
.addr(ram_addr),
...
);
この場合、リンカスクリプトでVMA=0x8000_0000, LMA=0x0000_0000を設定してみよう。
SECTIONS
{
.text.reset 0x80000000 : AT(0x00000000) {
*(.text.reset)
}
.text 0x80001000 : {
*(.text)
}
.rodata : {
*(.rodata.*) *(.rodata*)
...
AT(...)
で記述されているのがLMAだ。これでコンパイルしてみる。objdumpしても、VMAで表記されているのが分かるだろう。
Disassembly of section .text.reset:
80000000 <_start>:
80000000: 00007197 auipc gp,0x7
80000004: 90818193 addi gp,gp,-1784 # 80006908 <_gp>
80000008: 0000a117 auipc sp,0xa
8000000c: 11010113 addi sp,sp,272 # 8000a118 <_sp>
80000010 <format_regs>:
80000010: 000000b3 add ra,zero,zero
80000014: 00000233 add tp,zero,zero
...
ところが、objdump -h
で実行してみると、LMAとVMAが異なっていることが分かる。
$ riscv64-unknown-elf-objdump -h coremark.bin
coremark.bin: file format elf32-littleriscv
Sections:
Idx Name Size VMA LMA File off Algn
0 .text.reset 000000a4 80000000 00000000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .text 00003784 80001000 00001000 00002000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .text.startup 0000078c 80004784 00004784 00005784 2**2
...
下記のエントリを使ってLMAをベースにreadmemh用のファイルを作ってみると、以下のようになる。
sourceware.org
@00000000 110101130000a1179081819300007197 // 0x8000_0000
@00000001 00000333000002b300000233000000b3 // 0x8000_0010
@00000002 00000533000004b300000433000003b3 // 0x8000_0020
@00000003 00000733000006b300000633000005b3 // 0x8000_0030
@00000004 00000933000008b300000833000007b3 // 0x8000_0040
@00000005 00000b3300000ab300000a33000009b3 // 0x8000_0050
@00000006 00000d3300000cb300000c3300000bb3 // 0x8000_0060
@00000007 00000f3300000eb300000e3300000db3 // 0x8000_0070
...
RAMに置かれる場合は0x0000から始まるように配置されるが、CPUからフェッチされるときは0x8000_0000になっている。絶対アドレスのジャンプとかは、0x8000_0000をベースにして計算されるようになっている。