# nlux - nlcc フルスクラッチで開発された,簡単なCコンパイラ - nlsh フルスクラッチで開発された,簡単なシェル - nllibc フルスクラッチで開発された,簡単な標準Cライブラリ - nlline フルスクラッチで開発された,簡単なreadline互換ライブラリ - nll フルスクラッチで開発された,簡単なスクリプト言語 - nltl フルスクラッチで開発された,簡単なコンパイラ型言語 - nlux それらの集合体の総称 No Look, No Listen, No Learn, No Library 何も見ず,何も参考にせずに,フルスクラッチで開発しています. (理由は,そうしたいからです) すべてをnlccでコンパイルすることができます. すべてをnllibcやnllineをリンクしてビルドすることができます. つまりnlccやnlshを,nllibcやnllineをリンクしてビルドすることで, システムに付属のライブラリを一切使わずに閉じてビルドすることができます. またそれらをnlccでビルドすることができます. なので,例えばnlccをビルドして,そのnlccでnllibcをビルドして,そのnllibcを 利用してnlccをnlccでビルドして,...といったことができます. 基本としてFreeBSD/Debian環境で動作します. # nlccの特徴 セルフビルドができます.(自分自身で自分自身をコンパイルできます) gccでnlccをビルドして,そのnlccでまたnlccをビルドして,そのnlccでまたnlccを ビルドして,それらの実行ファイルが一致することを確認してあります. つまりgccでビルドしたnlccと,nlccでビルドしたnlccの動作が一致する,ということ です.nlccはセルフビルド向けに書いているということはなく,作者が普段どおりの コーディングスタイルで書いていますので,作者が普段書くようなプログラムは コンパイルできる,ということでもあります. 各種アーキテクチャへの対応(クロスコンパイラの作成)が非常に低いコストで できます.asm_code_*.c というファイルを作成することでアーキテクチャ対応が できます(6百行程度のファイルを作成するだけで,新たなアーキテクチャに対応 できるということです).実際に様々なアーキテクチャにサンプル対応してあります. - x86/amd64 セルフビルドや各種テストが通ることを確認 - ARM/AArch64/Thumb/Thumb2/MIPS/MIPS16/PowerPC/RISC-V/RX cross-gcc11の環境で動作を確認 - OSECPU 実験的対応 ABIはgccに合わせているので,gccでビルドしたオブジェクトファイルとnlccで ビルドしたオブジェクトファイルをリンクして動作させることができます. 高度な最適化をしません.このため生成される実行コードは非常に無駄が多く速度も 速くはありません.多種アーキテクチャにいかに楽に対応できるか,というところを 重要視して開発しています.このため動作や生成コードを可能な限りアーキテクチャ間 で共通化しており,そのため無駄が多くなっています.でもアーキテクチャ対応は簡単 にできます.(基本とするレジスタを2つ選んで,各種命令を登録するだけです) アーキテクチャ対応の簡単さは,ビルトインで実現している部分が多くあります. 例えばかけ算や割算は,それらのための命令を登録しなくても構いません. その場合,かけ算をソフトウェアで行うビルトインが組み込まれます.ループを回して かけ算を行うため非常に低速ですが,それでもかけ算命令を利用せずに実行ファイルを 生成できます.シフト演算や符号拡張なども,そのための命令を登録しなくても ビルトインで実現可能です. 可変長引数,ビットフィールド,構造体の引数渡しにも対応しています. 構造体の返り値には,不十分ですが対応しています. (返すことはできるがスタックの解放済み部分を利用してしまう) 64ビット変数(long longなど)は,64ビット環境でビルドした場合には64ビットとして 動作します.32ビット環境でビルドした場合には,64ビット変数は使えますし 変数サイズも64ビットで確保されますが,内部の演算等は32ビットで行われます. (64ビット演算のエミュレーションはしていない,ということです) - nlcc1 Cコンパイラ # nllibcの特徴 標準Cライブラリです.nlccやnlshをビルドするための最低限+αくらいしか実装 されていません. システムコール・ラッパーやスタートアップ(いわゆるcrt)やリンカスクリプトも含んで います.このためシステムの標準Cライブラリを一切使わずに,完全に閉じてビルド することが可能です. 各種アーキテクチャに対応しています.以下を参照してください. % ls nllibc/crt % ls nllibc/lib/arch ビルドすると,以下の3種類のライブラリが生成されます. - nlcrt.o スタートアップ - libnlc.a 標準Cライブラリ - libnlterm.a 簡単なtermios(nllineで入力制御を行うため) # nlshの特徴 極小のシェルです.tcshに似ています. プロセス管理(バックグラウンド実行やサスペンド等)ができます. パイプやリダイレクションには対応しています. シェルスクリプトには対応していません. 行編集にreadlineライブラリを利用します.editlineやlibeditを利用することも できます.それらが利用できない場合にはgetline()やfgets()によって代替することも できます.(ただし行編集はできなくなります) nllineを用いることでreadline不要でビルドできます.(この場合は行編集が可能です) またnllibcも用いることで,外部ライブラリを用いず完全に閉じてビルドが可能です. # 使いかた Makefileを用意しています. まずトップにあるMakefileの,以下を環境に合わせて修正してください. BUILD = freebsd ARCH = x86_64 以下は利用例です. % make build.nlcc1 nlccをビルド % make install.nlcc1 nlccをビルドしインストール % make install.nllibc nllibcをビルドしインストール % make clean ビルドされたモジュールを削除 % make uninstall インストールされたモジュールをアンインストール インストールはnluxのトップディレクトリにされます. % make build.nlcc1.nlcc1 インストールされたnlccを用いて,nlccをビルドします % make install.nlcc1.nllibc インストールされたnllibcを用いて,nlccをビルドし インストールします % make install.nlcc1.nllibcall インストールされたnllibcを用いて,nlccをビルドし インストールします (crtやリンカスクリプトもnllibcのものを利用します) % make install.nlcc1.nlcc1_nllibcall インストールされたnlccとnllibcを用いて,nlccをビルドし インストールします % make install.nllibc.nlcc1 インストールされたnlccを用いて,nllibcをビルドし インストールします % make install.nllibc.nlcc1_nllibc インストールされたnlccを用いて,nllibcをビルドし インストールします (ヘッダファイルもnllibcのものを利用します) これらを組み合わせることで,まずnlccをインストールして,そのnlccを用いてnllibc をビルドして,そのnllibcを用いてnlccをビルドして...のようなことができます. (例えば以下の順番でビルドします) % make install.nlcc1 % make install.nllibc.nlcc1_nllibc % make install.nlcc1.nllibcall nlccを用いてnlccをビルドしたい場合には,以下のようにします. % make install.nlcc1 % make clean.nlcc1 % make install.nlcc1.nlcc1 makeのすべてのターゲットはmakeの-jオプションに対応しています. マルチコア環境では以下のようにすることで,高速にビルドできます. % make -j 4 build.nlcc1 以下はnlcc/nllibc/nlline等を利用して,一通りを生成する例です. (上から順に実行します) % make clean ; make uninstall % make install.nlcc1 nlccを生成 % make clean.nlcc1 ; make install.nlcc1.nlcc1 生成したnlccでnlccを再生成 % make install.nllibc.nlcc1_nllibc 生成したnlccでnllibc/nltermを生成 % make clean.nlcc1 ; make install.nlcc1.nlcc1_nllibcall 生成したnlcc/nllibcでnlccを再生成 % make install.nlline.nlcc1_nllibc_nlterm 生成したnlcc/nllibc/nltermでnllineを生成 % make install.nll.nlcc1_nlline_nlterm_nllibcall 生成したnlcc/nllibc/nlterm/nllineでnllを生成 % make test.nlcc1 % make test.nll % make test.nllibc 生成したnlcc/nllibc/nllをテスト % ./bin/nll nll> 生成したnllを起動 # テスト テストを自動化しています. nlccのテストは,テスト用のプログラムをgcc等でビルドした場合とnlccでビルドした 場合の実行ファイルの実行結果が一致することを確認しています. nllibcのテストは,テスト用のプログラムをシステムの標準Cライブラリをリンクして 動作させた場合とnllibcをリンクして動作させた場合で,実行結果が一致することを 確認してあります. このように,nlcc/nllibcに対して,gccやシステムの標準Cライブラリを利用した場合 の動作と比較して一致性を確認することで,テストプログラムの作成コストを大幅に 下げています. トップディレクトリで以下のようにして,テストが行えます. % make test.nlcc1 インストールされたnlccの動作のテストをする % make test.nllibc インストールされたnllibcの動作のテストをする 例えば以下のようにすれば,nlccでビルドしたnlccの動作のテストができます. % make install.nlcc1 % make install.nlcc1.nlcc1 % make test.nlcc1 以下のようにすれば,nlccでビルドしたnllibcの動作のテストができます. % make install.nlcc1 % make install.nllibc.nlcc1 % make test.nllibc テスト用のターゲットも,makeの-jオプションに対応しています. マルチコア環境では以下のようにすることで,高速にテストができます. % make -j 4 test.nlcc1 テスト時にはtestsuite以下にワーキングファイルが生成されますが,以下の TESTOBJDIR を修正することで,/tmpを利用することができます./tmpをメモリファイル システムにしている場合には,テストが高速化できます. testsuite/nlcc1/Makefile testsuite/nllibc/Makefile -TESTOBJDIR = testobj -#TESTOBJDIR = /tmp/testobj_nlcc1 +#TESTOBJDIR = testobj +TESTOBJDIR = /tmp/testobj_nlcc1 以下でテスト結果を再生成します.サンプルプログラムをgccやシステムの 標準Cライブラリを利用してテスト結果を生成し,その後にテストを行った場合の 比較元を生成します.(新たにテストを作成した場合に行います) % cd testsuite/nlcc1 % make testset # クロスコンパイル クロスコンパイルに対応しています. (ただしアセンブラ・リンカが必要なため,以下のcross-gcc494もしくはcross-gcc11 の環境を利用します) https://kozos.jp/books/asm/ 以下はnllをAndroid(ARM)向けにクロスコンパイルする例です. % make USE_FLOATING_POINT=yes install.nll-android-arm 以下はMinGWを用いてnllをWindows向けにクロスコンパイルする例です. (SDLが必要です.SDLを用いない場合は USE_SDL2= を指定してください) ※ SDL未使用でも,(ウィンドウ画面は出ませんが)グラフィック機能は使えます % make install.nll-mingw 以下はnlccでクロスコンパイルしたり,nllibcをクロスコンパイルする例です. (cross-gcc11を用いてnllibcをARM向けにビルド) % make CCDIR=/usr/local/cross-gcc11 CROSS=arm-eabi CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc install.nllibc (nlccを用いてnllibcをARM向けにビルド) ※ ただしアセンブラ等のためにcross-gcc11も利用 ※ Linuxの場合はOSDEFを__linux__にする.(システムコール・ラッパー等の選択のため) % make CCDIR=/usr/local/cross-gcc11 CROSS=arm-eabi CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc \ CC1="$NLROOTDIR/bin/nlcc1 -march=arm" \ install.nllibc (nlccを用いてnllibcをRISC-V向けにビルド) % make CCDIR=/usr/local/cross-gcc11 CROSS=riscv-elf CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc \ CC1="$NLROOTDIR/bin/nlcc1 -march=riscv32" \ install.nllibc (nlccを用いてnllibcをThumb向けにビルド) % make CCDIR=/usr/local/cross-gcc11 CROSS=arm-eabi CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc \ CC1="$NLROOTDIR/bin/nlcc1 -march=thumb" \ AOPTFLAGS="-mthumb" LOPTFLAGS="-mthumb" install.nllibc (nlccを用いてnllibcをThumb2向けにビルド) % make CCDIR=/usr/local/cross-gcc11 CROSS=arm-eabi CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc \ CC1="$NLROOTDIR/bin/nlcc1 -march=thumb2" \ AOPTFLAGS="-march=armv7-a -mthumb" LOPTFLAGS="-march=armv7-a -mthumb" \ install.nllibc (nlccを用いてnllibcをMIPS16向けにビルド) % make CCDIR=/usr/local/cross-gcc11 CROSS=mips-elf CCOMPILER=gcc \ OSDEF=__FreeBSD__ STDCHEADERS=nllibc \ CC1="$NLROOTDIR/bin/nlcc1 -march=mips16" \ AOPTFLAGS="-mips16" LOPTFLAGS="-mips16" install.nllibc (cross-gcc11/execの環境で,nlccを用いてサンプルプログラムをARM向けにビルドし実行) % make CROSS_CC1="$NLROOTDIR/bin/nlcc1 -march=arm" \ arm-eabi.sot (cross2-gcc11/printfの環境で,nlccとnllibcを用いてサンプルプログラムをビルドし実行) ※ Linuxの場合はPOPTFLAGSで__linux__を指定 % make CROSS_CC1="$NLROOTDIR/bin/nlcc1 -march=arm" \ INCDIR="$NLROOTDIR/include" LIBDIR="$NLROOTDIR/lib" \ POPTFLAGS="-nostdinc -D__FreeBSD__" LIBS="-lnlc -lgcc" arm-eabi.sot # nlccでの新アーキテクチャ対応 新アーキテクチャに対応する場合,asm_code_(アーキテクチャ名).[ch] というファイル を作成し,コンパイル対象にしてください.ファイルの作成方法は,既存のファイルを 参考にしてください.また asm_code.h にアーキテクチャを登録し,main.cに オプションを追加してください. nlccでは演算などのためにレジスタを2つ使います.これをR0/R1と呼びます. R0/R1にするレジスタを選んでください.基本的には以下のように選びます. - R0 関数の戻り値を渡すレジスタ - R1 R0との演算が行えるレジスタ 例として,x86ではR0にEAX,R1にEDXを利用しています. それらのレジスタを利用して演算などを行う関数を asm_code_XXX.c に作成して ください.asm_code_i386.c あたりを参考にしてください. nlccは主にR0を利用して値のやりとりをするようなコードを生成します.また補助的に R1を利用します.メインで利用するレジスタを2つに限定することで,コード効率は 悪いのですがアーキテクチャ対応をシンプルなものにしています. 一時的な保存場所として利用するレジスタを,tmp_regs[]に登録しておきます.これら のレジスタは値が上書きされるため,基本としてcallee-savedのレジスタ (asm_code_XXXX_function_register_save()によって保存されるレジスタ)を登録して ください.ただしよくわからなければ,無しでも構いません. (その場合,保存場所としてスタックが利用されます) 以下の関数は命令を登録しなくても,-1を返すことでビルトインが利用されるように なります.他の命令を組み合わせたりすることで,ソフトウェア的にエミュレーション します.ただし動作は遅くなります.(例えばかけ算はループを回して加算を行うことで 計算するため,非常に低速になります) |命令 |ビルトインの強制利用のためのオプション | |---------------|---------------------------------------| |符号拡張 |USE_BUILTIN_EXTENSION | |ビット反転 |USE_BUILTIN_INV | |符号反転 |USE_BUILTIN_MINUS | |排他的論理和 |USE_BUILTIN_XOR | |かけ算 |USE_BUILTIN_MUL | |除算 |USE_BUILTIN_DIV | |剰余算 |USE_BUILTIN_MOD | |左シフト |USE_BUILTIN_LSHIFT | |右シフト |USE_BUILTIN_RSHIFT | |分岐(一部) |USE_BUILTIN_BRANCH | |比較(一部) |USE_BUILTIN_CMP | |関数呼び出し |USE_BUILTIN_CALL | |R1に対する処理 |USE_BUILTIN_R1 | |---------------|---------------------------------------| |上記すべて |USE_BUILTIN | どの関数がビルトイン利用可能かについては,asm_code.c 内で上記のオプションに よってビルトインに切り替えている箇所を参照してください.ビルトインへの切替えを 行えるようになっている関数は,基本的に-1を返すことでビルトイン利用が可能です. 逆に,ビルトインを利用することで,命令は未登録でもひとまず(遅くてもいいので) 動作させるということができます.新規アーキテクチャ対応時には,ひとまず ビルトイン利用とすることで初期の対応コストを低くして,動作しはじめたら徐々に 命令を登録していく,というように段階を踏んで実装していくことができます. ここまで