#include <stdio.h> int main(int argc, char *argv[]) { puts("Hello, world"); return 0; }なるhello.cを
% sh-elf-gcc hello.cでコンパイルできます。a.outというファイルが新しくできるはずですが、 これがELF形式の実行ファイルになっています。
…あれ? あっけなさすぎてちょっと不安ですね。 ターゲットシステムのメモリマップとか知らせないで大丈夫なんでしょうか。 ちょっと検証してみましょう。
% sh-elf-objcopy -O srec a.out a.motというコマンドで、ELFフォーマットで記述してある 実行ファイル(?)a.outを、モトローラS形式でa.motに書き出します。 これを直接読んでもいいんですが、もっと分かりやすい表示がいいなら
% sh-elf-objdump -x a.outが便利でしょう。
…ありゃりゃ、なんだかメモリの配置が決め打ちですね。 おいおい修正することにします。で、次はこれを実行してみましょう。 んっふっふっ。そうです。実行できるんです。
% sh-elf-gdb a.out GNU gdb 5.0 [略] (gdb) target sim Connected to the simulator. (gdb) load a.out Loading section .text, size 0x1dbc vma 0x1000 Loading section .rodata, size 0x32 vma 0x2dbc Loading section .data, size 0x73c vma 0x2e70 Loading section .stack, size 0x4 vma 0x30000 Start address 0x1000 Transfer rate: 76144 bits in <1 sec. (gdb) run Starting program: ......../a.out Hello, world Program exited with code 012. [Switching to process 0]いやあ、GNUのツールってすごいですねえ…^o^ という個人的な感慨はさておき、ちょっと解説。
まず、target文にてターゲットシステムを指定します。 本物のリモートデバッグ環境があるのならここで指定しても良いのですが、 そんなものないという場合は内蔵シミュレータに「接続」します。
そして、load文でシミュレータのメモリに実行イメージを「ダウンロード」し、 run文にて実行を開始します。
この流れは、ICEを使っている方ならおなじみだと思います。 おのおののgdbのコマンドがわからない場合は、 コマンドラインから「help コマンド」とすれば参照できます。
個人的にはなんでホストコンピュータ環境に 出力ができるのかすんごい不思議なんですが、 おそらくシミュレータ環境がソフトウェア割込みをトラップしてるんでしょう。 GNUの優秀な開発者に感謝しつつ、先に進みましょう。
そもそも、リンケイジエディタとは、 コンパイルしてできたオブジェクトファイルを、 セクション(というおなじ属性をもったメモリ空間)ごとに集め、 指定されたアドレスに配置するプログラムです。 さきほどgccの動作確認を行いましたが、 そのときのようにsh-elf-gccに-vオプションをつけて実行ファイルを作ってみると 最後にリンクエディット作業が(実際にはcollect2経由で)実行されるのが観察できます。 現在構築中の開発環境の場合、sh-elf-ldコマンドがリンケイジエディタです。 今後は、GNU ld、もしくはgldと呼ぶことにします。
GNU ldはldscriptと呼ばれる設定ファイルに従って オブジェクトの配置を行います。 デフォルトではビルトインのldscriptを使用しますが (GNU ldのgldshelf_get_script関数中にあるものです)、 実際には/usr/local/sh-elf/lib/ldscripts/shelf.xと同じ内容のようです。
メモリへの配置方針は、組み込みシステムの場合以下のように行います。
メモリマップのほうは、以下のように仮定します:
SECTIONSで囲まれたブロックがありますが、 この中の記述がセクションの配置を決定します。この書式は
出力セクション [start-address]: { ファイル名(入力セクション [...]) [...] }となっています。例として出力セクション".rodata"から見ていきましょう。
.rodata : { *(.rodata) *(.rodata.*) *(.gnu.linkonce.r*) }まず、"*(.rodata)"という部分ですが、これは 「入力ファイルの中にあるすべての.rodataセクション」という意味になります。 入力ファイル中のセクション名と、出力ファイル中のセクション名とは独立で、 入力ファイル中で.rodataセクションであったからといって、 必ずしも出力ファイル中でも.rodataセクションになるとは限りません。 現に、次の"*(.rodata.*)"という指示は、 「入力ファイルの中にあるすべての.rodataほにゃららというセクションを、 出力ファイル中の.rodataに配置する」という意味になります。 また、出力ファイル中の.rodataセクションは、.gnu.linkonce.rほにゃららというセクションも含みます。
ここでは、特にセクションの配置関係自体は変更しないことにします。 また、デバッグ用の情報を含むセクションについてですが、例えば
.debug_info 0 : { *(.debug_info) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_line 0 : { *(.debug_line) }となって0番地に配置されるように見えますが、 実際にはこのようなセクションは"実行ファイル中には存在するが、ターゲットメモリに配置しない"というフラグが付加されるので特に問題ありません。sh-elf-stripコマンドにて実行ファイルから除去することができます。
ldscriptの中では、他にシンボルの定義や、 ロケーションカウンタ(オブジェクトの配置をするアドレス)の設定を行います。 ロケーションカウンタを適切に設定することでターゲットシステムのメモリマップに合わせることができます。
変更点1
/* Read-only sections, merged into text segment: */ . = 0x1000; .interp : { *(.interp) }自由に使えるROM領域は0x2000からなので、これを
/* Read-only sections, merged into text segment: */ . = 0x2000; .interp : { *(.interp) }に変更します。
変更点2
.rodata1 : { *(.rodata1) } /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = .; .data : {ここは、read onlyなデータセクションから、writableなデータセクションに切り替わるところです。 以降のセクションがRAM上に存在するように、次のように変更します。
.rodata1 : { *(.rodata1) } .data 0x100000 :
変更点3
.debug_varnames 0 : { *(.debug_varnames) } .stack 0x30000 : { _stack = .; *(.stack) } /* These must appear regardless of . */ここは、ヒープ領域が終わり、スタック領域が始まる部分です ("_stack"というシンボルも定義しています)。 これも、個々のターゲットシステムのスタック領域の開始アドレスに変更します。
.stack 0x280000 : { _stack = .; *(.stack) }
Makefile
include Rules.make CFLAGS = -Wl,-Tdefault.ld run: var_addr $(RUN) var_addrRules.make
EXEC_PREFIX =sh-elf- CC =$(EXEC_PREFIX)gcc AS =$(EXEC_PREFIX)gas LD =$(EXEC_PREFIX)ld AR =$(EXEC_PREFIX)ar OBJDUMP =$(EXEC_PREFIX)objdump OBJCOPY =$(EXEC_PREFIX)objcopy NM =$(EXEC_PREFIX)nm RANLIB =$(EXEC_PREFIX)ranlib STRIP =$(EXEC_PREFIX)strip STRINGS =$(EXEC_PREFIX)strings GDB =$(EXEC_PREFIX)gdb PROF =$(EXEC_PREFIX)gprof RUN =$(EXEC_PREFIX)run CC_GENMAP =-Wl,-Mvar_addr.c
#include <stdlib.h> char *extern_bss; char *extern_data = "string literal for extern"; static char *filescope_bss; static char *filescope_data = "string literal for filescope"; void extern_func(void) { } static void private_func(void) { } int main() { int auto_var; static char *funcscope_bss; static char *funcscope_data = "for funcscope"; extern_func(); private_func(); printf("string literal for extern scope %p\n", extern_data); printf("string literal for file scope %p\n", filescope_data); printf("extern func addr at %p\n", &extern_func); printf("private func addr at %p\n", &private_func); printf("initilized extern variable at %p\n", &extern_data); printf("initilized filescope variable at %p\n", &filescope_data); printf("initilized funcscope variable at %p\n", &funcscope_data); printf("uninitilized extern variable at %p\n", &extern_bss); printf("uninitilized filescope variable at %p\n", &filescope_bss); printf("uninitilized funcscope variable at %p\n", &funcscope_bss); printf("auto variable at %p\n", &auto_var); printf("malloc returns addr %p\n", malloc(4)); return 0; }これらを、コンパイル&実行します:
% make var_addr sh-elf-gcc -Wl,-Tdefault.ld var_addr.c -o var_addr % make run sh-elf-run var_addr string literal for extern scope 0x8e80 string literal for file scope 0x8e9c extern func addr at 0x2034 private func addr at 0x2040 initilized extern variable at 0x100000 initilized filescope variable at 0x100004 initilized funcscope variable at 0x100008 uninitilized extern variable at 0x100790 uninitilized filescope variable at 0x100760 uninitilized funcscope variable at 0x10075c auto variable at 0x27ffec malloc returns addr 0x100ba8-Wl,-Tdefault.ldというのが個別のldscriptを使うgccのオプション(*)なのですが、 これを付けないときと比較すると確かにアドレスの配置が変化しているのがわかります。
(*)GNU ldにとっては、-Tdefault.ldというオプションになります。 gccに"-Wl,opt1,opt1,..."というオプションを指定すると、ldに"opt1 opt2 ..."というオプションを渡すことになります。