ちょっと話が逸れるが、今回は組み込みシステムにおける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; // Offsetを切り落とす。 ram_16k ( .addr(ram_addr), // アドレスは8000_0000のアクセスが0x0000_0000になるように ... );
この場合、リンカスクリプトで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用のファイルを作ってみると、以下のようになる。
@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をベースにして計算されるようになっている。