目的

メモリマップを自分で決めて、リンカスクリプトを(途中まで)記述する


メモリマップ

これまで普通にC言語を使ってきたときには、OSがありました。第1回の時にもプログラムを動かしていたのはLinuxでした。

プログラムはOSによって、様々なリソース割当を行われます。CPU時間もそのひとつですが、最も重要なのがメモリ割当です。実際にプログラムが使うメモリを予約して、その空間に適切な形でプログラムを読出し、プログラムを実行します。必要に応じてアドレス変換やメモリ保護なども行います。

しかし、この「始めてのC言語」ではOSはありません。メモリ割当は自分で行う必要があります。このメモリ割当を行うために必要なのがリンカスクリプトです。

リンカスクリプトを作る前に、ターゲットコンピュータのメモリマップを知る必要があります。メモリマップとは、どのアドレスに何が置いてあるかの表みたいなものです。今回対象にしているV850プロセッサでは、次のようなメモリマップを持ちます (ROMが64k, RAMが4kの場合)。実際にROMやRAMがどのくらい入ってるかは、実際にプロセッサの型番などから調べてください。今回はエミュレータなんで、常識的な範囲で何でもアリです。

アドレス内容
0xffffffff
|
0xfffff000
メモリマップドレジスタ
0xffffefff
|
0xffffe000
内蔵RAM (4k)
0xffffdfff
|
0x00010000
外部デバイスとか
0x0000ffff
|
0x00000000
内蔵ROM (64k)

ちなみに、V850はアドレスの上位8ビットは無効なので、実際にはRAM空間は0x00ffe000からスタートしています。また普通のプロセッサと異なり、RAMの終わりが常に0x00ffefffになっているので、スタックは0x00fff000からのプリデクリメントスタックとして定義できます。

これを踏まえて、リンカスクリプトを記述していきます。


おまじない

リンカスクリプトの始まりは、出力フォーマットやターゲットアーキテクチャの定義から始まります。

OUTPUT_FORMAT("elf32-v850","elf32-v850","elf32-v850");
OUTPUT_ARCH(v850);

OUTPUT_FORMATは、出力するファイルの形式(BFD)を指定します。フォーマットは3つありますが、それぞれ左からデフォルトで使われるフォーマット、ビッグエンディアンのときのフォーマット、リトルエンディアンのときのフォーマットを示しています。今回のV850はそもそもリトルエンディアン固定なので、全部同じのを指定しておけば間違いないでしょう。

OUTPUT_ARCHは、ターゲットアーキテクチャです。説明はするまでも無いでしょう。

[2003/12/20 追記]

いきなり「出力ファイル形式(BFD)」とか「ターゲットアーキテクチャ名」とか言われて「なんじゃそりゃ?」と思った人は、第2回で作成したobjdumpのヘルプを表示してみてください(v850-nec-elf-objdump --help)。それっぽい名前が表示されるはずです。出てきた名前の中から、一番それっぽいものを選べばOKです。

v850-nec-elf-objdump: supported targets: elf32-v850 elf32-little elf32-big srec symbolsrec tekhex binary ihex
v850-nec-elf-objdump: supported architectures: v850 v850e v850ea

エントリーポイント

大体次に来るのがエントリーポイントの定義です。エントリーポイントとは、プログラムの入り口となるアドレスです。過去の回でコンパイルしたときに"warning: cannot find entry symbol _start; defaulting to 00100000"というメッセージが出ていたのを覚えていませんか? (第4回参照) これは、"エントリーポイントが見つからないので、0x00100000から始めるよ"というメッセージだったのです。

エントリーポイントの定義にはENTRY()を使います。エントリーポイントはスタートアップルーチンで、今回は_startupという名前で定義したので、次のように記述します。

ENTRY(_startup);

メモリ空間の定義

続いてはメモリ空間と空間の属性定義を行います。

リンカであるldが必要とするメモリの情報には、次のようなものがあります。

開始アドレスや大きさはいいとして、プログラムなのかを判断しなければならないのは、Intelプロセッサのようにメモリ空間に実行属性がある場合、この属性を付けてあげないとプログラムとして認識しないからです。また、ホントにメモリを必要とするかというのは、デバッグ情報などのように、プログラムにくっついてはいるけど実行するときには不要なデータを格納するためです。

実際にメモリ領域を宣言するには、MEMORYブロックを利用します。次にその書式を示します。

MEMORY {
領域名 (属性) : ORIGIN = 開始アドレス, LENGTH = 長さ
領域名2 (属性) : ORIGIN = 開始アドレス, LENGTH = 長さ
...
}

属性自体は省略可能ですが、せっかくなんでちゃんと書きましょう。属性指定子として、次の文字を指定してあげることで、メモリ領域の属性を設定できます。

属性文字意味
R読出しのみ可能な空間 (ROMとか)
W読出しも書込みも可能な空間 (RAMとか)
Xプログラムを置く空間
A実際にターゲット上に取られる空間
I初期化対象となる空間

今回対象となるV850のメモリ定義は次のようになります。

MEMORY {
ROM(rxai) : ORIGIN = 0, LENGTH = 64k
RAM(wxai) : ORIGIN = 0x00ffe000, LENGTH = 4k
}

セクション

ついにリンカスクリプトの一番重要な場所、セクション定義です。セクション定義では、プログラムを作成する際に配置の基準となるセクションと呼ばれる単位が、実際メモリのどこに置かれるのかを記述します。アドレス解決などは、このセクション定義を元にして行われます。

セクションは、次のようにして作成します (簡略化してありますので、ちゃんとした内容が知りたければinfoをどうぞ)。セクションブロックでは、セクションの定義と共にシンボルを生成するコマンドなどを利用できます。

SECTIONS {
セクション名 : {} > メモリ領域名
コマンド
...
}

で、覚えておいたほうが後々便利なコマンドが次の通り。

意味
.現在の位置に相当するアドレス
シンボル名 = .;現在のアドレスに、シンボル名で指定した名前のシンボルを生成する
. = ALIGN(n);現在のアドレスをnバイトアラインする

実際にここで定義しなければならないセクションは、ELFのマニュアルに書いてあります。具体的には、少なくとも次のようなセクションを作らなければなりません。

セクション名意味
.textプログラムコードが入る領域 (SHT_PROGBITS,SHF_ALLOC|SHFEXECINSTR)
.rodata変更されない文字列とか定数とかが入る領域 (SHT_PROGBITS,SHF_ALLOC)
.data初期値を持った変数とかを置く領域 (SHT_PROGBITS,SHF_ALLOC|SHF_WRITE)
.bss初期値を持たない変数とかを置く領域 (SHT_NOBITS,SHF_ALLOC|SHF_WRITE)

特に.bssに関しては前回初期化しろと言われていたので、領域がどこからどこまでなのかを知るために、適切なシンボルを置いておく必要があります。


リンカスクリプト

上の条件を満たすようなリンカスクリプトを作るとこうなります (めちゃくちゃシンプルですが、本当はもっといろいろ書く必要があります)。

ld.script
OUTPUT_FORMAT("elf32-v850", "elf32-v850","elf32-v850");
OUTPUT_ARCH(v850);
ENTRY(__startup);
MEMORY
{
ROM(rxai) : ORIGIN = 0, LENGTH = 64k
RAM(wxai) : ORIGIN = 0x00ffe000, LENGTH = 4k
}
SECTIONS
{
.text : {} > ROM
.rodata : {} > ROM
.data : {} > RAM
. = ALIGN(4);
__bss_start = .
; .bss : {} > RAM
__bss_end = . ;
}

さぁこれでリンカスクリプトが完成しました。やっとスタートアップルーチンが書けます。


次回予告

リンカスクリプトの完成により、次回ではついにスタートアップルーチンを作り直す作業に移れるようになった。やってる内容もだんだん組込み色が強くなり、これまで高級言語であるC言語が隠していた世界がどんどん暴かれていく。さぁそろそろちゃんと起動するプログラムが書けるのか。それはやってる本人にもわかんない。

次回はスタートアップを作りなおします。