libstand で遊ぶ

FreeBSD には( NetBSD にもあるようです)スタンドアローンなプログラム( OS などのない生の計算機環境)で使うための、OS の下の環境をある程度再現するライブラリ libstand があります。網羅的なドキュメンテーションは man 3 libstand で確認してください。
最終的にはそういった OS なしの環境で実行するためのプログラムを、開発中は OS の下で実行する、ということはよく行われます。ここでは、FreeBSD の環境で hello, world を実行するまでの簡単な実験を解説します。
まず、内容が空のソースコードを作り、コンパイルしてみます。

$ touch hello.c
$ gcc -m32 -ffreestanding -static -nostdlib hello.c -lstand

オプションについて解説します。

  • -m32 そういった組込み環境で AMD64 はまだ少ない、ということからか、AMD64 環境でも libstand は 32 ビット(IA-32)ですので、gcc を 32 ビットモードにします。
  • -ffreestanding GCC の組込みライブラリなどを使わないコードを生成するよう指示します。これはコンパイルオプション。
  • -static リンク時にスタティックリンクするよう指示します。OS なし環境用なのですから、動的リンクはできない前提です。
  • -nostdlib スタートアップルーチンや標準ライブラリなど、普通のビルドであれば暗黙のうちにリンクされるモジュールをリンクしないよう指示します。
  • -lstand libstand をリンクする指示です。

gcc を実行してビルドすると、

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074

というウォーニングが出力されます。エントリポイントである _start が見つからなかったので、デフォルトで(コード領域の先頭の)0x08048074 を設定した、というメッセージです。
実行ファイルは生成されますが、当然ながら、実行しても segmentation fault で落ちます。
libstand ではスタートアップルーチン(普通のプログラムにおいて main より前に実行されるコード)が提供されていないことがわかりましたので、自分で _start を定義することにします。

$ cat hello.c
void
_start(void)
{
}
$ gcc -m32 -ffreestanding -static -nostdlib hello.c -lstand

今度はウォーニングが出ずにビルドが終了します。
ここで readelf -a a.out として、生成された実行ファイルの中身を見てみるとわかりますが、これだけでは中身には何もリンクされません。当然といえば当然かもしれませんが。ですので、hello world の表示のコードを加えてみます。ヘッダのインクルードも追加しました。

$ cat hello.c
#include <stand.h>

void
_start(void)
{
  printf("hello, world\n");
}
$ gcc -m32 -ffreestanding -static -nostdlib hello.c -lstand
/usr/lib/../lib32/libstand.a(printf.o)(.text+0x1016): In function `vprintf':
: undefined reference to `putchar'
/usr/lib/../lib32/libstand.a(printf.o)(.text+0x107c): In function `printf':
: undefined reference to `putchar'

すると putchar が解決できず、リンクが失敗します。libstand は、文字出力を行う組込みプログラムなら、たとえばシリアルポートに 1 文字出力するような関数を putchar として libstand にプログラマが提供する、というように設計されています。
本格的な組込み開発であれば、本番用と OS 内用でモジュールをさしかえるところですが、ここでは OS の write を呼んで標準出力に1文字出力するコードを単に追加してみます。

$ cat hello.c
#include <stand.h>

void
_start(void)
{
  printf("hello, world\n");
}

void
putchar(int c)
{
  char cc = c;
  write(1, &cc, 1);
}
$ gcc -m32 -ffreestanding -static -nostdlib hello.c -lstand
/usr/lib/../lib32/libstand.a(open.o)(.text+0x83): In function `open':
: undefined reference to `devopen'
/usr/lib/../lib32/libstand.a(open.o)(.text+0xb5): In function `open':
: undefined reference to `file_system'
/usr/lib/../lib32/libstand.a(open.o)(.text+0xe3): In function `open':
: undefined reference to `file_system'
/usr/lib/../lib32/libstand.a(open.o)(.text+0x103): In function `open':
: undefined reference to `file_system'
/usr/lib/../lib32/libstand.a(open.o)(.text+0x1aa): In function `open':
: undefined reference to `devclose'
/usr/lib/../lib32/libstand.a(zalloc_malloc.o)(.text+0x124): In function `Free':
: undefined reference to `panic'
/usr/lib/../lib32/libstand.a(zalloc_malloc.o)(.text+0x146): In function `Free':
: undefined reference to `panic'
/usr/lib/../lib32/libstand.a(zalloc.o)(.text+0x1ce): In function `zfree':
: undefined reference to `panic'
/usr/lib/../lib32/libstand.a(zalloc.o)(.text+0x274): In function `zfree':
: undefined reference to `panic'
/usr/lib/../lib32/libstand.a(zalloc.o)(.text+0x294): In function `zfree':
: undefined reference to `panic'

えらくエラーが増えました。これは libstand の中の write を呼んでしまった結果です。よく考えれば当然ですが、libstand の write は、OS を呼ぶためのものではないので、OS の代わりの環境を提供するための関数が必要になってしまったわけです。
OS のシステムコールをおこなうコードを、自分で書くことにします。

$ cat hello.c
#include <stand.h>

static void sys_exit(int);
static void sys_write(int, char [], int);

void
_start(void)
{
  printf("hello, world\n");
  sys_exit(0);
}

void
putchar(int c)
{
  char cc = c;
  sys_write(1, &cc, 1);
}

__asm__ (".text\n"
        "\t.type\tsys_exit, @function\nsys_exit:\n"
        "\tmov\t$0x1, %eax\n"
        "\tint\t$0x80\n"
        "\tret\n");

__asm__ (".text\n"
        "\t.type\tsys_write, @function\nsys_write:\n"
        "\tmov\t$0x4, %eax\n"
        "\tint\t$0x80\n"
        "\tret\n");
$ gcc -m32 -ffreestanding -static -nostdlib hello.c -lstand
hello.c:1: warning: 'sys_exit' used but never defined
hello.c:2: warning: 'sys_write' used but never defined
$ ./a.out
hello, world

インラインアセンブラで関数定義を直接書いています。Cで定義していないために、宣言だけで定義がない、というウォーニングが出てしまっていますが無視します。putchar のための write の他、_exit を呼ぶ関数も定義しました。これで hello, world を表示して、正常終了もできています。
スタンドアローン環境のためのライブラリ libstand を使って hello, world を動かすまで、を解説しました。