【メモ】Makefile
Makefileについて調べた際のメモです。
- Microsoft nmake、Borland make、GNU make、Solaris makeなどいろんな種類があるらしい
- フォーマットは
ターゲット名: 依存ファイル名
を書いて次の行からコマンドを書いていく。- 「作りたいもの、必要な材料、作り方」の順
- コマンド行の先頭はタブで空ける必要がある(
ほんとに?本当だった。vs codeの場合勝手にタブにしてくれた。) - ターゲットが複数の場合別々に書けば変更があったファイルだけビルドし直すので効率がいい。
make
だけで実行した場合、先頭にあるターゲットが実行される。- 先頭のターゲットとして
all: 本当のターゲット
を書く習慣がある。
- 先頭のターゲットとして
TOOLCHAIN = arm-none-eabi
というように変数を定義できる。これをマクロと呼ぶ。- 必ずしもターゲットで指定した名前のファイルがコマンドにより作られる必要はない。
clean
やinstall
などの命名でターゲットが定義されることも多い。
- 依存ファイルがない場合はなにも書かなくていいし、ターゲットと同じ行に書いてもいい。
- コマンド行のないターゲットを書くこともでき、主に複数のターゲットをまとめる際に使われる。
CFLAGS
という変数が定義されている場合、*.c
を*.o
にコンパイルするときに暗黙的に使用される。- その他デフォルトのルールや変数は山ほどあり、
make -p
で確認することができる。 - ビルドし直すべきかどうかをファイルの更新日時を使って判断する。
- 再ビルドを矯正したいときはターゲットのファイルに対してtouchコマンドを実行すればいい。
- 必ずしもC言語のプログラムである必要はなく、材料とそれから作りたいものがあるときにはかなり汎用的に使えるコマンドになっている。
-f
オプションで対象のファイルを指定し、省略した場合GNUmakefile
、makefile
、Makefile
の順で探す。- 依存ファイルがなく、生成するファイルもないターゲットのことをphony targetと呼ぶ。phony tagetは同名のファイルが存在すると動かなくなってしまうため、
.PHONY: ターゲット名
を書くことでこれを回避できる。.PHONY
はまとめてどこか一箇所に書いておけばオッケー。
- ヘッダファイルを依存ファイルに含めることで、ヘッダファイルだけの更新があったときでもリビルドを走らせることができる。
- コマンド内で
$@
によりターゲット名を参照できる。これを内部マクロという。また$<
では依存ファイルの先頭のファイル名を参照できる。- 一覧はここ: Special macros
.SUFFIX: 依存先の拡張子 依存元拡張子
のフォーマットで拡張子ごとの依存関係を記述できる。- 発展形としてOSごとの差異を吸収したツールも存在する。
- シェルスクリプトと同様に
\
で改行できる。 - ターゲット名は複数書いてもよく、これは単に複数のルールをまとめて書いたことになる。
- 関数が用意されている。see: GNU make: Functions
- shell関数を使うことでマクロ定義の際などでコマンドを実行することができる。
- コマンドの先頭に
@
をつけるとその行の出力を省略できる。-
をつけるとコマンド失敗を無視できる。 - 慣例として変更の可能性があるマクロは大文字、Makefile内で閉じているマクロは小文字で書く。
- マクロ宣言の際にコロンをイコールの前につけて
:=
とすると、定義内のマクロが即時展開される。それ以外では実行の際に展開される。 ?=
により値が定義されていないときだけ新たに定義する、という動作にできる。makedepend
コマンドよりルールを動的に生成することができる。include
で他のMakefileをインクルードできる。ifdef
やifeq
などの分岐分を使える。
Refs
- 今更聞けないmakefileの書き方
- Makefileの解説
- Makefileの書き方 - $ cat /var/log/shin
- - 自動化のためのGNU Make入門講座 - Makefileの基本:ルール
- トリビアなmakefile入門
- Makefileの書き方に関する備忘録 - minus9d's diary
- Makefileの書き方 - Web就活日記
- Make Tutorial: How-To Write A Makefile - Michael Safyan
- c - How do I make a simple makefile for gcc on Linux? - Stack Overflow
- C Programming Tutorial: Make and Makefiles
- Make と Makefile の説明
Raspberry PiでLチカ
ハードウェア版Hello, WorldであるところのLチカ(LED点灯)をRaspberry Pi(以下RPI)でやってみたいと思います。使用するのはRaspberry Pi B+モデルです。
やることは2ステップです。
- LEDに対応するピンを出力モードに設定する。
- 対応するピンの出力ビットを立てる。
がその前にRPIにおけるハードウェア制御について触れます。
なおビルドや実機での動かし方などは割愛するので以下のサイトを参考にしてください。
- Department of Computer Science and Technology – Raspberry Pi: Baking Pi – Operating Systems Development
- Tutorial
RPIでのハードウェア制御
ハードウェアはそれぞれ自身を制御するための記憶領域を持っており、これをレジスタと呼びます。このレジスタにCPUから値を読み書きすることで制御を行います。例えばLEDの場合「このレジスタの値がxxxxの時にLEDがつく」とハードウェアの仕様で決められているので、この値をCPUから書き込むことで点灯させます。
CPUからレジスタの値を読み書きするにはいくつか方法があるらしいのですが、RPIはメモリマップトIOと呼ばれる方式を採用しています。 これはハードウェアのレジスタをメモリに対応づけることで、メモリの読み書きを通してハードウェアを制御する方式です。レジスタに値を書き込みたいときはそのレジスタに対応するメモリ上のアドレスに値を書き込むだけで同時にハードウェアのレジスタを書き換えることができます。読み出しに関しても同様にメモリ上の値を読むだけです。メモリに対する通常のロード・ストア命令でハードウェアを制御できるシンプルさが利点です。 LEDのレジスタもこの方式に従いメモリ上にマップされているので、まずはじめにこれがどこにあるのかを調べることからはじめます。
ところでRPIにはいろんなハードウェアを接続するための汎用的な入出力ピンが存在します。この汎用IOピンの集合をGPIOと呼び全部で53のピンがあります。RPIにはじめから組み込まれているLEDもこの汎用ピンの一つを利用しており、53あるピンのうち47番目(RPI B+の場合)に接続されています。 つまり「LEDを制御するレジスタがメモリ上のどこにマップされているか」は言い換えれば「GPIOの47番ピンがメモリ上のどこにマップされているか」という問題になります。
どのレジスタがメモリ上のどこにマップされているのか、というのはARMのペリフェラル(周辺装置)マニュアルに書いてあります。これのp.89以降にGPIOに関する記述があり、GPIOがアドレス0x20200000
の位置(RPI B+の場合)からはじまることがわかります1。今回必要なレジスタはモードの設定用のGPSELレジスタおよび出力用のGPSETレジスタです。書き込む値については実際にコードを見ながら触れたいと思います。
実際にLEDを点灯してみます。先にkernel_mainのコード全体は以下のとおりです。
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) { (void) r0; (void) r1; (void) atags; *(volatile uint32_t *) 0x20200010 = (1 << 21); *(volatile uint32_t *) 0x20200020 = (1 << 15); while (1) { } }
出力モードの設定
GPIOピンの制御モードには1ピンあたり3bit与えられているため、一つのレジスタ(32bit)で管理できるのは32 / 3 = 10
ピンです。よって53ピンあるGPIOの制御用レジスタGPSELは0~5の6つに分かれており、それぞれ10ピンずつ割り当てられています。
今回の目的である47番ピンはSEL4ということになり、対応する3bitは21~23番目ということになります。SEL4はメモリ上0x20200010にあり、出力のモードは0b001ということになっているため21番目のビットを立てれば完了です。
*(volatile uint32_t *) 0x20200010 = (1 << 21);
(volatileというのが気になるかもしれません。この行はメモリに値を書き込むだけの処理のため、コンパイラが勝手に「この処理いらん」と判断するなどの最適化がはたらき、意図しているのと違うコードが生成されてしまう可能性があります。volatileはそれを回避するために付け足しています。)
出力レジスタのビットをたてる
ピンをHIGHにするGPSETレジスタは2つあり、1つめが0~31番ピン、2つめが32~53番ピンに対応します。今回47番ピンなので、2つ目のレジスタの47 - 32 = 15
番目のビットを立てれば良いです。SET1レジスタはメモリ上0x20200020にあります。
*(volatile uint32_t *) 0x20200020 = (1 << 15);
以上をビルドして実機で動かすとLEDが点灯するのが確認できると思います。
Refs
- Department of Computer Science and Technology – Raspberry Pi: Baking Pi – Operating Systems Development
- Tutorial
- BareMetalで遊ぶ Raspberry Pi - 達人出版会
もし点灯だけではなく点滅もしたい場合も手順は同じで、今度はピンをLOWにするGPCLRレジスタを使います。CLR1レジスタは0x2020002Cにあるため、
*(volatile uint32_t *) 0x2020002C = (1 << 15);
をするだけです。
点滅しているのががわかるように、CPUを空回しするdelay関数を以下のように用意します。
// https://jsandler18.github.io/tutorial/boot.html より void delay(int32_t count) { asm volatile("__delay_%=: subs %[count], %[count], #1; bne __delay_%=\n" : "=r"(count): [count]"0"(count) : "cc"); }
これをつかったkernel_main全体は、
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) { (void) r0; (void) r1; (void) atags; *(volatile uint32_t *) 0x20200010 = (1 << 21); while (1) { *(volatile uint32_t *) 0x20200020 = (1 << 15); delay(500000); *(volatile uint32_t *) 0x2020002C = (1 << 15); delay(500000); } }
になり、これを実行するとLEDが点滅するのが確認できるはずです。
Goのpluginパッケージを試してみる
例えば強いこだわりにより、switchを使いたくない時
eg)
func main() { if len(os.Args) < 2 { fmt.Println(help()) os.Exit(1) } switch os.Args[1] { case "a": a.F() case "b": b.F() } }
Go1.8で導入されているpluginのパッケージを使えば以下のように書ける。
func main() { if len(os.Args) < 2 { fmt.Println(help()) os.Exit(1) } p, err := plugin.Open(os.Args[1] + ".so") if err != nil { fmt.Fprintf(os.Stderr, "Unexpected command: %s", err) os.Exit(1) } f, err := p.Lookup("F") if err != nil { fmt.Fprintf(os.Stderr, "Function F not found: %s", err) os.Exit(1) } f.(func())() }
トリックはそれぞれのファイルを-buildmode=plugin
でビルドすることでライブラリにしておき、さらにpluginパッケージにより動的に読み込んでしまう。
具体的には
1.ソースファイルを-buildmode=plugin
オプションでビルド
$ go build -buildmode=plugin a.go $ go build -buildmode=plugin b.go
2.main.goをビルドしたのち引数付きで実行
$ go build main.go $ ./main a This is a function in "a.go" $ ./main b This is a function in "b.go"
サンプルコード全体はgistに。
GoでもちょっとしたLL言語っぽいことができるみたい。
Refs
【日記】30日でできる! OS自作入門
筆者はそうしなくていいと書いていますが、律儀な性格なので1日1Chapter進めていきたいと思っています。
(4日目)一度で理解できる内容ではなく、2週したいので予定変更。
0日目 - 2018/03/31
本はWindowで開発するために書かれているので、macで開発できるよう環境を整える。
三等兵さんという方がmac向けの開発環境を整えてくれているのでそれを利用する。
こちらのQiita記事によるとエミュレータに問題があるらしく、そのままでは途中でつまずくとのことだったので記事の筆者がMakefileを修正したこちらのレポジトリをもとにセットアップする。(本家は2012年以来コミットがないから今のところフォークしてない。)READMEの通りにやってうまくいった。
環境:
- macOS10.13.3 (High Sierra)
- Homebrew 1.5.13
14日目につまづきポイントがあるらしいのでメモ。
参考
- 「30日でできる! OS自作入門」をMac向けに環境構築する - Qiita
- GitHub - tatsumack/30nichideosjisaku: 『30日でできる! OS自作入門』川合 秀実氏(著)のMacOSX開発環境を整えることができます
- 初めてのC言語で『30日でできる!OS自作入門』の通りに初めてのOSを作ってみた - 三等兵
- GitHub - sandai/30nichideosjisaku: 『30日でできる! OS自作入門』川合 秀実氏(著)のMacOSX開発環境を整えることができます
- 30日でできる! OS自作入門 Macで頑張る(環境構築) - Qiita
- 30日でできる! OS自作入門をMac + VirtualBoxでやり始めた - でこてっくろぐ ねお
- Macで「30日でできる! OS自作入門」の開発環境準備 その0 - 技術メモ。
- MacOSXでOS自作覚え書き 下準備編 - It works!
- 「30日でできる! OS自作入門」 開発環境構築 for mac - blue9's 外壁
しかし三等兵さんのこのブログ記事、文章がすごい面白い。笑
1日目 - 2018/04/01
バイナリエディタ0xEDをダウンロードする。バイナリエディタを使うのは初めて。 本の通りに打ち込んでmake runすると無事Hello, Worldが表示された。
書いたものを手元のUSBにインストールしようとしたけどさすがにmacでそれはできないっぽい(batファイルを対応するシェルスクリプトに書き換えればいける?)。ちなみにmacでUSBをフォーマット(削除)する方法はここ。
ここに沿っていけばとくに詰まることなく最後までできる。最後のアセンブリ言語のあたり本当に自分で書いたものが動いている実感がない...笑
ポイント
2日目 - 2018/04/02
今日は手を動かすことはほとんどなく、解説を読むのがほとんどだった。主にアセンブリ言語の読み方でその中でレジスタの紹介とかメモリのリテラルとかがでてきた。3章「アセンブリでブートセクタだけ作り、残りのディスクイメージはツールでつくる」の部分はまだしっくりきていない。またMakefileの入門もあり、Makefileについてはいつか時間を割いて習得しておきたい。
解説中心だったけどもCPUやレジスタ、メモリとの関係など大事なことが多かったのであとで見返したいChapterだった。
ポイント
3日目 - 2018/04/03
いきなり多いし難しい。2日目に導入したIPL(Initial Program Loader: 初期プログラムローダ)を改良し、ディスクから両面10シリンダ分読み出す処理の実装。ディスクの物理的な構造とそれを読み込んでいくイメージがついた。
次にOSの本体を記述し、ディスクイメージに保存。その後ブートセクタから実行する処理。よくわからない。 その後16ビットモードでしか動作しないBIOSの処理を行ったのち、32ビットモードに切り替えて(32ビットでのみ動く)C言語で記述していく。
一度で飲み込みきれないのでこれは2週目が必要な雰囲気だし、2週する以上は1週目はもっとスピードをあげて駆け抜けたい感じ。うーん。
ポイント
4日目 - 2018/04/04
- Chapter 4 ~ 6
というわけで一日一チャプターの予定を変更。
よくある386、486というのなんとなく聞いたことがある程度だったけどもよく考えたらx86系でインテルの系列だった。C言語でforループを回してVRAMに値を書き込んでいって描画する。描画の仕組みを初めて知ってなるほどという感じ。はじめはアセンブリ言語の関数をCで呼んでいたがポインタを使って書き直し。今までで一番詳しくてわかりやすいポインタの説明だった。a[1]が*(a+1)のシンタックスシュガーというのにはびっくり。
Chapter5および6の内容はエグい。というかほとんど理解できなかった。フォントの定義や文字の表示はいいのだけど、セグメンテーションがさっぱり。やりたいことはマウスを動かすことで、そのためにGDT(global descriptor table)とIDT(interrupt descriptor table)というのを初期化しないといけない、という流れ。セグメンテーションはメモリの競合を防ぐための仕組みで、IDTは割り込みとハンドラのマップ、という理解でとりあえず大丈夫かな...?
ポイント
- VRAMへの書き込み
- ポインタ
- フォントの設定と文字の表示
- セグメンテーション
- GDTとIDT
- ヘッダファイル
- 割り込み処理とPICの設定
5日目 - 2018/04/05
- Chapter 7 ~ 9
どうやら鬼門は超えたらしい。
割り込み処理は次々とくるのですぐに抜け出さないといけない。ということで基本的には処理をバッファに積んで処理を返す。キーボード用のバッファ、マウス用のバッファでというように別々に作成しFIFOかつリングバッファで構築。
プロテクトモードはセグメントレジスタの解釈が16の倍数ではなく、GDTを使うようにするモード。つまり仮想アドレスを導入するということかな。よくわからないけどアプリケーションからセグメントを直接操作できないようにするモードらしい。 またOS開発の際にはメモリのレイアウトを設計する必要があって、これを最初に作っておくと順調に開発できるそう。初期設定は最低限のことだけをして早く割り込みを受け付けられるようにしないとハードウェアに割り込みが溜まって不具合を起こすらしい。
次に必要なのはメモリの管理で、空き領域を構造体で表現してリスト形式で管理した。ブロックのメモリが空いているかどうかをフラグで表現して並べてもいいのだけれど、配列/リストで管理したほうが省スペースで処理も一括でできるから速い。また配列よりもリストで管理したほうが挿入削除が高速にできていい。とのこと。その他初めて知ったこととして、CPUにもキャッシュがついており、わざわざメモリを読みにいかなくてもいい場合があること、プログラムの9割以上はループで消費されていることなどがあった。
ポイント
- 割り込み処理とバッファ
- プロテクトモード
- メモリ管理
- CPUキャッシュ
6日目 - 2018/04/06
- Chapter 10 ~ 12
最初は画面描画について。ウィンドウが重なった時にどう効率よく表示するかの話。まずいとすごい時間がかかったりチラつきが発生したりするのでうまく必要な範囲だけを更新する。この辺りは正直デバイスの機能なので一番理解したいUNIXのカーネル的なところとは関係ないんだけど、実際にOSを作ろうと思ったら一番成果が見やすいところだなーとも思う。
最後はタイマーの話。CPUは200MHzとかで動いているわけだけど、PITという補助的なデバイスから指定した時間おきに(100Hzとか)割り込みが入るように設定してOS側でカウンタ制御して時間を測る。また割り込みハンドラに処理を積んでおくことで「指定時間後に処理」するタイマを設定することもできる。
読んでいて改めてちゃんと動作するOSを作ってみたいなという気になった。
ポイント
- 重ね合わせの描画処理とチラつきの解消
- タイマの設定
7日目 - 2018/04/07
- Chapter 13 ~ 15(途中まで)
昨日に引き続きタイマーの改善。デバイスごとに設定していたタイマ用のFIFOバッファを一つにまとめた。さらにforループごとにアップするカウンタを置いてベンチマークをとるようにする。さらにFIFOをLinkedListに変更して配列要素をずらす処理をカット、配列後端に番兵をおくことで空配列や最終要素への追加などをスッキリさせた。そういえば同時に読んでいるUNIX V6の本でブロックデバイスのバッファに使うbfreelist要素も番兵の役割をしているなと思った。
14日目は画面を高解像度にする処理。ビデオカードがなんであるかがよくわからなかったけど、高解像度かどうかを調べて対応していたらそれに応じて画面のモードを変更する。またキー入力と文字とを対応させて画面に文字を入力、さらにはカーソル描画も追加してラインエディタの完成。さらにさらにマウス入力を認識してウィンドウの移動描画。すごい。
15日目はマルチタスク。マルチタスクは「分身の術」という表現がしっくりきた。コンテキストスイッチの頻度は30Hz~100Hzらしい。この頻度とスイッチにかかるクロック数でCPU時間の何パーセントをスイッチにかけているか計算できる。そしてこのスイッチにかかる時間というのは概ねレジスタからメモリへの書き出しとその逆の読み込みにかかる時間といっていい。
15日目の続きはまた明日。
ポイント
- ベンチマークのとり方
- LinkedListと番兵で効率化
- ビデオカードと高解像度対応
- マルチタスクとコンテキストスイッチ
8日目 - 2018/04/08
- Chapter 15(続き) ~ 18
昨日の続き。taskAとBで交互にタスクスイッチを読んで0.02秒ごとに切り替える。またカウンタを表示してタスクが切り替わっていることを確かめる。さらにタスクから明示的にスイッチを呼び出すのはかっこ悪いのでタイムアウト時に切り替えが起こるようにする。
次にタスクを構造体として定義して管理する。現状はタスクAはHLTするだけの一方Bはひたすらカウントアップしているのに50:50でCPU時間を使っているため効率が悪い。タスクAはスリープするようにし、割り込み発生時にwakeupするよう修正。
次に優先度の仕組みを導入し、優先度に応じてCPU時間が割り当てられるように修正。タスクAはほとんど何もしない一方割り込み時は切り替えなしで動いて欲しい(マウス操作とか)種類のタスク。こういうタスクが同じ優先度になったら先にCPUをとったほうが思う存分使えてしまうので、優先度レベルを導入して同じレベル内で切り替えが起こるようにする。最後にHLTするだけのidleタスクを番兵として優先度最低でおくことで、タスクAは他にタスクがあるかどうかに関わらずスリープできるようにする。
残りはコンソールを作る作業。入力の切り替え、文字・プロンプトの表示、記号(shift)・大文字小文字(CapsLock)の入力、改行・スクロール処理、最後に特定の文字列("mem","cls","dir")で改行された際に対応するデータを表示する、コマンドをいくつか作成し終了。たった1,2日でかなりシェルっぽいものができていてすごい。
ポイント
- タスクを定義する構造体(Unixのプロセス構造体?)
- スリープとwakeup
- 優先度と優先レベル
- コンソールの作成とコマンドの実行(.exeの実行はまだ)
- ディスク上のファイル情報の位置取得
さて、6日目で「実際に動くOSを作ってみたい」と思って思いついたのは「xv6をRaspberry piに移植する」というもの。本当にできるかどうかわかんないけど調べたものをメモしておく。
- Raspberry piはARM
- Raspberry piで動くOSを作るCambridge Univ.のプロジェクト。かなり有名らしい。Department of Computer Science and Technology – Raspberry Pi: Baking Pi – Operating Systems Development
- 同じコンセプトで別のプロジェクト。Tutorial
- これも。GitHub - BrianSidebotham/arm-tutorial-rpi: Raspberry-Pi Bare Metal Tutorial
- これも?GitHub - SharpCoder/rpi-kernel: A Basic (non Linux) C++ Kernel for Raspberry Pi.
- 実際にxv6をrpiに移植したプロジェクト。Google Code Archive - Long-term storage for Google Code Project Hosting.
- xv6をrpiに移植したレポジトリ。GitHub - zhiyihuang/xv6_rpi_port: This is an xv6 port to Raspberry Pi
- xv6のコード解説ブログ。xv6ソースコードリーディング – 日曜研究室
- 必要な知識はARMの仕様とxv6の知識?
- ARM用のアセンブラ、コンパイラは多分見つかる。
- xv6のx86向けに書かれたアセンブリ言語をARM用に書き直すだけかなぁ。。
- HariboteOSをRaspberry Piに移植した話。ツールチェインとかも揃えていてすごい。。Raspberry Pi Bare Metal Programming(「OSを使わないプログラム」のことらしい)やARM関連のリンクも多数。「30日でできる!OS自作入門」のHaribote OSをラズベリーパイに移植してみた - Moiz's journal
すごい面白そうだけどできるかどうか不安。
9日目 - 2018/04/09
- Chapter 19 ~ 21
今日もガツガツ進む。ついにアプリケーションをファイルに作成し実行する処理。はじめにdirコマンド(cat in Unix)でディスクからファイルの内容を読み出す手順に慣れる。その際FATというUnixでいうスーパーブロックかな?の情報から読む方法も学ぶ。ファイルの中身を読めるようになったらそれを表示するのではなく実行することでアプリケーション実行処理の完了となる。
Chapter20ではAPI(システムコール)の作成をする。最初にアプリから1文字表示用の関数を呼び出し、OS内であらかじめ番地を割り振っておいた呼び出しに対応する関数を呼び出す方式をとる。その際アプリとOSではセグメントが異なるので通常のCALL命令ではなくfar-CALLというセグメントを切り替える呼び出し(およびそれに対応するfar-RET)が必要。しかしこの方式だとOSを書き直すたびに関数のアドレスが変わってしまい、アプリケーションをコンパイルし直さないといけない。これを解決するのは呼び出し関数をあらかじめ登録しておく方法で、それは割り込みによって実現される。ここでなんでシステムコールがトラップによって実装されているのかの理由が与えられたわけだ。引数で呼び出すAPIを指定できるようにすることで一つのIRQ登録で全てのAPIコールに対応できるようにする。Chapter21ではさらにアセンブリでAPI呼び出し関数を用意して、C関数でコールできるようにしC言語でアプリを書く。
21日目のメインの内容はOSの保護で、アプリがOSが管理するメモリにアクセスできないようにする。そのためにアプリ用のセグメントを用意してOSセグメントと区別できるようにする。本来はこのOS-アプリ間のセグメント切り替えはOSの仕事なのだが、今時の(?)CPUはOS保護機能が付いておりこの切り替えを代わりにやってくれるらしい便利。その際保護例外が発生した時にCPUから割り込みが送られるのでそれに対応する処理を追加してあげる。最後にセグメント定義でアプリ用かOS用かの値を設定してあげることで、アプリがレジスタにOS用セグメントを代入することを防いだ。
システムコールやOS保護などの今時必須な機能の実装はとても興味深かった。Unixと共通する部分が多くあった気がする。
ポイント
- ディスクからファイルの中身をFAT情報を元に読み出す処理
- APIの作成と割り込みによる呼び出しのハンドリング
- OSの保護と例外処理
10日目 - 2018/04/10
- Chapter 22 ~ 24
C言語でアプリをガンガン書いていく段階。はじめに昨日やったOS保護の仕組みのおかげで「アプリがOSを呼び出すにはAPIを使うしかない」ことを確認、また例外処理をリッチにすることでバグを踏んだ際に必要な情報が残るようにする。強制処理機能を追加することでアプリを開発する際OSから必要なフィードバックか返ってくるようにする。
本題はC言語を使ったアプリの作成。OSにやってもらわないとけない作業はAPIとして定義していく。ウィンドウ、文字列、点の描画、線の描画、ウィンドウの削除、キーの入力を次々にAPIとして追加していく。その過程でデータ領域の確保だったりメモリ割り当ての関数を定義したりもする。
24日目にはウィンドウをWindowsのように操作できるよう改造してよりOSらしい、使い慣れたUIへと進化させていった。最後にタイマようのAPIを作成してアプリから利用できるようにした。
ポイント
11日目 - 2018/04/11
- Chapter 25 ~ 27
OSとしての機能が充実してきたのであとはアプリを作り込んでいくだけのよう。アプリを2つ同時に出すためにコンソールを増やす改造をしたり、ウィンドウを高速化するために書き込み命令が4の倍数になるようにしてバルクで処理させるなどの最適化をする。またコンソールを好きに増やしたり消したり、アプリを新しいコンソールで実行したりコンソールから切り離して実行したりする処理を追加する。
これまではOSのセグメントとアプリのセグメントという分け方しかしていなかったため、あるアプリから他のアプリのデータセグメントを操作したりといったことが可能であったが、タスクごとにLDT(Local Descriptor Table)を割り当てることで自分のセグメント以外への書き込みを防ぎ、アプリを保護する仕組みを追加する。
最後にAPIやその他の関数をライブラリとしてまとめることで不要な関数をアプリから取り除く。またライブラリやその背後にある構造化プログラミングの概念にも触れる。
ポイント
- 描画処理の高速化
- LDTとアプリの保護
- ライブラリ
12日目 - 2018/04/12
- Chapter 28 ~ 31
Chapter28でははじめにスタックを4Kバイト以上割り当てられるように修正。そしてメインであるファイルの読み出しAPIを作成。最後に日本語フォントを用意、全角に合わせて描画の修正も行って日本語表示ができるようにする。
残りはひたすらアプリを作る。途中で標準関数を整備したり、ブート時にセクタを一気に読み出す高速化を行う。
これで1週目は終わりだけども、最後に書いてあるように「テキストエディタの実装」「コンパイラ、アセンブラ、リンカの移植」「 HariboteOS内でアプリを作成」そして最後の「OSのセルフホスト」と夢が広がった。次、2週目。
ポイント
- ファイルの読み出し
- 標準関数
- セクタをバッチで読む処理
13日目 - 2018/04/13
- Chapter 0 ~ 2
さて2週目。はじめにやっていたのはディスクイメージを直接バイナリで書いて作ることで、それは手間がかかるので次はアセンブラで書く。最初はDB命令しか使わないのでやっていることはバイナリでやったことと大きくは変わらない(Chapter1, 2を通してバイナリの内容は変わらないけど書き方としてという意味で)。Chapter2で本格的にアセンブラの命令を使う。いくつかの命令を動かすためにORG 0x7c00
でメモリ上の位置を指定したり、より簡潔に書くためにレジスタの説明と使い方、メモリから値を読む方法を学んでいく。文字列表示のためにBIOSプログラムの呼び出し命令INTについての説明もある。
次にこれまでフロッピーディスク1440KB分全てアセンブラで書いていたものを、ブートに必要な1セクタ512バイト分だけ書くようにし(これをIPLと呼ぶ)、残り(今はほとんど0が並ぶのみ)をディスクイメージ管理ツールでがっちゃんこするように修正。
最後にMakefileに軽く入門して終了。
2週目ということもあり最初に読んだときよりだいぶ理解が進んでいい感じ。
14日目 - 2018/04/14
- Chapter 3 ~ 4
2週目、だいぶ調子がいい。昨日はブートセクタにhello, worldの表示だけをさせていたが、ブートストラップらしく次のセクタを読み込む処理を入れる。やることはシリンダ、セクタ、ヘッド、(デバイス)それから読み出し先のメモリ番地を指定してBIOSを呼び出すだけ。読み出し先はメモリマップをもとに自由に決めていい。
ディスク読み出しをしたらブートの仕事はOSのエントリポイントにジャンプするだけ。OS本体を別のファイルに保存してコンパイルすると、そのファイルがイメージ内のどこに書き込まれるかをバイナリエディタで確認できるので、それがメモリ上のどこに展開されるかを計算しブートセクタからジャンプしてあげるようにすればブートは完了。
CPUははじめ16ビットモードで動いている。16ビットモードだと32ビットモード用のレジスタや命令が使えない。一方C言語をコンパイルすると基本的に32ビットモードで動くような機械語に翻訳されてしまう。つまりC言語で開発を進めるにはCPUを32ビットモードに切り替える必要がある。 ここで一つ困ったことにBIOSは16ビットモードのCPUでしか動作しない。よってBIOSを必要とする処理(キーボードの状態取得など)はCPUを32ビットモードに切り替える前に完了しておかなければいけない。ここではBIOS関連の処理としてハードウェアの情報をメモリ上に読んでおく。
切り替えが完了したらあとはCで書いていける。アセンブラでしかかけない部分(32ビットモード切り替え前)をasmhead.nas
に書き、それ以外をbootpack.c
に書いていく。そしてこの二つはコンパイル時にcat asmhead.bin bootpack.hrb > haribote.sys
というように単純に連結される。そのためasmhead.nas
の最後はbootpack:
というラベルで終わっている。切り替え後にアセンブラでしか書けないものはnaskfunc.nas
に書いてobjファイルに静的にリンクする。
この辺りそれぞれのツールの役割の理解がまだ曖昧な感じ。
Cで始めにすることはメモリへの書き込み。これは代入命令(MOVとかDBとか)をCでどう書くか、という話。最初はMOVするだけの関数をアセンブラで書いてCから呼ぶ。しかし実はこれを簡潔に、Cだけで書けるようにするのがポインタという機能である。p番地にaという値を書きたいときに*p = a;
だけでいい。なるほど便利。さらに驚きなのはp[0]
という表現は*p
の糖衣構文だし、p[i]
は*(p+i)
と等価である(同じ機械語)。これはもう配列に対する見方が全然変わってしまう。
ここまでくればあとはポインタといくつかのアセンブラ関数でVRAMがマッピングされたメモリ番地に適当なカラーを代入するだけで画面に模様や絵を書くことができる。
15日目 - 2018/04/15
- Chapter 5 ~ 6
この本で一番難しい部分、GDT/IDTに差し掛かる。どちらもCPUへの設定が書いてある表であり、メモリ上に展開されている。GDTはセグメントに関する情報で、これは物理メモリを切り分けたブロックに関する情報である。このセグメントのおかげでメモリに管理属性(アクセス制限やモード)をつけたり、セグメントを使う側が物理アドレスを知らなくていい状態(仮想アドレス)を実現できる。このGDTを表現したらあとはCPUの特定のレジスタにこの表の場所とサイズを書き込めば準備は完了。IDTは割り込みに関する表であり、割り込み時にどの関数を呼び出すかが書いてある。CPUへ設定する方法はGDTと同じ。メモリ上のどこにGDT/IDTを置くかは自由に決めていい。GDTのセグメント番号1にはメモリの全域、2にはbootpack領域が割り当てられる。 セグメンテーションは「メモリ(例えば4GB)の分割」という考え方で、ページングは「タスク毎にメモリ全域4GBが割り当たっているとする」という考え方。 ちなみにCPUがシステムモードで動いているか、ユーザモードで動いているかは実行しているコードがあるセグメントがシステム用かユーザ用かで判断している。
IDTの設定が完了したので次はPICを初期化して割り込みを有効にする。IMR(各割り込みの有効フラグ)、ICW(配線情報、マスタスレーブ接続情報)、ICW2(IRQと割り込み番号の対応) の各レジスタを設定する。次にICW2に設定した割り込み番号に対応するハンドラを書く。注意として割り込みハンドラの場合普通の関数と違いRET命令では返れないので割り込み専用のIRETDで返るアセンブラ関数でC関数をラップしてあげる。最後にこのアセンブラ関数をIDTに登録すれば割り込み時に呼び出されるようになる。
このほかC言語での構造体の書き方((*a).b
とa->b
が等しいことがわかればよさそう)や、コード分割とヘッダファイルの整備でコードを綺麗にする方法も学ぶ。
16日目 - 2018/04/16
- Chapter 7 ~ 8
今日はアルゴリズム中心の内容。FIFOをリングバッファで実装する。この利点は取り出しの際に配列要素を前にずらす処理をしなくてもいいところにある。マウスに関する設定は細々としたものが多いが、それが済んだらキーボードと同じ処理で入力を受け取れるようにする。最後に入力に合わせてマウスが動くようにすれば完成。やはりマウスが動くというのはかなり気持ちがいいものである。
Chapter8の残りはChapter3でやった32ビットモードへの切り替えの際のアセンブラの解説。ここはちょっと難しいので手順だけを箇条書きしておく。
- 割り込みを禁止にしてキーボードの設定を変更。メモリを1MB以上使えるようにする。
- 適当なGDTを作成し、CR0レジスタのフラグを変更。「ページングなしのプロテクトモード」で動くようにする。プロテクトモードはセグメントを使うためGDTの設定が必要。パイプラインをフラッシュするためにジャンプ命令を入れる。
- ブートセクタ、読み出した残りのディスクデータ、そしてOS本体をあらかじめ設計しておいたメモリマップに合わせてmemcpyする。
- OSファイルのヘッダーを解析して必要な位置にデータをコピーする。
最後のポイントとしてブートの際はハードウェアが割り込みを溜め込まないようにできるだけ早く割り込みを受け付けられるようにするべきであるとのこと。
17日目 - 2018/04/17
- Chapter 9 ~ 10
気がついたら30日も半分を折り返していた。Chapter9の内容はメモリ管理。はじめにメモリの容量を調べる。「書き込み→上書き→読み出し」で期待した値と比べてあっていたら次のメモリをチェックする、という方法をとる。32ビットモードに移行しているのでBIOSに聞くことができない、というのが主な理由だがなぜこれでうまくいくのか、なんでこんな回りくどい方法なのかよくわからない。この際キャッシュメモリが邪魔になることがあるので無効にしておく。(このチェックをCで書くとコンパイラの最適化により期待した機械語が得られないのでアセンブラで書く。)
メモリ管理は管理マネージャが空き情報構造体を配列で持つ形で行う。配列はフラグで管理する方法と比べて場所を取らないし処理が速いが、取得の際の要素を前にずらす処理や解放の際の左右のブロックと結合する処理が必要になってやや複雑さが増す。メモリは1バイト単位でも管理することができるが、それだとフラグメントがたくさんできてしまい取れる連続した領域が小さくなってしまうため、4KB単位で管理することにする。
Chapter10の大部分は重ね合わせと移動の描画だけどもあまり興味がないので割愛。
18日目 - 2018/04/18
- Chapter 11 ~ 12
はじめにウィンドウの表示。差分更新などでチラツキがなくなるよう描画処理を最適化する。
今回の目玉はタイマーの設置。タイマのハードウェア設定(割り込み間隔など)をして有効化すれば一定時間の経過を割り込みとして受け取ることができる。ハンドラ内で変数をインクリメントすればカウンタの実装になるし、タイマ構造体を定義して一定時間後に構造体のfifoに(構造体に設定した)データをputするようにハンドラを更新すればタイムアウト機能を実装することができる。これでカーソルの点滅などの機能が実装できる。さらに割り込み処理が最短で済むように最適化して今日は終わり。
19日目 - 2018/04/19
- Chapter 13 ~ 14
昨日に続きタイマ。カウンタでベンチマークを取れるようにし、性能の改善をしていく。はじめに割り込みの種類毎にあるFIFOを一つにまとめる。それぞれの入力(キーボードなら文字毎)で書き込まれるデータを定義してしまえば割り込みの種類とその値を特定することができる。これによりそれぞれのFIFOを見なくてもいいので高速化される。次にタイマの配列をリストで管理することでずらしを無くして改善、さらに番兵を置いて分岐をシンプルにしてさらに改善。
明日のタスク管理に備えて残りは画面の高解像度化と文字の出力、マウスを使ったウィンドウの移動をやって終わり。
頭では理解している(つもり)だから書き写すだけだとちょっと淡々としていていまいち身についてない感じがする。画面やマウスの試行錯誤だったりパフォーマンスの改善は実際に自分で考えて実験してみる経験をしないと本当の意味で学べなさそう。。
20日目 - 2018/04/20
- Chapter 15
今日(昨日)は天気が良すぎたので公園で寝そべっていたら1章しか進めなかった。
15日目はマルチタスクの導入。切り替え間隔は100HzとかCPU時間の1%以下とかにするのが基本らしい。マルチタスク機能はCPUにサポートされており、具体的にはGDTにTSS(Task Status Segment、タスク状態セグメント)を登録すればいい。TSSの中身は主にレジスタで、タスクスイッチの際のレジスタの退避先になる。大事なのはEIP(Extend Instruction Pointer)レジスタで、次に実行が始まった時に開始する命令の番地を書き込む先になる。(ちなみにJMP命令はEIPへの代入命令であり次の実行番地を更新する処理である。)
タスクスイッチの際、通常のJMP命令ではセグメントを跨いでジャンプすることができないため、EIPとCS(Code Segment)レジスタを同時に切り替えるfar-JMP命令を使わないといけない。CPU側ではfar-JMP命令の際、受け取ったCSがTSSだった場合(タスクスイッチの時は切り替え先TSSの先頭番地)にタスクスイッチと判断して通常とは違う(たぶんレジスタの切り替えとかも行う)JMP命令を実行する。またこのとき受け取ったEIPの値は無視される。
タスクスイッチの際TRレジスタという、現在実行しているタスクを格納しておくレジスタの更新も行う。TSS設定の際にエントリポイントとなる関数の番地を取得してEIPに代入したり、切り替え先用のスタックを確保してESPに代入する方法も押さえておく。
タスクBからタスクAに戻ったり、0.02秒毎にタスクを切り替えたりする。またスタックを使ってタスクBに引数を渡す方法も学ぶ。これはC言語の仕様である、引数がESP+4に置かれることを利用している。ちなみにESPの位置には関数の戻り先がpushされており、ここに適切な値をいれておけばタスクのエントリポイントでreturnしたときに思い通りの場所に帰ってこれるようになる。
最後にタイマ割り込みハンドラ内でタスクスイッチを勝手に行うことでよりOSらしいマルチタスクを実現する。このときタイマのセットや割り込みの処理はハンドラ内で行うのでfifoを使わなくても「この割り込みはタスクスイッチだな」と判断することができる。
実はまとめは次の日にやったほうが定着度がよさそうだな...と感じつつおわり。
21日目 - 2018/04/21
- Chapter 16 ~ 18
昨日の残りも含めて3つ分。20日目に感じたことを鑑みて今日の内容については復習を兼ねて明日書こうと思う。
マルチタスクの続き。3つ以上のタスクを手軽に動かせるようにタスク配列を管理するTASKCHL構造体を導入。セグメント番号のことをセレクタというらしいので覚えておいたほうがよさそう。task_initを改造してTASKCTL構造体の初期化。ここが面白いのだけれどもtask_initにより、この関数の呼び出し元も0番目のタスクとして管理されることになるので自身を表すタスク構造体が戻り値として返ることになる。
タスクAは割り込みの処理がメインで、それ以外の時間はhltしているだけのためCPU時間を使うのが勿体無い。sleepを導入してhltの間タスクBを動かせるようにする。sleep関数内では呼び出したタスク自身がsleepに入るときに切り替え先のタスクにfarJMPするのがポイント。割り込みハンドラ内でFIFOに値をputした時にsleepタスクを起こす処理も忘れずに追加する。
次に優先度の導入。優先度の値がそのままタイマにセットする時間になる(例えば優先度2なら0.02秒ごとに切り替わる)ので、優先度に応じてCPU時間が割当たるようになる。ここで問題なのは優先度が同じ場合どちらが実行されるかは運次第になってしまうため、マウス操作と音楽再生のように優先度が高いものがぶつかったときにユーザビリティが下がってしまう。これを解決するためにタスクレベルというのを導入して、同じレベル内でのみタスクスイッチをかけるように変更する。よってオブジェクトの関連としてはTASKCTL構造体が複数のタスクレベルを管理し、それぞれのタスクレベルが複数のタスクを保持するという形になる。最後に一番下の優先度にhltするだけのアイドルタスクを番兵としておくことで処理をシンプルにしてマルチタスクは完成。
残りの時間はコンソールを作る作業。コンソールができればなんでもできる気がするしワクワクする。といってもやることは淡々としており、表示の切り替え、文字の入力、FIFOを使ったシグナル(みたいなもの)の送信、Enterの対応、コマンドの作成と進んで行く。mem,cls,dirのコマンドを作成して昨日は終了。
22日目 - 2018/04/22
- Chapter 19 ~ 20
最初はtypeコマンド(Unixのcat)の作成。ファイルの中身がディスク上のどこに存在するかはFILEINFO構造体のクラスタ番号から計算できるのでそこから読み出す。このままだとセクタ毎にデータを読んでしまい最悪他のファイルを読み出し始めてしまうので、FAT(file allocation table)情報を利用して続きの内容がどのセクタに収められているかを調べてたどるようにする。FATには総クラスタ個数の要素が並んでいて、それぞれの要素には次の内容が入っているクラスタ番号が書かれたリレー方式となっている。FAT自体はディスクのフォーマット(FAT形式のディスクならセクタ番号××にFAT情報がある、みたいな)。多分。
章の最後に読み出した内容をset_segment
でセグメント化、その先頭番地にfarjmp
することでアプリを実行する処理を実装する。Unixでいうexecの処理。
アプリの実行ができたのでついにAPIの実装に入る。やりたいことは実行したアプリケーションからcons_putchar
関数を呼び出せるようにし、アプリからコンソールに文字を出力する。ここでAPI関数へのアセンブラ命令CALLを使うが、JMP命令との違いはスタックにレジスタをPUSHするかどうかのみである。これによりCALL先でRETした際に呼び出し元に帰ってこれるようになる。実際にはアプリケーションとコンソールのセグメントが異なるのでただのCALLではなくセグメントを跨ぐfarCALLを使う。同様にCALL先から戻るときもfarRETであるRETFを使ってセグメントレジスタの更新も行う。実際にはAPI利用側からC関数を直接呼ぶのではなく、アセンブラのラッパー関数を呼ぶようにする。これはC言語の仕様として引数はスタックを使って受け渡しをするため、アセンブラからC関数を呼び出すときも引数をレジスタからスタックに移しておく必要があるためである。最後にアプリケーションの終了に対応するため(アプリ終了後にコンソールに帰ってこれるように)コンソールはアプリをfarJMPではなくfarCALLで呼び出し、アプリも終了後にRETFで呼び出し元に帰るようにする。
このままだとOSを書きなおすたびにAPIであるアセンブラ関数のメモリ番地を調べてアプリを書き直さなければならず、相当不便である。そこで「OSに関数を登録しておく仕組み」である割り込みにアセンブラ関数を登録する。これによって固定の割り込み番号でINTを呼び出せば登録したアセンブラ関数が呼び出されるようになる。アプリを書き換えなくていい。すごい。ちなみにINTで呼ばれた関数から呼び出し元に帰るときはRETFではなくIRETDを使わなければいけないのと、割り込み処理で呼び出し元のレジスタが書き換わると不便なので最初にPUSHAD、最後にPOPADを呼んでレジスタの内容をスタックに退避するようアセンブラ関数も修正。
今はAPI関数毎に割り込みを定義しているがそれだとIDTの数が足りなくなってしまうので、どのAPIを呼ぶかを引数にして一つのINT命令ですべてのAPIが呼べるように修正する。対応するアセンブラ関数ではPUSHADによりレジスタを退避しているのでC関数では全てのレジスタの中身を受け取ることになる。APIの種類に応じて別個のC関数を引数が入っているはずのレジスタを引数に呼び出せばよい。
23日目 - 2018/04/23
- Chapter 21 ~ 22
まずはC言語からAPIを使えるように引数をスタックからレジスタに移してINT 0x40
をするアセンブラ関数を定義。
今日の本題はOS保護の話。セグメントレジスタの退避や更新をアセンブラで書く部分が難しい。はじめにアプリに専用のデータセグメントを設定し、それ以外のセグメント(例えばOS)にアクセスできないようにする。OSとアプリの間でセグメントを行ったり来たりするので頭が混乱するところだが、流れは 1) アプリのスタック上にある関数への引数をOSのスタックにコピー 2) セグメントをOSに切り替える 3) システムで動作する関数のコール 4) アプリ用レジスタの復元 という感じ。一般保護例外用の割り込みハンドラを定義して例外をキャッチできるようにする。
今の設定だとアプリがセグメントレジスタを勝手にいじれてしまうので、セグメントのアクセス権を設定してこれを防ぐ。この設定をするとOSがアプリをfarCALLで起動することができなくなってしまうので(理由は謎らしい)アプリからCALLされた状態になるようスタックを操作し、RETFによりアプリに返るふりをして起動する。これによりアプリ終了時にRETFで返ることはできなくなるので終了APIを用意する。スタックの切り替えはCPUが勝手にやってくれるようになるのでAPIのコールや割り込み処理は簡潔になる。割り込み命令はアプリのコードセグメントが設定されていると使えなくなってしまうので、API割り込みだけはアプリから呼べるようIDTに設定する際にフラグを立てる。このCPUの保護機能によりアプリが使える命令の多くは制限がかかり、アプリからOSを呼び出す方法は実質INT 0x40
のAPIコール割り込みに限られる。
いくつかの例外割り込みハンドラを設定して問題が起こった時に原因を表示できるようにする。さらにアプリの強制終了をbootpack側で実装。これはアプリ起動はコンソールのFIFOに入力を入れても反応しないため。最後に実行ファイル.hrb
のフォーマットの解説と、アプリ起動時にフォーマットを読んでデータをデータセグメントにコピーする処理を追加。
いくつかのAPIを定義してウィンドウを表示できるようになったら今日は終わり。
24日目 - 2018/04/24
- Chapter 23 ~ 24
今日はひたすらAPIとそれを使ったアプリを作る。新しいことはそんなにないので割愛。
ウィンドウの移動とタスクの切り替えがあり割と大掛かりな変更なのに加えてあまりエキサイティングな作業とは言えないけど、ウィンドウ操作が洗練されるとぐっとOSっぽく見えるなという印象。
25日目 - 2018/04/25
- Chapter 25 ~ 26
今日はひたすらOSらしい動作の追求。コンソールをいくらでも出せるようにしたり画面移動を高速化したり新しいコンソールでアプリを起動と、コンソールなしでアプリを起動を追加したり。新しい内容が出てくるわけではないので簡潔におわり。
26日目 - 2018/04/26
- Chapter 27 ~ 28
今の状態ではアプリはOSのセグメントにアクセスすることはできないが、他のアプリのセグメントにアクセスすることは可能でありこれを防がなくてはいけない。GDTと同様にLDT(Local Descriptor Table)という仕組みを利用する。これまではアプリのセグメントもGDTに登録していたがこれをLDTに登録するとタスクスイッチの際にLDTRレジスタが更新され他のLDTセグメントへのアクセスができなくなる。
次に今ではAPIは全てa_nask.nas
ファイルに記述されているためAPIの一部しか使わない場合でも全てインクルードされている。APIをそれぞれのファイルに分割し必要なものだけリンクすることでアプリサイズを小さくする。しかしこれだとどのアプリがどのAPIを利用するかをいちいち調べてビルドしなければいけないため不便。そこで登場するのがライブラリである。ライブラリはオブジェクトファイルをまとめたものであり、ビルド時にこれ一つ指定すれば必要な関数だけがリンクされるという便利ツールである。合わせてAPIのヘッダファイルも作ることでMakefileおよびC言語アプリの先頭がかなりすっきりする。
Chapter28のはじめにC言語の仕様で呼び出される__alloca
関数を定義してスタックの動的拡張に対応する。これによりこれまでmallocで回避していたような巨大なローカル変数を扱える。__alloca
の実装の際ESPを変更するためRETでは返れなくなるので注意。スタック上にある呼び出し元へ直接JMPすることで解決する。
ファイル取得機能はすでにtypeコマンドを作っているのでそんなに難しくはない。また日本語表示とシフトJIS・EUCの実装があるけどもそんなに興味もないので割愛。
27日目 - 2018/04/27
- Chapter 29 ~ 31
最終日、と言ってもやることはアプリを作ることなのでOSには直接関係ないから終わり。とてもいい本だった。特にChapter31に書かれている以下の部分は響いた。金言としてこれからのプログラマ人生におけるモットーにするまである。
...最初からOSを作ろうと思わないこと、これはかなり重要です。それに加えて、気に入らないところはあとで直せばいいや、と思うことです。なんならあとで全部作り直したっていいんですよ。最初から完璧にしようと思うと、本当にまったく前に進めなくなります。
明日からは実際に何か動くものを作っていこうと思う。楽しみ。
1~3月振り返り
やったこと
受講したオンラインコース
読んだ本
- オブジェクト指向における再利用のためのデザインパターン
- 増補改訂版Java言語で学ぶデザインパターン入門
- UNIXネットワークプログラミング〈Vol.1〉(まだ途中)
- ITエンジニアのための機械学習理論入門
- ゼロから作るDeep Learning
- 詳解 ディープラーニング TensorFlow・Kerasによる時系列データ処理
- 深層学習 (機械学習プロフェッショナルシリーズ)
- Python Machine Learning: Machine Learning and Deep Learning with Python, scikit-learn, and TensorFlow, 2nd Edition
まとめたブログ
- kerasを使ったreuter記事分類のexampleをなぞる
- Seq2Seqを使った英日翻訳機
- GANについて
- 強化学習について
- その他公開に至らなかった記事いくつか
そもそもはデザインパターンを学ぶこと、UNIXネットワークプログラミングの本を読むことが目標でしたがDL4USのコースに当選してしまったため予定を変更して機械学習をがっつりやることになりました。最初の2週間で用意していたデザインパターンの本を両方読み切ってしまった、しかしながらデザインパターンが身についた感覚がないのでデザインパターンに関しては目標未達です。もっとリソースを集めること、アウトプットをすること、それからオブジェクト指向・設計論含めて学ぶことが必要かなという気がしているので再挑戦します。前述の理由でUNIXネットワークプログラミングは第一部を2周、第二部を1周することしかできませんでした。TCPの基礎的な事項(3ウェイハンドシェイクなど)の理解を深められた一方でまだOSの理解が足りないかもという不安を感じたので、OSを学んだのち改めて読み直したいと思います。機械学習は一旦終了です。次回は未定です。
良かった点・反省点・改善点
- オンラインコース
- オンラインコースを受講することで週サイクルが強制されたため、リズムよく学習することができました。Courseraのコースの負荷がそんなに高くなかったというのがうまくいったポイントの一つだと感じているので、実力に丁度あったコースを見つけるのは簡単ではなさそうですが、一つの方法として非常に使えると思いました。
- アウトプット
- 最後にアウトプットの期間を設けることでインプットした知識を効果的に定着させることができそうです。今回たまたま最後の一週間することがない状態になってしまったのでいくつか文章を残す形でアウトプットをしましたがこれは続けていきたいです。余力があればミートアップでトークすることも挑戦してみたいです。
- 振り返りと準備期間
- これもたまたま時間が余ったので気が付いたことですが、振り返りと次への準備の時間をとれたのはとても良かったです。試してみて良かった方法は武器の一つとして保存しておき、反省点・改善点を洗い出すことでさらに効率よく学習が進められそうです。改善点を元に準備を整える時間までとれればなおいいですね。プロジェクトの方法論を独学にも当てはめている感覚ですが、そもそも一人でできないことがチームでできるわけがないという考え方をすればこれは逆で「独学の方法論がプロジェクトにも適用できる」という仮説を持って続けていきたいです。
- リソースを十分用意する
- 毎度のことながら振り返ってみると読んだ本の冊数が意外に少ないという印象を抱きます。必ずしもすべて読む・計画して読む、必要は全くないので、気になった本はとにかく手元に置いて、学習の途中で気になったら都度ちらちら読む方法をとることでインプットをさらに多角化し、一つのトピックに関する理解を深めることができるんじゃないかと思います。
setコマンドについて
-e
オプション
実行したコマンドが失敗したときにシェルスクリプトの実行を終了する。
-x
オプション
実行するコマンドを出力する。
例
foobar:~ foobar$ echo $HOME + echo /Users/foobar /Users/foobar ++ printf '\e]7;%s\a' file://foobar.local/Users/foobar
Refs
ssh-keygen: command not foundのとき
$ sudo apt-get install -y openssh-server
で一緒にインストールされる。