FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

組み込みシステムにおけるアドレスの考え方 (VMA, LMAについて)

ちょっと話が逸れるが、今回は組み込みシステムにおけるVMAとLMAの考え方についてまとめておく。

例えばリンカスクリプトなどを組み上げるとき、「CPUから見たアドレス」と「外部からデータをRAMに配置するときにみる領域」が違うことがある。 例えば、こんな場合はどうだ。まずは0x0000_0000のROMにCPUが実行するべき命令とデータを置き、CPUがまずはそれらの情報を0x3000_0000から始まるRAMにコピーし、そこからフェッチして実行するとしたら? プログラムのアドレスはどこに設定して置けばよいのだろう?

  • マイコンの外部からデータを流し込む時:流し込みアドレスを0x0000_0000から始めたい(外部バスから見た場合、RAMが0x0000_0000にあるので)
  • プログラムをCPUが実行するとき:RAMにコピーされているので0x3000_0000から始まるアドレスで進めたい

f:id:msyksphinz:20170209005337p:plain

  • 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用のファイルを作ってみると、以下のようになる。

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をベースにして計算されるようになっている。