自作OSでマルチタスクやった
この記事は coins Advent Calendar 2019 3日目の記事です。
この記事は WORDIAN Advent Calendar 2019 3日目の記事です。。
この記事は OS-CPU Advent Calendar 2019 3日目の記事です。。。
師走の挨拶
どうもこんにちは、突撃隊です。
64bit自作OS「minOS」をちまちま作っとるのですが、11月でついにマルチタスクを実装できたので解説します。
設計
スレッドかプロセスか
OSのタスク単位としてとりあえずスレッドを実装することにしました。 プロセスはメモリ空間を分離しなくてはいけないので、その分コーディングが大変だからです。 スレッドならスタックを用意してあげるだけなので簡単簡単。
thread
構造体
スレッドごとの構造体はこんな感じにしてみました。
struct thread_func { void (*func)(int, char**); int argc; char **argv; }; enum thread_state { RUNNABLE, SLEEP, DEAD }; struct thread { uint64_t *stack; uint64_t rsp; // uint64_t rip; struct thread_func func_info; enum thread_state state; int index; };
struct thread
ですが、上から順に
- スレッドのスタック領域の開始アドレス
- スレッドごとのスタックポインタ
- スレッドで実行する関数の情報
- スレッドの状態
- スレッドキューに登録した際のインデックス
です。
順番に解説していきます。
スレッドのスタック
自作の malloc
関数を用いてスタック領域を確保します。
大きさは 0x1000 = 4kB
としています。
malloc
関数のソースコードはこちら。
https://github.com/Totsugekitai/minOS/blob/develop/kernel/mm/memory.c
スレッドごとのスタックポインタ
汎用レジスタはスタックに push
しますが、スタックポインタは構造体に持たせるほうがやりやすそうです。
スレッドで実行する関数の情報
関数ポインタとその引数を受け取ります。
スレッドの状態
RUNNABLE
SLEEP
DEAD
の3種類を用意しておきました。
スレッドキューに登録した際のインデックス
minOSではスレッドのキューはただの配列です。 スレッドの削除時に配列に登録したときのインデックスが必要になるのでパラメータとして持たせてあります。
どのタイミングでスレッド切り替えをするか
タイマ割り込みのタイミングでスレッド切り替えをすることにしました。 タイマ割り込みの何回かに一回にスケジューラを呼び出してタスクを切り替えます。
coinsLT#110で話したときの資料が詳しいです。
実装
今回のキモはスレッドのスタックの初期化処理とスレッドを切り替える瞬間の処理です。
スタックの初期化処理
/** スレッドの生成 * thread構造体の初期化とスタックの初期化を行う */ struct thread thread_gen(void (*func)(int, char**), int argc, char **argv) { struct thread thread; thread.stack = (uint64_t *)minmalloc(STACK_LENGTH); thread.rsp = (uint64_t)(thread.stack + STACK_LENGTH); thread.rip = (uint64_t)thread_exec; thread.func_info.func = func; thread.func_info.argc = argc; thread.func_info.argv = argv; put_str_num_serial("thread stack bottom: ", (uint64_t)thread.stack + STACK_LENGTH); put_str_num_serial("thread rsp: ", thread.rsp); return thread; } void thread_stack_init(struct thread *thread) { thread->rsp = init_stack(thread->rsp, thread->rip, thread); put_str_num_serial("thread stack bottom: ", (uint64_t)thread->stack + STACK_LENGTH); put_str_num_serial("thread rsp: ", thread->rsp); } void thread_exec(struct thread *thread) { thread->func_info.func(thread->func_info.argc, thread->func_info.argv); thread_end(thread->index); minfree(thread->stack); thread_scheduler(); }
thread_gen
関数でスレッドの生成を行います。 thread
構造体に各種情報を登録していきます。
thread_stack_init
でスレッドのスタックの初期化を行います。この時スタックポインタの値も調整されます。
init_stack
関数の実装はレジスタをいじるのでアセンブラで書きます。
.global init_stack .align 16 init_stack: # uint64_t init_stack(uint64_t stack_bottom, uint64_t rip, struct thread *thread); cli mov rcx,rsp # save rsp at rcx mov rsp,rdi # move rip to rsp # push general registers to stack push rsi # push rip(second argument) to stack bottom # because function switch_context requires return rip push 0 # rbp push 0 # r11 push 0 # r10 push 0 # r9 push 0 # r8 push rdx # rdi push 0 # rsi push 0 # rcx push 0 # rdx push 0 # rax mov rax,rsp # set current rsp to return value mov rsp,rcx # load rsp from rcx sti ret
まず現在の rsp
を rcx
に保存し、今のスタックに戻ってこれるようにします。
次に init_stack
関数の第1引数で受け取ったスレッドのスタックポインタをロードして、スレッドのスタックの初期化作業を始めます。
コード中のコメントと push
している数値が初期化のデフォルト値に対応しています。
rdi
だけ rdx
つまり init_stack
に渡された第3引数をセットしていますが、これは thread_exec
に渡す引数を設定しています。
少しややこしいので説明すると、まずスレッドで実行する関数を thread_exec
で包みます。
そして thread_exec
をスレッドで実行する関数として登録します(thread_gen
をよく見ると関数の設定で thread_exec
を登録している)。
というのも、 thread_exec
でスレッドの関数が終わった際の終了処理とスケジューラ呼び出しを行うからです。
thread_exec
には thread
構造体へのポインタが必要なので、レジスタの初期化を行うときに rdi
つまり第一引数に thread
構造体へのポインタを設定してやることで引数を渡します。
thread_exec
の末尾3行はスレッドの状態を変更し、mallocした領域を開放し、スケジューラを呼び出します。
thread_gen
と thread_stack_init
はこんな感じで使います。
// スレッドを生成 struct thread thread0 = thread_gen(console, 0, 0); struct thread thread1 = thread_gen(task_a, 0, 0); struct thread thread2 = thread_gen(task_b, 0, 0); struct thread thread3 = thread_gen(task_c, 0, 0); // initialize stack thread_stack_init(&thread0); thread_stack_init(&thread1); thread_stack_init(&thread2); thread_stack_init(&thread3);
スレッドを切り替える処理
まずタイマの割り込みハンドラでスケジューラを呼び出す処理を見ましょう。
/** * タイマ割り込みハンドラ * クロック値を進めて、スケジューラを呼び出す */ __attribute__((interrupt)) void timer_handler(struct InterruptFrame *frame) { // クロック値を進める milli_clock++; // EOIをPICに送る io_outb(PIC0_OCW2, PIC_EOI); io_outb(PIC1_OCW2, PIC_EOI); /** 周期が来たらスケジューラを呼び出す * 各種パラメータはint_handler.hで設定 */ if (milli_clock > previous_interrupt + timer_period && milli_clock > 100) { previous_interrupt = milli_clock; put_str_num_serial("timer_handler old rip: ", frame->rip); puts_serial("\n"); thread_scheduler(); } }
if
の条件式でスケジューラを呼び出す条件を設定しています。
間隔をグローバル変数 timer_period
で指定しています。
一定時間が経ったら thread_scheduler
を呼び出すようになっています。
次に thread_scheduler
を見てみましょう。
void thread_scheduler(void) { // update current_thread_index int old_thread_index = current_thread_index; int i = 1; while (current_thread_index == old_thread_index) { if (threads[(current_thread_index + i) % THREAD_NUM]->state == RUNNABLE) { current_thread_index = (current_thread_index + i) % THREAD_NUM; } i++; } put_str_num_serial("next thread index: ", (uint64_t)current_thread_index); put_str_num_serial("next start rsp: ", threads[current_thread_index]->rsp); put_str_num_serial("next start func address: ", (uint64_t)(threads[current_thread_index]->func_info.func)); puts_serial("\n"); puts_serial("dispatch start\n\n"); switch_context(&threads[old_thread_index]->rsp, threads[current_thread_index]->rsp); }
前半部でスレッドキューから次に実行するスレッドを選択しています。
後半部で重要な処理は switch_context
です。見てみましょう。
switch_context: # void switch_context(uint64_t *current_rsp, uint64_t next_rsp); # push current task's general registers to stack push rbp push r11 push r10 push r9 push r8 push rdi push rsi push rdx push rcx push rax # switch rsp mov [rdi],rsp mov rsp,rsi # pop next task's general registers from stack pop rax pop rcx pop rdx pop rsi pop rdi pop r8 pop r9 pop r10 pop r11 pop rbp # return to next task ret
まず現在実行しているスレッドのレジスタの値を保存するために、現在のスレッドのスタックに現在のスレッドのレジスタを保存しています。
次にスタックポインタ rsp
の処理です。
現在のスタックポインタを現在のスレッドの構造体に保存し、次に実行するスレッドのスタックポインタを読み出します。
するとここでスタックが切り替わるため、次に実行するスレッドのスタックから次に実行するスレッドのレジスタを読み出すことができます。 ですので次に実行するスレッドのレジスタをスタックから読み出します。
最後に ret
で rip
を読み出し、次のスレッドにジャンプします。
最後の ret
でどこにジャンプするのか?
これは少しややこしいですので説明します。 switch_context
が呼ばれた瞬間のスタックが属するスレッドをAt、次に実行するスレッドをBtとします。
まず switch_context
中で rsp
が切り替わったときにスタックがBtのものに切り替わります。
レジスタをpopしていって最後に ret
を発行すると、 スタックはBtのもので rip
は thread_scheduler
関数の switch_context
から抜けた所に飛びます。
thread_scheduler
関数はこれ以上の処理はないため、 スタックはBtのもので rip
はタイマ割り込みハンドラの timer_handler
の thread_scheduler
から抜けた所に飛びます。
timer_handler
はこれ以上の処理はないため、 スタックはBtのもので rip
は割り込み前に実行していた所がロードされます。
スタックがBtのもの なので、ロードされる rip
はスタックBtに積まれた関数中のアドレスであり、スタックBtに積まれる関数というのはスレッドBtに登録されている関数です。
実装は以上
たぶんこれで動くと思います。
まとめ
タスクスイッチの実装はスタックにレジスタを積まなくてはならず、かなりややこしい部分です。 実装は大変苦労しましたが、喜びもひとしおでしたね。
WORD編集部の本棚
この記事は WORDIAN Advent Calendar 2019 の1日目の記事です。
本棚を整理しました
そのままです。11月の上旬の深夜にガッとやりました。
もう誰も読まないであろう本やダブっている本をどかし、奥底に眠っていた有用と思われる本をゆるくジャンル分けして整頓しました。 ですので最近編集部に来ていない人は自分の本が見つからない場合は自分まで声をかけてください。
ピックアップ技術書の紹介
今回の整理であんな本やこんな本が見つかりました。 その中で自分が独断と偏見で、これは!と思う本を紹介していきたいと思います。
プログラミング言語編
Programming in Lua
Programming in Lua プログラミング言語Lua公式解説書
- 作者: Roberto Ierusalimschy,新丈径
- 出版社/メーカー: アスキー・メディアワークス
- 発売日: 2009/08/28
- メディア: 大型本
- 購入: 5人 クリック: 78回
- この商品を含むブログ (18件) を見る
奥底から発掘しました。 自分はLuaを全く書いたことがないのですが、WORDの標準執筆環境がLuaLaTeXなので、いずれかは読んで覚えたいですね。
Smalltalkで学ぶオブジェクト指向プログラミングの本質
SMALLTALKで学ぶ オブジェクト指向プログラミングの本質
- 作者: 青木淳,浅岡浩子,澤本依里
- 出版社/メーカー: 日経BP
- 発売日: 2008/07/24
- メディア: 単行本
- 購入: 14人 クリック: 408回
- この商品を含むブログ (39件) を見る
オブジェクト指向言語の原点Smalltalkの本です。 自分は普段Cばかり書いているのでオブジェクト指向はあまり馴染みがないのですが、どうせやるならオブジェクト指向の原点を学んでみたいものですね。
アルゴリズム・データ構造編
The Art of Computer Programming
The Art of Computer Programming Volume 1 Fundamental Algorithms Third Edition 日本語版
- 作者: Donald E.Knuth,有澤 誠,和田 英一,有澤誠,和田英一,青木孝,筧一彦,鈴木健一,長尾高弘
- 出版社/メーカー: KADOKAWA
- 発売日: 2015/06/26
- メディア: 単行本
- この商品を含むブログ (2件) を見る
クヌース先生のライフワークPart.1。 生きているうちにvol.7まで出してくり〜
計算困難問題に対するアルゴリズム理論
計算困難問題に対するアルゴリズム理論―組合せ最適化・ランダマイゼーション・近似・ヒューリスティクス
- 作者: J.ホロムコヴィッチ,Juraj Hromkovic,和田幸一,増澤利光,元木光雄
- 出版社/メーカー: 丸善出版
- 発売日: 2016/01/10
- メディア: 単行本
- この商品を含むブログを見る
難しそうkonami
言語処理系
コンパイラ
コンパイラ―原理・技法・ツール (Information & Computing)
- 作者: A.V.エイホ,R.セシィ,J.D.ウルマン,M.S.ラム,Alfred V. Aho,Jeffery D. Ullman,Ravi Sethi,Monica S. Lam,原田賢一
- 出版社/メーカー: サイエンス社
- 発売日: 2009/06/01
- メディア: 単行本
- 購入: 1人 クリック: 128回
- この商品を含むブログ (30件) を見る
アホじゃないよエイホだよ。 コンパイラ作るなら必読?
計算機プログラムの構造と解釈 第2版
- 作者: ハロルドエイブルソン,ジュリーサスマン,ジェラルド・ジェイサスマン,Harold Abelson,Julie Sussman,Gerald Jay Sussman,和田英一
- 出版社/メーカー: 翔泳社
- 発売日: 2014/05/17
- メディア: 大型本
- この商品を含むブログ (4件) を見る
魔術師本。Lisp書きたいときにどうぞ。
OS
詳解Linuxカーネル 第3版
- 作者: Daniel P. Bovet,Marco Cesati,高橋浩和,杉田由美子,清水正明,高杉昌督,平松雅巳,安井隆宏
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/02/26
- メディア: 大型本
- 購入: 9人 クリック: 269回
- この商品を含むブログ (73件) を見る
辞書。 カーネル2.7だけどまだまだ読めます。
BSDカーネルの設計と実装
- 作者: マーシャル・カークマキュージック,ジョージ・V.ネヴィル‐ニール,砂原秀樹,Marshall Kirk McKusick,George V. Neville‐Neil,歌代和正
- 出版社/メーカー: アスキー
- 発売日: 2005/10/18
- メディア: 単行本
- クリック: 122回
- この商品を含むブログ (58件) を見る
悪魔本。 来年はBSDも触るのでその時お世話になりそう。
CPU・アーキテクチャ編
マイクロプロセッサ・アーキテクチャ入門
マイクロプロセッサ・アーキテクチャ入門 インターフェース増刊
- 出版社/メーカー: CQ出版社
- 発売日: 2004
- メディア: 雑誌
- この商品を含むブログを見る
並列処理アーキテクチャ2の授業を受けると読みたくなる。
コンピュータの構成と設計 第3版
コンピュータの構成と設計~ハードウエアとソフトウエアのインタフェース 第3版 (上)
- 作者: デイビッド・A.パターソン,ジョン・L.ヘネシー,David A. Patterson,John L. Hennessy,成田光彰
- 出版社/メーカー: 日経BP
- 発売日: 2006/03/16
- メディア: 単行本
- 購入: 14人 クリック: 356回
- この商品を含むブログ (89件) を見る
コンピュータの構成と設計~ハードウエアとソフトウエアのインタフェース 第3版 (下)
- 作者: デイビッド・A.パターソン,ジョン・L.ヘネシー,David A. Patterson,John L. Hennessy,成田光彰
- 出版社/メーカー: 日経BP
- 発売日: 2006/03/16
- メディア: 単行本
- 購入: 12人 クリック: 115回
- この商品を含むブログ (63件) を見る
パタヘネ第3版です。早く第6版出て欲しいね。
LaTeX編
[改訂第7版]LaTeX2ε美文書作成入門
- 作者: 奥村晴彦,黒木裕介
- 出版社/メーカー: 技術評論社
- 発売日: 2017/01/24
- メディア: 大型本
- この商品を含むブログ (2件) を見る
WORDIANのお供。ちょくちょく読みます。
(LuaLaTeX 微文書作成入門)
OBひだるま氏の著作。技術書典7で領布されました。 役立ちます。
数学編
多様体入門
- 作者: 松島与三
- 出版社/メーカー: 裳華房
- 発売日: 2017/04/06
- メディア: 単行本
- この商品を含むブログを見る
多様体を勉強するときに読む本です。
論理と計算のしくみ
- 作者: 萩谷昌己,西崎真也
- 出版社/メーカー: 岩波書店
- 発売日: 2017/02/10
- メディア: オンデマンド (ペーパーバック)
- この商品を含むブログを見る
目はつけてたシリーズ。 なかなか買えないのよね、これ。
競技編
セキュリティコンテストチャレンジブック
セキュリティコンテストチャレンジブック -CTFで学ぼう! 情報を守るための戦い方-
- 作者: 碓井利宣,竹迫良範,廣田一貴,保要隆明,前田優人,美濃圭佑,三村聡志,八木橋優,SECCON実行委員会
- 出版社/メーカー: マイナビ出版
- 発売日: 2015/09/30
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
実は著者の1人のサインが入っているとか……?
プログラミングコンテストチャレンジブック 第1版
- 作者: 秋葉拓哉,岩田陽一,北川宜稔
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2010/09/11
- メディア: 単行本(ソフトカバー)
- 購入: 52人 クリック: 1,538回
- この商品を含むブログ (83件) を見る
だれか第2版置いてくり〜
その他
(Dragon University 2019.9)
編集部rizaudo氏とれっくす氏、そして自分が執筆した同人誌。びしょ〜じょ氏も協力。
邪悪編
関数型プログラミングに目覚めた!
関数型プログラミングに目覚めた!IQ145の女子高校生の先輩から受けた特訓5日間
- 作者: 岡部健
- 出版社/メーカー: 秀和システム
- 発売日: 2015/04/22
- メディア: 単行本
- この商品を含むブログ (7件) を見る
いつかは消去したい。
いかがでしたか?
読書して知識を身に着けましょう。
セキュリティキャンプ2019 OS開発ゼミ体験記
この度セキュリティキャンプ2019全国大会を修了しました。関係者各位はお疲れ様でした。
この記事では自分が受講したYトラックのOS開発ゼミ、テーマ「フルスクラッチOSを書こう!」を中心に、キャンプについて感想を述べていきたいと思います。
事前学習期間
キャンプの選考に通ってから、キャンプ当日に向けて事前学習を行いました。
当初の予定
まず選考時点では自分のOSは
- ブートローダ
- 画面描画関数
しかありませんでした。
当初は、キャンプ当日ではファイルシステムを実装して、コマンドを打ち込んで操作できる状態までやろうと思っていました。
なので、事前課題では以下のことをやろうと思いました。
- 64bitモードへの移行
- GDTとIDTの初期化
- キーボード入力
- ページングテーブルの初期化とページングの有効化
- コンソールを作り、コマンド入力をできるようにする
実際の進行
kintoneで講師の内田さん (@uchan_nos) やチューターさんに質問したり進捗報告したりしていました。 やっていてだらけそうだなと思った(事前学習期間が2ヶ月くらいある)ので、途中から専用スレッドを立ててもらいそこで日報を書き始めました。
日報には、一日のはじめに目標とそれに対してやることを書き、一日の終わりにやったことと疑問点を書きました。 これは結構良かったです。最後の方は学校の行事とかぶって少し失速しましたが、概ねペースを保てたかなと思います。
最終的に事前学習でやれたことは
- 64bitモードへの移行(実は最初からできていたことが調査でわかった)
- GDT,IDTの初期化
- キーボード入力
- ページングテーブルの初期化とページングの有効化
- コンソールを途中まで作った
でした。
キャンプ当日のやること決め
キャンプ数週間前に、当日にやることを決めました。
当初はファイルシステムの実装を目標にしていましたが、自分がファイルシステムがなんなのかわかっておらず、またファイルシステムについて勉強する時間が取れなさそうだったので、タイマ割り込みの実装と、タイマ割り込みを使ったコマンドの作成に変更しました。
前日のトラブル
事前学習中に運営局から実機検証用のボードが送られてきたのですが、自分はめんどくさがって実機検証を行っていませんでした。
キャンプ前日に実機のことを思い出し実機検証をしようとしたら、シリアル通信用のケーブルが入っていないことに気づきました(キーボード入力をシリアル通信で行うように設計していた)。 運営局も用意はしていないということで、前日に大慌てで秋葉原まで行き、シリアル通信ケーブルを購入しました。 秋月電子が夏季休業していたときは絶望しましたが、マルツが開いていたのでなんとか手に入れることができました。
これを読んでいる2020年以降のキャンパーさんたちは、キャンプ1週間前くらいには必要機材の確認などは済ませておきましょう。おじさんとの約束だぞ!!!
キャンプ期間中
1日目
メシ食って開会式や全体講義、グループワークがありました。
講義は法律についてとコミュニティについてでした。倫理観身につけていきたいです。
名刺交換もいろんな時間にしました。200枚持っていったんですけどコミュ力不足で50枚位しか消費しませんでした。自分のコミュ力に合った枚数を持っていこうな。
2日目
この日から専門講義が始まりました。と言っても自分のゼミはひたすら開発!開発!開発!でした。
消すと動かなくなるバグが発生
この日の前半はバグを潰す活動をしていました。バグの症状としては、無駄な処理を消すと動作が止まってしまうというものでした。 例外ハンドラは作成していて検知できるようになっていましたが、例外ハンドラを読み込む部分より前でバグっているようだったのでそれは使えません。
そこでシリアルに出力をしてどこが原因かまず特定したらどうかと講師の内田さんから提案されたのでそうすることにしました。 色々やった結果、GDTの設定周辺の無駄な処理を消すと動作が止まることがわかりました。 バイナリを逆アセンブルしてみたりQEMU Monitorでメモリを覗いたりと四苦八苦していたところ、なんだかわからないけど安定動作するようになりました。 何だったんだろう……
ACPI PMタイマとLocal APICの設定
後半はLocal APICのタイマを動作させるべく、ACPI PMタイマの設定を始めました。
ACPI PMタイマは周波数が決まっているので、PMタイマでLocal APICタイマの周波数を計測して、その周波数をもとにメインのタイマとしてLocal APICを使用するといったことをしました。 チューターのPG_MANAさんが持ってきてくれた「Local APICタイマー入門」(著者は講師の内田さんです)を読んで設定をしました。 この日はPMタイマの設定まで終わりました。
3日目
この日も開発です。
タイマ割り込みが呼び出せない
この日は1日中タイマの設定がうまく行かず苦しんでいました。
タイマ割り込みのハンドラを呼び出そうとすると一般保護例外が出ているようだったので、一般保護例外のエラーコードを取ろうということになりました。
コンパイラの拡張の __attribute__((interrupt))
を関数の頭につけると引数にエラーコードが取れるので、それを使おうとしましたが、GCCではうまくいかなかったのでclangに変更したらうまくいきました。
そうしてエラーコードを読もうとしましたがエラーコードを正しく取れておらず、結局QEMU Monitorでメモリを直接覗いて確認しました。
そうしてエラーコードを見たのですが、なんだかよくわからない値が入っていて解決ならず。
IDTへのハンドラの登録がおかしいのではないかという話になったので確認してみてもおかしいところは……ありました。 IDT自体の設定がミスっていて、IDTのリミットを設定する部分が小さくなっていました…… 一般保護例外とページフォルトのハンドラはギリギリカバーできるリミット設定で、タイマ割り込みのハンドラはリミット外に位置していたので範囲外にアクセスしていたようです……
講義時間終了後にちょっと作業して、タイマが動くようになりました。
自作OSのタイマー動いた!タイマー動いたあああああああああ!!!!
— Note:まずソースを探す癖をつける (@totsugeki8) 2019年8月15日
今日ずっとバグ取りしてたので喜びもひとしお😂😂😂#seccamp pic.twitter.com/S2L8ZaNVEL
4日目
開発最終日! sleepコマンドとdelayコマンドを作るべく頑張りました。
sleepから戻ってこないバグ
午前中はsleepコマンドを作っていました。
簡単にできるかと思いきや、デバッグ用の関数を消してsleepしたら一生戻ってこないバグが出ました。
一般保護例外やページフォルトも出なかったのでobjdumpしてみると [rbp+0x8]
のようなアドレスを読んでいて、むちゃくちゃな値がsleepの引数に渡っていることがわかりました。
しかしそうなった原因がわからず、ウンウン唸ってもどうにもわからなかったので、同じ部屋で活動しているコンパイラゼミの講師であるhikaliumさんに助けを求めたところ、Red Zoneの可能性があると言われました。
そこでRed Zoneを抑制する -mno-red-zone
をコンパイラオプションにつけてビルドしてみると、正常に動くようになりました(Red Zoneについては下にアップするスライドで解説しています)。
時間切れ
午後はdelayコマンドの実装をはじめました。
delayコマンドは第一引数に秒数を、第二引数以降に行いたいコマンドを取り、指定秒数後に指定コマンドを実行する、なお指定秒数までは通常のコマンド操作はできるように実装する、といったようなものを想定して作りました。
戦略としては、
- コンソール側にタスクキューとフラグをもたせる
- タスクは実行する予定時刻と文字列を持つ
- delayコマンドはタスクキューにタスクを詰める
- タイマはタスクキューを見て、予定時刻を過ぎているタスクが詰まっていたらフラグを立てる
- コンソールはフラグが立ったらキューに詰まっているタスクを実行する
というものを考えました。
これを実装しようとガチャガチャやっていましたが、自分のコンソール関数の設計上の問題でプログラムが難しく、あえなく時間切れになってしまいました。
5日目
成果報告会で発表しました。
その後は他の人の発表を聴いたり、閉会式で修了証書を受け取るのを見たりしました。
そんなこんなしてセキュリティキャンプ2019は閉会しました。
感想
月並みですが参加してよかったです。
もちろん講師さんやチューターさんが横にいる環境で開発できたというのもそうですが、同年代の優秀な人たちと触れ合えたというのも良かったし、モチベーションが高まったというのも得たことの1つです。 今後も精進するぞ。
今後の予定
OS開発は続けていきます(学校での自主課題でもあり、ラボユースのテーマでもあるので)。 キャンプ中にできなかったdelayコマンドの実装もやりたいと思います。
また並行してOSのドキュメントも作っていきたいと思います。
最後になりましたが
講師陣の皆さん、チューターの皆さん、運営に関わった皆さん、全ての関係者各位に感謝します。
ありがとうございました!
終了
終了!
セキュリティキャンプ受かりました
受かった
今年が年齢的に最後だということで出してたら通りました。やったぜ!
集中開発トラック(だっけ?)のOS開発ゼミです、同じゼミの人はよろしくお願いします。
恒例の応募シート晒し
GitHubに上げときます。全文は seccamp_apply_sheet.txt
を見てください。
応募の経緯と意気込み
「30日でできる!OS自作入門」をやって、今度はx64のOSを作ってみたくなったので、セキュリティキャンプに応募してみました。 募集が4月から始まって時間があったので、わりと練って書いたのですが、今見ると結構短いですね……10000字無かった……
友達や先輩がキャンパーで、キャンプの存在を身近に感じていたのも応募した要因の一つです。キャンパーの知り合いはみんないい経験だったと言っているので当日が楽しみです。
現在開発している自作OSは以下のリポジトリなのですが、
当日までに
- メモリ管理(ページング)
- 割り込み
- タスク管理
くらいは実装しておきたいですね。がんばり〼。
来年応募する人へ
応募シートですが、自分はただ闇雲に長く書くのではなく、凝縮して書くことを努めました。 審査員の方たちは何百通と応募シートを見るので、わかりやすい言葉遣いで、意図がしっかりと伝わる文章にするのが良いと思います。読みにくい文章は読むのが嫌になってしまいますからね。 学校の先生とかに見てもらうのもいいかもしれません。
プログラムの課題が出たときは、最低限の要件は満たしたほうがいいですが、できなくても、ここをこう試行錯誤したとか、ここはできなかったけど代わりにこの機能を追加したとか、とにかく食らいついてみるのがいいと思います。逆にできる人はもっと機能追加するとか、性能測定するとか(自分はこれですね)、いろいろやってみるといいかもしれません。
最後に
SecHackのページなので微妙に違いますが、多分似たところはあるので引用させていただきます。選考に関するコメントの部分です。
SecHack365で私を指名してくれたけど落ちてしまった人たちは、実力がなくて落ちたのではなく、相対的な競争や、この人はできれば今年ではなく来年以降に!ということで選にもれただけなのです。自信を持ってください。そして通過した人も自分よりも実力がありそうな人が落ちたということを忘れずに、是非努力をしてください(その人が「こんな成果が出たのなら自分が翌年に回されたのもしょうがないな」と思えるように)。
まさにこのとおりだと思います。これから夏にかけて頑張っていきます。
「30日でできる!OS自作入門」を今更ながらやってみる - 5日目
今日もOSがんばるぞい
4日目は こちら
5日目 - 構造体と文字表示とGDT/IDT初期化
1 - 起動情報の受け取り
ポインタを用いて asmhead.nas
の情報を受け取っていますね。それだけです。
2 - 構造体を使ってみる
構造体の登場です。構造体は変数をかたまりで扱える便利なものです。まあここも難しくないでしょう。
もし構造体のポインタの扱い方に混乱したら、また 3日目 を見直すか、本のコラムをじっくり読んでください。わからなくなったら基本に立ち返ることが重要です。
3 - 矢印表記を使ってみる
(*binfo).scrnx
と binfo->scrnx
は等価です。この本では ->
を積極的に使っていくようです。
おそらくこの ->
にはもっと深遠な機能があると思うのですが、自分はC言語ナニモワカラナイのでここで詳しくは解説しません。
4 - とにかく文字を出したい
ドットをちょんちょんちょんと打って「A」を描いています。気が遠くなりますね…
5 - フォントを増やしたい
OSASKに用いられているフォントデータとコンパイラを用いてフォントを導入しています。省略〜
6 - 文字列を書きたい
文字列を描いています。はい。
文字列の補足ですが、文字列は文字コードを順番にメモリに並べて最後に 0x00
をつけたもの…のようです。
7 - 変数の値の表示
GO(ジーオー)というコンパイラは sprintf
という関数を提供してくれます。
sprintf
関数は指定した文字列をメモリの中に生成する機能を持っています。これを使ってどこかの番地に文字列を保存しておいて、それを putfonts8_asc()
で読み出せば無事文字が表示されます。やったネ!
8 - マウスカーソルも描いてみよう
はい描くだけ〜カット!!!
9 - GDTとIDTを初期化しよう
ここまでで1000文字ですが、ここから多分3000文字くらい書くことになりそうです〜んご〜頑張るお
まず何がしたいかをはっきりさせておきましょう。私達は マウスカーソルを動かしたい のです。それには マウスが動いたときの信号をキャッチして、マウスカーソルの絵を動かせばいい のです。信号をキャッチするには IDT
を設置する必要があります。 IDT
を設定するには今度は GDT
なるものを設定する必要があります。こういう流れで GDT
と IDT
を設定する必要があるのです。それでは書いていきます〜
まずはセグメンテーションです。手っ取り早く言うと メモリを切り分けてそれぞれのブロックの最初の番地を0として扱える という機能です。今まで ORG 0x7c00
とか書いていましたが、セグメンテーションを設定すれば自動的に ORG 0
が設定されるというわけです。3日目 にセグメントレジスタというのがありましたが、今回もそれを使います、使いますが、あの頃はまだ16ビットモードでした。あの時 MOV AL,[SI]
は MOV AL,[DS:SI]
で、 DS
の16倍が SI
に足されていました。しかし今は32ビット、事情が違います。 MOV AL,[DS:EAX]
という時 AL
に代入されるのは、 DSの開始番地 + EAXの値
です。ご注意を。
セグメントレジスタを省略したら自動的に DS
が足されるというのは32ビットモードでも16ビットモードでも共通です。
そのセグメンテーションですが、セグメントを表すには次の情報が必要です。
- セグメントの大きさ
- セグメントの開始番地
- セグメントの管理用属性(書き込み禁止、実行禁止、システム専用etc.)
CPUではこれらのデータを64ビット(8バイト)で表していますが、セグメントを指定するためのレジスタは16ビットしかありません。じゃあできないのかと思いますがそんなことはありません。以下の図がわかりやすいですので、この図を参照しながら説明します。
まずセグメントレジスタにはセグメントの番号(一般にセグメントセレクタと呼ぶ)を格納します。セグメントレジスタとは別に、メモリ上に GDT(Global Descriptor Table)
というセグメントの必要情報(箇条書きで挙げた3つですね)とセグメントセレクタを記載したテーブルを用意します。そしてメモリの空き領域を都合よく区切ります。そしたらセグメントレジスタに格納したセレクタと GDT
のセレクタを紐づけて、更に GDT
のそれぞれの情報と合致するメモリ領域を紐づけます。
こうすると、セグメントセレクタを指定したら対応する GDT
の要素が指定され、 GDT
の要素と対応するメモリが指定される、というわけです。2段クッションがかまされているんですね。
セグメントセレクタの数ですが 0~8191
が扱えます。セグメントレジスタは16ビットですが、CPUの都合上、下位3ビットが使えないためです。ということは8192個の GDT
の要素が作れるわけで、つまり GDT
は 8192 * 8 = 65,536
バイト(64KB)扱えるわけです。
GDT
の作り方ですが、メモリの何処かにセグメントの情報をずらっと並べて、その先頭の番地と有効設定個数をCPUの GDTR
というレジスタに設定すればよいです。
IDT(Interrupt Descriptor Table)
は、割り込み番号とそれに対応した呼び出し関数の対応表です。まずは割り込みについて説明しましょう。割り込みとは、 CPU外部のデバイスが変化した時や内部エラーが起きたときに臨時の処理を発生させる機能 です。CPU外部のデバイスの変化とは、例えばキーボードが押されたとか、マウスがクリックされたとか、そういった類のことです。
なぜそういったものがあるかというと、CPUの処理スピードに比べて外部デバイスの変化が圧倒的に遅いからです。CPUは1秒間に100万回以上(今のCPUは1億回以上)動作します。それに比べればキーボードだって1秒に3回押されるくらいですし、マウスだって16連打が限界です。100万分の1秒とは次元が違うわけです。なので外部デバイスからの信号は、一種の例外として処理したほうが都合がいいのです。説明終わ〜り。
プログラムの解説は本を読んでください、わかると思います。ひとつだけ注意する点としては、ポインタの足し算です。 ポインタに足し算すると、もとの型のバイト分だけn倍されたものを足します。 何を言っているかわからないと思うので例を出しましょう。以下のポインタ型変数を宣言したとします。
int *p = (int *) 0x1000;
これに足し算をしていくと、結果は以下のようになります。
printf("+0 : %d\n", p); // +0 : 4096 printf("+1 : %d\n", p + 1); // +1 : 4100 printf("+3 : %d\n", p + 2); // +3 : 4108
int
型は4バイトなので、1足すと4バイト、2足すと8バイト増えていますね。 char
だと1バイトずつ、 short
だと2バイトずつ増えます。8バイトの構造体だったら8バイトずつ増えます。覚えておきましょう。
追記: どうやら型のサイズはCPUによって変わるようです。 ここ とか ここ などを参考にするとよい。
今日はここまで
ふー疲れましたね、それではまた明日〜
「30日でできる!OS自作入門」を今更ながらやってみる - 4日目
さてさて今日もOSやるぞ
3日目は こちら
4日目 - C言語と画面表示の練習
1 - C言語からメモリに書き込みたい
ここはまだアセンブリですね。プログラムをみると以下のようになっています。
_write_mem8: ; void write_mem8(int addr, int data); MOV ECX,[ESP+4] ; [ESP+4]にaddrが入っている MOV AL,[ESP+8] ; [ESP+8]にdataが入っている MOV [ECX],AL RET
コメントをみるとわかると思いますが、 void write_mem8(int addr, int data);
のようなC言語の関数と対応しています。ここでやりたいことは、 addr
番地に data
を書き込むことです。
ここでC言語の引数とアセンブリの対応ですが、
第一引数 : [ESP+4] 第二引数 : [ESP+8] 第三引数 : [ESP+12] ...
となっております。
現在32ビットモードでプログラムを組んでいるので積極的に32ビットレジスタを使っています。16ビットレジスタを使うこともできるのですが、機械語のバイト数が増え、実行速度も遅くなるらしいです。
C言語と連携する際には、自由に使ってもいいレジスタが制限されていて、 EAX
, ECX
, EDX
の3つだけです。他のレジスタは読み込みはOKですが書き込みはだめなので注意です。
C言語側のプログラムは簡単ですな。 0xa0000 ~ 0xaffff
番地に 15
を書き込んでいます。
さてどうでしょう、みなさんは画面が真っ白になったでしょうか?
2 - しましま模様
AND演算が登場です。AND演算は2進数の世界でよく出てきます。こんな感じの演算です。
11111111 AND 10010001 = 10010001
筆算するとこんな感じです。
11111111 AND) 10010001 ------------- 10010001
...法則性がわかったでしょうか?
AND演算は 同じ桁どうしを比べて、両方1だったら1、それ以外は0を出力する演算 です。コンピュータの世界ではよく出てくるので要チェックです。
本ではこれを使って縞模様を出力していますね。しましまになりましたか?
3 - ポインタに挑戦
さて幾多のプログラミング入門者を苦しませてきたポインタです。ポインタはアセンブリと密接な関係があるので、アセンブリと比較しながら見ていきましょう。
まずC言語にはメモリの番地に直接データを書き込むための表現があります。つまり以下のことをする表現です。
MOV [0x7ffc],0x0f ; メモリの0x7ffc番地にデータ0x0fを書き込む
これをC言語で表現しようとすると以下のようになります。
char *p; p = (char *) 0x7ffc; *p = 0x0f;
ゆっくり見ていきましょう。まず char *p;
があります。どうやら変数宣言のようですが、変数 p
の前に *
がついています。この記号で 変数 p
はメモリの番地 を表すことになります。つまり、この p
をいじることによって、メモリのとある番地のデータを読み込んだり、これまたメモリのとある番地にデータを書き込んだりすることができるのです。
しかしなぜ宣言に char
を用いているのでしょうか。というのもアセンブリとC言語に対応があって、以下のようになっています。
char *p; // BYTE用番地の場合 short *p; // WORD用番地の場合 int *p; // DWORD用番地の場合
今回は1バイト分のメモリ番地を指定したかったので char
を用いています。
次に p = (char *) 0x7ffc;
を見ましょう。これは p
にメモリの番地を指定する作業 です。変数 p
を宣言してメモリ内のデータを操りたいのですが、メモリのどの番地を操作するのかまだ指定してませんでした。ということでここで(例として) 0x7ffc
を p
に指定しています。注意する点として、メモリの番地を指定するときは *
はつけません。ここテストで出ます。 (char *)
でキャストらしきことをしているのは、Cコンパイラではどうやら普通の数値とメモリの番地用の数値を区別するようで、警告が出るのでそれを回避するためです。
最後に *p = 0x0f;
です。これは p
に指定されたメモリ番地にデータを書き込むこと を表現しています。今回の場合は 0x7ffc
番地に 0x0f
を書き込んでいますね。注意ですが、データを書きこむときは *
をつけます。ここもテストで出ますので注意。
4 - ポインタの応用(1)
for (i = 0; i <= 0xffff; i++) { *(p + i) = i & 0x0f; }
これは p + i
番地にデータ i & 0x0f
を書き込んでいますね。for文で i
が1つずつ増えていくので実はさっきとやっていることは変わりません。
5 - ポインタの応用(2)
for (i = 0; i <= 0xffff; i++) { p[i] = i & 0x0f; }
p[i]
という表現は *(p + i)
と完全に同じです。なので p[i] == i[p]
なのです…と言われても納得行かないかもしれません。でも自分の力ではこれ以上説明できそうにありません(泣) この本 とか読むといいんじゃないでしょうか。申し訳ない(汗)
6 - 色番号設定
色番号の章ですが、色番号の解説は割愛します。これはただの決まりですもんね。
さてこの章では、値をまとめて宣言する方法が紹介されます。 C言語の特性ですが、
char a[3];
と書くと、アセンブラでは
a: RESB 3
に相当します。でも微妙にアセンブラとC言語では違いがあって、 RESB
では0で埋めてくれましたが、C言語の書き方だと0で埋めてくれなくて、中にゴミデータが入っているかもしれませんので注意です。そのため、データの初期値を設定することもできます。
char a[3] = { 1, 2, 3 };
これは以下の表記とほぼ同等です。
char a[3]; a[0] = 1; a[1] = 2; a[2] = 3;
ここで注目してほしいのが、 a
が最初からポインタとして認識されていることです。上のアセンブラを見てもらえるとわかるのですが、 a
はラベルで、つまりメモリのどこかの番地を指しているわけです。
今回のプログラムだと、上の方法では48個の値を MOV
しなくてはなりません。これは(機械語的に)大変なので DB
命令で一気に指定したいところです。C言語では DB
命令に相当する表記があって、それが static
です。今回のプログラムでは static
を宣言の前につけていますね。
次に bootpack.c
の set_palette()
を解説します。
CPUにはメモリ以外にも沢山のデバイスが接続されていて、そこに命令を送るのが OUT
命令と IN
命令です。この命令は装置番号(portといいます)を指定してデータを送ったり受け取ったりします。この命令はC言語では書けないのでアセンブラで書く必要があります。
さてどんなことをすればいいのでしょうか。本によると、以下の操作をすれば良さそうです。
- 割り込みを禁止する
0x03c8
に設定したいパレット番号を書き込み、続いてR,G,Bの順に0x03c9
に書き込む- パレット状態の読み込みは、
0x03c7
にパレット番号を書き込み、続いて0x03c9
を3回読み出す - 割り込み状態をもとに戻す
割り込み禁止処理と割り込み許可処理ですが、 CLI
と STI
が登場します。 CLI
というのはclear interrupt flagの略で、割り込みフラグを0にして割り込みを禁止する処理です。 STI
というのはset interrupt flagの略で、割り込みフラグを1にして割り込みを動作させる処理です。
割り込みが何かって?その説明はちょっと先になります。もう少し我慢。
次に EFLAGS
という特別なレジスタの説明です。
簡単に言うといろんなフラグが詰まったレジスタで、キャリーフラグや割り込みフラグもここに入っています。キャリーフラグは JC
とか JNC
などで簡単に中身が調べられるのですが、割り込みフラグはそういう命令がないので、 EFLAGS
を読み込んでから第9ビットが0か1かチェックするしかないのです。
EFLAGS
もC言語からは操作できないのでアセンブラでがんばります。 naskfunc.nas
へ移りましょう。
ここで私達がやりたいのはつまり MOV EAX,EFLAGS
なわけですが、これはできません。そういう命令がないのでしょうがないです。なのでスタックを使う必要があります。スタックって何って?それはまた後日。
PUSHFD
と POPFD
命令が初登場です。それぞれ、push flags double-wordとpop flags double-wordの略で、フラグをスタックに押し込む/取り出すという意味です。つまり、
_io_load_eflags: PUSHFD POP EAX RET
というのは、スタックに PUSHFD
で ELFAGS
を押し込んで、 POP
で飛び出してきたところを EAX
に代入しているのです。
また値を返す関数ですが、C言語の規約では RET
したときに EAX
に入っていた値が戻り値としてみなされます。要チェックです。
7 - 四角形を描く
いままでのことを総動員して絵を描いていますね。特に難しそうなところはなさそうですね。
8 - 今日の仕上げ
根気よく絵を描いています。これ大変そうだなー
今日はここまで
お疲れ様でした〜それではまた明日〜
5日目は こちら
「30日でできる!OS自作入門」を今更ながらやってみる - 3日目
今日も今日とてOSやるぞ
2日目は こちら
3日目 - 32ビットモード突入とC言語導入
今日は重いですね…頑張ってまとめます。
1 - さあ本当のIPLを作ろう
さて書き足された部分のまとめをしていきます。
フロッピーディスクの構造を学ぼう
INT 0x13
は前回やったとおり割り込み命令ですね。
INT : ソフトウェア割り込み命令のこと これだけだと何がなんだかわかりませんが、今のところはBIOSの関数呼び出しの一種だと思っておけばいいらしいです。 BIOSというのは「basic input output system」の略です。いかにも画面に文字表示したりキーボード入力できそうな雰囲気ですね。これを用いてハロワを表示させていたんですね〜
「30日でできる!OS自作入門」を今更ながらやってみる - 2日目 - 無限遠まで突撃中
さて何をさせるのでしょうか。本によると
ディスクの読み込み、書き込み、セクタのベリファイおよびシーク
らしいです。パラメータの解説をします。
AH
: モードの選択AL
: 処理するセクタ数CH
:シリンダ番号 & 0xff
CL
:セクタ番号(bit0-5) | (シリンダ番号 & 0x300) >> 2
DH
: ヘッド番号DL
: ドライブ番号ES:BX
: バッファアドレス(ベリファイ時とシーク時は参照しない)- 戻り値 :
FLAGS.CF == 0
: エラーなし、AH == 0
FLAGS.CF == 1
: エラーあり、AH
にエラーコード(リセットファンクションと同じ)
今回は AH = 0x02
でディスクの読み込みです。
まずは戻り値です。
CF
というのは「キャリーフラグ」というもので、1ビット記録するレジスタです。本来はキャリー状態というものを記録するのですが、扱いやすいのでいろいろ使われるらしいです。
ちなみにキャリーというのは carry 、つまり けた上げ のことです。計算していてレジスタが桁あふれしたときにキャリーフラグがぴょこっと1になることでうまいこと計算ができるんですね〜大学の論理回路の授業でやりました。詳しい解説は この方の記事 を読むといいです。(キャリーフラグはどうやら符号なし演算のときに使うらしいですね)
さて、残りを解説するにはフロッピーディスクの構造を説明しなくてはいけません。以下の図を見てください。
この図はフロッピーディスク内部の磁気ディスクの構造です。円筒状にシリンダ(図ではトラック)が配置されており、セクタという区分で分割統治されていて、磁気ヘッドから読み込みます。シリンダは全部で80あり、セクタは各シリンダ内に18あります。磁気ヘッドは表と裏で2つあります。今の解説で上のパラメータは大体わかりましたね。
ドライブ番号というのは、フロッピーディスクドライブが沢山つながっている場合に識別するための番号です。ドライブが1つの場合は0番を指定します。
残りはバッファアドレスですね。これは メモリのどこに読み込むか を表すものです。なぜ2つのレジスタで表すのでしょう?というのも 前回 を思い出してほしいのですが、 BX
というレジスタは16ビットでしたね。ということは、 BX
一つだけだと 0x0000 ~ 0xffff
までしか、つまり64KBまでしか表すことができないのです。
これを回避するために2つレジスタを用いてメモリの番地を表します。具体的にはバッファアドレスは ES * 16 + BX
で表されます。 ES
でおおざっぱに指定してから BX
で細かく指定、という感じですね。これで 0xffff * 16 + 0xffff = 1,114,095
バイト、つまり約1MB使えます。やったー。
セグメントレジスタ
ところで先程登場した ES
レジスタですが、これはセグメントレジスタと呼ばれるレジスタです。末尾に S
がつくレジスタですね。セグメントとは何ぞやと思ったので調べてみました。
セグメント方式 (memory segmentation)は、メモリ管理の方式の一つ。プログラムやデータをセグメントまたはセクションという「可変な」大きさのまとまりで管理する。セグメントは、メモリ空間上で、情報の属性などによって分類されたグループである。セグメント方式でメモリ位置を参照するには、セグメントを識別する値とセグメント内のオフセットを指定する。 (Wikipediaより)
なんだか難しくてよくわかりませんが、つまりセグメントレジスタでメモリをおおざっぱに区切っているということなのかな?(違うよ!ということであればコメントください)
大事なのは最後の文章で、「メモリ位置を参照するには、セグメントを識別する値とセグメント内のオフセットを指定」とあります。さっきの例だと、セグメントを識別する値とは ES
のことで、セグメント内のオフセットとは BX
のことなのでしょう。
メモリの番地指定の裏設定
ここで新事実が発覚。なんと今までメモリの番地指定では、実は暗黙的に DS
がセグメントレジスタとして指定されていたんだよ!!!(ΩΩΩ < ナ、ナンダッテー!?)
つまり我々が
MOV CX,[1234]
だと思っていたのは、実は
MOV CX,[DS:1234]
だったのです。この理由から、 MOV DS,0
の初期化が必要だったのです。(0以外にすると番地の計算がややこしいことになりますから…)
2 - エラーになったらやり直そう
AH = 0x00
、 DL = 0x00
で INT 0x13
してやると、ドライブのシステムリセットが走るようです。
3 - 18セクタまで読んでみる
4 - 10シリンダ分を読み込んでみる
特に難しいところはないので省略!!!
5 - OS本体を書き始めてみる
ここでやることは、 OSのプログラムが保存されているメモリ番地にブートセクタからジャンプする ことです。そのためにはまずOSのプログラムがどの番地に保存されているか調べる必要があります。とりあえず普通にコピーしてバイナリエディタで覗いてみましょう。
…
本によって進めると、空ディスクにファイルを保存すると
- ファイル名は
0x002600
以降に入る - ファイルの中身は
0x004200
以降に入る
ということがわかりました。ブートセクタから 0x004200
に飛べばいいですね。
6 - ブートセクタからOS本体を実行させてみる
ブートセクタからOS本体に飛ばす処理をしていますね。
7 - OS本体の動作を確認してみる
真っ黒画面表示です。画面関係なので INT 0x10
で割り込みさせればいいですね。
パラメータは以下のとおりです。
AH = 0x00
AL = モード
- 戻り値 : なし
8 - 32ビットモードへの準備
C言語で開発したいので32ビットモードへの移行を目標にします…が32ビットモードではBIOSの機能が使えなくなります。(BIOSは16ビットモードを前提に書かれているので…)なのでI/O関係のことは今のうちにやっておきましょう。具体的にはキーボードの状態をBIOSから教えてもらいます。
AH = 0x02
: シフトフラグステータス取得
詳細は ここ で調べました。
あとVRAMはメモリマップの色々なところに散らばっているらしいです。びっくりしないようにしましょう。
9 - ついにC言語導入へ
asmhead.nas
に100行くらい書き足されたようです…が今はその内容は明かされません。今後の解説に期待ですね。
C言語で書かれたOS本体は簡単ですね。割愛します。
さてこのcファイルをコンパイルして ipl10.nas
や asmhead.nas
なんかとリンクするわけなんですが、そこら辺は筆者であるKさんが用意した(のを先人が整備した)ツールが大活躍です。あまり気にせず進められるでしょう…と言いたいのですが、一つ悩んだのがあって、hikalium氏のツールの使い方で1時間位つまりました。ちょっと分かりづらいのでここで共有しておきます。
z_tools
内の haritol concat
の使い方ですが、
copy hoge+fuga piyo
は、
haritol concat piyo hoge fuga
と対応しているようです。参考にしてください。
10 - とにかくHLTしたい
C言語ではHLTできないのでアセンブリでHLTする関数を書きます。少し気になったところなのですが、 naskfunc.nas
の
[SECTION .text]
ってリンカスクリプトの .text
セクションと同じなんですかね?知っている人、教えてくださいm(-_-)m
あと内部のリンクの仕組みがまだわからないですね…分かり次第追記します。
今日はここまで
お疲れ様でした。また明日〜
4日目は こちら