トップページ

サーバーのMake処理&実行時インターフェース

Loaderによってサーバーがロードされていよいよ実行されるところまで来ました。 しかしサーバーの動作の解析に移る前にサーバープログラムがどのようにして コンパイルされてリンクされているのか、そしてサーバー本体が実行される前に どのような手続きが行われているのかを検証することは有益です。普段gccのコマンド 一発でmain関数から始まる実行可能なファイルが出来上がってしまいますが、 その裏で実行開始アドレスを定めたりmainを呼び出すスタートアップルーチンを 作ったり、ライブラリやシステムコールを呼び出せるようなインターフェイスを 構築したり、などいろいろなことをやっています。しかし、MONAのようにOSを一から 作成する場合はこれらの処理をすべて自前で記述しなければなりません。 そこで、この文書では大きく次の2点について説明します。


サーバーのMakefile探訪

プログラムがどのようにして出来上がるのかを調べるためにはMakefileを解析するのが 一番です。ここではFILE.BINのMakefileを例にとって解析することにします。 次に示してあるのがサーバーFILE.BINのMakefileです。 結構すっきりとしていますが、実はinclude命令によって$(MONADIR)/env/monapi-bin.inc の内容がごっそりとここにコピーされます。またmonapi-bin.incの中でも include命令が使われているので、さらに別のファイルの内容もごっそりとコピーされます。 一方monapi-bin.incのほかにdependenciesというファイルもインクルードされています。 これはどの.cppファイルが.hファイルを必要としているか、というような情報が書かれて います。このファイルだけを見てもわからないのでmonapi-bin.incの内容の一部も 示します。

TARGET  = FILE
SOURCES = main.cpp file.cpp bzip2.cpp dtk5s.cpp
INSTDIR = $(BINDIR)/root/SERVERS

MONADIR    = ../../..
ADDLINK    = -lbz2
ADDLINKDEP = $(MONADIR)/lib/libbz2.a
include $(MONADIR)/env/monapi-bin.inc

include dependencies

次に示してあるのがmonapi-bin.incの内容です。ここにきてやっとCXXとかCCやLDといった 名前が出てきました。これらはそれぞれg++(gccのC++コンパイラ), gcc, ld(リンカ)を あらわしています。CXXはCCというコマンドが実行される部分は、「ああ、コンパイルして オブジェクトコードを吐いているんだな」という程度の認識でかまいません。重要なのは LDでリンクする部分です。どのようにリンクを行うかでオブジェクトコードがどのメモリ アドレスに配置されるのかが決まり、はじめに何が実行されるのかが決まります。つまり、 リンク作業を解析することで、プログラムの初めに何が実行されるのかがわかるように なります。

($LD)コマンドを実行させている行を注意深く見ると入力ファイルに$(LIBDIR)/monapi.oと $(OBJECTS)があるのがわかります。$(OBJECTS)というのはソースコードをコンパイルして できたオブジェクトファイルです。このことから、FILE.BINの作成にはサーバーの ソースコードのほかにmonapi.oの中にあるコードを実行しなければならないことが わかります。これらのオブジェクトファイルがどのようにして1つのファイルに まとめられるかは$(LFLAGS)にあるリンカオプション次第です。例によってですが このオプションの定義はmonapi.inctというファイルに書かれているので今度は それを見ることにします。

include $(MONADIR)/env/monapi.inc

.SUFFIXES: .cpp .o
.cpp.o:
	$(CXX) -c -o $@ $(CXXFLAGS) $(INCLUDE) $<

.SUFFIXES: .c .o
.c.o:
	$(CC) -c -o $@ $(CFLAGS) $(INCLUDE) $<

.SUFFIXES: .asm .o
.asm.o:
	$(NASM) $(NFLAGS) -o $@ -felf $<

all: $(TARGET).BIN

$(TARGET).BIN: $(OBJECTS) $(LINKDEP)
	$(LD) $(LFLAGS) -o $@ $(LIBDIR)/monapi.o $(OBJECTS) -L$(LIBDIR) $(LINK)
	$(STRIP) -O binary $@

install: all
	mkdir -p $(INSTDIR)
	$(INSTALL) $(TARGET).BIN $(INSTFILES) $(INSTDIR)

//以下、略

下のリストがmonapi.incのファイルの内容です。リンカオプションは条件によって 中身が変わるようになっています。サーバーを構築する場合は$(IMPSFX)という変数には 何も入っていないのでelse以降の定義が採用されます。LFLAGSで指定されているオプションは 次の意味を持っています。

-n
a.out形式でマジックナンバをNMAGICにするものです。といっても何の事だかわかりませんね。 実は私自身もなぜこのオプションがあるのかわかりません。しかし、このオプションをはずすと 一部のデータが4キロバイトのアライメントを喰らう“らしく”、フロッピーディスクで 動いているMONAには死活問題になる為、オプションがついていると“推測”しています。 詳しいことを知っている人がいたら教えてください。
-Ttext 0xA0000000
これはプログラムの開始アドレスを0xA0000000にするということです。つまり、 プロセスのメモリの0xA0000000にロードされなければならないことをあらわしています。 覚えている人もいるかもしれませんが、Loaderクラスはまさに0xA0000000にサーバーを ロードしています。
-e $(USER_START_FUNCTION)
プログラムが初めに実行する関数が$(USER_START_FUNCTION)であることをあらわしています。 この変数の中身は“_user_start”となっています。このことはmonapi.incと同じ ディレクトリにあるMakefile.incに記述されています。尚、C言語では初めのアンダースコアは 抜けるので“user_start”という表示になります。
include $(MONADIR)/env/dirnames.inc
include $(MONADIR)/env/Makefile.inc

ifeq ($(IMPSFX),-imp)
LFLAGS    = $(MONAELF_LDS) -n --image-base=0xa0000000 -e $(USER_START_FUNCTION)
else
LFLAGS    = $(MONAELF_LDS) -n -Ttext 0xA0000000 -e $(USER_START_FUNCTION) -Bstatic
OFLAGS    = --output-target=elf32-i386
endif
OBJECTS   = $(SOURCES:.cpp=.o) $(CSOURCES:.c=.o) $(NASMSRCS:.asm=.o)
INCLUDE   = -I$(INCDIR)
LINK      = $(ADDLINK) -lmonapi$(IMPSFX)
LINKDEP   = $(LIBDIR)/monapi.o $(LIBDIR)/libmonapi$(IMPSFX).a $(ADDLINKDEP)

_user_startからMonaMainへ

サーバープログラムのMakefileを解析した結果次のようなことがわかりました。

それでは_user_start関数(実際には始めのアンダースコアは抜けるのでuser_start関数) はどこにあるのでしょうか。サーバープログラムのソースコードを探してみましたが 見つかりませんでした。

tkralia:~/Mona/src/servers/file$ ls
FILE.BIN*     Makefile2*  bzip2.o       dtk5s.h    file.h*   main.cpp*
FileServer.h  bzip2.cpp*  dependencies  dtk5s.o    file.map  main.o
Makefile*     bzip2.h*    dtk5s.cpp     file.cpp*  file.o
tkralia:~/Mona/src/servers/file$ grep user_start *.cpp
tkralia:~/Mona/src/servers/file$

ではどこにあるのかというと/Mona/src/lib/monapi.cppので定義されています。 つまりFILE.BINの実行開始アドレスにはmonapi.cppのuser_start関数があって、 それが初めに実行されるわけです。このような理由のためmonapi.oをリンクしなければ なりませんでした。

tkralia:~/Mona/src/lib/monapi$ grep user_start *.cpp
monapi.cpp:extern int user_start_impl(FuncMonaMain* monaMain);
monapi.cpp:extern "C" int user_start()
monapi.cpp:    int result = user_start_impl(MonaMain);
monapi_impl.cpp:int user_start_impl(FuncMonaMain* monaMain)
tkralia:~/Mona/src/lib/monapi$

では早速monapi.cppで定義されているuser_start関数の中身を見てみましょう。 関数は下のように定義されており、そこから呼び出される関数はmonapi_impl.cppに 定義されています。

まず、monapi_initalize_memoryでユーザーが使用できるメモリ空間を定めています。後に示すように 0xC0000000から8メガバイト分の領域を取得できるようになっています。そして、setConstructorListと invokeFunctionListはそれぞれC++言語の実装のためにあります。C++言語の静的なオブジェクトは main関数が呼び出される前にコンストラクタが呼び出されるようになっています。そして、main関数での 処理が終了した後デストラクタが呼び出されるようになっています。これをCPUレベルで 実現しているのがこれらの関数で、setConstructorListでコンストラクタのポインタのリストを設定し、 invokeFunctionListでリストで示されている関数を実行します。__CTOR_LIST__がコンストラクタの リストであり__DTOR_LIST__がデストラクタのリストです。

このリストに何が格納されているのかをあきらかにし、実際にこれらの関数が呼び出される ということを示したいのですが、__CTOR_LIST__、__DTOR_LIST__には0xffffffff(-1)が 格納されており、これは何も格納されていないことを意味しています。したがって、 コンストラクタの呼び出しの様子を示すことはできません。

コンストラクタおよびデストラクタのリストはgccによって作成されるので通常は考慮する必要は ありませんが、このように実行時環境を作成するときは意識する必要があります。

extern "C" int user_start()
{
    monapi_initialize_memory();
    setConstructorList(__CTOR_LIST__);
    invokeFuncList(__CTOR_LIST__);
    int result = user_start_impl(MonaMain);
    invokeFuncList(__DTOR_LIST__);
    return result;
}
static MonAPI::MemoryManager um;

void monapi_initialize_memory()
{
    um.initialize(0xC0000000, 0xC0000000 + 8 * 1024 * 1024);
    MonAPI::MemoryMap::initialize();
}
static FuncVoid** ctor_list = NULL;

void setConstructorList(FuncVoid** ctors)
{
        ctor_list = ctors;
}
void invokeFuncList(FuncVoid** list)
{
    int count = (int)*list++;
    list = (FuncVoid**)((((dword)list) + 3) & ~3);
    if (count == -1)
    {
        for (; *list != NULL; list++) (**list)();
    }
    else
    {
        for (int i = 0; i < count; i++, list++) (**list)();
    }
}

適当なコンストラクタを呼び出したらいよいよMonaMain関数を呼び出します。MonaMain関数は 引数をリストクラスでとるのでその設定をsetupArguments関数で行っています。 引数の受け渡しはシステムコールを用いて行っています。引数のリストができたらそれをMonaMain関数の 引数に設定して呼び出します。MonaMain関数の実行が終わればexit関数を呼び出して終了します。

関数exitは適当なメッセージを送信してプロセスを終了させています。 この辺はまた別の機会に説明する予定です。

int user_start_impl(FuncMonaMain* monaMain)
{
    bool dll = isInDLL(__CTOR_LIST__);
    if (dll) invokeFuncList(__CTOR_LIST__);

    List<char*>* arg = new HList<char*>();
    setupArguments(arg);

    int result = (*monaMain)(arg);

    delete arg;
    if (dll) invokeFuncList(__DTOR_LIST__);
    exit(result);
    return 0;
}
void setupArguments(List* arg) {

    char* str;
    int num = syscall_get_arg_count();

    for (int i = 0; i < num; i++) {

        str = (char*)malloc(32);
        if (syscall_get_arg(str, i) == 1) break;
        arg->add(str);
    }
}
トップページ
inserted by FC2 system