30日でできる! OS自作入門の1~18日目

この記事は圧倒的令和ッ!!ぴょこりんクラスタ Advent Calendar 2019のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事を見てください。

はじめに

コンピュータをかじったことがある人間であれば、誰しも1度位はOSとかコンパイラを作ってみたいと 思ったことがあるだろう。ただ、実際に作る人はそんなに多くない。 自分自身はと言うと、時折発作のように思い出しては今更かなと思って何もしないクズ系ワナビーである。 そんな折、たまたまAmazonで"30日でできる! OS自作入門"が半額セールで買えたので、 これをひととおりやってみることにした。つまり、本記事はこれの作業記録である。

環境用意

あまりがんばらなくても用意できるものでやるという方針のもと、以下とした。 基本的な作業は、Virtualbox上のManjaro Linuxでやる。

以下、x日目は、 本の上での日数であり、実際の作業時間とは一致しない。

また、リポジトリは以下である。 github.com

1日目

Vimバイナリエディタとして使おうとして拾った設定がうまく動かずじたばたした。 拾ったやつはBufWritePreの%!xxd -r後に| endifが書いてあったんだけど、 それだと!の引数として解釈されるようで正しく動かなかった。 全部書いて:wqしたら、Command Not Foundだけ書いてあるファイルが出てきてつらくなった。

以下は正しく動く版のもの。

"バイナリ編集(xxd)モード(vim -b での起動、もしくは *.binファイルを開くと発動)
augroup BinaryXXD
    autocmd!
    autocmd BufReadPre  *.bin let &binary =1
    autocmd BufReadPost * if &binary | silent %!xxd -g 1
    autocmd BufReadPost * set ft=xxd | endif
    autocmd BufWritePre * if &binary | %!xxd -r
    autocmd BufWritePre * endif
    autocmd BufWritePost * if &binary | silent %!xxd -g 1
    autocmd BufWritePost * set nomod | endif
augroup END

バイナリファイルはとりあえず全部書いてみた。 バイナリファイルを作成するアセンブリコードについては、 バイナリファイルを読んでアセンブリコードを吐き出すスクリプトを作り、 写経したことにした。本に従って、アセンブラIntel形式で書き出した。

作ったイメージファイルは、Virtualboxに仮想フロッピーディスクとして認識させたら普通に動いた。 もっと苦労すると思ったけど、案外あっけない、hello world

f:id:nbisco:20191203223039p:plain
1日目

2~3日目

イメージの作り方がわからない。しかし、同じことをやっている人はたくさんいて、 その中でもきちんとログを残してくれた偉大な先人Makefile等を参考になんとか実施。 本では、スクリプトの中身の解説はまだなので、理解する前に写した。 画面はまっくらだけど、hlt命令を実行しているだけなので、これで正しい。

f:id:nbisco:20191203223138p:plain
2~3日目

4日目

Win98みたいなツールバーが出せるようになった。結構あっという間に画面の大枠が出来てしまった。

f:id:nbisco:20191203223219p:plain
4日目

ところで、なぜ第1引数がESP + 4なのか?それは単純でx86のCalling Conventionによるものであった。 32bitに関して言えば、ESP + 0は戻り先アドレス、ESP + 4は最左の引数、ESP + 8は次・・・みたいな感じで積まれるようである。 ここを参考にした。

5日目

フォントを手作りして、何か表示する回。 途中、sprintfがなくて困る。gcc-multilibを入れてもstaticにリンクできなかったので、 ここからsprintfを拝借した。 最初は小さいlibc(muslとか)をリンクすればいいかなと思ったけど、 ほしいのはsprintfだけだったので止めた。

また、手作りフォントについては、ここを参考に、pythonスクリプトをえいやで作ってなんとかした。

真ん中の数値は、緑画面の中央点の座標を表している。

f:id:nbisco:20191203223330p:plain
5日目

6日目

キーボードからの割り込みを拾ったよ、ということを表示する回。内容ではまるというよりは、Makefileではまる。 haribote.sysをddする際、countが正しく指定されていないことが問題で、原因はワンライナーの計算方法だった。 Makefileの変数内の$マークをエスケープ($を$$に置き換え)して、0.5足して、小数点以下切り捨てることで対応。

f:id:nbisco:20191203224035p:plain
6日目

7日目

キーボード入力とマウス入力を拾って、単にそのまま生の値を表示する回。 矢印は動かないし、自由に文字を打てないが、入力を拾えるようになったのは嬉しい。

定数をマクロに置き換えた際に写経ミス(8で割るのを忘れた)発生により、動作せずにしばらくはまった。 デバッガがないせいで原因がよくわからず、発見まで時間がかかった。デバッガがないのは本当につらい。 これ以外は特に問題なかった。

f:id:nbisco:20191203224532p:plain
7日目

8日目

マウス入力を拾って、どのボタンが押されたか、どちら向きに移動したか、矢印が今どこに いるかを表示する回。

マウスの矢印が動き始めたので感慨深い。拝借したsprintf関数が負の数に対応していなかったので修正した。 ふと思ったんだけど、BIOSでUSBキーボードが使えるということは、BIOSそのものがUSBドライバとキーボードドライバを持ってるってことなんだよな。OSのサポートなしでドライバ作るなんてBIOSメーカも大変だなあ・・・

f:id:nbisco:20191203224858p:plain
8日目

9日目

メモリ管理ができるようにして、メモリ総容量と現使用量を表示する回。 ただ、矢印がメモリ容量の文字のところに重なると、文字が消えてしまう。。。

memtest_subについては、Cでもvolatileをつければ大丈夫。ただし、今使っている gcc 7.4.0では、xor命令ではなくnot命令として出力されている様子。

f:id:nbisco:20191203225200p:plain
9日目

10日目

矢印が文字の上を通っても消えなくなるように、描画方法を工夫する回。

描画の重ね合わせ処理でメモリ管理関数を使ったが、特に問題なし(写経しているだけなので当然といえば当然なんだけど)。 マウスの矢印を動かしても、下の文字が消えないというのは結構感動する。何より、 動いているものをグラフィカルに見られるというのはよいもので、やったかいがあると思える。

f:id:nbisco:20191203225615p:plain
10日目

11日目

Windows98を思い出すウインドウを作り、その中でカウンタを表示する回。

カウンタにリーディング0がほしかったので、sprintf関数を修正した。 難しいところはなかったけど、しょうもない写しミスで苦労した。 手書きで写しているから、こういうミスがあると大変なんだよなあ・・・ ウインドウといっても、まだ動かしたり閉じたりはできないが、ずいぶん形になってきた。

f:id:nbisco:20191203225818p:plain
11日目

12日目

タイマー割り込みを使って、カーソルの点滅や、時間カウンタを実装する回。

8254タイマは初めて使ったけど、シンプルでわかりやすい。 今はもっぱらLocal APIC + TSCだから使う機会がない。HPETですら使わないんだもんね。 タイマーが使えるようになって、より一層OS感が出てきた気がする。

f:id:nbisco:20191203230022p:plain
12日目

13日目

割り込み周りの性能を改善をする回。 具体的には、割り込みハンドラごとに用意していたバッファを1つにまとめたこと、 バッファのデータ長を1バイトから4バイトに変えたこと。特に問題なし。

f:id:nbisco:20191203230351p:plain
13日目

14日目

解像度を大きくし、テキストボックスを作ってみる回。

解像度を変えられる(動的にではないけど)とぐっとOSらしくなってくる。 今まで320x200でやってた分、1024x768になるとめちゃ広く感じる。 左クリックすると、矢印の位置にウィンドウが移動する。左クリックをしたままだと、 ウィンドウがそのままついてくる。

ウィンドウを動かすこともできるようになったし、いよいよOSらしくなってきて 盛り上がってきた。

f:id:nbisco:20191203230754p:plain
14日目

15日目

マルチタスクにする回。

ついにマルチタスク!と言っても、まだスケジューラを用意したわけではなく、 一定周期で2つのタスクを切り替えるというもの。シンプル。

f:id:nbisco:20191203231117p:plain

16日目

複数のタスクの切り替え、タスクごとの優先度付けをできるようにする回。

優先度の付け方はLinuxでいうところのSCHED_FIFOに近い。 優先レベルごとにグループを作成し、動作可能なプログラムが存在するグループのうち、 優先レベルの最も高いグループ内で 優先度に従ってスケジューリングを行う。 優先レベルの低いグループに属するプログラムは動作させない。

今回は構造が変わるところが多く、複雑なので、こまめにコミットするようにした。

図では、4タスク(タスクA、タスクB0~B2)動作させているところ。 タスクB0への割当時間を1とすると、タスクB1への割当時間は2、 タスクB2への割当時間は4となるようにしている。 カウンタもだいたい合ってそうだよね。

f:id:nbisco:20191203231533p:plain
16日目

17日目

コンソール画面を作って、キーボード入力をできるようにする回。 tabを押すことで、タスクの切り替えもできるようになった。

これまでアルファベット大文字と数字だけだったのが、 アルファベット小文字、記号も入力できる。小文字と記号を入力できるように、 shiftキーをサポートし、ついでに*Lockをサポートした。 まだEnterキーは未サポート。

f:id:nbisco:20191203231857p:plain
17日目

18日目

Enterキー、画面スクロールのサポートと、コンソール用コマンドを作成した回。

Enterキーをサポートし、コンソール画面もスクロールができるようになった。 文字を打つだけでなく、 3つのコマンドも定義したので、 コンソールもそれっぽくなってきた。

  • mem: 全メモリ量と使用可能メモリ量の表示
  • cls: 画面のクリア
  • dir: FAT32メタデータを読んでファイル名リストの表示

コマンド入力するのに、strcmpが欲しくなったので自作。また、dirコマンドはFAT32を直接読むので、 これまでみたいに適当にやるのはダメである。ということで、 再度偉大な先人の記事に習って、 ディスクイメージ作成にddではなく、mformatコマンドとmcopyコマンドを使うようにした。

f:id:nbisco:20191203232304p:plain
18日目

19日目以降

そのうちやるつもり。

おわりに

手を動かして物事を理解する、というのは大変楽しいですね。

ソフトウェア工学素人がソフトウェアテストについて調査する

この記事は圧倒的令和ッ!!ぴょこりんクラスタ Advent Calendar 2019のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事を見てください。

この記事について

何を思ったかソフトウェアテストについてちょこっと調べたので、その結果をまとめる。

はじめに

ソフトウェアテストは、趣味でコード書いていたりするとあまり関わらない分野であると思う。 自分の場合、過去に家計簿システムを作ったが、そこでも特にテストらしいテストは書かず、 つど適当にデバッグして終えた。

しかし、テストを書かなくていいのは趣味の範囲までである。 ソフトウェアを製品として送り出す場合、品質を保証するという観点からテストは欠かせない。 品質が特に重要な場合、ISOのような標準規格でどのようなテストをしなければならないかを定められていることも有る。 例えば、自動車機能安全規格ISO26262では、ユニットテストにおいて、C0カバレッジ、C1カバレッジ、 MC/DCを100%達成する必要がある*1。 自動車ほど頑張らないかもしれないが、それでも似たような基準を満たす必要があるところは多いだろう。 ちなみに、C0カバレッジ、C1カバレッジ、MC/DCの意味は以下の通り。

  • C0カバレッジ: 全ての命令、つまり、全てのソースコードの行のうち、少なくとも1回実行されたものの割合。
  • C1カバレッジ: それぞれの判定における、それぞれの条件で、全て可能な結果を少なくとも1回は取ったものの割合。
  • MC/DC(Model Condition Decision Coverage): 以下を満たす*2ものの割合。
    • プログラムの全入口/出口を少なくとも1回はテストすること
    • プログラムの判定に含まれる全条件は可能な値を少なくとも1回はテストすること
    • プログラムの全判定は可能値を少なくとも1回はテストすること
    • プログラムの判定の全条件は判定の出力に独立して影響することを示すこと

これらの基準を満たすようなユニットテストを人間の手で書くのは大変である。 極端な例だが、以前少し話題になったこのソースコードを 見てみれば、上記の条件を満たすことが結構大変かもしれないと感じられるかも知れない。 要は、条件分岐が多いような複雑なソースコードの場合、そもそも人手でユニットテストを書くというのは重労働だ。 ユニットテストですら重労働、いわんや結合テスト(関数単体でなく、必要に応じて関数を結合してテストを行うこと)、統合テスト(システムとしてテストをすること)をや、というのは想像に難くない。 重労働なんて定性的なことを言っても仕方ないので数字を出すと、 ソフトウェア開発データ白書2018-2019によれば、 ソフトウェア開発にかかるコストのうち、だいたい4割超がテスト(結合テストと統合テスト。ユニットテストは分類にないので、設計工程に含まれている?) に割かれている。半分くらいテストしているというわけだ。

今回は、こんな大変なテストで楽をするための技術について調べた結果をざっとまとめる。

ソフトウェアテスト技術の分類*3

ソフトウェアテストと一口に言っても、目的は様々である。 ユニットテストはその部品が期待通り動作するか、結合テストはあるユニットが他のユニットと連動した際に 期待通り動作するか、統合テストは、ユニットをシステムとして組み上げたときに、期待通りの動作ができるか、など。 他にも、実際に意図通りに動作するかを顧客にテストしてもらう受け入れテストや、期待通りの性能が出せるかの性能テストなんてのもある。 偉大な先達は、これらの共通点を"observing a sample of executions"と表現し、 以下のとおり4W2Hで整理してくれた*4

  • WHY: なぜやるのか?バグを見つけるためなのか、リリース判定するためなのか、はたまたUIのユーザビリティを見るものか?この観点は、 テストオラクル(多少不正確かも知れないがテストの期待値と言い換えてもよいはず)の生成技術に関わるもの。
  • HOW: どのサンプルを観察するのか、また、そのサンプルはどうやって選ぶのか?この観点は、テストの入力をどうするか、 どうやってテストをするのか、いくつかあるテストのうち何を選択するか、の技術に関わるもの。
  • HOW MUCH: サンプルをどれくらい取ればよいか、また、得たサンプルのうちどれくらい採用すればよいか?この観点は、 テストの選択、停止ルール、十分性評価の技術に関わるもの。一般には、カバレージ分析や信頼性測定がよく使われる手法。
  • WHAT: 何を実行するか?一部に注目して実行するか、または全体を通して実行するか?この観点は、 テストの粒度を決める技術や、大きなシステムをテストできるようにするスタブなどのテスト支援技術に関わるもの。
  • WHERE: どこで実行するか?開発現場か、シミュレーション環境か、はたまた実環境か?何を実行するかとも関係が ある観点。この観点は、組み込みソフトウェアのテストに特に関わる。
  • WHEN: プロダクト・ライフサイクルのどこでやるか?一般には早く実施するほど、問題への対策が低コストで済むと 言われているが、実環境でなければわからないこともある。

今回は、HOWの観点のうち、特にテスト自動化技術に着目する(というか調査したのがここというだけで、 他が重要でないというわけではない)。

テスト自動化技術について

テスト自動化技術は、さらに以下の3つに分類できる。

  • テスト実行・結果確認の自動化
  • テスト環境構築の自動化
  • テスト入力生成の自動化

御存知の通り、完全なるテスト自動化は未だ達成できていない。 しかし、ユニットテストに関しては技術開発が進んできており、一般に使えるツールもそれなりにある。 例えば、テスト環境構築の一部*5とテスト実行・結果確認を支援してくれるツールとして、 xUnitフレームワークや、 Google Testが挙げられるだろう。

テスト入力生成の自動化はというと、こちらは現在も活発に研究がなされている。 テスト入力生成には3つのアプローチがある。

  • ランダムベースのアプローチ: ランダムに入力を作るアプローチ。DART*6と呼ばれる技術が 有名。DARTでは、関数のインタフェースを静的解析技術で抽出し、ランダムに生成したテストケースを入力して自動でテストを行う技術である。
  • モデルベースのアプローチ: ソフトウェアの振る舞いモデルや、仕様の形式記述をベースにテストを生成する技術*7。 このアプローチでは、シンボリック実行と呼ばれる技術が有名。シンボリック実行は、ソースコードを静的解析して実行パスを抽出し、各パスを通すにはどのような入力を与えればよいかを SATソルバを用いて求めることで、テスト入力を生成する*8。 シンボリック実行では、実際にソフトウェアは実行されず、あくまで形式的に検証する。網羅性という観点では大変効果的な手法であるが、 経路数の爆発等で実適用への大きな壁がある。経路数の爆発を抑えるため、一部の入力をランダムベースのアプローチで生成するコンコリックテスト(シンボリック実行に、一部具体的な値を用いて 実際にプログラムを動作させる)と呼ばれる技術の研究が行われている。 シンボリック実行の他にも、有界モデル検査(モデルの状態遷移を一定範囲に抑えて検査する技術)を利用してテスト生成する技術もある*9
  • サーチベースドソフトウェアテスティング: 何らかの評価関数(例えば、C1カバレージ)を予め定義し、それがよくなる方向に入力を生成する技術。入力生成には、遺伝的アルゴリズムのような メタヒューリスティクス*10を 応用している。

今回調べたのはサーチベースドソフトウェアテスティングのため、以降はこれについて述べる。

サーチベースドソフトウェアテスティングの現在

サーチベースドソフトウェアテスティング(長いのでSBSTと略す)の実装のうち、最も有名なツールの1つはEvoSuiteと言えるだろう。 EvoSuiteは、Java向けのユニットテストを生成してくれるツールであり、コンペでよい成績*11を残している優秀なツールである。 EvoSuiteはチュートリアルも充実しているため、Javaほぼ初心者の自分でも、チュートリアルを1から順にやっていくだけで簡単にユニットテストが自動生成できる(なので、この記事中で使い方に関するものは記載しない)。

EvoSuiteは優秀であり、ソフトウェアテストを作成するための作業時間を短縮できるという結果も示されているが*12、 EvoSuiteが苦手とすること、改善すべきこともまだまだたくさんある。いくつかを抜粋したものを以下に箇条書きで示し、関連しそうな最近の研究についても軽く述べる。

  • 評価関数の最適化方法の改善
  • マルチスレッドのような並列動作プログラムのテスト生成
  • 現実のバグを見つけること

評価関数の最適化方法の改善

SBSTは、すごくざっくり言えば、評価関数の出力をよくするように入力を調整する技術だと言うことができる。 Generating Test Input with Deep Reinforcement Learningでは、 その最適化に強化学習を応用したものが報告されている。ただ、現時点ではものすごく効果が出たというわけではなく、定式化をして簡単な評価をするにとどまっている様子。

マルチスレッドプログラム向けのテスト生成

マルチスレッドプログラムは、当然ながらシングルスレッドプログラムよりも設計・テスト作成が難しいので、 マルチスレッドプログラム向けのテスト入力生成技術も研究されている。 Effectiveness and Challenges in Generating Concurrent Tests for Thread-Safe Classesでは、 世の中にある並列動作プログラム向けのテスト入力生成ツールについて、現実のバグを見つけられるかどうかを評価した論文である*13。 論文で評価した技術を以下に箇条書きでまとめる。並行動作するプログラムの動作パターン数の爆発をどう抑えるかというところでみんな苦労している。

  • 評価対象の分類とツール名
    • random based techniques:ランダムな関数コールシーケンスと入力を生成し、テストするアプローチ。 テストオラクルには、linearizability*14を利用。 難しい解析をせずに入力が作れるところが利点。欠点は、ランダムに入力生成するので、狙ってケースを作成できない点である。
      • BALLERINA(ICSE 2012): 計算量を減らすために、同時に動作するスレッドは2つだけ、かつ、スレッド間で共有するオブジェクトは1つだけ、という仮定を置いてモデル化。 さらにテストオラクルをクラスタリングし、判定にかかる計算量を減らした。
      • CONTEGE(PLDI 2012): 基本的にはBALLERINAと同じだが、こちらはlinearizabilityをうまく判定する機構を作った様子。BALLERINAよりもfalse alarmを減らした。
    • coverage based techniques: ランダムに生成していると冗長なパターンが増えて効率が悪いので、 重複しないように、つまり、スレッド間のインターリービングパターンのカバレージを増やすようにテストケースを生成するもの。 なお、このインターリービングカバレージの計算の効率化が技術課題。そもそもカバレージ計算が難しく、考慮しなければいけない事項が落ちがちというのが悩みどころ。
      • consuite(ICST 2013): EvoSuiteを拡張し、並行テストの生成をできるようにしたもの。カバレージとして満たすべきところを 統計的に算出し*15、テスト対象を絞るもの。 共有メモリのコンテキストを考慮しないため、特定のケースのエラーしか報告できない。また、共有メモリへのアクセス順序が保証できない。
      • AutoConTest(ICSE 2016): consuiteの改善。共有メモリのコンテキストを意識したインターリービングカバレージ計算ができるようになった。
      • covcon(ICSE 2017): 並行実施されるメソッドペア集合の頻度情報を収集。特に頻度の低いものについてテストを生成することで高速化。
    • sequential-test-based techniques: 検出するバグの種別を絞ることで計算量を減らすアプローチ。 並列動作を並列動作のままモデル化するのではなく、直列化*16をしているので、 モデル化としては不十分というところがつらい点。また、こういうモデル化をしたとしても、計算は相変わらず大変である。
      • OMEN: deadlock検出
      • NARADA: データ競合
      • INTRUDER: atomicity違反
      • MINION: assertion違反

評価の結果、上記のどれか1つでもバグを検出できた件数は、8件/47件であった。原因分析の結果は以下の通りで、なかなか厳しい。

  • 原因1: 現実と仮定が合っていない。いずれも2つのメソッド、1つの共有オブジェクトという仮定をおいているが、現実と離れている。
  • 原因2: 環境依存。DB接続とか、固有のファイルが必要とかでバグが再現しない。concurrentな環境だと mockがうまく使えなかったりするらしい。
  • 原因3: wait-notify機構に未対応

現実のバグを見つけることについて

マルチスレッドプログラムのバグを見つけるのが難しいことはわかったが、シングルスレッドプログラムではどうか? そんな疑問に答えてくれるのがUsing Search-Based Test Generation to Discover Real Faults in Guavaである。 この論文では、EvoSuiteを用いて、Google作のJavaライブラリであるGuava*17のリアルバグを検出できるかどうかを評価した。 結果、3件/9件のバグを検出できた。検出できなかった原因の分析は以下の通り。

  • 入力に特殊な値が求められるもの(論文中の例: 1文字の文字列に対して、マッチしない正規表現パターンを適用してsplitしようとすると、空文字列が返ってくる)
  • 特定のデータ型でのみバグが発現するもの
  • 複雑なクラスのインスタンスが入力として与えられるもの
  • 特定のメソッドコール列が必要なもの(論文中の例: 優先度付きキューに対し、add/removeを繰り返していると、正しいものがremoveできなくなる)

試しに、"1文字の文字列に対して~"のを見てみたが、 人間がソースコードを見ても理解するのが大変であった。そもそも、何が特殊かを判断する基準をどう定義するかが難しい問題であり、 これを見つけるのは難しいだろうなあ・・・

終わりに

今回の記事では、ソフトウェアテスト、特にテスト入力自動生成について軽く調べた結果をまとめた。 テスト自動生成は夢があるが、まだ道半ばと言ったところ。いつか、テストを勝手に作れるようになる日は来るかなあ。

*1:"安全系組み込みソフトウェア開発におけるユニットテストの効率化 ~Concolic Testingの活用事例~", ソフトウェア・シンポジウム 2015 in 和歌山, 岸本渉, 株式会社デンソー

*2:https://hldc.co.jp/06/02/14688/

*3:"Software Testing Research: Achievements, Challenges, Dreams", FOSE2007, Antonia Bertolino

*4:"Software Testing Research: Achievements, Challenges, Dreams", FOSE2007, Antonia Bertolino

*5:完全に環境を再現するわけではないので一部

*6:DART: Directed Automated Random Testing, Patrice Godefroid, et al.

*7:"A Symbolic Framework for Model-Based Testing", L. Frantzen, et al.

*8:http://jasst.jp/symposium/jasst15tokai/pdf/S4-1.pdf

*9:CBMC https://www.cprover.org/cbmc/

*10:http://www.orsj.or.jp/~wiki/wiki/index.php/%E3%83%A1%E3%82%BF%E3%83%92%E3%83%A5%E3%83%BC%E3%83%AA%E3%82%B9%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9

*11:SBST 2017 Unit Tool Competitionで優勝

*12:"AUTOMATED UNIT TEST GENERATION DURING SOFTWARE DEVELOPMENT", ISSTA 2015, José Miguel Rojas, et al.

*13:こういう論文があると初心者としては全体が何となく知れるのでうれしい

*14:スレッド間のatomicityが正しく保たれたと仮定を置き、スレッド実行を直列化した際に得ることができる結果か否かを判定する技術

*15:具体的には調べていないのでわからないが・・・

*16:自分の理解では、あるスレッドが動作しているときは 他のスレッドが動作しないというモデル化、というところだけど、誰か正確なところを教えてほしい

*17:https://github.com/google/guava

Top Gearについて

この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事を見てください。

サマリ

自分の大好きなTop Gearについて書く。

Top Gearについて

Top Gearとは、イギリスBBC作の自動車好きのためのTV番組である。

Jeremy Clarkson、Richard Hammond、James Mayの3人*1が、 自分の趣味を全面に押し出して、自動車を紹介する。 紹介すると言っても、そこらのグルメ番組とは異なり、 単においしさのリアクション芸を競うのではなく、何がよくて何がよくないかを 試乗した上できちんと両方伝える。もちろん、ここの判断基準にも趣味が反映されるので 大衆の自動車選択には全く役にたたないが、見ていて気分がよい。 自動車の紹介だけでなく、自動車を使ったユニークな*2企画が 最高におもしろい番組である。

今回はTop Gearのうち、特に印象に残っているエピソードを、出演者の名言とともに紹介する。

Season 21 Episode 1

概要

1980~1990年代の古き良きホットハッチ*3を紹介する回。 各自が限られた予算の中でお気に入りのホットハッチを購入し、課題にチャレンジする。

ちなみに、それぞれ以下の車を選んでいる。

  • Jeremy
  • Hammond*5
    • Vauxhall Nova SRi*6
    • 700ポンドで購入
  • James
    • Ford Fiesta XR2i 16V*7
    • 750ポンドで購入

名言

  • タイヤよりもルーフで走る時間が長い車だ
    • Jeremyの、HammondのNovaに対する発言。このNovaは本当にひどくて、運転席側ドアと助手席側ドアが全く違うものになっており、 前の持ち主がどのように扱っていたかがわかるような状態になっていた。
  • そもそもNovaは買う必要はなく、盗めばよい車です
    • Jeremyの、HammondのNovaに対する発言。キーなしでも簡単にエンジンがかかってしまうような車であり、 気持ちはわからんでもない。
  • スーパーマーケットを襲う際に、Golfの盗難防止アラームが突然鳴り出す
    • "80年代は車泥棒が多かったので、各自が買った車でスーパーマーケットを襲う"というコンセプトのもと、 スーパーマーケット然としたセットの中を走り抜けるタイムを競う企画での出来事。 走り出す前にタイミングよく"ピューイwwwwピューイwwww"みたいな感じで突然鳴り出したのが笑えるので、 名言ではないけどこの枠に入れた。
  • ファミリーといえば、元気そのものの姉を埋葬しかけたそうだね?
    • Jeremyの、ゲストのヒュー・ボネヴィルに対する発言。ヒュー・ボネヴィルが小さいころの微笑ましい出来事の話。

Season 21 Episode 3

概要

一般人が買える車をレビューしろという苦情が入ったためか、コンパクトカー特集。 コンパクトカー(1リッター3気筒)に乗ってクリミア半島に向かう途中で様々な課題にチャレンジする。

各自が選んだ車は以下。Volkswagen Up!は日本でもたまに走っているのを見かけるが、 あれを見るたびにカールおじさんを思い出すんだよなあ。

  • Jeremy: Volkswagen Up!
  • Hammond: Ford Fiesta
  • James: Dacia Sandero

名言

  • 道路沿いの"カフェ"へ
    • 全くカフェではない。道の駅にある農産物を直販しているような感じの場所。
  • ドバシ ノギ
    • 伝えたかったこと: 魚以外の食べ物をください
    • 実際の意味: Where is your legs?
    • Jeremyの、"カフェ"のお姉さんに対する発言。運転中の暇を持て余したJeremyが、ウクライナ語の 語学教材で練習した成果を見せようとした場面。笑ってはいけないシリーズにおける ジミー大西を彷彿とさせる意味のわからなさが笑える。お姉さんも苦笑い。
  • ヴァシ スーベニーァビ
    • 伝えたかったこと: りんごをください
    • 実際の意味: I will eat your sourvenirs
    • これもドバシ ノギと同じ文脈。
  • Jeremyはりんごを1個注文
    • 出てきたものは生キャベツ1玉。その後キャベツをかじるJeremyに、この番組の妥協のなさが感じられて本当によい。

まとめ

これらは全部Amazon Prime Videoで見られますので、直接名言を感じてください。

*1:何回かメンバーが変わっているが、自分が見たことがあるのはこの3人のとき

*2:やることはもちろん会話さえもめちゃくちゃな

*3:https://ja.wikipedia.org/wiki/%E3%83%9B%E3%83%83%E3%83%88%E3%83%8F%E3%83%83%E3%83%81

*4:https://en.wikipedia.org/wiki/Volkswagen_Golf_Mk2

*5:何故かHammondだけHammondと呼ばれている

*6:https://en.wikipedia.org/wiki/Opel_Corsa

*7:http://www.guide-autosport.com/guide/ford-fiesta-xr2i-16v.php

ある合宿参加者の記録

この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事を見てください。

サマリ

  • ぴょこりんクラスタ 夏の大ハッカソン(仮)*1が10月開催
  • そのときの作業ログをもとに合宿を振り返りと、今だから言えるコメントを追記する

合宿における自分の役割

  • 会議室予約、ホテル予約、会計
  • 参加者

参加者(全部はてなidで表記、順不同)

企画者によるメモ

合宿で何をやったか

これの前身を作っていた。

合宿前日以前

  • 09/13

    • 宿泊地が決まり、予約を取る
  • 09/22

    • darumapからしおりが届く
    • テルチェックイン前の作業スペースとして、貸会議室を予約する
  • 10/08

    • LT用の原稿を作成する
    • markdownをpandocでスライドにしようとして、フォント設定でちょっとはまる
    • 最終的にこれをヘッダとして差し込んで解決した。WSLでやってたのでYu Gothic使いたかったけど、 Source Han Serifはきれいなフォントだったのであきらめることとした。
    • Windows 10 Homeだと、野良exeファイルを拡張子に紐付けられないことに気づかず*2、1時間くらい消費した。
---
title: Pyokkorin hackathon camp first lightning talk
author: nbisco
date: 2018/10/27
header-includes:
    - \usepackage{luatexja-otf}
    - \usepackage{luatexja-fontspec}
    - \setmainjfont{Source Han Serif}
    - \setsansjfont{Source Han Sans JP}
    - \hypersetup{unicode=true}
    - \renewcommand{\kanjifamilydefault}{\gtdefault}
---

合宿日当日

  • 1日目 10:00 合宿開始
    • みんな時間どおり集まり、ライトニングトークを始める
    • 自分のPCはUSB Type-CもしくはType-Aしかなくて、プロジェクタにつなぐのに手間取った。 ドライバ落としてきたらなんとかなった。
    • 各自担当分野が全く違うので、話を聞いてて楽しい。ただし、助け合いは無理そう。
  • 1日目 11:15 ライトニングトーク終了、作業開始
  • 1日目 12:08 手作業による/proc/{pid}/memの読み書きを確認
    • readelfで読み取った値を手計算してアドレス算出
    • 環境は以下
  • 1日目 12:13 DWARFについての勉強着手

    • 今だから言える:ここですでに方向性を間違えている。自分が必要だったのはシンボルテーブルの内容であって、デバッグ情報じゃなかった。 以降、ゴールに関係ない迷走で2時間消滅する。
    • 参考資料1: https://qiita.com/mhiramat/items/8df17f5113434e93ff0c
    • 参考資料2: http://www.dwarfstd.org/Home.php
    • DWARF表現: DWARF内でlocationを表現するために用いるスタックマシンに対する演算命令(せっかくmarkdownで表形式にしたので載せておく)
      • reg0、reg1、...、reg31: 汎用レジスタ。個々のアーキと対応付けられる。
      • regx: 汎用レジスタを示すオペコード。引数がレジスタ番号になる(例: regx 0はreg0)
      • fbreg: フレームレジスタの値からの相対オフセットでメモリ上に保存されている場所を示すためのオペコード。 オフセットを表す引数を1つ取る。
      • breg0、breg1、...、breg31: 汎用レジスタからの相対オフセットでメモリ上に保存されている場所を示す。 オフセットを表す引数を1つ取る。
      • bregx: 汎用レジスタからの値からの相対オフセットでメモリ上に保存されている場所を示す。 このオペコードはレジスタ番号とオフセットを表す引数を2つ取る。
      • addr, const*: アドレス値や定数値を示すオペコード。仮引数が定数の場合などに使われる。
      • x86のアドレスとの対応(http://www.ucw.cz/~hubicka/papers/abi/node14.html)
      dwarf x86_64 memo
      r0 rax general purpose
      r1 rdx general purpose
      r2 rcx general purpose
      r3 rbx general purpose
      r4 rsi general purpose
      r5 rdi general purpose
      r6 rbp Frame Pointer
      r7 rsp Stack Pointer
      r8-15 r8-r15 extended integer register
      r16 return address
      r17-24 xmm0-7 SSE registers
      r25-32 xmm8-15 Extend SSE
      r33-40 st0-7 floating point register
      r41-48 mm0-7 MMX register
  • 1日目 14:00 会議室から出て移動

  • 1日目 16:22 宿チェックイン
    • 部屋は広いし、無線LANはそこそこ速いしでよかった
    • 眺めもよい
  • 1日目 17:00 libdwの使い方がさっぱりわからないので、src/addr2line.cを覗いてみるも、やっぱりよくわからない
    • 今だから言える:この時点でも迷走は続いている。libdwなんて見る必要なかった。
  • 1日目 18:00くらい libdwを使うのは一旦諦め、小さいサンプルが手に入ったlibbfdを使うことにする
  • 1日目 18:24 libbfdのサンプルをビルドして期待通り動くことを確認した
    • ツールはこんな感じにする
      • input: グローバル変数名と設定する値
      • output: 以前の値と更新後の値
      • 処理フロー
        1. 対象バイナリのグローバル変数のシンボルとアドレスの一覧を作成する
        2. シンボルと入力されたグローバル変数名のマッチング
        3. マッチングがとれればアドレスを計算して、メモリにI/O
  • 1日目 18:50 darumapを迎えにホテルロビーへ。ついでに晩ごはん。
  • 1日目 20:30 晩飯終了。やはりというかやむなしというか、酒を各自購入して部屋に戻る。
    • Before: これは真面目な会なので酒はNG
    • After: やっぱりなんかアルコールほしい気がするんだよな~~~~~
    • 今だから言える:やはりお祭りとしての側面もあるので仕方ないよね。
  • 1日目 20:35 作業再開
  • 1日目 22:00 darumap、突如キングギドラ公開処刑を大音量で流し始める。僕はリアルなので手を叩いた。
  • 1日目 22:30 温泉タイム。露天風呂に入れないのが残念。
  • 1日目 23:20 再開
  • 1日目 24:05 いろいろあきらめた手法で4Byteの書き換えに成功。動くのが正義だ。
  • 1日目 24:15 区切りがついてしまったので、あきらめて酒を飲み始めた
  • 1日目 24:45 全員に悪い空気が伝染してしまい、mow*3開始。 頑なに酒を拒否していたdarumap、「酒を入れざるを得ない」といいおもむろに飲み始める。
  • 1日目 25:15 2ラウンドして終了
  • 1日目 25:30 就寝
  • 2日目 5:00 起床。だがすぐに2度寝。
  • 2日目 6:30 再度起床。
  • 2日目 6:45 朝食
  • 2日目 7:30 露天風呂
  • 2日目 8:00 作業再開
    • ここでは主にスライドづくり。内容を詳しく知らない人に伝える文章というのは難しいので、訓練が必要だと切実に思った。
    • Google Slidesが書きやすくていい。LibreOfficeなんかよりもずっと軽快に動く。
  • 2日目 9:30 成果発表
    • みんなの力作が見られて本当に楽しかった。自由に質問が出るし、誰も怒る人はいない。
  • 2日目 10:45 チェックアウト
  • 2日目 12:00 昼飯 & 解散
  • 2日目 16:00 帰宅・休憩して作業再開
  • 2日目 17:00 binutilsのソースを覗いたら、libbfdを使わずに実装しました and libbfdでは取れない情報もある みたいなことが書いてあったので諦めてelfutilsを使うか、直接自分でバイナリ読むソース書いたほうがいいんじゃないかという気がする。 しかし今日のところはここまで。疲れた。
  • N日後 これを作ってアドベントカレンダーの記事を作成、今に至る。

反省

  • よかった
    • 知らない分野に触れられた
    • 発表までしたことで理解が深まった
    • 終始集中して作業できた
  • いまいち
    • 調査不足でだいぶ迷走した
  • どちらともとれる
    • 1泊は短いような、ちょうどよいような。
    • みんなで助け合いみたいなこともやってみたかった。助け合いがなかったので集中できたという説はある。

おわりに

学びも成果も大きいのでまたやりたい。

*1:もくもく会だったりいろいろ呼び方があるので仮扱い

*2:あくまで挙動からの推測

*3:みんな知能を失う楽しいゲームです

我が家の家計簿システムについて

この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事をみてください。

サマリ

  • 我が家の家計簿(支出しか記録してないけど)システムについて紹介する
  • 家計簿システムの機能
    • 月ごとの累計支出額計算
    • 月ごとの各自の負担分の計算
    • 支出状況の可視化

はじめに

お金を貯めたい、でも貯まってる感がない。ボーナスでなんとなく貯まることもあれば、 財形で別口座に逃がしているという謎の安心を感じることもある。 そう、俺たちは雰囲気で家計をやりくりしている。

例えば、こんなことはないだろうか。

  • 今月は飲み会多かったけど飲み会だし大したことない
  • 今月は自分へのご褒美だから出費は多少増えているけど費用対効果は高いのでOK
  • 今月はボーナス出たからだいたいプラマイゼロかプラスになっているはず
  • 毎日コーヒーを買うくらいの自由はあるし、まあそんなに高くないから大丈夫でしょ

こんな感じの理屈で自分を納得させたことは数え切れないほどあるはずだ。 これらの共通点は定量性のなさだ。定量的に把握できないから、雰囲気でやりくりすることになってしまう。

お金を使うことは問題ではない。いくら使っているかわからないのが問題である。 いくら使っているのかわからないと、毎月赤字なのか黒字なのかわからない。だから、お金が貯まらない。 そんな状況を脱出するためには、まず自分が何に対して使っているのか知らなければならない。

収支を知るための道具の1つに、家計簿がある。 今回は、家計簿への支出の記録・集計・可視化のために何をやっているかをまとめる。

家計簿システムの要件と実現方式

以下の表にまとめる。

# 分類 要件 実現方式 理由など
1 記録 家族が各自支出を入力できる Zaimの共通アカウントを用意して、それぞれのスマホからZaimアプリで入力する 使ったことがあったから
2 記録 過去データの取得 自作の取得ツール ZaimにはRead onlyだけどAPIがあるから。手動は絶対に続かない。
3 記録 過去データの保存 SQLiteの利用 慣れてるPythonから特別なライブラリ不要で使えて、あとで加工も簡単にできそうだから。何より新しくデータベースをインストールして依存関係を増やすのが嫌だから。
4 集計 各自の支出分と負担分を算出する 自作の計算ツール Zaimの共通アカウントにはそういう機能がないため
5 可視化 集計結果の表化 Google Spreadsheet + 自作のアップロードツール どこからでもブラウザがあれば見られるから。あと、最悪Zaim乗り換えても、Google Spreadsheetは乗り換えないから。
6 可視化 集計結果のグラフ化 Redash SQLiteをデータソースとして選択できて、それなりに開発がされてそうだったから。あと、ブラウザから結果を見るのはもちろん、SQLクエリも書けるから。

我が家の家計簿システムの概要

我が家の家計簿システムの概要は以下の図のとおり。

f:id:nbisco:20181208224442p:plain
家計簿システムの概要

使い方は以下のとおり。

  • 記録・集計
    1. 各自がZaimのアプリから逐次支出を記録
    2. 毎日1回、Zaimのサーバからデータを取得して、SQLite3に反映
    3. 毎月最終日には、その月分の支出をまとめてGoogle Spreadsheetにアップロード
  • 可視化された結果の確認
    1. 月の初日に、各自の支出額、負担額を確認
    2. 週1回くらい*1に今月の支出状況の確認

家計簿システムのソースコード

github.com

Google Sheets APIのサンプルを流用して作ってるので、Apache License。 個人情報っぽいところは全部落とした・・・はず・・・。 正直申し訳ないのは、githubに上げたにも関わらず他人が使えるような状態じゃないところで、 ある意味技術ポエムと言うべきものになっている*2

道具の紹介

ここでは使ってるもの、作ったものについて紹介する。

Zaim

Zaimとは、有名な家計簿アプリの1つ。 我が家では、家族用アカウントを作って、それぞれのスマホから入力するようにしている。 見た目はわかりやすいし、入力にも困らないので、他*3を試すことなく使い続けている。

だいたいにおいて困らないZaimだけど、いくつかいまいちな点がある。例えば以下。

  • APIがRead Onlyなところ。定期的な固定出費(例えば家賃とか)を自動入力したい。 1/19に再確認したら普通にWriteできるAPIがあった。何かと誤解していたのかな?
  • 誰が入力したかわからないところ。そのせいでカテゴリをかなり細かく分けざるを得なかった。
  • カテゴリの追加が面倒。

Google Spreadsheet

もはや説明はいらない。本当に便利でありがたいですね。 家計簿以外に何かを集計したり、作業リスト代わりにしたりと、我が家では大活躍。

SQLite

SQLデータベースエンジンの実装の1つ。データベースエンジンと言っても、単にライブラリPythonさえあれば使えるので、今回のような小規模利用で依存関係増やしたくない場合にちょうどいいと思う。

Redash

データ可視化ツールの1つ。きれいなグラフを作ってくれるし、Dockerイメージも配布されているので導入も簡単。 案外、SQLiteがデータソースに選べる可視化ツールはあまりなくて、選択肢は実質これくらいだったような気がする。

自作ツール

ファイルとやってくれること

以下のとおり。

  • get_zaim_auth_key.py: ユーザが直接実行する
    • ZaimのAPIキーを取得してくれるが、完全自動じゃない(いまいち)
  • db_build.sh: ユーザが直接実行する
    • 現在あるデータベース(固定でzaim.db)を消す
    • SQLiteファイルを作成する(実際に処理をするのはdbgen.py)
    • Zaimから過去データを吸い上げてSQLiteファイルに格納する(実際に処理するのはzaimapi.py)
  • dbgen.py: ユーザが直接実行しない
    • SQLiteファイルを作成する
  • gspread.py: ユーザが直接実行しない
    • Google Sheets APIを叩いて、Zaimから取ってきたデータをアップロードする
  • zaimapi.py: ユーザが直接実行しない
    • Zaim APIを使ってデータを取得する
    • 取得したデータをSQLiteに格納する
  • zaim.py: ユーザが直接実行することもある
    • Zaim APIを使ってデータを取得し、SQLiteを更新する(実際に処理するのはzaimapi.py)
    • 各自の負担分を計算する
    • (指示されたときだけ)Google Sheets APIを使って、データをアップロードする(実際に処理するのはgspread.py)
  • kakeibo.sh: ユーザが直接実行する
    • zaim.pyをキックして出力をログファイルに残す(ずっと残り続けるのでいまいち)
    • 最終日かどうかを判定して、Google Sheets APIを叩くかどうか決める
  • packages.txt
    • pipで使っているパッケージ群

使い方の流れ

以下、全てcloneしたディレクトリ直下にいることを前提とする。 カテゴリ作成と認証が面倒だけど、最初1回だけなので耐える。

Zaimでのカテゴリ作成

負担分を明確にするには、誰が何のために払ったかを明確にする必要がある。 Zaimのいまいちなところは誰が払ったかわからないところなので、そのための情報を カテゴリにいれる。つまり、入力者分だけカテゴリを作らなきゃいけないので重労働が発生する。 さらに、個人出費も記録できるようにするため、さらにその倍カテゴリを作らなければならない。これは苦役である。

githubソースコード上は、alphaさんとbetaさんの2人で記録するという体にしてある。

ZaimのOAuth認証~DB作成まで

一部蛮族的操作が必要なのはすまない気持ちでいっぱいだが、初回1回だけなので耐える。

  1. Zaimの開発者アカウントを作成する(単にログインすればよい)
  2. ここを参考にZaimアプリケーションを登録し、Consumer IDとConsumer Secretを入手する
  3. 入手したConsumer KeyとConsumer Secretを、.credentials/zaim_secret.jsonに以下のように記入する gist.github.com
  4. get_zaim_auth_key.pyを実行すると、認証用URLが表示される。ブラウザでアクセスする。
  5. ログイン後、get_zaim_auth_key.py内のcallback_urlで設定したURLへリダイレクトされるが、リダイレクト先をちゃんと作ってないとアクセスに失敗する。 失敗しても慌てず、ブラウザのURLを見て、oauth_tokenとoauth_verifierをメモする。
  6. .credentials/zaim_secret.jsonを更新する gist.github.com
  7. db_build.shを実行し、SQLiteファイル(zaim.dbという名前で作成する)を作成する。このスクリプトは指定日(YYYY-MM-DDで指定)から今日までの全部のデータを取得するので、 自分が取得したい日を引数で指定する。

Google Spreadsheetの認証~Zaimからデータ取得~Google Spreadsheetへのアップロードまで

  1. これとかこのあたりを読んで、 sheet IDとか、client_secret.jsonを入手しておく。入手したら、gspread.pyのSPREADSHEET_IDとCLIENT_SECRET_FILEを変更する。
  2. ./zaim.py --spreadsheet --noauth_local_webserverを実行すると、Zaimからデータ入手したあと、SQLiteを更新する。
  3. --spreadsheetオプションを付けているので、SQLite更新後、Google Spreadsheetにアップロードしようとするが、初回はOAuthのトークンがないので、 トークン取得処理が走る。ここのサンプルコード実行時みたいに、認証URLが出るので、指示に従う。
  4. 認証がうまくいくと、"YYYY-MM"という名前のシートができていて、以下が入力されているはず。
    • それぞれが支払った金額
    • それぞれが分担する金額
    • 清算金額
    • Zaimで入力したデータ

Redashを用いた可視化

こんな感じのものが日々更新されていく。以下の画像は雰囲気だけの例。

f:id:nbisco:20181209091906p:plain
redashでの表示例

  1. Redashコンテナを起動する。自力で入れてもよいが、依存関係解決がけっこう大変だと思うので、コンテナがおすすめ。
  2. データソースにzaim.dbを指定する。
  3. 自分の好きなようにSQLで集計する。

日々の動作

kakeibo.shを実行する。cronとかに登録しておくと楽でよい。 入力漏れとかでシートを作り直すときは、ブラウザでシートを消してから実行する必要がある(これもいまいちだけどたまになので残っている)。

これを作って変わったこと

  • プラス
    • 自分たちのやりくりの実力がわかるようになった
    • いくら使うかだいたいわかるので、先々の計画が立てやすくなった
    • 何を減らすべきかを雰囲気でなく数字で考えられるようになった
  • マイナス
    • "あっ、こんなになんで使っちゃったんだろう(悲しみ)"みたいな気持ちになってしまうことがある。強い心が必要。

おわりに

支出が追いかけられるようになり、自分たちが雰囲気に流されることは減ったが、 それでも使うときは使っちゃうし、記録できてもダメなときはダメという感じである。 ただ、先々の計画が立てやすくなったのは本当によかった。

*1:最近は気が向いたときになってしまっている・・・

*2:自分の書いた記事に技術ポエムじゃないものが果たしてあっただろうか

*3:例えばマネーフォワードとか

ハリーポッターの感想の変遷

この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。

まとめ

年齢とともに感想は変わるし、名作は何度読んでもおもしろい。

中学生

大学生

  • 爆発したりしない分刺激が足りない
  • 実用的な魔法がそろってて文化の違いを感じる
  • ハリーポッターが生意気だしきちんと教育すべき
  • どうしてスネイプ先生主人公じゃないの???

社会人

  • よく考えてみればポッターはまだ10歳、年齢の割にしっかりしてるし、多少生意気だったり調子に乗るのは仕方ない
  • ダーズリー家に入れた判断は正しかった、こんな調子でちやほやされるとろくなことがなさそう
  • マルフォイ見てると、バック・トゥ・ザ・フューチャーのBiff一味を思い出す。成敗されないところが違う点だな。

おわりに

好きなシーンは何回読んでも変わらなくて、スネイプ先生がダンブルドアの前でエクスペクトパトローナムするところです。 10年たったらまた読もう。

グローバル変数を横から覗く

この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。

サマリ

Linux/proc/${pid}/memを使って、グローバル変数を読み書きするものを作りました。 ptraceを使わなくても読み書きできます。使い方は以下のとおりで、WRITE_VALUEを省略すると リード、WRITE_VALUEを書くとライトします。1、2、4、8バイトの変数に対応できますが、 配列の途中とか構造体のメンバ変数はまだダメです。ELFバイナリを読むので、 C/C++などで作ったバイナリでないと動きません。

# ./gvtool.py </path/to/binary> <PID> <GLOBAL_VARIABLE_NAME> {<WRITE_VALUE>}

ソースコードはこちら。 github.com

動作はこんな感じです。

背景など

プログラムをチューニングする場合、パラメタをどこに置くかは結構悩ましい問題ですが、 ともかく手を抜くことを考えた場合、グローバル変数にえいやと書いてしまうことがあります*1

そのグローバル変数gdbなどのデバッガで書き換えながらいろいろ動作させるんですが、 デバッガを使うのが億劫なケースがたまにあります。例えば、他のプログラムと連動するようなものをチューニングする場合です。 デバッガを使う場合は、変数書き換え時に逐一プログラムをbreakする必要があるので、 タイミングによっては連動する他のプログラムが異常終了してしまうことがあります。 また、デバッガはインタラクティブに動かす必要があったりで、自動化がちょっとやりにくいというのもいまいちですね。

そういうちょっとした面倒さを解決するために、今回このgvtool.pyを作りました。 pet.py*2がELFを読むためのもの、gvtool.pyが/proc/${pid}/mapsを読んだり/proc/${pid}/memを読み書きするためのものです。

どうやって実現しているか

概要

要はメモリのある一部を書き換えるだけなので、メモリアドレス(=先頭アドレスとオフセット)さえわかればいいわけです。 つまり、シンプルに以下の3ステップでできます。

  • ELFバイナリのシンボルテーブルからアドレスのオフセットを求める
  • /proc/${pid}/mapsで先頭アドレスを調べる
  • 先頭アドレスとオフセットがわかったので、/proc/${pid}/memを読み書きする

ELF: Executable and Linkable Format

Linuxで採用している実行可能なバイナリのフォーマットです。 ELFには、シンボルテーブルと呼ばれる、変数の名前とメモリ空間上のオフセットを 保持しているセクションがありますので、そこを直接読みます。

シンボルテーブルのエントリのフォーマットは、 身近なところだと/usr/include/elf.hに載ってます。64bitだと以下のようになっていて、 ここのst_valueのところがアドレスオフセットになります*3

typedef struct
{
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */
  unsigned char st_info;        /* Symbol type and binding */
  unsigned char st_other;       /* Symbol visibility */
  Elf64_Section st_shndx;       /* Section index */
  Elf64_Addr    st_value;       /* Symbol value */
  Elf64_Xword   st_size;        /* Symbol size */
} Elf64_Sym;

シンボルテーブルはデバッグ情報ではないので、-gをつけてコンパイルしていなくても読めます。

/proc/${pid}/maps

Linux Kernelが提供する、プロセスのメモリマップを教えてくれる疑似ファイルです。 例えばこんな感じで見えます。左から、

  • メモリの開始アドレス
  • メモリの終端アドレス
  • パーミッション
  • ファイルのオフセット
  • バイス番号
  • inode
  • ファイルパス

です。詳しくはここ

/home/bisco% cat /proc/$(pidof a.out)/maps
5591a7f3e000-5591a7f3f000 r--p 00000000 00:15 103135   /home/bisco/test/a.out
5591a7f3f000-5591a7f40000 r-xp 00001000 00:15 103135   /home/bisco/test/a.out
5591a7f40000-5591a7f41000 r--p 00002000 00:15 103135   /home/bisco/test/a.out
5591a7f41000-5591a7f42000 r--p 00002000 00:15 103135   /home/bisco/test/a.out
5591a7f42000-5591a7f43000 rw-p 00003000 00:15 103135   /home/bisco/test/a.out
5591a9395000-5591a93b6000 rw-p 00000000 00:00 0        [heap]
7f32b4401000-7f32b4423000 r--p 00000000 00:15 46396    /usr/lib/libc-2.28.so
7f32b4423000-7f32b456e000 r-xp 00022000 00:15 46396    /usr/lib/libc-2.28.so
7f32b456e000-7f32b45ba000 r--p 0016d000 00:15 46396    /usr/lib/libc-2.28.so
7f32b45ba000-7f32b45bb000 ---p 001b9000 00:15 46396    /usr/lib/libc-2.28.so
7f32b45bb000-7f32b45bf000 r--p 001b9000 00:15 46396    /usr/lib/libc-2.28.so
7f32b45bf000-7f32b45c1000 rw-p 001bd000 00:15 46396    /usr/lib/libc-2.28.so
7f32b45c1000-7f32b45c7000 rw-p 00000000 00:00 0
7f32b45cf000-7f32b45d1000 r--p 00000000 00:15 46385    /usr/lib/ld-2.28.so
7f32b45d1000-7f32b45f0000 r-xp 00002000 00:15 46385    /usr/lib/ld-2.28.so
7f32b45f0000-7f32b45f8000 r--p 00021000 00:15 46385    /usr/lib/ld-2.28.so
7f32b45f8000-7f32b45f9000 r--p 00028000 00:15 46385    /usr/lib/ld-2.28.so
7f32b45f9000-7f32b45fa000 rw-p 00029000 00:15 46385    /usr/lib/ld-2.28.so
7f32b45fa000-7f32b45fb000 rw-p 00000000 00:00 0
7fff149b7000-7fff149d8000 rw-p 00000000 00:00 0        [stack]
7fff149d8000-7fff149db000 r--p 00000000 00:00 0        [vvar]
7fff149db000-7fff149dd000 r-xp 00000000 00:00 0        [vdso]

グローバル変数は、heapでもstackでもなく、bssという領域にあります。 bssという領域は読み書きできるので、上から5番目の領域にいそうだな、ということが何となくわかりますね。

シンボルテーブルに載っているオフセットは、x番目の領域のオフセットではなく、先頭からのオフセットです。 a.outのグローバル変数にアクセスしたければ、a.outの先頭アドレス(例で言うところの0x5591a7f3e000)がわかればよいです。 この先頭アドレスに、 シンボルテーブルから得られたオフセットを足せばメモリアドレスがわかります。

/proc/${pid}/mem

Linux Kernelが提供する、プロセスのメモリ空間へアクセスするための疑似ファイルです。 setuid-rootなバイナリ(例えば実行者がrootとか)であれば、 プロセス${PID}をbreakすることなくメモリの読み書きが可能になります。 書き込みが可能になったのはKernel 2.6.38-rc8(2011/03/08)からであり、 昨今の大抵のディストリビューションで使えます。

以下、LKMLより抜粋。デバッグ用途で使ってくれとのことみたいですね。

  This patch series enables safe writes to /proc/pid/mem.  Such functionality is
  useful as it gives debuggers a simple and efficient mechanism to manipulate a
  process' address space.  Memory can be read and written using single calls to
  pread(2) and pwrite(2) instead of iteratively calling into *ptrace(2)* 
  In addition, /proc/pid/mem has always had write permissions enabled, so clearly it
  *wants* to be written to

まとめ

  • /proc/${pid}/mem/proc/${pid}/maps、シンボルテーブルの情報があれば、動いているプログラムのグローバル変数を読み書きできる

*1:そんなことはしないのが一番だと思いますが

*2:Python Elf Tool

*3:バイナリの種類によっては意味が違いますが、ここでは触れません