いものやま。

雑多な知識の寄せ集め

『人工知能のための哲学塾・東洋編 第弐夜「井筒俊彦と内面の人工知能」生中継』を観てみた。

以前、三宅先生の書いた『人工知能のための哲学塾』を読んだ感想を書いた。

その続きということで、東洋編が現在進行形で展開されている:

上に挙げた通り、ニコ生でタイムシフト視聴出来るので、両方とも観てみた。
で、第弐夜に関してちょっと思うことがあったので、そのことを。

ちなみに、第弐夜のスライドは、SlideShareで見ることが出来るので、そちらから。
めっちゃ枚数あるけどw

意識に関する2つの図

さて、自分が話題にしたいのは、次の2つの図。

まず1つ目は、西洋型のボトムアップ機能モデルの図:

f:id:yamaimo0625:20170711220557p:plain

※いくつかの図をまとめて整理してある

そして2つ目は、東洋型のトップダウン存在モデルの図:

f:id:yamaimo0625:20170711224013p:plain

※スライドだと、矢印の向きが上から下に降りてきてるけど、あとで説明する通り、下から上への矢印が正しいと思われる

この2つの図は、形こそ似ているものの、実際には全くの別物。
というか、2つ目の方は上下を逆にした方がいいw

実際、三宅先生はあとでこの2つの図を融合させようとしているんだけど、何を表そうとしているのかが正直分からない(^^;
実のところ、この2つの図を融合させるには、この2つの図がそれぞれまったく別のレベルで書かれていることに気づかないといけない。

クラス図とオブジェクト図

このことに関して、自分は次のようなツイートをした:

そう、クラス図とオブジェクト図というのが、ここでは重要な鍵になってくる。

まず、クラス図というのは、簡単にいうと、「モノ」(オブジェクト)の設計図。
「モノ」がどんな「種類」(クラス)のモノで出来ているかを示してくれる。

例えば、車の(すごく簡単な)クラス図を描いてみると、次のような感じ:

f:id:yamaimo0625:20170711225905p:plain

車体があって、ハンドルがあって、アクセルペダルとブレーキペダルがあって、エンジンとブレーキがあって、タイヤがある、というような感じ。

重要なのは、「どんな種類のモノ」によって出来ているかを示していて、「どんなモノ」によって出来ているかを示しているわけではない、ということ。
なので、車にタイヤは4つついているけど、図には「タイヤ」という種類(クラス)は1つしか出てきていない。
(※厳密には、「多重度」というのを使って、そのクラスのオブジェクトが何個あるのかを示したりするけど、ここでは省略)

一方、オブジェクト図というのは、クラス図を元に実際に作られる「モノ」(オブジェクト)自体を描いた図。

例えば、車の(すごく簡単な)オブジェクト図は、次のような感じ:

f:id:yamaimo0625:20170711231430p:plain

ポイントは、クラス図では「タイヤ」は図に1つしか出てこないけど、オブジェクト図では「タイヤ」が図にちゃんと4つ出てきてるということ。

ここがクラス図とオブジェクト図との大きな違いで、

  • クラス図は「種類」(クラス)のつながりを描くので、ある「種類」のモノは図に1つしか出てこない
  • オブジェクト図は「モノ」(オブジェクト)のつながりを描くので、同じ「種類」のモノが図にいくつも出てくる

となっている。

ちなみに、これはあくまで1つの車について描いたオブジェクト図で、複数の車について描いたオブジェクト図というのも考えることが出来る。
その場合、以下のようになる:

f:id:yamaimo0625:20170711233717p:plain

2つの図の違い

さて、元の2つの図について、改めて考えてみる。

この2つの図はどちらも層になっていて、下から上へ積み上がっているように見える。
なので、とても似ている。

ただ、前にも述べた通り、後者の図は実際には逆さまなので、次のようになっている。

f:id:yamaimo0625:20170711234820p:plain

この形だからこそ、「トップダウン型」なわけ。

そして、それ以上に重要なこと。
それは、前者がクラス図であるのに対し、後者がオブジェクト図だということ。

後者の図をちゃんと描くと、次のようになる:

f:id:yamaimo0625:20170712000237p:plain

一番下にあるのは、分割される前の「あるがままの世界」。
この世界を「そのまま」見ることが出来るのは(そして、それはつまり「何も」見てないということになるのだけど)、(あえて名前をつければ)神様だけとなる。
「絶対一者性の領域」というのは、つまりそういうこと。
客観的世界、とも言えるけど、その「客観」というのは科学の「客観」をはるかに超えたもので(「観測」されてしまったら、それはもう「客観」ではない!)、観測される以前の、可能性の海が広がっていて、全てがそこにあるけど、何者でもないというような、そんな世界。

その上にあるのが、生物として知覚される「知覚の世界」。
この世界は、生物がその身体をもって知覚することで、「あるがままの世界」からその生物が知覚できる要素が切り出された世界。
ポイントは、生物のその身体的特徴によって、ここで切り出される世界というのは生物の種類ごとに異なってくるということ。
なので、オブジェクトは複数存在している。
しかし、それはまだ「1種類の生物としての世界」であって、「1個体の生物の世界」というところまでは分割されていない。
それがゆえ、「潜在的分節化の領域」となる。

そして、そのさらに上にあるのが、言葉や文化、あるいはその人の経験などから意味をもって切り出された「意識の世界」。
知覚された世界にさらに「意味」ーー例えば「これはお父さん」とか「このりんごは美味しそう」だとかーーが与えられた、主観的な世界になっている。
このレベルになると、各個体ごとにその世界は異なってくるので、オブジェクトは各個体分(多重人格なら、複数もありえる?)存在している。
なので、「存在的多者の領域」となる。

なんで東洋哲学でこんな図を考えるのかというと、一言でいえば、「主観=客観」の先入観を取り除くため。

普通だと、自分が見ている世界(主観的世界)が世界そのもの(客観的世界)であり、他の誰もが自分と同じように世界を見ているものだと思ってしまう。
自分の見ていること、感じていること、考えていること、信じていることが、世界の真理、すべてであり、それ以外に別の世界(のインスタンス)が存在するだなんて、思いもしない。
例えば、自分が「ツラい」と思っていることは、世界においても「ツラい」という「客観的真理」であり、それが覆ることはない、と思ってしまう。

でも、そうやって「客観的真理」だと思っていたことが、実は「主観的真理」に過ぎないよね、と。
人間は言葉によって、あるいは、様々な関係性において、世界を切り分け、「それ」を「それ」として認識しているに過ぎないでしょ、というのが、仏教や道家の教えるところ。
言葉によってレッテルを貼ったり、固定した考えに捉われてしまったりするのではなく、それらは自分がそのように世界を切り出しているだけだと気付き(悟り)、そこから自由になったらより善く生きていけるでしょ、と。

飲茶さんの『史上最強の哲学入門 東洋の哲人たち』に書かれている次の内容が面白い。

 このことを理解するために、ちょっとこんな想像をしてみてほしい。
 あなたが「耳を見て興奮を感じる文化の国」に生まれたとする。 その国には「女性はみんな生まれたときからずっと耳を隠して暮らしており、本当に愛した男性にしか耳を見せない」という奇妙な風習があった。 あなたが男性だと仮定してそういう国で子供の頃から育ったとしたら・・・間違いなく、あなたを含めた国中の男たちはみんな「うおおおぉ、女の子の耳が見てええ!」と思うはずである。
(中略)
 そして、あなた自身、「耳」のことを考えては下劣な情欲に燃え上がり、そのことについて自己嫌悪に陥って苦しみもだえていた。
(中略)
 そして、ついには思考が途切れ、分別が消え去ったその瞬間ーー。 それはたった一瞬の隙間。 だが、その一瞬のなかに「智慧」が現われ、ひとつの奇跡がおとずれる。 あなたは、赤子のような無垢な境地で、知識ではなく、論理ではなく、言葉ではなく、「いま起きていることの本質」を実感として、体感として理解する。
 あなたはついに究極の真理を悟った。

「こ れ は た だ の み み だ」

「うわああああ!」と、あなたは頭を抱えて、へたりこむ。
(中略)
 あなたは、今まで自分が必死になってきたことの、あまりの馬鹿馬鹿しさと恥ずかしさに笑うしかなかった。
 夜が明け、あなたが街に下りてみると世界がすっかり変わって見えた。 物心がついたときから、まとわりついていた鎖が外れたような、晴れ晴れとした自由な気分。 そんな幸福感とともに世界を見ることができた。
(『史上最強の哲学入門 東洋の哲人たち』飲茶、著より引用)

全く余談だけど、この本はめっちゃ面白いしすごく読みやすいので、オススメ。
西洋編もあって(というか、西洋編が先に出ている)、そっちも面白いので、やはりオススメ。

閑話休題

そんなわけで、言葉や思考(思い込み)から解放され、より自由になろう、と。
さらにいえば、環世界の話があるように、生物が見ている世界というのは、その身体(インタフェース)を通して見られた、各生物ごとの世界でしかない。
なので、さらには身体まで捨て、そのさらなる奥の根源、あえて名前をつけて(本質的に名前がつけられない)「空」や「道」に至ろう、と。
これが図でいう「上昇過程」。
ニコ生でも話題に上がってた、十牛図の8枚目の絵に至るまで過程になっている。

でも、ここで勘違いしてしまってはいけなくて。

心頭滅却すれば火もまた涼し」なんていうけど、熱いものは熱い。
心の持ちようでどうとでもなるでしょ、という話ではないことに気をつけないといけない。
「熱くないんだ、熱くないんだ」と必死に否定するのでも、「心を無にすれば熱くないんだ」と冷静に否定するのでもなく、「あ、自分は今『熱い』と感じているんだ」と気づいてそれを認めることが重要。

色即是空のあとに空即是色と繋がるように、あるいは、十牛図の9枚目、10枚目で再び戻ってくるように、「主観≠客観」なんだと気づいた後で、その主観を否定するのではなく、その主観を「主観である」と肯定しないといけない。
これが図でいう「下降過程」。
なんでそうしないといけないかというと、だって、何事からも解放され、自由になろうとしたって、実際には自分たちは身体があり、その身体を通してでしか世界と交われないのだから。
そんなに身体が嫌で嫌で仕方ない「身体の軽蔑者」は、ニーチェがいうように、さっさと身体を捨て去ってこの世からいなくなってしまえばいい。

 身体を軽蔑する者に、わたしはわたしの言葉を言いたい。 かれらが考えなおし、説をあらためることなどは、わたしは求めるところでない。 かれらはさっさと自分の身体に別れをつげて、ーー口をきかなくなってもらいたいものだ。
(『ツァラトゥストラはこう言った』「身体の軽蔑者」より引用)

ニーチェは「三段の変化」を説いたわけだけど、これはこの「上昇過程」「下降過程」というのにとても似ている。
盲目的な信者(駱駝)からあらゆる価値を否定し壊していき(獅子)、しかし最後には肯定して世界を創造する(幼子)。

 精神はかつては「汝なすべし」を自分の最も神聖なものとして愛した。 いま精神はこの最も神聖なものも、妄想と恣意の産物にすぎぬと見ざるをえない。 こうしてかれはその愛していたものからの自由を奪取するにいたる。 この奪取のために獅子が必要なのである。
(中略)
 そうだ、創造の遊戯のためには、わが兄弟たちよ、聖なる肯定が必要なのだ。 ここに精神は自分の意志を意志する。 世界を失っていた者は自分の世界を獲得する。
(『ツァラトゥストラはこう言った』「三段の変化」より引用)

あるいは、客観というものを一度忘れ(エポケー)、主観から世界を構築していき、そして身体性へと還ってきた現象学に似ているとも言える。

2つの図を融合させる

東洋哲学の話が長くなってしまったけど、元の2つの図の話に。

このように、後者の図はオブジェクト図になっているというのが重要なこと。
なので、クラス図の前者とそのまま融合させようとしたって、レベルが違うのでうまく組み合わさらない。

じゃあ、どうすればいいかというと、これは簡単で、そう、前者もオブジェクト図にしてしまえばいい。
これがツイートで書いていた「オブジェクト図で考えればいいのに」という発言の意図。

生物の種類だけ環世界は存在し、そして個体の数だけ主観的世界は存在するわけだから、次のようになる:

f:id:yamaimo0625:20170712004921p:plain

こうやって見ると、2つの図がキレイに融合しているのが分かると思う。

西洋型の図で見ていたのは、1個の個体に関するボトムアップの図。
世界から環世界へ、環世界から主観的世界へと情報はインプットされ、主観的世界から環世界へ、環世界から世界へと作用がアウトプットされていくことを示している。

一方、東洋型の図で見ていたのは、複数の個体に関するトップダウンの図。
主観的世界から言葉や思考を取り去って環世界へ至り、環世界からさらに身体を取り去って世界に至る。
逆に、世界から生物の種類だけ環世界が生まれ、さらに環世界から個体の数だけ主観的世界が生まれる。
そうやって、各々に異なった主観的世界が生まれてきているんだということを示している。

人工知能は「悟る」のか?

さて、こうして融合された図から、人工知能のエンジニアリングに活かせることは何か。

一つは、オブジェクトの構成のさせ方。

具体的な構成の議論は省くけど、上の図に書いたようなオブジェクトが生成されれば、各個体ごとが異なる「自分の世界」を持つことになるので、そこから生成される行動は、各個体ごとに個性的(けど、「身体」のレイヤーがあるので、そこからは大きく外れられない)ものとなると考えられる。

もう一つは、人工知能に「悟り」を与えるということ。

これは、普通の「悟り」とはちょっと違うけど、「この見ている世界は一つの世界の見方にすぎないんだ」という気づきから、意識レイヤーの内容を固定したままにせず、柔軟に組み替えていくようなアルゴリズムになる。
人工知能が、自分自身の世界の見方を柔軟に変化させていく。
これが出来れば、決まりきった行動をするのではなく、ダイナミックに行動が変化する人工知能が生まれてきそう。

今日はここまで!

人工知能のための哲学塾

人工知能のための哲学塾

史上最強の哲学入門 (河出文庫)

史上最強の哲学入門 (河出文庫)

ツァラトゥストラはこう言った 上 (岩波文庫 青 639-2)

ツァラトゥストラはこう言った 上 (岩波文庫 青 639-2)

『OS自作入門』を読んでみた。(その6)

前回はリンカとリンカスクリプトを使ってアドレスの問題を解決した。

これで最初のOSっぽいプログラムは一応完了で、次はちゃんとフロッピーディスクの内容をメモリに読み込んで実行するIPL(Initial Program Loader)を作っていくことになる。
ただ、その前にもう一仕事。

フロッピーディスクのイメージ作成の改善

ここまではフロッピーディスクのイメージをまるごと作っていたわけだけど、この先はファイルも増えてくるし、ブートセクタに書き込むプログラム(IPL)とOS本体をちゃんと分けておきたいので、フロッピーディスクのイメージ作成の改善を。

具体的には、IPLとOS本体はそれぞれ別のリンク単位にして、ELFファイルおよびバイナリファイルをそれぞれ作成し、IPLはブートセクタとして、そしてOS本体はフロッピーディスクの最初のファイルとして、ディスクイメージに配置するようにしていきたい。

本ではここでedimgという著者お手製のツールを使っているんだけど、これはWindows用のプログラム。
Windows用だからそもそもMacだと動かないというのはあるんだけど、それを抜きにしても、なんともよく分からないツールを使うというのはあまり気が進まない。
(ネットで検索しても情報が出てこないから)
それしか方法がないのなら仕方ないけど、よく使われている標準的なツールがすでにあるなら、そっちを使った方がいい。

ちなみに、Macの場合、hdiutilというコマンドラインツールが標準としてあるみたい。
少し触ってみたけど、使えなくはなさそうな感じ。

ただ、問題点として、当然Macでしか使えないのでUnixではダメだし、もう少し重い問題として、ディスクイメージを操作するためにマウントすると、隠しファイルがコソッと作られてしまうというのがあった。
これから作ろうとしているIPLは、OS本体が一番先頭のファイルとして置かれていることが前提となっているので、これだとダメ。
もちろん、ちゃんとFATファイルシステムを扱えるようにIPLを作ればいいんだけど・・・それだと本よりかなり頑張らないといけなくなる。

そんなわけで、どうしたものかと調べていたんだけど、どうやらGNUのmtoolsに含まれるmformatおよびmcopyというツールを使ってやるといいみたいだった。

Mtools - GNU Project - Free Software Foundation

mtoolsはUnix環境でMS-DOSファイルシステムを扱うためのユーティリティで、mformatを使うとディスクイメージの作成が、そしてmcopyを使うとそのディスクイメージへのファイルのコピーが出来る。
(他にもいろいろツールが入ってる)

このmtoolsは多くのLinuxディストリビューションなら最初から入っているみたい。
なので、Linuxなら特に何もせず使えると思う。

ただ、Macには入っていないので、ビルドするところから。

mtoolsのビルド

毎度おなじみ、tarボールを落としてきて、展開。

$ tar zxvf mtools-4.0.18.tar.gz
$ cd mtools-4.0.18

で、あとは./configureしてmakeといきたいところだったんだけど、ちょっと問題があって、修正が必要だった。

そのままビルドすると、以下のようなエラーが出てしまう:

gcc  -DHAVE_CONFIG_H -DSYSCONFDIR=\"/usr/local/etc\" -DCPU_i386 -DVENDOR_apple -DOS_darwin16_6_0 -DOS_darwin16 -DOS_darwin -g -O2 -Wall -fno-strict-aliasing -I.  -I.  -c mainloop.c
mainloop.c:89:15: error: expected ')'
int unix_loop(UNUSED(Stream_t *Stream), MainParam_t *mp, char *arg,
              ^
./sysincludes.h:106:47: note: expanded from macro 'UNUSED'
#  define UNUSED(x) x __attribute__ ((unused));x

そこで、ネットで調べてmainloop.cを以下のように修正:

--- mainloop.org.c
+++ mainloop.c
@@ -86,7 +86,7 @@
 }

 int unix_dir_loop(Stream_t *Stream, MainParam_t *mp); 
-int unix_loop(UNUSED(Stream_t *Stream), MainParam_t *mp, char *arg,
+int unix_loop(Stream_t *Stream UNUSEDP, MainParam_t *mp, char *arg,
           int follow_dir_link);

 static int _unix_loop(Stream_t *Dir, MainParam_t *mp,
@@ -95,7 +95,7 @@
     return unix_dir_loop(Dir, mp);
 }

-int unix_loop(UNUSED(Stream_t *Stream), MainParam_t *mp,
+int unix_loop(Stream_t *Stream UNUSEDP, MainParam_t *mp,
           char *arg, int follow_dir_link)
 {
     int ret;

そして、単に./configureをすると、iconv関係のシンボルが見つからないというリンクエラーが出たので、以下のようにライブラリを追加で指定してビルド:

$ LIBS='-liconv' ./configure
$ make
$ sudo make install

これでOK。

mformatを使ったディスクイメージの作成

mtoolsがインストールできたので、さっそくmformatを使ってディスクイメージを作ってみる。

mformatを使ってディスクイメージを作るには、以下のようにすればいい:

$ mformat -f 1440 -B (ブートセクターのバイナリファイル) -C -i (イメージファイル名) ::

ちなみに、-f 1440は、サイズの指定。
あと、-vオプションを使うと、ボリュームラベルを指定できるみたい。
(指定しない場合、NO NAMEというボリュームラベルになるっぽい)

そして、一番気になるのは、最後の::の部分だと思う。

実は、mtoolsでは:というのが特別なドライブレター(AドライブのAやCドライブのCのこと)になっていて、:というドライブレターはディスクイメージのドライブレターになっている。
つまり、A:でAドライブ、C:でCドライブを指し示すように、::でディスクイメージのドライブを指し示すことになっている。
そして、::を使った場合には、一緒に-iオプションを使ってディスクイメージのファイルを指定してやる。

mcopyを使ったファイルのコピー

これでディスクイメージは作れるようになったので、あとはOS本体のバイナリファイルをこのディスクイメージにコピー出来さえすればいい。
それを行うには、mcopyを使う:

$ mcopy -i (イメージファイル名) (OS本体のバイナリファイル) ::

一番最後の::は前述の通り。
これでOS本体のバイナリファイルがディスクイメージにコピーされることになる。
(そして、一番最初のファイルとして配置される)


これでIPLを作っていくための準備は完了。
次はIPLを作っていく予定。

今日はここまで!

30日でできる! OS自作入門

30日でできる! OS自作入門

『OS自作入門』を読んでみた。(その5)

またまた間が空いてしまったけど、前回の続き。

今回はアドレスの問題を解決していく。

アセンブル

おさらいで、何が問題だったかというと、ブートセクタはBIOSによってメモリの0x7c00に読み込まれるのだけど、このコードだと先頭が0x0000だと思ってリンクが解決されてしまうので、正しく動作しないということ。

とはいえ、実際にどのようになっているのか分からないので、まずは逆アセンブルして確認を。

アセンブルには、objdumpを使えばいい。
これもBinutilsに入っているツール。
-dオプションをつけると、逆アセンブルをしてくれる。

$ i386-elf-objdump -d hello-os.o

この結果は、以下のとおり:

hello-os.o:     file format elf32-i386

Disassembly of section .text:

00000000 <pbr>:
       0:   eb 4e                   jmp    50 <entry>
       2:   90                      nop
// 省略

00000050 <entry>:
      50:   b8 00 00 8e d0          mov    $0xd08e0000,%eax
      55:   bc 00 7c 8e d8          mov    $0xd88e7c00,%esp
      5a:   8e c0                   mov    %eax,%es
      5c:   be                      .byte 0xbe
      5d:   74 00                   je     5f <putloop>

0000005f <putloop>:
      5f:   8a 04 83                mov    (%ebx,%eax,4),%al
      62:   c6 01 3c                movb   $0x3c,(%ecx)
      65:   00 74 09 b4             add    %dh,-0x4c(%ecx,%ecx,1)
      69:   0e                      push   %cs
      6a:   bb 0f 00 cd 10          mov    $0x10cd000f,%ebx
      6f:   eb ee                   jmp    5f <putloop>

00000071 <fin>:
      71:   f4                      hlt    
      72:   eb fd                   jmp    71 <fin>

00000074 <message>:
// 以下略

ただ、ちょっとおかしいのに気づくかもしれない。

例えば、0x50を見てみると、直値0xd08e0000をレジスタeaxに代入している。
ここはホントは、0x00をレジスタaxに代入すると出て欲しいところ。
それに、0x5cを見てみると、命令があるはずなのになぜかデータが出てしまっている。

これはなぜかというと、32ビットモード(プロテクトモード)で逆アセンブルされてしまっているから。
前回書いたとおり、ここは.code16擬似命令を使って16ビットモード(リアルモード)の出力をしているので、逆アセンブルも16ビットモードで行ってやらないと、正しくならない。

16ビットモードであることを指定するには、-M addr16,data16オプションを追加する:

$ i386-elf-objdump -d -M addr16,data16 hello-os.o

すると、以下のような出力になる:

hello-os.o:     file format elf32-i386

Disassembly of section .text:

00000000 <pbr>:
       0:   eb 4e                   jmp    50 <entry>
       2:   90                      nop
// 省略

00000050 <entry>:
      50:   b8 00 00                mov    $0x0,%ax
      53:   8e d0                   mov    %ax,%ss
      55:   bc 00 7c                mov    $0x7c00,%sp
      58:   8e d8                   mov    %ax,%ds
      5a:   8e c0                   mov    %ax,%es
      5c:   be 74 00                mov    $0x74,%si

0000005f <putloop>:
      5f:   8a 04                   mov    (%si),%al
      61:   83 c6 01                add    $0x1,%si
      64:   3c 00                   cmp    $0x0,%al
      66:   74 09                   je     71 <fin>
      68:   b4 0e                   mov    $0xe,%ah
      6a:   bb 0f 00                mov    $0xf,%bx
      6d:   cd 10                   int    $0x10
      6f:   eb ee                   jmp    5f <putloop>

00000071 <fin>:
      71:   f4                      hlt    
      72:   eb fd                   jmp    71 <fin>

00000074 <message>:
// 以下略

このとおり、ちゃんと16ビットモードで逆アセンブルがされるようになる。

アドレスの確認

さて、注目したいのは、0x5cの部分:

      5c:   be 74 00                mov    $0x74,%si

もともとのコードは、次のようになっていた:

    mov     $message, %si

つまり、messageのアドレスをsiレジスタに代入していたわけだけど、messageのアドレスが0x74として解決されてしまっている。
このファイルだけ見れば、それは正しそうに思えるんだけど、実際にはこのファイルの先頭が0x7c00にロードされるので、messageのアドレスは、0x7c00 + 0x74 = 0x7c74でないといけない。
なので、このままでは正しく動作しない。
(messageが置かれている0x7c74ではなく、何が置かれているか分からない0x74のデータを読み込んで出力しようとする)

リンカとリンカスクリプト

ということで、このセクションが0x7c00にロードされて動作することを伝え、正しくアドレスの解決を行わないといけない。

そこで必要になるのが、リンカとリンカスクリプト

リンカは、各オブジェクトファイルのセクションをまとめ、それが動作するアドレスにもとづいてアドレスの解決を行ってくれる。
そのとき、どのセクションをどのアドレスにロードし動作させるか記述するのがリンカスクリプト

今回の場合、このセクションは0x7c00にロードされ、そのまま動作するので、そのようなリンカスクリプトを書いてリンクを行えばいい。

ということで、以下のような簡単なリンカスクリプトhello-os.ldscriptを用意した:

SECTIONS
{
    .text 0x7c00 : { *(.text) }
}

このリンカスクリプトを使うようにリンカ(ld、これもBinutilsに入っている)に指示してリンクを行うには、以下のようにすればいい:

$ i386-elf-ld -o hello-os.elf -T hello-os.ldscript hello-os.o

こうして出来たELFファイルを逆アセンブルしてみると、以下のような感じ:

hello-os.elf:     file format elf32-i386
    
Disassembly of section .text:
  
00007c00 <pbr>:
    7c00:       eb 4e                   jmp    7c50 <entry>
    7c02:       90                      nop
// 省略

00007c50 <entry>:
    7c50:       b8 00 00                mov    $0x0,%ax
    7c53:       8e d0                   mov    %ax,%ss
    7c55:       bc 00 7c                mov    $0x7c00,%sp
    7c58:       8e d8                   mov    %ax,%ds
    7c5a:       8e c0                   mov    %ax,%es
    7c5c:       be 74 7c                mov    $0x7c74,%si

00007c5f <putloop>:
    7c5f:       8a 04                   mov    (%si),%al
    7c61:       83 c6 01                add    $0x1,%si
    7c64:       3c 00                   cmp    $0x0,%al
    7c66:       74 09                   je     7c71 <fin>
    7c68:       b4 0e                   mov    $0xe,%ah
    7c6a:       bb 0f 00                mov    $0xf,%bx
    7c6d:       cd 10                   int    $0x10
    7c6f:       eb ee                   jmp    7c5f <putloop>

00007c71 <fin>:
    7c71:       f4                      hlt
    7c72:       eb fd                   jmp    7c71 <fin>

00007c74 <message>:
// 以下略

ちゃんと正しくアドレスが解決されているのが分かると思う。

ちなみに、このELFファイルをobjcopyでバイナリファイルに変換した場合、どう出力されるのか気になるところだけど、objcopyはロードされるアドレスの一番先頭をファイルの先頭として出力してくれるようなので、ちゃんと期待したバイナリファイルになってくれる。

Makefileの修正

最後の仕上げで、以前書いたMakefileを修正して、リンクも行うように。

.PHONY: all clean do

all: hello-os.img

hello-os.img: hello-os.elf
  i386-elf-objcopy -O binary $< $@

hello-os.elf: hello-os.ldscript

hello-os.elf: hello-os.o
  i386-elf-ld -o $@ -T hello-os.ldscript $<

hello-os.o: hello-os.s
  i386-elf-as -o $@ $<

clean:
   -rm *.img *.elf *.o

do: hello-os.img
  qemu-system-i386 -fda $<

これでmakeを実行したときに、リンクを行ってELFファイルを作成し、そこからフロッピーディスクのイメージを作成するようになる。

今日はここまで!

30日でできる! OS自作入門

30日でできる! OS自作入門

コーダーブルームの江ノ島ライドに行ってきた。

もうすっかりおなじみになってきた、コーダーブルームのオーナーズライド。
6月17日(土)に開催されたので、自分も参加してきた。

実は、自分が前回参加した手賀沼ライドの後にも、2回ほど企画があったり。
ただ、体調的な問題で、残念ながら2回とも参加できず。
なので、今回はひさびさの参加となった。

なお、前回のオーナーズライドの様子は、以下から:

今回のルート

さっそく今回のルートから。
今回のルートは、以下のような感じ:

町田駅の南口に集合して、そこから境川をひたすら南下。
そして、江ノ島に到着したら、そこで美味しい海鮮丼を食べて解散、という、30km強のコース。

ただ、これまた前回と同じように、自分はもうちょっと頑張って、実際に走ったルートは、以下のような感じ:

町田までは輪行をするけど、オーナーズライドを走って江ノ島で解散した後は、輪行では帰らず、自宅まで頑張って自走するというルート。
これで約120km。

正直、久々に自転車に乗るので、この距離はキツかった(^^;
素直に輪行して帰るのが正解だったかも。
ただ、頑張ったおかげで、思わぬ出来事もあったので、それはかなり嬉しかったり。
それについてはまた後ほど。

前日

さて、オーナーズライドの話をする前に、ちょっと前日の話など。

前回の記事などを見てもらえれば分かる通り、自分のロードはバーテープを白にしてたんだけど、この黒と白のモノトーンが渋くていいものの、白いバーテープは汚れがとにかく目立つ・・・
そこで、前日にちょっとバーテープの替えを買いに行ったんだけど、そしたら他にもいろいろ商品が目に入るわけで。
そんな中、目に留まったのが、ハンドルバー。
これまで使ってたハンドルバーは幅が420mmで、ちょっと大きいなと思ってたんだけど、400mmのコンパクトなやつを試してみたら、なかなか良さげな感じで。
しかも、思ってたよりも全然安い。
なので、バーテープと一緒に買ってしまったw

そして、せっかく買ったのだから、さっそく付け替えてみたいと思うのが人間というもので、帰宅後に作業を始めてしまったのが大失敗というか。
交換自体は問題なく出来たんだけど、そこからいろいろと調整をしたり、バーテープを巻いてたりとかやっていたら、0時を回ってしまった(^^;

町田駅

ということで、当日。

本当だったら前日の段階で輪行状態にまでしておくつもりだったんだけど、ハンドルバーの交換なんて作業をやってたものだから、やっていなくて。
タイヤの空気圧を確認したり、ポジションを調整するために外してたサドルバッグやライト、ベルを付け直したり、久々にやった輪行セットアップに手間取ったりで、家を出る時間が予定よりも30分近く遅れてしまった・・・
(それでも8時少し過ぎには家を出てるんだけど)

さらに、電車が少し遅れて府中本町での乗り換えに失敗したり、途中でトイレに行きたくなって駆け込んだりしたせいで、町田駅に着いたのが10時10分とか。
町田駅に着いたあとも、小田急線の駅に到着していたので、そこから横浜線の駅の南口に移動するのに時間を取られて、結局集合場所に着いたのが10時20分近くになってしまった。
9時半集合開始、10時出発予定だったので、大遅刻。。。
すみませんでした。。。

今回は何班かに分かれて走るということだったので、すでに出てる組もあるかなと思ったんだけど、ありがたいことに待っててくれたらしく、みんな揃ったところで集合写真。
そのあと、各班ごとで出発していった。

自分は、いそいそと輪行状態を解除して、最後の班で出発することになった。

境川

さて、そんな感じで迷惑かけまくりの波乱の出だしだったんだけど、走り出してしまえば平穏そのもので。
梅雨の間の晴れ間で、気候は穏やか、風もほとんどなく、なんとも気持ちいいサイクリング日和だった。
走りやすい境川沿いの道をゆるゆると進んでいく。

境川といえば、アニメにもなった『ろんぐらいだぁす!』で、主人公の亜美ちゃんが最初にポン太くんで走ったコース。

ろんぐらいだぁす!: 1 (REXコミックス)

ろんぐらいだぁす!: 1 (REXコミックス)

つきみ野(作中だと「ほしみ野」)駅のすぐそばがそのスタート地点で、走ってしばらくしたら、その聖地に到着。
さっそくみんなで撮影大会w

f:id:yamaimo0625:20170622233852p:plain

作中の亜美ちゃんのごとく写真を撮るスタッフさん、の写真を撮る自分w

f:id:yamaimo0625:20170622233919p:plain

ちなみに、ここに来るのは自分は2回目。
前回来たときも、やっぱり写真を撮ったw

余談だけど、このときは『ろんぐらいだぁす!』の聖地巡りをしてて、他にも写真を撮ったので、せっかくだからアップw

https://twitter.com/yappy0625/status/583485302188085248:ebmed

さてさて、閑話休題

写真を撮った後は再出発して、ひたすら境川沿いを進んでいく。
途中、ちょっと迷ったっぽかったけどw

飯田牧場へ

そんなこんなで、ゆるゆる進んでるうちに、あっという間に飯田牧場に到着。
先に進んでいたグループとも合流して、みんなでジェラートを食べながらゆるりと休憩。

f:id:yamaimo0625:20170622235554p:plain

うーん、ここのジェラートはやはり何度食べても美味しいw
特に、気温がけっこう上がってきていたので、冷たいジェラートは最高に気持ちよかったw

サイクルラックにはたくさんのコーダーブルームw

f:id:yamaimo0625:20170622235800p:plain

それにしても、GIGLIOいいなぁ・・・
以前、試乗させてもらったとき、こう、重心がしっかり下にあって安心して乗れる感じがすごくよかった。
ロングライドだと、この安心感は絶対にいいと思うんだよね。
緑色というのも、自分の大好きな色だし。
欲しい・・・

さて、一休みしたあとは、またグループに分かれて出発。
ゴール地点の江ノ島を目指していく。

江ノ島

飯田牧場を出発して、しばらくしたら再び境川沿いの道に戻り、さらに南下。
そして、境川サイクリングロードもいよいよ終わり、国道467号に合流。

と、ここで、左折ではなく右折。
左折して国道467号を進めば江ノ島は目と鼻の先なのに、右折したもんだから、内心穏やかでなかった。
江ノ島からどんどん離れ、小田原の方へ進んでいくw

「これ、道あってますかねぇ・・・?」
「さぁ・・・?」

なんて会話を交わしつつ、ぐんぐん西へと進んでいく。
かなり不安だったんだけど、引地川を渡ったところで左折し、江ノ島方向に進んでいったので、一安心。
おそらく、藤沢駅周辺が混んでいるだろうから、迂回したのかな・・・?

引地川沿いの歩道をゆるゆると走り(思った以上に交通量があった)、国道134号に合流。
あとは、国道134号を東に進んでいけば、江ノ島に到着!

f:id:yamaimo0625:20170625113853p:plain

ゑじま

さっそくお昼ということで、自転車を駐輪場に停め、お店へ。
今回お昼を食べたのは、『ゑじま』さん。
大賑わいの通りから細い裏道に入ったところにひっそりとあって、こんなところにお店があったんだな、とw

自分が注文したのは、5色丼。

f:id:yamaimo0625:20170625115532p:plain

最初、丼の上に乗せるお刺身だけが出てきたのかと思ったw
よく見てみると、お刺身の下にひっそりとご飯がw

すごく美味しかったんだけど、ちょっと物足りなかったかも。
大盛りにしておけばよかったか(^^;

お昼を食べた後は、少しぶらぶらと観光したり、記念撮影をしたり。

f:id:yamaimo0625:20170625120002p:plain

片瀬江ノ島駅

みんなで集合写真を撮った後は、片瀬江ノ島駅へ。
これで今回のオーナーズライド自体は一応完了。

f:id:yamaimo0625:20170625123139p:plain

ただ、走り足りないという人たちは、スタッフさんたちと一緒に町田まで走って帰ってたみたい。

そして、自分はというと、ここからが本番というか、自宅まで自走w
ということで、80km強のサイクリングをスタートw

江ノ島〜戸塚〜横浜

片瀬江ノ島を出発した後は、もう一度江の島入口まで戻り、そこから国道467号へ。
混雑する道を北上し、藤沢駅を越え、藤沢橋まで進んだところで右折して、県道30号を進んでいく。
ここが今回一番の登りだったw

県道30号から国道1号に合流してしまえば、あとはひたすら進んでいくだけ。

ただ、戸塚のあたりがかなりよく分からない。。。
オーバーパスは軽車両進入禁止になっているので、下に降りるしかなく、なんとか戻れないかなと道を探ってみたものの、ダメ。
仕方ないので、戸塚駅の方までぐっと進み、しばらく線路沿いに進んで、県道401号経由で国道1号に復帰するというルートをとった。
戸塚駅のすぐ北のアンダーパスを通れれば簡単なんだけど、そこも軽車両は通れないんだよねぇ・・・
みんな、どんなルートで戸塚周辺を越してるんだろう?

まぁ、ここさえ越えてしまえば、あとは完全に道なり。
少し登ったり下ったりはあるけど、斜度も距離もそんなにないので、気にならない。

そんなこんなで、横浜に到着。

横浜〜川崎〜品川〜秋葉原

このあたりから、暑さのせいか、頭痛がけっこう酷いことに。。。
持ち歩いてたアスピリンを飲んだり、塩飴を舐めたり、頭から水かぶって体を冷やしたりして、なんとか対処しようとするも、あまり改善せず。
水分自体はこまめに補給しているんで、たぶんミネラルのバランスが崩れてるんだろうなぁ・・・
(ネットで見かけて評判がよかった「スーパーマルチビタミン&ミネラル」を、買ってはあるんだけど、持ってくるのを忘れてた)

とりあえず、水を補給しなきゃということで自販機で水を購入。
と、そしたら、なんか当たったw

f:id:yamaimo0625:20170625123123p:plain

こんなの初めて見たよw
(ちなみに、なんかワンピースのキーホルダーが入ってたw)

そんな休憩をしつつ、再び走り出したんだけど、国道1号と国道15号の分岐地点で右折レーンに入りはぐって、あ、右折できない、と、引き返したり(^^;
そんなときに、なんか見覚えのある顔が・・・

なんと、その日一緒に走ってた天野さん(@Gp1Nao)と合流!

同じ方へ進むということだったので、ご一緒させてもらった。

頭痛とかでかなりへばっていたので、まさに天の助けというか。
このあと、ずっと引いてもらって、すごく助けられた。
ホントにありがとうございました。

ということで、天野さんと一緒に、川崎、品川、そして秋葉原へと進んでいった。
そして、天野さんは千葉方面へ向かうということだったので、秋葉原でお別れ。

ちなみに、天野さんは行きも町田まで自走、帰りも自走で、160kmくらい走ってたみたい。
すごい。

秋葉原〜自宅

天野さんのおかげもあって、なんとか秋葉原まで戻ってこれたので、あともうちょっと。

と、その前に、だいぶお腹も減っていたので、ご飯。
カロリーと塩分の補給を兼ねて、すた丼を食べる。

f:id:yamaimo0625:20170625124724p:plain

そうそう、ドンブリっていうのは、こういうのをいうんだよ・・・
たっぷりあるご飯にニンニクの効いたお肉が乗っかってて、めちゃ旨い。
そして、塩分を失った身体に、お味噌汁の旨さが染み渡る。

がっつりと食べて元気を取り戻した後は、国道4号をひたすら北上し、自宅を目指す。

頭痛が治ったわけではないので、国道4号のガタガタ道はかなり頭に響いてキツかったんだけど、よく知ってる道なので、あまり不安は感じず。
事故だけは起こさないように、慎重に進んでいく。

千住大橋を越え、千住新橋を渡り、埼玉に入ればあともう少し。
そして、やっとのことで自宅に到着!

いや〜、無事に走りきれてよかった(^^)

それにしても、やはり頭痛対策は何か考えないとなぁ。
今回に限らず、暑い時期に自転車に乗るといつも頭痛に悩まされるので・・・
塩飴は舐めてるんだけどねぇ。

今日はここまで!

ろんぐらいだぁす!: 1 (REXコミックス)

ろんぐらいだぁす!: 1 (REXコミックス)

『OS自作入門』を読んでみた。(その4)

またまた間が空いてしまったけど、前回の続き。

今日は実際のブートコードの部分を見ていく。

Intel構文とAT&T構文

前回も少し触れた通り、本はNASMに似た筆者作のアセンブラを使っていて、その構文はIntel構文になっている。
一方、自分の使っているGNU asは、AT&T構文で、いろいろ違ってくる。
詳細は以下のページなどを参照:

Linux のアセンブラー: GAS と NASM を比較する

基本的なところを押さえておくと、以下のとおり:

  • オペランドのディスティネーションとソースが逆
    • NASMはディスティネーション、ソースの順
    • GNU asは、ソース、ディスティネーションの順
  • 即値の指定の仕方が違う
    • NASMは数字やラベルをそのまま書く
    • GNU asは数字やラベルの前に$をつける
  • レジスタの指定の仕方が違う
  • 数字やラベルによるメモリの参照の仕方が違う
    • NASMは数字やラベルを[]で囲う
      バイト幅の指定が必要な場合、[]の前にbyte(1-byte)、word(2-byte)、dword(4-byte)をつける
      なお、NASMは数字やラベルの前にbyte ptr(1-byte)、word ptr(2-byte)、dword ptr(4-byte)をつけるのでもOKっぽい(※)
    • GNU asは数字やラベルをそのまま書き、バイト幅を示すためにオペコードにb(1-byte)、w(2-byte)、l(4-byte)をつける
  • レジスタによるメモリの参照の仕方が違う
    • NASMは
      [セグメントレジスタ:ベースレジスタ + インデックスレジスタ * スケーラ + オフセット]
      とする(使わないところは省略可能)
      (セグメントレジスタ x 16 + ベースレジスタ + インデックスレジスタ x スケーラ + オフセット)を参照することになる
    • GNU asは
      セグメントレジスタ:オフセット(ベースレジスタ, インデックスレジスタ, スケーラ) とする(使わないところは省略可能)
      (セグメントレジスタ x 16 + ベースレジスタ + インデックスレジスタ x スケーラ + オフセット)を参照することになる

※上記のIBMのページだとこう書かれているけど、なんか怪しい・・・

なお、NASMに関してと、レジスタによるメモリの参照は、ネット上の資料を漁っただけなので、正しくないかも・・・
(しっかりした資料が見つかってないので、自信がない・・・ちなみに、これを調べてたせいで更新が遅くなった)

16ビットモード

まず、x86のCPUには、16ビットモード(リアルモード)と32ビットモード(プロテクトモード)がある。
起動した直後は16ビットモードになっているので、アセンブラで出力されるコードも16ビットモード用のものになっていないと困る。

そこで、次のように、擬似命令を使ってこのコードが16ビットモード用のものであることを示してやる:

    // 16-bit code
    .code16

ブートコード

そして、前回見た通り、ジャンプコード、BIOSパラメータブロックが続いて、そのあとにブートコードが来ることになる。
ジャンプコードでは、このブートコードの先頭に飛んでくるようにジャンプを行なっている。

最初にやっているのは、レジスタの初期化。

entry:
    mov     $0, %ax
    mov     %ax, %ss
    mov     $0x7c00, %sp
    mov     %ax, %ds
    mov     %ax, %es
    // 続く

まずアキュムレータレジスタ(ax)を0で初期化し、さらにスタックのセグメントレジスタ(ss)、データのセグメントレジスタ(ds)、追加のセグメントレジスタ(es)をaxを使って0に初期化。
そして、スタックポインタ(sp)を0x7c00で初期化している。

ここからは文字列の出力。

まず、文字列が置かれているmessageというラベルのアドレスをソースインデックスレジスタ(si)に代入。

    // 続き
    mov     $message, %si
    // 続く

アドレスの内容ではなく、アドレスの値をそのまま入れるので、即値になるように$をつける必要があるのに注意。
(最初、つけるのを忘れて動かなかった)

そしたら、siの指す内容をaxの下位8ビット(al)に読み込む。

    // 続き
putloop:
    movb    (%si), %al
    // 続く

最初はsiの値はmessageのアドレスになっているので、改行文字¥nが読み込まれることになる。
そして、次々と読み込んで出力したいので、siの値を1つずつ増やしていくことになる。

    // 続き
    add     $1, %si
    // 続く

ここで、もし読み込んだ文字がヌル文字¥0だった場合、文字列の終端になっているので、ループを抜け出すことになる。

    // 続き
    cmp     $0, %al
    je      fin
    // 続く

あとは、実際に文字を出力する処理。
これにはBIOSAPIを利用する。

    // 続き
    mov     $0x0e, %ah
    mov     $15, %bx
    int     $0x10
    // 続く

決められたレジスタに値をセットして、ソフトウェア例外を起こしている(int命令)。
これで制御がBIOSに移って、文字を出力したらまた戻ってきてくれる。

最後に、次の文字を出力するために、ループの先頭に戻る。

    // 続き
    jmp     putloop
    // 続く

一つ一つ読み解くと、こんな感じ。

Cで書いてみると、次のようなイメージ:

char* si = message;
while (1)
{
    char al = *si;
    si++;
    if (al == '¥0')
    {
        break;
    }
    putchar(al);
}

文字列の出力が終わったら、あとは無限ループ。

    // 続き
fin:
    hlt
    jmp     fin

hlt命令はCPUを休止状態にする命令で、例外が起きるまでCPUを休ませておいてくれる。


さて、これでブートコードも見たわけだけど、実はこれではまだ正しく動かなかったり。
というのも、ブートセクタはBIOSによってメモリの0x7c00に読み込まれるのだけど、このコードだと先頭が0x0000だと思ってリンクが解決されてしまうから。
なので、アドレスが正しくなっていない。

このアドレスの問題を解決するには、もう一工夫必要になってくる。
それについては、また次で。

今日はここまで!

30日でできる! OS自作入門

30日でできる! OS自作入門

『OS自作入門』を読んでみた。(その3)

前回からだいぶ間が空いてしまったけど、続き。

前回は擬似命令を使ってただデータを作っただけだったけど、それではアセンブラを使ったとは言い難いので、今回はちゃんとしたアセンブラを書いてみる。

ちゃんとしたアセンブラのコード

と言うことで早速、ちゃんとしたアセンブラのコード。

    .file   "hello-os.s"

    // 16-bit code
    .code16

    // partition boot record (1sector)
pbr:
    // jump to boot code
    jmp     entry
    nop

    // OEM name (8byte)
    .ascii  "HELLOIPL"

    // BIOS Parameter Block (FAT12/FAT16)
    .short  512                 // bytes per sector
    .byte   1                   // sectors per cluster
    .short  1                   // reserved sectors
    .byte   2                   // number of file allocation table
    .short  224                 // root directory entries
                                // (32byte/entry * 224entry = 7168byte = 512byte/sector * 14sector)
    .short  2880                // total sectors
    .byte   0xf0                // media type (0xf0: floppy disk, 0xf8: hard disk)
    .short  9                   // sectors per file allocation table
    .short  18                  // sectors per track
    .short  2                   // number of heads
    .int    0                   // hidden sectors
    .int    2880                // large total sectors
    .byte   0x00                // physical disk number
    .byte   0x00                // current head
    .byte   0x29                // extended boot signature (0x29 means DOS 4.0 EBPB)
    .int    0xffffffff          // volume serial number
    .ascii  "HELLO-OS   "       // volume label (11byte)
    .ascii  "FAT12   "          // file sytem type (8byte)

    .skip 18, 0x00

entry:
    mov     $0, %ax
    mov     %ax, %ss
    mov     $0x7c00, %sp
    mov     %ax, %ds
    mov     %ax, %es
    mov     $message, %si
putloop:
    movb    (%si), %al
    add     $1, %si
    cmp     $0, %al
    je      fin
    mov     $0x0e, %ah
    mov     $15, %bx
    int     $0x10
    jmp     putloop
fin:
    hlt
    jmp     fin

message:
    .string "\n\nhello, world\n"

    .org    0x0200 - 0x02
    .byte   0x55, 0xaa  // boot signature

    // file allocation table (9sectors = 512byte/sector * 9sector = 4608byte)
fat:
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   0x200 * 9 - 8, 0x00

    // file allocation table (copy)
fat_copy:
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   0x200 * 9 - 8, 0x00

    // root directory entries (14sectors = 512byte/sector * 14sector = 7168byte)
root_directories:
    .skip   0x200 * 14, 0x00

    // data area (2847sectors = 2880 - 1 - 9 * 2 - 14)
files:
    .skip   0x200 * 2847, 0x00

本のコードを参考にしてるけど、だいぶ違ってる。
というのも、まず本はNASMに似たnaskという著者作のアセンブラを使っていて、Intel構文だけど、自分はGNU asを使っているので、AT&T構文になっているから。
それに、いろいろ分からなかったことを調べた結果が盛り込まれているというのもある。

FATファイルシステム

まず、FATファイルシステムについてから。

フロッピーディスクはセクタと呼ばれる区画で区切られていて、セクタ単位でデータを読み書きする。
1つのセクタが512バイトで、フロッピーディスクの場合、2880セクタ用意されている。
このセクタがディスク上にどう並んでいるのかはあとで扱うとして、論理的にはセクタが配列のように一列に並んでいると考えておいていい。
そう考えたときに、このセクタの配列は、FATフォーマットではいくつかの領域に分けられる:

  • ブートセクタ
    PCが起動したときにBIOSによってメモリにロードされて実行されるセクタ
  • FAT(File Allocation Table)領域
    クラスタ(1つ以上のセクタの集まり)の情報を管理するための領域
  • ルートディレクトリ領域
    ルート直下のディレクトリの情報が置かれる領域
  • データ領域
    各ファイルの情報が置かれる領域

上のコードだと、以下のように分けられている:

pbr:

    // ブートセクタ

fat:

    // FAT領域

root_directories:

    // ルートディレクトリ領域

files:

    // データ領域

ブートセクタは1セクタ、FAT領域は1つのFile Allocation Tableが普通9セクタで、冗長化でコピーが用意されているので合計18セクタ、ルートディレクトリ領域はFAT12だと普通は14セクタ、そして、残りがデータ領域となるので、データ領域は2847セクタということになる。

Cでイメージを書いておくと、以下のような感じ:

// セクタは512バイトのデータ
typedef struct sector_ {
    unsigned char data[512];
} sector_t;

// フロッピーディスク
typedef union floppy_ {
    // 2880セクタからなる
    sector_t sectors[2880];

    // 領域
    struct {
        // ブートセクタ
        sector_t boot_sector;

        // FAT領域
        struct {
            // File Allocation Table
            sector_t fat[9];

            // File Allocation Table (copy)
            sector_t fat_copy[9];
        } fat_area;

        // ルートディレクトリ領域
        sector_t root_directories_area[14];

        // データ領域
        sector_t data_area[2847];
    } areas;
} floppy_t;

アセンブラのコードを見てみると、今のところちゃんとしたデータが置いてあるのはブートセクタだけで、他の領域は.skipを使ってほぼ0にしていることが分かると思う。
これは、今書いているのがブート時に読み込まれて実行されるプログラムで、他にファイルをディスク上に作っていないから。

ブートセクタ

さらに、ブートセクタを見てみる。

パーティションが1つの場合、ブートセクタはただ1つなんだけど、ハードディスクとかだと複数のパーティションに分かれている場合があって、その場合、各パーティションの1つめのセクタが論理的にブートセクタになる。
そこで、ディスクの先頭にあるブートセクタを、マスターブートレコードMBR)、各パーティションの先頭にあるブートセクタをパーティションブートレコード(PBR)と言ったりする。
フロッピーディスクの場合、パーティションは分けられていない(=パーティションは1つだけ存在する)ので、MBRがそのままPBRになっている。

そして、MBRとPBRでは微妙に構造が違っていて、MBRはセクタの最後の方にパーティションの情報を持つパーティションテーブルというのが置かれることになっている。
もちろん、フロッピーディスクの場合、パーティションテーブルは不要なので、構造的にはPBRになっている。
ということで、ラベルはmbrではなくpbrにしている。

    // partition boot record (1sector)
pbr:
    ...

PBR(およびMBR)は、以下のような構造になっている:

  • ジャンプコード (3byte)
    以下のデータを飛び越して実際のブートコードに行くためのジャンプ命令+α
  • OEM (8byte)
    ブートセクタの名前とか
  • BIOSパラメータブロック
    ディスクの情報をまとめた領域
  • ブートコード
    ブート処理を行うコード
  • パーティションテーブル (64byte) ※MBRのみ
    パーティションの情報
  • ブートシグニチャ (2byte)
    このセクタがブートセクタであることを示す

まずは、ジャンプコード。

    // jump to boot code
    jmp     entry
    nop

見ての通り、実際のブート処理を行うコード(entry以下)へジャンプを行なっている。
なお、ジャンプ命令には2byteのものと3byteのものがあるので、データの位置を揃えるために、2byteのジャンプ命令の後にはnop命令を入れている。
(本だとニーモニックnopとは書かず、直接機械語DB 0x90と書いている)

次はOEM名で、特に書くことなし。

    // OEM name (8byte)
    .ascii  "HELLOIPL"

その次がBIOSパラメータブロック。

    // BIOS Parameter Block (FAT12/FAT16)
    .short  512                 // bytes per sector
    .byte   1                   // sectors per cluster
    .short  1                   // reserved sectors
    .byte   2                   // number of file allocation table
    .short  224                 // root directory entries
                                // (32byte/entry * 224entry = 7168byte = 512byte/sector * 14sector)
    .short  2880                // total sectors
    .byte   0xf0                // media type (0xf0: floppy disk, 0xf8: hard disk)
    .short  9                   // sectors per file allocation table
    .short  18                  // sectors per track
    .short  2                   // number of heads
    .int    0                   // hidden sectors
    .int    2880                // large total sectors
    .byte   0x00                // physical disk number
    .byte   0x00                // current head
    .byte   0x29                // extended boot signature (0x29 means DOS 4.0 EBPB)
    .int    0xffffffff          // volume serial number
    .ascii  "HELLO-OS   "       // volume label (11byte)
    .ascii  "FAT12   "          // file sytem type (8byte)

ここが正直大変だったところで、本だと「その値にしておくものだから」で片付けられている値がけっこうある(^^;
しかも、ネットで調べても仕様がハッキリしない記述が多くて、かなり困った。

以下のwikipedia(en)の記述が結構役に立った感じ:

BIOS parameter block - Wikipedia

ここから18byteほど隙間を空けて、適当にアラインを揃えたところから、実際のブートコードは始まっている。
その詳細はまた後で。

そして、ブートセクタの一番最後にあるのが、ブートシグニチャ

    .org    0x0200 - 0x02
    .byte   0x55, 0xaa  // boot signature

ブートセクタの最後の方までスキップさせるために、.orgを使っている。
この擬似命令は、ファイルの先頭のアドレスを0として、引数で指定したアドレスまでスキップをしてくれる。
ということで、1セクタが512(= 0x200)バイトなので、そこからブートシグニチャの2バイトを引いたところまで、一気にスキップ。
そして、ブートシグニチャ0x55, 0xaaを書き込んでいる。
BIOSはこのブートシグニチャが入っていないセクタはブートセクタとして扱わないらしい。

さて、あとは肝心のブートコードの部分だけど、けっこう長くなったので、一旦区切り。

今日はここまで!

30日でできる! OS自作入門

30日でできる! OS自作入門

『OS自作入門』を読んでみた。(その2)

前回はVimバイナリエディタとして使って直接フロッピーディスクのイメージを作った。

今回はアセンブラを使ってフロッピーディスクのイメージを作ってみる。

Binutilsのインストール

本ではNASMに似たnaskという著者作のアセンブラを使ってるけど、やっぱりアセンブラでよく知られているのは、GNUアセンブラ(gas)。
ということで、自分はnaskではなくgasを使ってみた。

gasはBinutilsというツール群に含まれている。
そこで、Binutilsをインストール。

Binutils - GNU Project - Free Software Foundation

インストールは簡単で、tarボールを落としてきて展開したら、configureスクリプトを実行してmakeするだけ。

$ tar zxvf binutils-2.28.tar.gz
$ cd binutils-2.28
$ ./configure --program-prefix=i386-elf- --target=i386-elf --disable-nls
$ make
$ sudo make install

configureスクリプト--program-prefixオプションをつけると、各ツールの頭に指定したプレフィクスがつく。
今回はi386-elf用のアセンブラその他を作るので、i386-elf-というプレフィクスをつけている。
(そうしないと、他のアーキテクチャ向けのアセンブラを作ったときに、名前がぶつかる)

また、--targetオプションで対象のアーキテクチャを指定している。

あと、--disable-nlsというオプションをつけているけど、これはNative Language Supportを無効にするもの。
このオプションをつけなかった場合、libintl.hが見つからないというエラーが出たので、つけている。
(このヘッダファイルはMacでQEMUのビルドをしてみた。 - いものやま。に書いたgettextの提供するヘッダファイルなので、同じようにインクルードパスを通してやるのでもOKのはず)

何はともあれ、これでBintuilsのインストールは完了。
i386-elf向けのアセンブラi386-elf-asというコマンドでインストールされている。

アセンブラソースの作成

Binutilsもインストールできたので、次はアセンブラソースの作成。
といっても、アセンブラでコードを書くのではなく、バイナリエディタと同じように、データを直接作ってみる。

gasの場合、.byte.intといった擬似命令(ディレクティブ)を使うことで、バイナリデータを直接書くことが出来る。
また、.skipという擬似命令を使うと、指定したサイズだけ隙間を作ることが出来る。

それらを使って書いたコードが、以下:

    .byte 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f
    .byte 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00
    .byte 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00
    .byte 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
    .byte 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff
    .byte 0xff, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f
    .byte 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41
    .byte 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00

    .skip 16, 0x00

    .byte 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
    .byte 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
    .byte 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
    .byte 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
    .byte 0xee, 0xf4, 0xeb, 0xfd, 0x0a, 0x0a, 0x68, 0x65
    .byte 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72
    .byte 0x6c, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00

    .skip 368, 0x00

    .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa
    .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

    .skip 4600, 0x00

    .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

    .skip 1469432, 0x00

これをhello-os.sというファイルに保存すれば、ソースは完成。

asによるアセンブル

アセンブラソースが出来たので、これをasでアセンブルしてやる。

$ i386-elf-as -o hello-os.o hello-os.s

問題なくアセンブルが終われば、hello-os.oというオブジェクトファイルが生成される。

objcopyによるバイナリファイルの生成

ただ、これでフロッピーディスクのイメージが出来たかというと、そうではなく。

実際、生成されたhello-os.oの内容を表示してみると、以下のとおり:

$ hexdump -C hello-os.o |less
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 03 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  a4 80 16 00 00 00 00 00  34 00 00 00 00 00 28 00  |........4.....(.|
00000030  07 00 06 00 eb 4e 90 48  45 4c 4c 4f 49 50 4c 00  |.....N.HELLOIPL.|
00000040  02 01 01 00 02 e0 00 40  0b f0 09 00 12 00 02 00  |.......@........|
00000050  00 00 00 00 40 0b 00 00  00 00 29 ff ff ff ff 48  |....@.....)....H|
00000060  45 4c 4c 4f 2d 4f 53 20  20 20 46 41 54 31 32 20  |ELLO-OS   FAT12 |
00000070  20 20 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ..............|
00000080  00 00 00 00 b8 00 00 8e  d0 bc 00 7c 8e d8 8e c0  |...........|....|
00000090  be 74 7c 8a 04 83 c6 01  3c 00 74 09 b4 0e bb 0f  |.t|.....<.t.....|
000000a0  00 cd 10 eb ee f4 eb fd  0a 0a 68 65 6c 6c 6f 2c  |..........hello,|
000000b0  20 77 6f 72 6c 64 0a 00  00 00 00 00 00 00 00 00  | world..........|
000000c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000230  00 00 55 aa f0 ff ff 00  00 00 00 00 00 00 00 00  |..U.............|
00000240  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001430  00 00 00 00 f0 ff ff 00  00 00 00 00 00 00 00 00  |................|
00001440  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00168050  03 00 01 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00168060  03 00 02 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00168070  03 00 03 00 00 00 2e 73  79 6d 74 61 62 00 2e 73  |.......symtab..s|
00168080  74 72 74 61 62 00 2e 73  68 73 74 72 74 61 62 00  |trtab..shstrtab.|
00168090  2e 74 65 78 74 00 2e 64  61 74 61 00 2e 62 73 73  |.text..data..bss|
001680a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
001680c0  00 00 00 00 00 00 00 00  00 00 00 00 1b 00 00 00  |................|
001680d0  01 00 00 00 06 00 00 00  00 00 00 00 34 00 00 00  |............4...|
001680e0  00 80 16 00 00 00 00 00  00 00 00 00 01 00 00 00  |................|
001680f0  00 00 00 00 21 00 00 00  01 00 00 00 03 00 00 00  |....!...........|
00168100  00 00 00 00 34 80 16 00  00 00 00 00 00 00 00 00  |....4...........|
00168110  00 00 00 00 01 00 00 00  00 00 00 00 27 00 00 00  |............'...|
00168120  08 00 00 00 03 00 00 00  00 00 00 00 34 80 16 00  |............4...|
00168130  00 00 00 00 00 00 00 00  00 00 00 00 01 00 00 00  |................|
00168140  00 00 00 00 01 00 00 00  02 00 00 00 00 00 00 00  |................|
00168150  00 00 00 00 34 80 16 00  40 00 00 00 05 00 00 00  |....4...@.......|
00168160  04 00 00 00 04 00 00 00  10 00 00 00 09 00 00 00  |................|
00168170  03 00 00 00 00 00 00 00  00 00 00 00 74 80 16 00  |............t...|
00168180  01 00 00 00 00 00 00 00  00 00 00 00 01 00 00 00  |................|
00168190  00 00 00 00 11 00 00 00  03 00 00 00 00 00 00 00  |................|
001681a0  00 00 00 00 75 80 16 00  2c 00 00 00 00 00 00 00  |....u...,.......|
001681b0  00 00 00 00 01 00 00 00  00 00 00 00              |............|
001681bc

見てのとおり、アセンブラに書いた内容だけではなく、なんか余計なデータが追加されているのが分かると思う。

これは、オブジェクトファイルが生のバイナリデータではなく、ELF形式のデータになっているため。
どのメモリにロードすべきかや、シンボルの情報、デバッグ情報なんかが一緒に詰め込まれている。
(ちなみに、アセンブラに書いた内容は0x34から始まっている)

普通のプログラムであれば、OSがこれらの情報を読み取って、適切なアドレスにロードしてくれるんだけど、ここではそういったローダはいないので、生のバイナリデータの形に自前で展開してやらないといけない。

ただ、Binutilsにはobjcopyというバイナリファイルの形式を変換するツールが含まれているので、これを使うことでオブジェクトファイルを生のバイナリデータに変換することが出来る。
(※もちろん、リンクされてすべてのシンボルが解決されていないとダメ)

以下のコマンドを実行:

$ i386-elf-objcopy -O binary hello-os.o hello-os.img

-Oが出力形式を決めるためのオプションで、ここでbinaryを指定すると、生のバイナリデータに変換されて出力がされる。

出来上がったイメージファイルの内容を見てみると、以下のとおり:

$ hexdump -C hello-os.img 
00000000  eb 4e 90 48 45 4c 4c 4f  49 50 4c 00 02 01 01 00  |.N.HELLOIPL.....|
00000010  02 e0 00 40 0b f0 09 00  12 00 02 00 00 00 00 00  |...@............|
00000020  40 0b 00 00 00 00 29 ff  ff ff ff 48 45 4c 4c 4f  |@.....)....HELLO|
00000030  2d 4f 53 20 20 20 46 41  54 31 32 20 20 20 00 00  |-OS   FAT12   ..|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  b8 00 00 8e d0 bc 00 7c  8e d8 8e c0 be 74 7c 8a  |.......|.....t|.|
00000060  04 83 c6 01 3c 00 74 09  b4 0e bb 0f 00 cd 10 eb  |....<.t.........|
00000070  ee f4 eb fd 0a 0a 68 65  6c 6c 6f 2c 20 77 6f 72  |......hello, wor|
00000080  6c 64 0a 00 00 00 00 00  00 00 00 00 00 00 00 00  |ld..............|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200  f0 ff ff 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001400  f0 ff ff 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00001410  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00168000

ちゃんと出来上がっているのが分かると思う。

Makefileの作成

これでとりあえずフロッピーディスクのイメージが出来たわけだけど、このコマンドを毎回打つのは面倒。
打ち間違いとかも怖いし。
(昔、ShellのTabによる補完でgcc -o hoge.c hoge.cを実行してしまって、せっかく書いたソースを空にしてしまったことがある・・・)

そこで、簡単なMakefileも用意してみた:

.PHONY: all clean do

all: hello-os.img

hello-os.img: hello-os.o
  i386-elf-objcopy -O binary $< $@

hello-os.o: hello-os.s
  i386-elf-as -o $@ $<

clean:
   -rm *.img *.o

do: hello-os.img
  qemu-system-i386 -fda $<

実行するコマンドの前にあるのは必ずタブでないといけないので、そこだけは注意。

これで、makeとすれば、イメージが作成されるし、make doとすれば、qemuが立ち上がって実行もされる。

今日はここまで!