次に、「ELF形式」について説明します。
OSの実行モジュールは、実際に動作するときのベタなメモリイメージでファイルになっているわけではありません。目的ごとに幾つかの領域に分割されており、ヘッダが付加された状態でファイルになっています。
このフォーマットには幾つかの種類があるのですが、現在はELF形式と呼ばれるものが主流になっています。図3は、ELF形式の構造です。
ELF形式は、モジュールの内部を「セクション」と「セグメント」という領域単位で管理します。セクションは「セクションヘッダ」、セグメントは「プログラムヘッダ」により管理されています。ブートローダーはセグメント単位で実行モジュールをメモリ上に展開します。
例えば、先に作成した「Hello World」の実行モジュールも、ELF形式になっています。試しに解析してみましょう。ELF形式は「readelf」というコマンドで解析することができます。リスト7が「readelf」の出力結果です。
% readelf -a kozos ELF Header: Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, big endian ……(中略)…… Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00ffc020 000074 0003e4 00 AX 0 0 2 [ 2] .rodata PROGBITS 00ffc404 000458 000036 01 AMS 0 0 1 [ 3] .data PROGBITS 00ffc43c 00048e 00000c 00 WA 0 0 4 [ 4] .bss NOBITS 00ffc448 00049a 000020 00 WA 0 0 1 [ 5] .shstrtab STRTAB 00000000 00049a 000024 00 0 0 1 ……(中略)…… Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x00ffbfac 0x00ffbfac 0x0048e 0x0048e R E 0x1 LOAD 0x00048e 0x00ffc43c 0x00ffc43c 0x0000c 0x0002c RW 0x1 ……(後略)……
リスト7 readelfによるHello Worldの解析 |
リスト7では、まずELFヘッダの情報が表示され、さらにセクションヘッダ、プログラムヘッダの情報が表示されています。このため、ブートローダーがELF形式を解釈しメモリ上に展開することができれば、「Hello World」を実行することができることになります。
リスト8は、「elf.c」からセグメント展開処理を抜粋したものです。「elf_load_program()」ではループによってプログラムヘッダを順次参照し、セグメント情報を取得して、「memcpy()」によりメモリ上に展開しています。これもやはり数十行で書くことができます。
/* セグメント単位でのロード */ static int elf_load_program(struct elf_header *header) { int i; struct elf_program_header *phdr; for (i = 0; i < header->program_header_num; i++) { /* プログラム・ヘッダを取得 */ phdr = (struct elf_program_header *) ((char *)header + header->program_header_offset + header->program_header_size * i); if (phdr->type != 1) /* ロード可能なセグメントか? */ continue; memcpy((char *)phdr->physical_addr, (char *)header + phdr->offset, phdr->file_size); memset((char *)phdr->physical_addr + phdr->file_size, 0, phdr->memory_size - phdr->file_size); } return 0; }
リスト8 ELF形式のセグメント展開部分 |
「elf.c」は、「xmodem.c」と同じ94行で書かれています。表1を見ると、ブートローダーのソースコードの総量は678行です。簡単なブートローダーならば、この程度の行数で書けてしまうものなのです。
今回起動した「Hello World」のソースコードは、表2のようなファイル構成になっています。
ファイル名 | 行数 | 内容 |
---|---|---|
defines.h | 11 | 各種定義 |
ld.scr | 47 | リンカスクリプト |
lib.c | 135 | ライブラリ |
lib.h | 18 | 「lib.c」のヘッダファイル |
main.c | 26 | メイン関数 |
serial.c | 112 | シリアルドライバ |
serial.h | 10 | 「serial.c」のヘッダファイル |
startup.s | 10 | スタートアップ(アセンブラ) |
合計 | 369 | 全ソースコードの総ステップ数 |
表2 Hello Worldのソースコード一覧 |
実は、「Hello World」のソースコード自体は連載第1回で紹介した「Hello World」とほとんど同じです(ライブラリ関数が幾つか追加されている点と細かい差異を除けば)。大きな違いは、フラッシュROMに書き込んで電源ONで「Hello World」を直接起動しているのではなく、ブートローダーを介して起動している点です。そのため今回は、割り込みベクターの設定(vector.c)は不要になり(削除され)、さらにメモリマッピングの調整でリンカスクリプト(ld.scr)を修正しています。
リスト9は、「Hello World」のリンカスクリプトです。本連載で利用している「H8/3069F」というマイコンは、16Kバイトの内蔵RAMを持っており、「0xffbf20」〜「0xffff20」というアドレスにマッピングされています。リスト9は、内蔵RAM上に「ram」という領域を定義し、テキスト領域やデータ領域は全てRAM上にマッピングしています。
OUTPUT_FORMAT("elf32-h8300") OUTPUT_ARCH(h8300h) ENTRY("_start") MEMORY { ramall(rwx) : o = 0xffbf20, l = 0x004000 /* 16KB */ ram(rwx) : o = 0xffc020, l = 0x003f00 stack(rw) : o = 0xffff00, l = 0x000000 /* end of RAM */ } SECTIONS { .text : { _text_start = . ; *(.text) _etext = . ; } > ram .rodata : { _rodata_start = . ; *(.strings) *(.rodata) *(.rodata.*) _erodata = . ; } > ram .data : { _data_start = . ; *(.data) _edata = . ; } > ram .bss : { _bss_start = . ; *(.bss) *(COMMON) _ebss = . ; } > ram . = ALIGN(4); _end = . ; .stack : { _stack = .; } > stack }
リスト9 Hello Worldのリンカスクリプト |
今回はブートローダーを作成し、ブートローダーから「Hello World」を起動しました。次回は、簡単なタスク切り替えを実装してみることで、“組み込みOSの本質”に迫っていきたいと思います。お楽しみに! (次回に続く)
坂井弘亮(さかいひろあき)
某企業でネットワーク系の開発を行いながら、個人で組み込みOS「KOZOS」の開発や書籍/雑誌記事執筆、各種セミナーでの発表やイベント出展、勉強会の開催など多方面で活動中。IPAセキュリティ&プログラミングキャンプ 2010講師。著書は「12ステップで作る組込みOS自作入門」など多数。
Copyright © ITmedia, Inc. All Rights Reserved.