vulsで差分-(マイナス)機能がリリースされました。
vulsで差分出力機能がリリースされました!
以前まで、前回の検知結果と比較し、増えた脆弱性やアップデートされた脆弱性一覧をservername_diff.jsonとして出力する機能はありました。要は、差分の+はありました。
しかし、-がなかったのです。
そこで、今回は、差分の-の機能が追加されました。前回の検知結果と比較し、無くなった(すでに対応された)脆弱性一覧もervername_diff.jsonとして出力できるようになりました。
使い方
差分の+
$ vuls report -diff-plus -to-localfile -format-json
とすれば、差分の+だけ、result/current/servername_diff.jsonとして出力されます。
差分の-
$ vuls report -diff-minus -to-localfile -format-json
とすれば、差分の-だけ、result/current/servername_diff.jsonとして出力されます。
+ - 両方
$ vuls report -diff -to-localfile -format-json
とすれば、+ - 両方のを含む結果が、result/current/servername_diff.jsonとして出力されます。
$ vuls report -diff-plus -diff-minus -to-localfile -format-json
とするのと同じことです。
tui
$ vuls tui -diff
とすれば、結果がcuiで解析できます。
見方
vuls tui
tui表示では CVE-xxxx-xxxxの左に+ - の表示がついています。
report -format-list -diff
-format-listの表示でも、同様ですね。( -format-full-text -diffでも同様です。)
report -format-one-line-text -diff
こちらでは、+ - の集計ができます。
まとめ
vulsのSaaS版やvulsrepoでも対応するかもしれません。乞うご期待!
これで、対応済の脆弱性が便利に確認できるようになりましたね。
それでは、良いvulsライフを!
ホスト型ハイパーバイザー の作り方 part.1
こんにちは、かずみん (@k2warugaki) | Twitter です。
この記事は、Linuxその2 Advent Calendar 2020 - Qiita の21 日目の記事です。
前日の記事は、 aiinkiestism - Qiitaさんによる Zorin OSとStarLabsの紹介 - Qiitaでした。
CPUの仮想化支援機能やバイナリートランスレーションを利用しないで、linux kernelの機能を用いたホスト型ハイパーバイザーの作り方を解説します。今回は、そのpart.1です。 ※続編が出るかもしれない...?まだハイパーバイザー を開発途中で序章しか書けないので、part1としています。
大まかな手順
- まず、好きなOSのエミュレーターを作ります。
- 次に、ハイパーバイザー 化します。
環境
エミュレーターを作る
今回、エミュレーターを作る部分ってのは省略します。 代わりに、昔私が書いたこの記事を見ると良いかも。エミュレーターの作り方(はりぼてOSが動く) - KazuminEngine
※バイナリトランスレーションしない、命令を全部エミュレートするものを用意してください。 エミュレートするOSによっては、ハイパーバイザー 化できないものもあるので、注意してください。
ハイパーバイザー化
概要
何ができれば、パイパーバイザーと言えるのかを説明します。
ゲスト用のプロセスを立てて、その中で、次のことをすれば良い。
センシティブな命令やアクセス不可の資源へのアクセスをトラップして、エミュレートしてあげる ・・・これを(1)とする
それ以外をcpuが直接実行 ・・・これを(2)とする
アルゴリズム
- (1)について、
sigactionというlinuxのシステムコールを使います。それを使うと、特定のシグナルが発生した際に呼び出されるハンドラーを設定することができる。
センシティブな命令とアクセス不可の資源へのアクセスをした時は、SIGSEGVシグナルかSIGILLシグナルが発行される。
それらシグナルに対して、ハンドラーを設定します。
ハンドラーが呼び出されると、その呼び出された時のコンテキスト情報がハンドラー関数の第3引数に渡されるので、それをsig_ucontext_t構造体に変換してやるとripが手に入るので、そこからオペコードなどの情報を得ます。また、レジスタなどの情報も得ることができます。 そのオペコードをエミュレーターで実行してあげます。この後に、忘れてはいけないのが、シグナル復帰するために、ripを更新することだ。
- (2)について、
cpuが命令を直接実行するのは簡単です。まず、動的メモリに動かしたいOSを読み込んであげます。そして、その先頭アドレスを実行するように、アセンブラのjmp命令に先頭アドレスを渡してあげることでできます。(プログラムカウンターをOSの先頭アドレスにする)
実装(サンプルプログラム)
コメントを入れておくので、それを読んでください。
以下のサンプルコードでは、0xFA(cli)にしか対応していない。
全部のコードを書くと大変なことになるので、必要最低限を載せています。
以下、main関数の中。in main.cpp
int main() { int p_id, status; if ((p_id = fork()) == 0) { //子プロセススタート emu = new Emulator();//エミュレーター本体を作成 emu->LoadBinary("../xv6-public/xv6.img", 0x7c00, 1024 * 1024 * 1024);//OSを動的メモリ上に読み込む struct sigaction sigact; sigact.sa_sigaction = trap; sigact.sa_flags = SA_RESTART | SA_SIGINFO; sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL);//SIGSEGVが呼ばれた時に呼ばれるハンドラーにtrap関数を登録 sigaction(SIGILL, &sigact, (struct sigaction *)NULL);//SIGILLが呼ばれた時に呼ばれるハンドラーにtrap関数を登録 _change_pc(emu->memory + 0x7c00);//アセンブラで書かれている_change_pc関数を呼び出す。 delete emu; exit(EXIT_SUCCESS); } wait(&status); // 子プロセス全部終わるまで待つ exit(EXIT_FAILURE); }
以下、SIGSEGVとSIGILLが起こったときに、呼び出されるtrap関数。in main.cpp
typedef struct _sig_ucontext { unsigned long uc_flags; struct ucontext *uc_link; stack_t uc_stack; struct sigcontext uc_mcontext; sigset_t uc_sigmask; } sig_ucontext_t; void trap(int sig_num, siginfo_t * info, void * ucontext){ instruction_func_t* func; sig_ucontext_t* uc = (sig_ucontext_t *) ucontext;//voidポインター のコンテキストの型変換 uint8_t * pc = (uint8_t *)uc->uc_mcontext.rip;//ripポインター を入手 func = instructions16[*pc];//オペコードに対応する関数をとってくる func(emu,uc);//実行 }
以下、命令が書かれている、 instructions16配列の本体。in instruction16.cpp。
instruction_func_t* instructions16[0xffff]; namespace instruction16{ void cli(Emulator *emu,sig_ucontext_t* uc){ emu->set_interrupt(false);//割り込みが起きないようにする。 uc->uc_mcontext.rip++;//ripを一つ増やす。 } void InitInstructions16(){ instruction_func_t** func = instructions16; func[0xFA] = cli; } }
以下が、プログラムカウンターを動かしたいOSの先頭に持っていくためのアセンブリ。in pc.S。
GLOBAL _change_pc SECTION .text _change_pc: jmp rdi//rdiは第一引数。jmp命令でemu->memoryアドレスにジャンプ ret//戻ってこないので、なくてもいいが一応、リターン
参考文献
最後に
私はまだ、ハイパーバイザー の開発途中です。これから命令をたくさん追加していき、割り込みなどに対応していく予定です。続きの記事を書けるように、どうか温かい目で見守っていてください。
私のリポジトリは、こちらです。一人で開発していて、読みやすさとか気にしていないので、汚いコードですが大目に見てください。
zen製fuzzerを書いた
なにをしたの?
zen言語で書かれた簡易Fuzzerをつくりました。cliコマンドにfuzzを送るものです。皆さんおなじみのCTFのcrackmeをfuzzingするイメージです。
遺伝的アルゴリズムとかでfuzzを生成するとかではなく、ランダムに文字をせいせいしています。
コードはGitHubにあります。 GitHub - kazuminn/kfuzz
使い方
最新のzenで、
zen build
してやると動かすことができます。
githubにあるコードのままだと、lsにfuzzを渡すものになっています。
実行したいコマンドを変更するには、
const argv = &mut [_][]u8{ "ls", self.fuzz };
の{}にコマンドを,区切りで”をつけて書いてやってください。そして、fuzzを出力したい場所に、self.fuzzを書いてやってください。
fuzzingに成功すると、
0,-Id2K_+}F/[Lqjc|]-QD[n^]4[j)BcRVsjiWe{X 0,-IX7Z;f'\8;"Z?G}1;$R.j#7{~TxCY:+
のように、dump.csvファイルに出力されます。 0がstatus codeで,以降がそのときのfuzzです。
fuzzの文字数を固定することができなかったので、固定されていません。(たぶん、書き方あると思うけど、固定なしに。
ある程度の長さを変更したいなら、holder関数の長さを変更すれば、多分できます。
どうやって書いたの?
この本を参考につくりました。この本ではpythonでFuzzerをつくっています。pythonと同じような関数がzenには無く、ここでのコードをそのまま流用することができなかったので、あくまで参考程度にしています。
なぜZen言語で書いたのか?
ちょうどコネクトフリーのインターンを三ヶ月ぐらい休んでいたので、リハビリと勉強のためにZen言語で書きました。
感想
インターンでは、ベアメタルで動くコードしか書いていないので、このようにOSの上で動くコードをzenで書いたのははじめてで新鮮でした。osがないと動かない関数を使うのは大変刺激的でした。
zenのドキュメントに書いていない関数をライブラリのコードを見て使い方を学ぶのは、結構難しかったです。
askiiの文字コードをアルファベットに変更する関数が見当たらないので、単純なコードをたくさん書いたんだけど、その関数がないのはな。。。(あるのかな?
文字を生成するコードもっとよく書けないかな?
zenのライブラリをもっとよく知ると、もっと良いコードを書くことができるだろう。(zenzen知らずに書いた。 https://blog.hatena.ne.jp/kazuminkun/kazuminkun.hatenablog.com/edit?entry=26006613613888079#textarea
vulsがciscoの脆弱性検知に対応できない理由
vulsにciscoの脆弱性検知を実装しようと試みたが、できなかったので、その理由を述べる。
cisco脆弱性データベース(go-cve-dictionaryやら)を作るのに、2通りの方法があって、OVALとCVRFで取ってくる方法がある。
OVAL
まず、試みたのが、OVALの方。vulsは影響の受けるversionでfilterをかけるときに、versionが必要なのだが、それのデータベースを作ろうとしたができない。
なぜかというと。
すでにサポート済みのredhatなどの ovalの
CVRF
上記の通り、影響の受けるversionがCVRFにあると、検知できそう。
しかし、影響の受けるversionが書いてあるには、書いてあるが、記述の一貫性がなく、とても正規表現で抽出したりできない。(少なくても、私は、)
そうゆうわけで、cisco検知に対応できない。
もし、できそうなアルゴリズムがあれば教えて欲しい。このブログのコメントにでも書いて欲しい。
このブログは、10分で書いたものであるので、グダグダです。
vulsのコアのアルゴリズムについて
こんにちは、
かずみん (@warugaki_k_k) | Twitter
です。
ブログのタイトルは、ほんとは違うかも。本当は全然コアじゃないのかもしれないです。
vulsって結局どうやって脆弱性判断しているのかわからなくて、コード読みました。そしてわかったことを書きます。
どっかで読んだ記憶によると、OVALと言うもので検知されてる。何それわからない。
ovalのデータ構造やら。わかりませんでした。
(ここで、コードを読むまでの予想だが、バージョンとcveが一対一やら、なんやかんやになってる。と予想。それをどうやって流のかが不明。
まー、scan時に取得されたversionが脆弱性を含むかをfillするのは、report時なので、そのへんのコードを読むと、こんなのがgetDefsByPackNameFromOvalDB関数の
245 definitions, err := driver.GetByPackName(r.Family, r.Release, req.packName, req.arch)
多分ここで、検査してるんだろうけど、どうやら、これは、goval-dictionaryのGetByPackName()を呼び出しているようだ。
ま、見てみると。ここで色々データを取ってくるみたい。
func (o *RedHat) GetByPackName(driver *gorm.DB, osVer, packName, _ string) ([]models.Definition, error) { osVer = major(osVer) packs := []models.Package{} err := driver.Where(&models.Package{Name: packName}).Find(&packs).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } defs := []models.Definition{} for _, p := range packs { def := models.Definition{} err = driver.Where("id = ?", p.DefinitionID).Find(&def).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } root := models.Root{} err = driver.Where("id = ?", def.RootID).Find(&root).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } if root.Family == config.RedHat && major(root.OSVersion) == osVer { defs = append(defs, def) } } for i, def := range defs { adv := models.Advisory{} err = driver.Model(&def).Related(&adv, "Advisory").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } cves := []models.Cve{} err = driver.Model(&adv).Related(&cves, "Cves").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } adv.Cves = cves bugs := []models.Bugzilla{} err = driver.Model(&adv).Related(&bugs, "Bugzillas").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } adv.Bugzillas = bugs cpes := []models.Cpe{} err = driver.Model(&adv).Related(&cpes, "AffectedCPEList").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } adv.AffectedCPEList = cpes defs[i].Advisory = adv packs := []models.Package{} err = driver.Model(&def).Related(&packs, "AffectedPacks").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } defs[i].AffectedPacks = filterByMajor(packs, osVer) refs := []models.Reference{} err = driver.Model(&def).Related(&refs, "References").Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } defs[i].References = refs } return defs, nil }
なになに、パッケージネームからディフィニッション(数字)をデータベースから取ってきて、そのディフィニッションからcveを取って来る。あ、パッケージ名に対するcveが影響するバージョンを取ってくる(def.AffectedPacks)。データベース操作がいまいち読めないから、間違ってそうだが、大筋そうだろう。絶対間違ってるけど。
これで比較する準備ができたので、vulsのisOvalDefAffected()で現在のバージョンとOVALで取ってきたバージョンを比較すればよくて。lessThan関数でgithub.com/knqyf263/go-deb-versionライブラリを使って、比較する。これで、検知が可能
と言うことで、初めの予想と同じで、バージョンとcveが一対一やら、なんやかんやになってる。それはgoval-dictionaryがやってくれているんだろうけど、わからない。今度、そこ読んでみるかと。
感想としては、バージョンごとにcveやらを持っていて、それで全部比較するとなると、すごい計算量になりそう。。。
ま、私は、vulsをこれぜ完全理解したつもりです。
vuls何もわからない......
この記事は10分で書いたので、適当です。何か、間違いや、指摘があったら、教えてほしいです。
あと、こんな記事もあります。
Vulsのコードを読む その4 Vuls reportを調べてみた - Security Index
こちらの記事は、cpeから、cveを検知する話になってます。
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が動く)
- 私について
- はじめに
- 筆者の開発したエミュレーターについて
- 開発のススメ
- はりぼてOSとは?
- プロテクトモード突入するための、16bitモード実装
- セグメンテーション
- far-jump命令
- メモリチェックルーチン
- LGDT、GDTR
- IN命令
- PortIO
- 割り込み・PIC
- 1 input output (IO)
- 10 Tips
- 筆者がつまずいた点
※この記事は、どんどん書き換えていきます。つまり、まだ途中です。
私について
私は、@warugaki_k_kと申します。
はじめに
この記事は、はりぼてOSが動くi386 PCエミュレータの作り方の解説記事です。ただし、「OtakuAssembly vol.1」@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)や「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!」内田公太(著) 上川大介(著)に書いていることは重複して書かないつもりです。なので、この記事を読む前に上記の本で基本的なエミュレーターの作り方を学んでから、本記事を読むのが筆者のおすすめです。従って、この記事は、上記の本には書かれていない、はりぼてOSを動かすための技術やtipsを説明します。読んでいて、分からないなと思うところがあると思うのですが、その分からないまま先を読めば、疑問の答えが書いてあることが多数あります。分からないまま読んでいきましょう。いずれ、答えば出てくるので。
ただし、パソコンの挙動を完全理解しているわけではないので、筆者のエミュレーターは、間違えているかもしれません。もし、間違いや、こうしたほうがいいよ、などはコメント欄に書いてもらえると嬉しいです。
エミュレータを作る上でPCの動作の仕組みをよく理解していなければいけません。 本記事の目的ですが、筆者は、はりぼてOSが動くエミュレータの仕組みや技術を知ったり実際に作ったりすることにより、PCの仕組みを理解する助けになればいいと思ってます。また、他のエミュレーターを作る際の参考にしてもらえば幸いです。実際に、筆者は、低レイヤーに興味があり、実際に作ることによりPCの仕組みを理解したいのでエミュレーターを作り始めました。
(本記事は、SecHack365 3期 学習駆動の成果物になります。SecHack365については、こちらをご覧ください。)
筆者の開発したエミュレーターについて
筆者の開発しているエミュレーターのURLはこちらです。
2020年2月9日時点で、1500行程です。
筆者はC++で実装しました。ここに載せるコードもC++です。他の言語でエミュレーターを書きたい人は、多言語に翻訳しよう。
この記事に掲載されているコードは全て、EEMUのものです。提示されている以外のコードを見たければ、githubの方を見ましょう。
開発のススメ
はりぼてOSが動くPCエミュレーターを作るのであれば、以下の資料に目を通せば、できるかな?(載せないけど、以下の他にweb上の記事を私は読んだ。)RISC-Vとかになるとわからないので、それを実装したければ、詳しい人に聞いてください。
オペコードの実装は、以下のSDMや、先人たちの(私を含む)コードを読んでいくと良いでしょう。少なくとも私は、そうしました。
・このブログ。
・OtakuAssembly vol.1」@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)
・「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!」内田公太(著) 上川大介(著)
・32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)蒲地輝尚 (著) こちらは、kindleで買えば良さそう。
はりぼて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/Makefile
でgdbを使うように以下のように書き換えます。ファイル名などは便宜書き直してください。
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の関係
。。。まだ書いている途中。。。