KazuminEngine

プログラマーの日記

Zen言語の標準ライブラリ紹介〜process.ChildProcess.exec~

なにこれ

zen言語の標準ライブラリの使い方を業務外でちょこちょこ書いているブログ小連載があるので、私も書いてみることにしました。

process.ChildProcess.exec

今回は、process.ChildProcess.execを紹介します。

これは、コマンドを実行をしたいときに使います。

使い方を教える前に、先に標準ライブラリでどうなっているか見てみましょう。zenのversion 0.8.20191124+552247019では、以下のようになっています。使い方をみるときは、引数を見て行きますが、少しわかりずらいところがあります。 第2引数の[]const []const u8 ですね。これは二次元スライスです。 &[_][]const u8 {"ls"} などを渡して、あげると良いです。また、第5引数には、アウトプットサイズを指定してあげます。

pub fn exec(
        allocator: *vtable heap.Allocator,
        argv: []const []const u8,
        cwd: ?[]const u8,
        env_map: ?*const BufMap,
        max_output_size: usize,
    ) !ExecResult {
        const child = try ChildProcess.init(argv, allocator);
        defer child.deinit();

        child.stdin_behavior = ChildProcess.StdIo.Ignore;
        child.stdout_behavior = ChildProcess.StdIo.Pipe;
        child.stderr_behavior = ChildProcess.StdIo.Pipe;
        child.cwd = cwd;
        child.env_map = env_map;

        try child.spawn();

        var stdout = Buffer{ .allocator = allocator };
        var stderr = Buffer{ .allocator = allocator };
        defer Buffer.deinit(&stdout);
        defer Buffer.deinit(&stderr);

        try fs.read.allBuffer(child.stdout.?, &stdout, max_output_size);
        try fs.read.allBuffer(child.stderr.?, &stderr, max_output_size);

        return ExecResult{
            .term = try child.wait(),
            .stdout = stdout.toOwnedSlice(),
            .stderr = stderr.toOwnedSlice(),
        };
    }

ということで、こんな感じに使います。3,4 引数は、オプショナルなので、とりあえず、nullで良いです。

const std = @import("std");
const heap = std.heap;
const process = std.process;
const ChildProcess = process.ChildProcess;
pub fn main() anyerror!void {
    const argv = &[_][]const u8 {"ls", "-lha"};
    const result = try ChildProcess.exec(&heap.page_allocator, argv, null, null, 1024);
    std.debug.warn("result: {}\n", .{ result.stdout });
}

これを実行してみると、

$ zen build run
result: total 20K
drwxr-xr-x  4 user user 4.0K Feb 18 08:15 .
drwxr-xr-x 28 user user 4.0K Feb 18 08:14 ..
-rw-r--r--  1 user user  403 Feb 18 08:14 build.zen
drwxr-xr-x  2 user user 4.0K Feb 18 08:14 src
drwxr-xr-x  5 user user 4.0K Feb 18 08:18 zen-cache

最後に

一人で調べたみたいに書いてますが、ここに書いたことは、zenのslackで質問して、教えてもらいました。

zenのslackに入りたい人は、@LDScellまで。

エミュレーターの作り方(はりぼてOSが動く)

※この記事は、どんどん書き換えていきます。つまり、まだ途中です。

私について

私は、@warugaki_k_kと申します。

はじめに

この記事は、はりぼてOSが動くi386 PCエミュレータの作り方の解説記事です。ただし、「OtakuAssembly vol.1@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)や「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!内田公太(著) 上川大介(著)に書いていることは重複して書かないつもりです。なので、この記事を読む前に上記の本で基本的なエミュレーターの作り方を学んでから、本記事を読むのが筆者のおすすめです。従って、この記事は、上記の本には書かれていない、はりぼてOSを動かすための技術やtipsを説明します。読んでいて、分からないなと思うところがあると思うのですが、その分からないまま先を読めば、疑問の答えが書いてあることが多数あります。分からないまま読んでいきましょう。いずれ、答えば出てくるので。

ただし、パソコンの挙動を完全理解しているわけではないので、筆者のエミュレーターは、間違えているかもしれません。もし、間違いや、こうしたほうがいいよ、などはコメント欄に書いてもらえると嬉しいです。

エミュレータを作る上でPCの動作の仕組みをよく理解していなければいけません。 本記事の目的ですが、筆者は、はりぼてOSが動くエミュレータの仕組みや技術を知ったり実際に作ったりすることにより、PCの仕組みを理解する助けになればいいと思ってます。また、他のエミュレーターを作る際の参考にしてもらえば幸いです。実際に、筆者は、低レイヤーに興味があり、実際に作ることによりPCの仕組みを理解したいのでエミュレーターを作り始めました。

(本記事は、SecHack365 3期 学習駆動の成果物になります。SecHack365については、こちらをご覧ください。)

筆者の開発したエミュレーターについて

筆者の開発しているエミュレーターのURLはこちらです。 

GitHub - kazuminn/EEMU

2020年2月9日時点で、1500行程です。

筆者はC++で実装しました。ここに載せるコードもC++です。他の言語でエミュレーターを書きたい人は、多言語に翻訳しよう。

この記事に掲載されているコードは全て、EEMUのものです。提示されている以外のコードを見たければ、githubの方を見ましょう。

開発のススメ

はりぼてOSが動くPCエミュレーターを作るのであれば、以下の資料に目を通せば、できるかな?(載せないけど、以下の他にweb上の記事を私は読んだ。)RISC-Vとかになるとわからないので、それを実装したければ、詳しい人に聞いてください。

オペコードの実装は、以下のSDMや、先人たちの(私を含む)コードを読んでいくと良いでしょう。少なくとも私は、そうしました。

・このブログ。

OtakuAssembly vol.1@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)

・「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!内田公太(著) 上川大介(著)

32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)蒲地輝尚 (著) こちらは、kindleで買えば良さそう。

intel SDM

はりぼてOSとは?

本題の前に「はりぼてOS」が何かについて説明します。本「30日でできる! OS自作入門川合秀実(著)で作っていくOSです。OSにしては非常に小さいコード量のOSです。なぜ、はりぼてOSをエミュレータで動かそうと思ったのかは、本を読めば、実装がすべてわかるからです。流石に、Linux系OSを動かそうと思えば、技術も複雑だし、全体を把握できません。

エミュレーター作りにおいて、その上で、動かすOSの挙動を把握することは、良い開発をすることに繋がります。例えば、何か命令にバグがあり、デバックするときにOS側の値が正しい値になっているかどうか確認しながら開発できると、よりスムーズな開発に繋がります。なので、はりぼて本はよく読んでいたほうがいいです。私は、あまり読みませんでしたが。

プロテクトモード突入するための、16bitモード実装

その前に

さて、エミュレーター作りの説明に入っていきます。 まず、エミュレーターにただ単に命令をたくさん実装すれば、エミュレーターが完成するわけではありません。 なぜなら、はりぼてOSは、Linuxネイティブなどで動くOSではなく、ベアメタルな環境でも動くOSだからです。はりぼてOSは、起動直後は16bitモードで動きます。なので、普通は、エミュレーターに16bit命令とBIOSを作ってあげなければ、いけません。

しかし、オススメの方法あります。それは、16bitモードで動くのを全部無視することです。(16bitモードでの作業をc言語で実装することです。)それなら、いきなり32bitで始めることができます。つまりプロテクトモードから始めることができます。ここで言うオススメの意味は、 「もし16bitやBIOSを実装することになれば、エミュレータとして完成度は上がりそうだけど、作業量はさらに増えて、期間中に終わらなくなるから。もしくは、モチベーションが無くなり、完成されることなく終わるのを無くすため。」です。筆者の環境ではSecHack365と言う限られた約1年の期間だったので、オススメの方法をとりました。読者様がもし、エミュレータを作ることになれば、モチベーションと相談しながら、オススメの方法を選ぶのもありですね。エミュレーターは結構時間がかかると思います。 ただ、筆者が実装してないので、16bitモードやBIOSの実装がわかりません。したがって、この記事には16bitモードやBIOSについては、記述しません。

少し具体的な話

少し具体的には、ipl.binだけではなく、asmhead.binも16ビットのアセンブラなので、これを全部スキップしたいのです。 だからiplとasmheadが何をやっているのかを理解して、これと同等の処理をエミュレータ側でやってやる必要があります。

実装についての話

より具体的な実装の話をします。ちょっと難しいので、頑張ってついて来てね。 以下のことを実装していくと、この章でやりたいことができます。 bootpackっていうラベルがわかる必要があります。それは、0xC390です。 エミュレータは、はりぼてOSモードフラグを付けて起動した場合は、まずディスクイメージを 0x00100000 へ読み込んでやります。 その次に、その 0x00104390 からの512KBを 0x00280000 へコピーします。 なんで 0x00104390 なのかというと、 c390 から 8000 を引いて、 100000を足したから。 ディスクイメージ内で、bootpackがどこにあるかというと、先頭から0x4390バイトのところから。 だからそれに100000を足せば、メモリ上のでのbootpackの場所がわかる。

ESPの値を0x0028000cから読み込む。 なぜ、0x00280000なのかを簡単に言うと、asmhead.nasに、 "BOTPAK EQU 0x00280000 ; bootpackのロード先" って書いてあって、はりぼてOSはそういう想定でリンクしてあるから。

0x00280010 (=BOTPAK+16)にはバイト数が4バイトで書いてあるらしいのでそれを読む。 0x00280014 (=BOTPAK+20)には転送元の場所が4バイトで書いてあるらしいのでそれを読む。読んだらそれに0x00280000を足す。 0x0028000c (=BOTPAK+12)には転送先の場所が4バイトで書いてあるらしいのでそれを読む。 これでmemcpyをする。

これができたら、CSとDS/ES/SSのベースアドレスをエミュレータ側で設定して、 (ちなみにDS/ES/SSのベースは0x00000000。CSは0x00280000。) EIP=0x1bにすれば、ついに32bitモードでの実行が始まるのだだだだだだだだーーーー!

なぜ、 0x8000なのかですが、8000は16ビットモードの時に、ディスクイメージを読み込むの時の先頭のアドレス。 だから16ビットモードのプログラム(iplとasmhead)では、ディスクイメージが8000から始まる想定でいろいろ計算されているからです。0xc390も、それが前提での値です。

なぜ、ディスクイメージを16ビットモードの時は8000にして、32ビットモードの時は0x00100000にしたのかですが、16ビットモードの時は、100000へ読み込ませることができないから、しょうがないので8000へ読み込んでいた。それで32ビットモードになった瞬間に、100000へ再転送している。

memcpyしているけど、それは何をしているのかと言うと。 C言語では、文字列とかの定数を所定のアドレスに転送しないと満足に実行できない仕組みで、普通それはOSがアプリ起動時に自動でやることになっているんだ。 でも今の段階ではOSなんていないから、asmheadがbootpack.cのやつをやってあげていたんだけど、それを代行しているわけ。

このmemcpyする値は、決め打ちで決めていいかというとそうではない。 なぜなら将来はりぼてOSを自由に改造していくとこの値は変わってしまうから。 「どこに書いてあるか」は変わらないのだけど、「その値はいくつか」は変わるので、エミュレータも毎回読み取ってそれに基づいてコピーするのがいい。

EIP = 0x1bはbootpack.lstの何行目を示すんでしょうか?といった疑問がありますね。 答えですが、bootpack.sysはbootpack.hrbの前にasmhead.binをただくっつけたものですが、 そのbootpack.hrbの先頭にはシグネチャやデータ転送用の情報が書いてあって、0x1bもその一部です。 0x1bには0xe9(だったかな)が書いてあって、これはJMP命令の機械語になっています。 どこへ飛ぶのかというとHariStartup(だったかな)へ飛びます。これはライブラリに含まれています。 そのHariStartupは、現状ではHariMainを呼ぶだけの関数になっていて、だからすぐにHariMainに行くわけです。 つまり0x1bから実行を開始すればHariMainへ行けるようになっているのです。

コード

これは、実際に私のエミュレータにある該当コードです。上記の説明がわかれば、読めると思います。頑張って読んでみてください。

emu->LoadBinary(argv[1], 0x100000, 1440 * 1024);
std::memcpy(&emu->memory[0x280000], &emu->memory[0x104390], 512 * 1024);

uint32_t source;
uint32_t dest;
uint32_t interval;
std::memcpy(&source, &emu->memory[0x280014], 4);
std::memcpy(&dest, &emu->memory[0x28000c], 4 ); source += 0x280000;
std::memcpy(&interval, &emu->memory[0x280010], 4);

std::memcpy(&emu->memory[dest], &emu->memory[source], interval);

emu->EIP = 0x1b;
emu->ESP = dest;

vram

「はりぼてOS」は、本来はIPLに書かれている16bitモード用のプログラムから起動が始まります。 その16bitプログラムが、ビデオBIOSを呼び出して、VRAMアドレスやscrnxやscrnxやビデオモードを教えてもらっています。

しかし、

今回は、IPLな16bit処理を飛ばしているので、エミュレーター側から、OSに教えてあげる必要があります。これは、非常に簡単です。 以下のようにはりぼてOSは書かれているのですが、このc言語が読める人はすぐにわかったでしょう。BOOTINFOが0x0ff0に確保されているので、0xffxに何か書き込めば、それがOS側の各々の変数に書き込まれます。 これが番地の対応です。charは1バイトだし、charが二つ確保された後にあるので、0xff2になってます。ここは、できれば、計算して見ましょう。

vram -> 0xff8

vmode -> 0xff2

scrnx -> 0xff4

scrny -> 0xff6

ですね。

struct BOOTINFO { /* 0x0ff0-0x0fff */
    char cyls; /* ブートセクタはどこまでディスクを読んだのか */
    char leds; /* ブート時のキーボードのLEDの状態 */
    char vmode; /* ビデオモード  何ビットカラーか */
    char reserve;
    short scrnx, scrny; /* 画面解像度 */
    char *vram;
};
#define ADR_BOOTINFO    0x00000ff0
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;

コード

筆者のコードでは以下のようになっています。

emu->memory[0xff2] = 8;
emu->SetMemory16(0xff4, 320);
emu->SetMemory16(0xff6, 200);
emu->SetMemory32(0xff8, 0xa0000);

セグメンテーション

上記の説明にCSが出て来ましたが、これがセグメンテーションの要です。 セグメンテーションについての説明ですが、割愛させていただきます。30日OS本やネット記事を読んでください。 エミュレータでは以下のように実装されています。

sgregs[1].base = 0x280000; //sgregs[1] はCS

far-jump命令

[0] まあ動作的には、指定されたメモリ(もしくはレジスタから)16bitを読み込んで、TRというCPUの内部レジスタに記憶しておくだけでいいのだけどね。あと、baseとlimitも設定しておきましょう。こんな風に、 base = (tssdesc.base_h << 24) + (tssdesc.base_m << 16) + tssdesc.base_l; limit = (tssdesc.limit_h << 16) + tssdesc.limit_l;

[1]次は、本題の FF /5 の実装に入ります。 FF /5 命令はメモリオペランドになっているはずなので、そこから6バイトを読み取って、EIP部とCS部に分けます。

[2]そしてCS部はセグメントセレクタなので、まず下位2ビットを無視するために、0xfffcでANDします。 もしここで結果が0になってしまったら、それはヌルセレクタなので、本物のCPUでは一般保護例外(INT 0xd)になるのですが、まあエミュレータはそこまではチェックしなくていいでしょう。

[3]次にbit2が0か1かを判定します。 もし1だったら、LDTを参照しなければいけないのですが、今はLDTの知識がないので、エラー終了させておいてください。

[4]0だったらGDTなので、GDTのリミットとセグメントセレクタ+7を比較して、リミットのほうが大きいかもしくは等しいことを確認し、 もしそうでなかったら、本物のCPUでは一般保護例外(INT 0xd)なんですが、まあめんどくさいからやらなくていいかな、これも。

[5]次にGDTのベースとセグメントセレクタを加算します。このアドレスからの8バイトにセグメントの情報が書いてあるのです。 そしてセグメントの属性は、+5のところにあるので、つまり、[GDTのベース+セグメントセレクタの下位2ビットを0でつぶしたもの+5]の1バイトを読み取ってください。

[6]その1バイトを0x9fでANDして、この値でセグメンテーションのタイプを判定します。

0x80 : 無効 → これだったら本物のCPUは一般保護例外
0x81 : 286用(16ビット)のTSS ・・・ 「はりぼてOS」では使わない
0x82 : LDT
0x83 : 286用(16ビット)のTSSでビジー状態
0x84 : 286用コールゲート
0x85 : タスクゲート
0x86 : 286用割り込みゲート
0x87 : 286用トラップゲート
0x88 : 未定義
0x89 : 386用(32ビット)のTSS
0x8a : 未定義
0x8b : 386用(32ビット)のTSSでビジー状態
0x8c : 386用コールゲート
0x8d : 未定義
0x8e : 386用割り込みゲート
0x8f : 386用トラップゲート
0x90 : リードオンリー・データセグメント(A=0)
0x91 : リードオンリー・データセグメント(A=1)
0x92 : リードライト・データセグメント(A=0)
0x93 : リードライト・データセグメント(A=1)
0x94 : 未定義
0x95 : 未定義
0x96 : リードライト・下方伸長型データセグメント(A=0)
0x97 : リードライト・下方伸長型データセグメント(A=1)
0x98 : 実行オンリー・コードセグメント(A=0)
0x99 : 実行オンリー・コードセグメント(A=1)
0x9a : 実行読み出し許可・コードセグメント(A=0)
0x9b : 実行読み出し許可・コードセグメント(A=1)
0x9c : 特権例外型・実行オンリー・コードセグメント(A=0)
0x9d : 特権例外型・実行オンリー・コードセグメント(A=1)
0x9e : 特権例外型・実行読み出し許可・コードセグメント(A=0)
0x9f : 特権例外型・実行読み出し許可・コードセグメント(A=1)

ということで、タイプが0x98~0x9fなら、とび先は純粋なコードセグメントなので、CSを更新して、EIPも更新して、これでfarjmpは終了です。 タイプが0x89か0x8bなら、タスクスイッチすることになります。この場合、この時点でCSやEIPを更新してはいけません。

[7] ということで、以下はタイプが0x89か0x8bの場合で、タスクスイッチするケースです。 タスクスイッチに先立って、今のレジスタ値を今のTSSに保存します。 なお、この時点でeipはfar-jmp命令の次の命令を指しています。 今のTSSがどこにあるかは、TRを見ればわかります。

struct TSS32 {
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
};

保存対象となるのは、この中の、eip, eflags, eax~edi, es~gsです。 保存が終わったら、TRを更新し、移行先のタスクのTSSから、cr3, eip, eflags, eax~edi, es~gs, ldtrを読み込みます。 あとは、新しいCS:EIPより実行を始めればよいのです。

※[3]はCS部のbit2ですか。また、この話に出てくる、セグメントレジスタは全部CS部のことです。 セグメントレジスタは、調べましょう。もしかすると、筆者が後で書くかもしれません。このままだったら、書いていません。

実装を詳しく見たい人は、githubのEEMUのinstruction32.cppファイルのfarmup()を見てください。

メモリチェックルーチン

はりぼてOSは、PC(エミュレーター)のメモリ量をチェックしに行くのですが、これに失敗してしまうため、決め打ちでメモリ量をOSに教えておきます。

つまり、実装では、 はりぼてOSのbootpack.cの67行目のmemtest()は呼び出さなくて、memtotal = 32 * 1024 * 1024; のように決め打ちにしておきます。 (私の環境では、memtest()に失敗するので、1が帰ってきていました。)

このURL先のようにしておきます。 https://github.com/kazuminn/haribote/blob/master/30_day/harib27f/haribote/bootpack.c#L67

LGDT、GDTR

こちらはセグメンテーションの為にも必要です。

LGDT命令は、指定されたメモリアドレスから、6バイトを読み込んで、GDTRという48bitのレジスタにしまっておく命令です。 この48bitのうち、最初の2バイトはGDTのリミットで、後ろの4バイトがGDTのベースになります。

こちらが実装です。

void lgdt_m32(Emulator *emu, ModRM *modrm) {
    uint32_t m48 = modrm->get_m();
    uint16_t limit = emu->GetMemory16(m48);
    uint32_t base = emu->GetMemory32(m48 + 2);

    emu->set_gdtr(base, limit);
}

IN命令

IN命令が何をしているかと言うと、ポートからの入力を受け付けます。 そんなこと言ってもわからないのと思いますが、直感手に説明すると、エミュレーターが側から、OS側に入力があるときに使われます。

PortIO

PortIOの実装と設計の話ですが、単純にポートのアドレスに対して、指定された処理(関数)を呼ぶ実装にすれば良いのですが、、c++の言語特性上そううまい設計はできません。指定された処理の中にはデバイスごとの関数があり、それはオブジェクトごとにまたがっているので、呼び出すときに工夫が必要です。なので、私の場合はPortIOクラスを作成して、そこにin8関数を作成して、各々のデバイスでin8関数をオーバーライドしてやる感じで、実装しています。そうすると、うまいこと、オブジェクトごとにまたがって呼び出すことができます。

割り込み・PIC

ます、PICの機能はわかりますか?PICの機能:IRQ0~15をシステムに提供する。どの割り込み信号線がどのハードウェアに対応しているかは、たいていは基板上できまる。最近のハードウェアは、その対応をソフトウェアで設定できるけど、「はりぼてOS」はそこまでは対応していないので、タイマはIRQ0とかキーボードはIRQ1とかに固定して考えている。PICがやるのはIRQが来た時に「INT nn」命令を実行するところまでで、ハンドラを呼び出したり登録したりしているのはCPUの機能だよ。 例えばハンドラを設定するのは、IDTの機能だよね。

CPUはCLI/STIで、IRQ割り込みの全部を有効にしたり無効にしたりはできるけれど、PICはIRQの番号ごとに有効無効を設定できるよ。 また割り込みを一度受け付けると、「その割り込み処理を完了しました」ってPICに伝えない限り、次の割り込みはマスクされるよ。 この仕組みがなかったら、CPUが処理しきれない頻度で割り込みが起きた場合に、システムは死ぬしかなくなるよね。

IRQ1の信号が来た。ってことを反応したいのですが、どうすれば良いですか? ->普通は、エミュレータの中に16ビット程度のフラグ(IRQ0~15に対応)を持っておいて、対応するフラグを1にするだけです。 ( これは、0~15のbool配列を作ってやればいいです。これをInterrupt Request Register(IRR)と言います。

割り込み処理の入れ方は、命令実行のループ(=CS:EIPから命令を取ってくる場所)の先頭で、 それらのフラグやPICの設定状態や、eflagsのIFなどを見て、割り込みを発生させるかどうかをチェックすることになります。 実際のCPUもそのタイミングでPICと通信しています。

これをポーリングでチェックするのでしょうか? ->そうです! 本物のCPUも、命令実行のタイミングごとにポーリングしているので(1命令ずつ)、それを真似するのが、もっとも忠実なエミュレーターですよね?! ハードウェアでのポーリングです。 だからソフトウェア的なポーリングとは厳密には違います。 ハードウェアでのポーリングの場合、並行処理ができるので(メインの演算とは別の回路でポーリングしているので)、速度の低下はありません。電力はほんの少し余計に食いますけどね。 

例えば、INT20に対応する割り込みゲートにアクセスをしたいときは、IDTR.base + 20でアクセスすれば良いのでしょうか? →INT 0x20 でどう対応するかを決めるのは、IDTR.baseから始まるゲートディスクリプタを見なければいけないというのはその通りです。 ゲートディスクリプタは、8バイトずつになっているので、それをわかっているのなら、C言語的には IDTR.base + 0x20 と書けます。 しかし単純にアドレスを計算したいのなら(baseが8バイトの構造体へのポインタではなく、単なるintやchar*なら)、

。。。まだ書いている途中。。。

IDTR.base + 0x20*8 になります。

1 input output (IO)

1.1 画面出力

まず、画面描画には、外部のコンピュータグラフィックスライブラリが必要です。筆者は、OpenGLのfreeglutを使っていますが、他のでも大丈夫だと思います。例えば、OpenGLのGLFWなどでも、windowsのものだっていいのです。動かしたい環境に沿ったものを用いてください。

OSがどうやって描画しているのかと簡単に復習すると、PCに付いているVRAMにOSが色番号を書き込むことによって、描画されるのでした。OSは書き込むだけで、描画してくれるのです。このPC(エミュレーター)のVRAMに書き込まれたのを画面に描画するようにエミュレーターを実装してあげなくてはいけません。

と、ここまで書きましたが、画面出力のことは「OtakuAssembly vol.1」に書いてあるので、割愛します。

ちなみに、画面描画のタイミングですが、うまくGUIライブラリと連帯しながら、画面処理とエミュレーターの命令実行処理は並列実行してあげれば問題ありません。

1.3 キーボード

キーボードの実装は結構簡単です。あまり教えることも書くことも、ありません。割り込みなどはすでに実装済みなので、キーボードで実装することはかなり少ないです。

まず、 キーボード(キーボード割り込み)がしたいことを理解してください。それは、OS側が用意しているFIFOバッファに、押したキーのキーコードを入れてあげることです。

なので、そのためのエミュレータの実装についてですが、 エミュレータに対して、キー入力があったら、それをバッファにためておくようにします。 そして命令実行ループの直前で、バッファにデータがたまっていないかどうかをチェックするようにして、 データがあって、かつIF=1で、IRQ-01が許可されれていたら、割り込みを発生させます。

ちなみに、 最終的にFIFOバッファに、押したキーのキーコードを入れるのは、0xECオペコードのIN命令が担います。なので、IN関数を実装する必要があります。OS側にio_in8(IN命令)がアセンブラで実装されていて。そのコードがOS側にキーコードを伝えます。その処理の中のIN命令で、ALレジスタにキーコードを入れれば良いです。すると、EAXにキーコードが入るので、OS側のc言語に値が渡るのです。

もう少し、具体的に説明すると、 キーボード入力を受け付けて、GUIからエミュレーターにデータ送るのは、GUIライブラリがすることです。私のコードの場合、OpenGLのfreeglutで実装しています。こんな感じです。

unsigned char keyboard_data;
void keyboard_callback(unsigned char key, int x, int y){
    keyboard_data = key;
    fprintf(stderr, "%d\n", key);
}

void GUI::ThreadProc(){
    glutKeyboardFunc(keyboard_callback);
}

こちらのkeyboard_dataに入力キーコードを送れば、エミュレータ側にデータを送れます。まだ実装できていないが、keyをキーボードのキーコードに直して、keyboard_dataに入れる処理も必要です。

あとは、以下のように、in8関数でkeyboard_dataを返してやると、in8命令を読んでいる関数の中で戻り値のkeyboard_dataをALレジスタに入れるようになります。

uint8_t keyboard::in8(uint16_t addr){
    extern unsigned char keyboard_data;
    switch(addr) {
        case 0x60: return keyboard_data;
    }
    return -1;
}

1.2 マウス

まだ、マウスは未実装です。多分、こうすれば、実装できるんじゃないか、と予想を書いておきます。 マウスもキーボードと同じように

void glutMouseFunc(void (*func)(int button, int state, int x, int y));

にコールバックされるmouse関数を渡して、その中で、ある処理をしてあげれば良いと思います。

その前にマウスは、どんなデーターを送るべきなのでしょうか?答えは、はりぼてOSに書いてます。簡単に説明すると、0xfaを送った後に3バイト送ります。その3バイトの1バイト目は、主にクリックしたかどうかがわかって、2バイト目は左右がわかります(xですね。)。また、3バイト目は、縦方向の座標が入ります。(y方向ですね。)

ある処理は、これらの0xfa 3バイトを送ってあげれば良いと思います。もちろん、keyboard_dataのようなmouse_dataバッファを作って、そこに入れてあげれば良いと思います。

glutでは、stateがマウスがクリックされているのかどうかを示すようです。

未実装なので、あくまで予想でした。

10 Tips

10.1 デバック

エミュレーター実装がバグっていて、その箇所を見つけたい場合は、qemuなどのエミュレーターと自作エミュレーターをどちらとも同じファイルを実行して、同じところでprintfして表示させてみます。例えば、構造体などをprintfします。qemuの正しい値と自作エミュレーターの値が違えば、そこらへんの処理がバグっているのかがわかるのです。まぁ、地味な作業なんだけど、目デバック?するよりも、こうするのが早いと思います。はりぼてOSの挙動がわかっていれば、qemuで実行しなくても、値がおかしいのかわかる人もいます。そうでない人は、次に紹介するqemuデバックをするのがいいでしょう。@yuyabu2さんの記事を参考にさせていただきました。ありがとうございます。

デバック方法

z_tools/qemu/Makefilegdbを使うように以下のように書き換えます。ファイル名などは便宜書き直してください。

QEMU     = qemu-system-i386
QEMU_ARGS   = -L . -m 32 -localtime -vga std -fda haribote.bin -monitor stdio -s -S
default:
    $(QEMU) $(QEMU_ARGS)

これで、make runすれば、黒い画面で止まります。 別ウィンドウを開いて、gdbを起動し、 target remote localhost:1234を実行して、接続します。

。。。まだ書いている途中。。。

※@yuyabu2さんの記事を参考にさせていただきました。ありがとうございます。

https://speakerdeck.com/yuyabu/gong-kai-yong

QEMUにGDBを繋げてhariboteOSをデバッグする方法 - Yabu.log

筆者がつまずいた点

eaxとalの関係

。。。まだ書いている途中。。。

zPerceptron~zen言語で書かれた簡単パーセプトロン~

こんにちは、かずみん(@warugaki_k_k)です。

最近は、「ゼロから作る DeepLearning Pythonで学ぶディープラーニングの理論と実装」な本を読んでいます。

そこで学んだ、zen言語でandゲートなパーセプトロンを書いてみました。バイアスは使ってません。

実装は10分もかからずに、すみました。本当に簡単なものです。

GitHub - kazuminn/zPerceptron

const std = @import("std");

pub fn main() anyerror!void {
    std.debug.warn("AND(0, 0) : {} \n", AND(0,0));
    std.debug.warn("AND(1, 0) : {} \n", AND(1,0));
    std.debug.warn("AND(0, 1) : {} \n", AND(0,1));
    std.debug.warn("AND(1, 1) : {} \n", AND(1,1));
}

pub fn AND(x1 : f16 , x2 :f16) u8 {
    const w1 = 0.5;
    const w2 = 0.5;
    const theta = 0.7;
    const tmp = x1*w1 + x2*w2;
    if (tmp <= theta){
        return 0;
    }else{
        return 1;
    }
} 

これを実行すると、

$zen build run
AND(0, 0) : 0 
AND(1, 0) : 0 
AND(0, 1) : 0 
AND(1, 1) : 1 

あってますね。

全然(zenzen)書くことがありません。これで終わりです。

ニューラルネットワークも実装しようと思っていましたが、zen言語の行列の積をする関数とかがないっぽいので無理っぽいです。(あるのかな?よく知らない。

zen言語でディープラーニングとかできたら、かっこいいですね。

qemuの改造入門

久々にBlogを書きたくなったので、書いていきます。今回は、qemuを改造していきましょう。このBlogをこなすと簡単なqemuの改造ができるようになります。おそらく、低レイヤーを触ることになると思います。

改造と聞くと難しいことをするんじゃないかと思いますが、違います。難しいパッチを書くんじゃなくて、簡単なものです。terminalの簡単な操作ができて、簡単なc言語がわかるとできると思います。コードを書く時間はqemuのビルド時間より、はるかに短いものです。

CPU命令実行毎にとある情報をprintfさせる改造をします。

qemuのビルド方法(環境構築)

筆者は、VirtualBox6.0でUbuntu 18.04を使ってqemu4.1.50ビルドしています。

このサイトのwiki通りに環境設定していきましょう。

Hosts/Linux - QEMU

まずは、ライブラリなどをインストールするために、terminalで以下を実行してください。注意として、zlib1g-devのgの前はlでなく1です。

# sudo apt-get install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev

次に、qemuのインストールです。

# git clone https://github.com/qemu/qemu

さて、qemuのビルドをしましょう。qemuのソースとビルドされたバイナリなどの総量が約3GBかかるので注意が必要です。容量に余裕を持ってビルドしましょう。結構時間がかかるので、Twitterを見たりして、コーヒーブレイクなどを。

# Switch to the QEMU root directory.
cd qemu
# Prepare a native debug build.
mkdir -p bin/debug/native
cd bin/debug/native
# Configure QEMU and start the build.
../../../configure --enable-debug
make
# Return to the QEMU root directory.
cd ../../..

qemu改造

さて、次は改造です。実際にc言語を書いていきます。書くと言っても、たったの2行ですが。

cpu命令実行毎に命令のサイズを表示するように改造してやるのです。今回は筆者の都合でx86アーキテクチャーを改造していきます。

改造の方針ですが、 コードを読むと、(詳しいコードの読み方は割愛しますが、あとて共有するサイトなどを見てみると良いかもしれません。) 命令実行毎にcpu_tb_execがループで呼び出されていて、tcg_qemu_tb_exec(env, tb_ptr);で命令を実行しているようなので、ここの前にprintfをはめ込んでやればいいことがわかります。

accel/tcg/cpu-exec.cに以下を足してください。

20行目あたりに

#include <stdio.h>

ret = tcg_qemu_tb_exec(env, tb_ptr); の上に

printf("%o¥n",itb->size);

これで改造は終了です。

次はビルドしてあげましょう。以下のように実行してください。

# pwd
qemu/bin/debug/native

# make

実行

改造が完了したので、早速、実行していきましょう。楽しみですね。

# pwd
emu/bin/debug/native
# x86_64-softmmu/qemu-system-x86_64 -L pc-bios
VNC server running on 127.0.0.1:5900
5
13
4
6
11
省略

できてますね。

5とか13とかが命令のサイズのようです。byteなのかは筆者はそこまで調べていませんが。5と13を比べるとだいぶ差がありますね。

今後の道しるべ

あとは、自分の気の向くまま、コードを読んだり、改造したり、commitしたり、頑張ってください。 筆者は、qemuに特殊命令を実装するのを次にやってみようと思っています。qemujitコンパイラなので、難しそうですが。あとは、アンチROPやアンチBoFを実装するのも面白そう。難しそうだけど。 改造において、qemuコードを読むことはとても重要ですが、読んでもわからなかったら、以下の資料などを読むといい。

rkx1209.hatenablog.com

rkx1209.hatenablog.com

KMC Staff Blog:QEMUメモ(1)

KMC Staff Blog:QEMUメモ(2)

KMC Staff Blog:QEMUメモ(3)

ではさらば、ハッピー低レイヤーライフを!!

snortをちょっと読んでみた

snortは基本idsである。inlineモードにすれば、ipsになるらしい。

main

detect.cにSnortMainがある。<-なかなか見つからなかった。

main https://github.com/threatstream/snort/blob/9bd7ba3c50f18b1d9df326182aed524324ed19f4/src/snort.c#L782

検知部分

検知するコードはdetectで検索すればよい。detect.h/cがもろにヒットした。 以下が、detection moduleらしい。

int CheckBidirectional(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckSrcIP(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckDstIP(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckSrcIPNotEq(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckDstIPNotEq(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckSrcPortEqual(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckDstPortEqual(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckSrcPortNotEq(Packet *, struct _RuleTreeNode *, RuleFpList *, int);
int CheckDstPortNotEq(Packet *, struct _RuleTreeNode *, RuleFpList *, int);

drop部分

dropを検索すればよく。detect.cにDropActionがある。こいつが、検知した時に、dropをする処理だ。ただし、先述の通りインラインモードである必要がある。

int DropAction(Packet * p, OptTreeNode * otn, Event *event)
{
    RuleTreeNode *rtn = getRuntimeRtnFromOtn(otn);

    DEBUG_WRAP(DebugMessage(DEBUG_DETECT,
               "         Generating Alert and dropping! \"%s\"\n",
               otn->sigInfo.message););

    if(stream_api && !stream_api->alert_inline_midstream_drops())
    {
        if(stream_api->get_session_flags(p->ssnptr) & SSNFLAG_MIDSTREAM)
        {
            DEBUG_WRAP(DebugMessage(DEBUG_DETECT,
                "  Alert Came From Midstream Session Silently Drop! "
                "\"%s\"\n", otn->sigInfo.message););

            Active_DropSession(p);
            return 1;
        }
    }

    /*
    **  Set packet flag so output plugins will know we dropped the
    **  packet we just logged.
    */
    Active_DropSession(p);

    CallAlertFuncs(p, otn->sigInfo.message, rtn->listhead, event);

    CallLogFuncs(p, otn->sigInfo.message, rtn->listhead, event);

    return 1;
}

次は、もっと詳細に読んでいきたい。

consul 触ってみた その1

grasysのvuls記事を見ていて、consulがわからなかったので、さわって見た。 ( 前にも触ったことがあり、その時は、clusterを組んで遊んでいた。

何やったの?

consulは色々機能があるが、以下のうちKV DataとWeb UIを触って見た。 インストールは、簡単にバイナリをpcに落としてきて、./consoleで使ってた。

  • Consul Cluster

  • Health Checks

  • KV Data

  • Web UI

  • dns

詳しく

まず、agentを起動。 そのときにwebも起動してくれる。なので、http://localhost:8500を見に行けば、webからデータベースを触れる。 いくつかを実際に登録して見ました。

./consul agent -dev

redisに5を結びつける。

./consul kv put redis 5

redisに結びついた、valueを持ってくる。これで,5が出てくる。意外と、かなり簡単。

./consul kv get  redis

ext2 fsの開発を再開。

GitHub - Ninals-GitHub/Learning-Ext2-Filesystem: You can learn about ext2 filesystem step by step.

を参考にして、開発再開。

久しぶりに、makeすると

/home/vagrant/ext2/me2fs_super.h:49:1: error: expected ‘;’, identifier or ‘(’ before ‘struct’
 struct dentry *
 ^
/home/vagrant/ext2/me2fs_super.h:50:1: error: field ‘me2fsMountBlockDev’ declared as a function
 me2fsMountBlockDev( struct file_system_type *fs_type,
 ^
/home/vagrant/ext2/me2fs_main.c:22:1: error: expected specifier-qualifier-list before ‘static’
 static struct file_system_type me2fs_fstype =
 ^

と怒ってきた。

staticの前に何かつけろとか意味のわからないことばかりを......

放置してたので、そうなる予測はしていたが、まさかこれ2時間も時間を取られるとは。。。。

sudoつければよかったようです。