就職して9年が過ぎる

転職して7年が過ぎたというのを読んで気づいたんだけど、そろそろ入社後9年が経過したらしい。僕は結構長い期間をここで過ごしたことになるんだなと思った。ちょっと以前のことを振り返ってみようと思う。言うまでもないけどこれは僕の書ける範囲での個人的な感想と体験談であって会社の見解等を表しているものではない。

きっかけ

わりと重要でない

Borgチーム (の周辺)

いつのまにやらBorgという名前を普通に言って良くなっている。嬉しい。まあ当時もぶっちゃけ、秘密だから出してないっていうよりは、単に誰もアカデミア的なキャリアに興味が無いから出してなかったんだと思う(私見)。

さて、当時Borgというかクラスタマネージメントのあたりでは、コンピュータのリソースて適当にたくさん使ってるけど、これ節約したらすっげー支出減ったりしない?みたいなのがホットで、なんかとりあえず色々な人々が色んなことをやっていた。いくつかうまくやったチームがあって、実際すっげー減ったんじゃないかな。最初の3ヶ月は練習期間的な感じで、そういうのの一つでチマチマPython書いたりしてた。

で、日本に帰ってきて僕の入ったチームでは、色々なクラスタの変更の結果が予想しやすくなる、みたいなのを作っていた。Borg自体のコード使ったりもしてたこともあり、ちょっとパッチ投げたりもしたけど、まあ総じて重要度はそれほど高くないプロジェクトだったのかなぁと、今となっては思う。でも新卒で入った僕にわかるわけもなく、そしてエンジニアリング的にはすごく勉強になった。指導してくれた人達がすごかった。

glog (憧れの20%プロジェクトは80%より楽しくなかったでござるの巻)

当時僕はGoogleさっさと辞めると思っていた。しかしGoogleにあるものは色々便利なので、なんか色々opensourceになってると良いなあとか思っていた。Google入って最初の頃に特に感銘を受けた文化に、とりあえずログ残して、用途は後で考える、てのがあった。ログつーとアクセスログみたいなやつと、プログラムがこんなことあったぜ、て端末に吐き続けるログがある。前者は当然すさまじく重要なんだけど、後者も有意義。

まークラウドみたいな環境だと、たまーに起きた問題にgdbで対処する、なんてのはあまり現実的でないことが多く、ログとコード見比べて、ああここNULLになりうるな、みたいな感じのデバッグシーンが多い。まあそんな感じで、使いやすいロギングライブラリがあるってのは重要なのである。

で、このくらいの規模感のものなら僕個人でもopensource化とかできないかな?とか思ってやってみた。当時社員番号3番(つまり最初の従業員)の偉い人が色んなものをopensource化しており(gflagsやperftoolsが一例)、この人に相談するといいかな、って思ってメールしたりした。その偉い人は即日色良い返事をくれていたのだけど、パソコン音痴の僕はgmailにおかしなフィルタを設定してしまっており、彼の返事はなんだか目に入らないところにいっていた。1ヶ月ほどしてから、あれ、そういえばあれ返事もらえてないな?とメールボックスを検索したら返事くれてるのに気付いて、さすがに青冷めた。クソ偉い人にいきなりメールして、即日反応してくれたのに1ヶ月放置するクソ新卒である。その後、クソ新卒のコードレビューをずっとやってくれてたんだから3番良い人すぎた。

そのエピソードと、社内のコードがどう育ってきたかというのを読みつつopensourceとして出せるものにしていく考古学的作業は楽しかったが、リリースしてしまうと、*BSDで動かないとかなんとか、うん、めんどくさいね、てなった。

GSLB (のクライアント)

GSLBてのがあって、これクラスタ内じゃなくて、どのクラスタに行くべき?みたいなレベルのロードバランスしてるんですが、それのクライアントライブラリみたいなのを作っていた。

思うになんかこれは、「なんでこんな当然あるべきもんが無いの?」系のプロジェクトだったと思う。よって、テクニカルにはその前のプロジェクトの方が面白いことをやっていたように思うけど、こっちは作れば確実に成功するていう。僕が言い出したとかでは全然なく、指導してた偉い人がUSに行って面白いネタ無いって聞いてきて、こいうのあるらしいからお前やったら、て言われて気付いたらテックリードという象徴天皇みたいなのになってた。

GSLB上流てのはクソ忙しいチームであり彼らとのコミュニケーションが必須ということもあり、たいしたコード量じゃないのにすごい時間がかかる…みたいなプロジェクトだったけど、まあなんか評価はされるみたいな不思議な感じだった。テックリード的な役割て俺向いてないなぁ…とも感じた。でもびっくりするくらいコード書いてない割には評価は悪くないんだよね不思議。

なるほど、企業というのはコーディング力じゃなくて会社に対するインパクトで評価するのだな、という当たり前のことを実感したように思う。

Chrome (で落穂ひろい)

小さいチームはコミュニケーションコスト的に損、ということでインフラぽいプロジェクトが解体される。今思えば、目をひくほどの成果があるわけでも無い、という要因も重要だったと理解している。評価は悪くはないというだけであり、まあ誰にでもできることをやってたという感じで。

僕はなんとなく something new …てことで Chrome に魅かれていた。周りの人はそうでもない人が多くて、2,3人で Chrome やるぜって盛り上がってたと思うんだけど、 Chrome の当時の偉い人が日本に来て、 html5 やるぜーて騒いで、なんか周りの人も乗り気になった。そして東京に Chrome の結構大きい部隊ができた。

まわりもほとんど全員 Chrome 初心者、みたいな感じなので、みんなパッチ修行と称して、細かいバグ潰しては周りの人と情報交換して…みたいな感じでコードベースに慣れていった。割と適当なバグを重要性無視してプチプチ潰す、みたいなのは好きなので、まあプチプチしてた。当時 Chrome てコードが割と雑然としてて、僕綺麗な環境で働くより雑然としたとこの方が得意だってのもあって、まあまあ効率は悪くなかった気がする。やってた作業が重要なものだったかは謎だが。まあEUC-JPのURLコピペして動くようにとかは普通に役に立ったんでないかな。

WebKit (Appleとの謎の関係)

ある程度 Chrome に慣れたら、 html5 の機能を実装していこう、みたいになって、手つけられてない CSS3 とかやったら、みたいなことで WebKit に親しんでいった。そこで CSS の細かいバグプチプチなんかもやったのだけど、それよりも WebKit の開発サポートツールみたいなスクリプト群の出来の悪さや、コーディングスタイルはあるが強制されてないのでみんな破ってる、みたいな状況がうへえ、て思って、後半は C++ より開発サポートツールのために Python 書いてることが多い、みたいな感じになった。

そんなこんなで難しい C++ のコードとか書かずに Python パッチ量産した結果、パッチ数だけは溜まってる上に、やってる作業的に Apple の人に悪印象持たれてないということもあってか、割とさっくり WebKit reviewer というのになった。あとその後は印刷系の CSS3 のなんかとか少しやったと思う。

WebKit は総じて Chrome よりさらに雑然としてたのもあり、やるべきことが無数にあって、まあなんか思いついたとこいじるみたいな感じだった。何をやるべきかって考える時間が無いてのは僕にとっては良いことで、総じてそれなりの効率だったように思う。やってた作業が重要なものだったかは依然として謎だが。次のプロジェクトに移った後で起きた Blink フォークにはびっくりしたな…

goma (名前に意味はない)

東京オフィスでなんか面白いことやろうぜ、ということでオフィス内アイデアコンペみたいなのが行なわれる。アイデアの段階で半分死に、1週間でまた半分、1ヶ月でもう半分、みたいな感じのやつ。

当時 Chrome の開発には割と満足してたけど、グーグルの社内コードベースに比べて格段にビルドが遅いのがむかついていた。で、社内にあるスゴdistccみたいなやつを、そのままパクったらいいんじゃね、的に goma てのを始めた。社内のやつは社内コードベースに特化してるのだけど、まあそうじゃないとこから簡単にセットアップできます、みたいなコンセプトで。

なんかビルド2,3倍くらいは速くなったなー、ユーザ増やすために宣伝しないとかな?とか思ってるうちに、USのちょっと人気者みたいな人が、「goma 試したけど、これあればdistccとかもういらねーよOMGOMG」的なことを言ってくれたので、苦手な宣伝作業というのをしないでも勝手に流行った。なんか気がついたらChromeチームは全員goma使ってた。口コミの強さよ。

みんなが使うと色々問題が起きたり、新しいコンパイラが欲しいとか言ってきたり、などなど…ということでぼんやり平和に過ごしていた。この時 Mac サポートのために半分ネタで作った maloader が本当に使いものになったのは愉快なできごとであった。

ARC (低レイヤおいしいです)

当時僕は Google Native Client というやつの大ファンだった。今でもグーグルが作った一番クールなものの一つくらいに思っている。それの上で Android アプリを動かそうとしてるプロジェクトがある、という噂を聞いていた。入れてもらえないかなとか思ってたら、なんか長期出張から一時帰国してた同僚が誘ってくれて、やることになった。

これはもう本当に想像よりもクレイジーなプロジェクトに、すごい優秀な人達が全力で挑んでる、みたいな感じですごい楽しかった。僕はだいたい、3ヶ月くらい楽しーて仕事したと思ったら、ぼんやり気味な期間が後で来ることが多いんだけど、この時は1年近く楽しさが持続してたと思う。

POSIXPOSIXじゃないものでエミュレートしたり、BionicていうlibcをNaClにポートしたり、seccomp-bpfをNaClの強いsandboxのかわりに使うモードをNaClに足したり、みたいなことやってた。つまり何やってんだかよくわからないことやってた。

僕、社内で ChromeAndroid の存在を知った時は、「なんてアホなプロジェクトだ、こんなの成功するわけない!」て思ってたんだけど、数年後にはその両方とからむプロジェクトをしてたってのは、僕の先見性の無さがよくわかる話だと思う。

NaCl Development Environment (趣味20%)

NaCl ファンとして 趣味で TinyCC を NaCl にポートした上に NaCl にリターゲッティングしたりしてたんだけどGCC もポートしたりしてたので、 NaCl ターゲットの nacl-gcc を NaCl 上で動かす遊びとかをしてた。

ついでに基本的な Unix ツールBash on NaCl の上で動くようにしたりと。つまり Chrome だけあればネットワーク無しでも開発できる、てのが夢というわけ。

したら NaCl チームの人が気にいって、なんかよくわからないクロームエクステンションを作ってくれてた。

https://chrome.google.com/webstore/detail/nacl-development-environm/aljpgkjeipgnmdpikaajmnepbcfkglfa

kati (負けない)

Chrome から Android に移った人が、ビルド遅くて不愉快、 ninja+goma 良かった、みたいなことを言っている、という噂を聞く。 goma 時代にちょっと見た時はとりあえず make がアカンすぎるという感想で、 ARC 時代に Android.mk をだいぶ見慣れてたこともあって、「あーありゃGNU make作りなおすんが手っ取り早いね」「んじゃやってみろ」てな感じでやってみた。

なんか色々トントン拍子でいって、これも宣伝工作とかを特にすることもなく、 Android 原住民が超協力的な感じで、あっというまに Android にチェックインされ、 kati+ninja がデフォルトになり、 make でビルドする手段が消された。

最近は Android.mk の方も少しいじったりしてる。

総じて

自慢じゃないけどキャリアパスとか微塵も考えてなくて、偉くなるには人を指導しなければ、みたいなアドバイスは左の耳から出ていっているので、特にそういう点では、なんの参考にもならないと思う。思うに僕は運が良いと思う。そういう意味でも参考にならないと思う。まああと環境的にも他の企業とだいぶ違うと思う。

でもなんか、特に最近意識的にやってて、結構他の人/環境でも適用できるんでないかなと思ってることとして、小さい投資をたくさんする、てのがある。例えば kati 始めた時とかは、 GNU make のクローン作るとかいかにもできなさそうなんで、「とりあえず一ヶ月ください無理そうならさっさとやめます」みたいな感じではじめた。僕は普段、もう少し細かい単位でも、例えば「来週やってみてダメそうならやめる」みたいな感じで時間を無駄にすることをかなり頻繁にやっている。そういう試みの半分以上は失敗に終わるけど、たいして時間を投資してないのでたいした問題にならない。

これについて、個人的に重要だと思ってることがいくつかあって。まずあまりマジメに相談せず勝手にやるってこと。相談すると、5割くらいの確率で「俺がやってるプロジェクトでその問題は解決するよー」とかいう返事が帰ってくる気がする。やる気が損なわれる。「それうまくいかないと思うよ」て言われてやっぱり失敗したら恥ずかしいよね。明らかな結論が出ない場合、どっちのアプローチが優れてるかとか机上で考えるのも時間のムダ。1週間から1ヶ月しか投資しないなら、作ってみて比べりゃいいんで。あと少人数でやること。人数増えると引っ込みがつきにくくなっちゃって、失敗させるのが難しくなると思う。要はサンクコストでかくしないてことか。当たり前の話だなあ。

あと投資対象、なんとなくだけど、ぱっと思いついただけの、いかにもダメそうなアイデアが割とうまくいく気がしている。「次なにするべきか…」とか熟考した上で出てきたそれっぽいアイデアとか、なんでか知らんけどだいたいうまくいかん気がする。ちょっと理由を考えてみると、ぱっと思いつくようなアイデアは本当に必要性があるからぱっと思いつく、みたいな話かな。

小さい投資たくさんするってのは、割とグーグルから学んだことのように思う。僕がグーグルのいいところだと思ってることに、プロジェクトをガンガン潰す、てことがある。それも失敗してるプロジェクトだから潰す、とかいうレベルじゃなくて、成功してないプロジェクトはふっと潰れるし、小さいプロジェクトは成功してても潰れる。これ、潰されるプロジェクトの人としては、もちろんガッカリ事件になる(特にこれといった失敗をしてない時)。僕も Chrome に移る前に少しガッカリしたと思う。

でも、細かいのガンガン潰さないと、リソースに限りある中で新しいことできないんだよなーと気付いて、なんか新しいことできたから、むしろ潰してくれてありがとーと思うようになり、これ日頃の仕事的にも良い考え方だなーと思うようになった。

bflisp.bf

https://github.com/shinh/bflisp

Lisp インタプリタを作りました。 Brainfuck で。

だいたい sedlispbeflispmakelisp と似たようなことができます。ちょっとバグあるみたいですが。

Malbolge は実装不能だと思うので、これ以上なくキツいターゲットじゃないかと思っています、ので Lisp シリーズはこれで最後でないかと。というか現世的な速度で動くとは思ってなかった。月なみですが、今のパソコン速いですねー。

実現は簡単な 16bit ハーバードアーキの CPU を定義して、魔改造した 8cc で lisp.c をその CPU のアセンブリコンパイル、そのアセンブリなどから Brainfuck を生成、という感じになってます。実行は最適化機能つきの 8bit Brainfuck インプリタでやってます。ある程度最適化しないとまともな速度で動かないと思います。

メモリとかなかなか大変で。 Brainfuck で 16bit アドレス空間の load/store を現実的な速度かつコードサイズで実現する方法がなかなか思いつかなかったのですが、今回思いついたから作業始めた、という感じでした。

仮想 16bit CPU を挟んだのは、 @rui314 さんが sedlisp くらいの時から、直接やるんじゃなくて間に中間的な複雑さのなにかを挟んだ方が簡単じゃないの?って言ってて、それを採用した感じです。 sed/befunge/make の時は挟まない方が簡単だと今でも思ってますが、 Brainfuck 直行はまあムリゲー感しか無いです。

Befunge の時は LLVM bit code からだったのですが、 8cc にしたのは Brainfuck 上で C コンパイラ自体を動かすという夢があるからです。これ実現するにはまだまだやること多いはず…です。 8cc 大変いじりやすくてありがたかったです。

TRICK 2015

口頭でも言いましたが、今回は落選したものでも良いできだなーというのが多かったです。前回が価値が無いというほどでは全然ないんですけど、しかしちょっと差があったのは事実かなと。

僕が高得点をつけたものについて

コラッツ

https://github.com/tric/trick2015/blob/master/ksk_1/entry.rb

すごい。これ見れば見るほどスルメみたいに味が増えていくというか、しかもその味やばいっていうか、この短さのコードで、いやここまで考えさせられるとは、っていう。圧巻でした。

がまあ普通に考えてこれの評価がそろわないのは予想できます。2位でも十分評価されていたと思われます。

ドラゴン曲線

https://github.com/tric/trick2015/blob/master/monae/entry.rb

これは初見の印象はかなり悪かった。単なる Quine てのは、もう相当なことをしない限りは僕の中ではマイナスだと言って良い。

ただコード読んでみると、なるほどなー的な感じでコードがふくらんでいっていたと思う。いやでも忘れつつある。

数独

https://github.com/tric/trick2015/blob/master/eregon/entry.rb

これも最初のイメージはイマイチ。しかしよく読むとよくできてる…ていうかデータの埋め込み方かっこいいよね。こうやると決めればテクニックが必要な領域ではないけど、しかしこの埋め込み方決めたのは感心する。

Fiber は正直解く部分ちゃんと読めてないからよくわかってないけど、それでなくても最高に近い点数あげるから文句言うな、的な感覚で読むの放棄した。。

文字数にエンコードされてるプログラム

https://github.com/tric/trick2015/blob/master/usak/entry.rb

いや本当にこういう一発ネタ好きで。コメントした通り中盤はこうグダグダ感あるな、って気付いたのと、あれこれそんな難しいわけでもないよね、てあたりで点数を落としてしまった。最初はあまり迷いなく10点つけてた…がさすがにおかしい気がして少し落とさせてもらった感じで。

いやーでもこれ好きだな。ここまでのやつと比較してみると、コラッツは発想も実装も僕の枠を越えてて、ドラゴン曲線と数独は、発想はわかるなって感じで、実装はがんばったなーて感じ。このコードは発想がすごくわかりやすいが僕の発想の範囲にないのが好きで。

http://www.garbagecollect.jp/~usa/d/201512b.html#id20151211_P1_6

に書かれてますが、 ; の行は僕も味わいがあって良いなとか思ってました。その前後がちょっと残念かなあとは。

円周率覚え歌

https://github.com/tric/trick2015/blob/master/kinaba/entry.rb

これなー。これが優勝なのは文句ない。ただ前回と違って満場一致ではなかった。それだけ今回レベル上がったって話もあると思う。

これ見てて、なんかぱっと見ムダに長くね、とかいうのが第一感で。しかしそういうなら自分でやれ感もあり。まぁいなばさん円周率計算のゴルフ最短から結構長いんだよな。

もちろん TRICK はゴルフコンテストではないので、このコードの素晴らしさにケチをつけているのは長いというのが直接の苦情ではなく。例えば普通 foo = foo ていう文があったら、えっこれムダじゃない?て思うのと同様の感覚で、なーんかムダな式多くない?ていう感覚がある気がする、て感覚があって。

ただこれ自分で同じコード書こうと少しやってみて、あっこれ大変だって気付いた感じでした。でまあ総じてかなり高い点数に。 srand かっこいいしな。

markdown

https://github.com/tric/trick2015/blob/master/yoshi-taka/entry.rb

いやーこれいいでしょ。あの手この手でパース通してる感が、僕の一発ネタをきちんと昇華してる感じがしてて良かった。特に好きなのは TRICK!!! の !!! で、これビックリマーク1つだとパースエラーなんですよねえ。

でも僕これ強くは押してないんですが、理由としてストーリーがよくわからないというのがあって。。 if not for path in ENV って実はうまいこと言ったりしてるんだろうか、僕には単に ruby の式にしか見えなくて。。。

SAT

https://github.com/tric/trick2015/blob/master/ksk_2/entry.rb

あっ、なるほど、すごいですね。というやつ。これは本当にすさまじいので、ぐぐって SAT て regexp で解けるんだと理解するまでは、感動的でした。ぐぐって出てくる URL は正直に remarks に書いてあって、まあ好印象…がまぁ既存研究かーという感あって。

それでこのへんに。

エスケープ

https://github.com/tric/trick2015/blob/master/ko1_2/entry.rb

これ、すごい好きなんだけど技術的に別にたいしたもんでもないんで、このエントリに書いてる他のやつより1点少なくつけたんだけど、でも個人的には好き。

おまけ

気にいったコードはだいたい上記のやつで、全て入賞したのは良かったなーとか思ってました。おまけで自分のコード

https://github.com/tric/trick2015/blob/master/shinh/entry.rb

バラバラのパーツで任意コード書くってやつ。これ、前回の TRICK から考えてたネタでした。前回の時は %I{} がまだ Ruby に無かったか、気付いてなかったか、それか気付いてたけど3文字連続は避けたいと思ってたか、いずれかの理由で TRICK 2013 には出しませんでした。

eval を %I{ #{ ?e + ?v + ?a + ?l } } で作ってるわけですが、これは遠藤本に載っているテクニックだったということで…全く気付いてなかったのは何故かっていうと、あまりマジメに読んでなかった章の、基礎的なことが並んでるところにしれっと書いてあったから、じゃないかなあと。

あとまあ remarks に書きましたが、整形手法も結構気にいってます。短いので。

a=File.read("remarks.markdown").split
puts a.pack("A15"*a.size).unpack("A61"*19)

もうひとつおまけ

落選した作品に言及するのはどうかと思うんですが、まあいいだろってことで。落選したやつに Yusuke Endoh て書いてある物があって、しかもやることもコードも比較的まあ、どうでもいいかな…的な。はたしてこれは本人なのか、 mame 騙りなのか考えましたが、どちらのケースでも Yusuke Endoh を名乗って有利になることはちょっと考えずらい。本人が捨てエントリに自分の名前を冠することで、他に Yusuke Endoh がいないとアピールしている…などなど困惑しました。

まあなんにせよこんなもん最低点でいいやろ、と容赦なく死んでもらいました。そしたら本人だったびっくり。

まとめ

投稿してくれた方々、ありがとうございました。前回以上に読むの大変でしたが、大変楽しかった。前回に引き続き、技巧的なものも一発ネタ的なのもほどよく混じった感じだったのが良かったかなーと思います。

printf 用のマクロの話

C言語 Advent Calendar 2015 - Qiita 向け


15年ほどC++書いてるんですが、どんどん自分の書くコードがbetter Cになっていっているような気がしています。どんどん先進性みたいなのが劣化している気がするので、特に高度なことは書かない…というか書けないです。

で、なんか C++ でも printf を使ってしまう傾向にあります。

  • cerr だと endl 書かないといけないのがめんどくさいけど自動で改行足すようなやつ作るのは少しめんどくさい
  • フォーマット指定子が思い出せない

という2点が主な理由かなあと思います。あとなんか % で書いてある方が出力が予想しやすいことが多いような気がするんですよね。特に括弧とかがたくさん登場する場合。

LOG("setenv(%s, %s)", name, value);
LOG << "setenv(" << name << ", " << value << ")";

のどっちが良いかというような話。

iostream 使えば operator<< のオーバーロードで自分の型でも表示できる、てのが嬉しいということがあるかと思うんですが、このへんはマクロ使うと少しラクになったりとか。例えば kati ではファイル名と行番号を保持する

typedef struct {
  const char* filename;
  int lineno;
} Loc;

みたいな型があったんですが、これを出力するために

#define LOCF(loc) loc.filename, loc.lineno

みたいなマクロを定義して

Loc loc;
fprintf(stderr, "%s:%d: something something\n", LOCF(loc));

みたいな感じで使っていました。このマクロだと LOCF に渡したものに副作用があると二度評価されてしまいますが、まあ普通問題にはならないでしょう。。

次の例はヌル終端してない、 Pascal 文字列のようなやつ。

class StringPiece {
 public:
  const char* data() const { return ptr_; }
  size_type size() const { return length_; }
};

これも

#define SPF(s) static_cast<int>((s).size()), (s).data()

みたいなマクロを定義してやると、

StringPiece str;
fprintf(stderr, "str=%.*s\n", SPF(str));

というようにすると出力できたりします。 %.*s で文字数を変数で指定できる、ていうちょっとマイナー気味な機能を使っています。

他のマクロネタも書こうかと思ってたんですけど、なんか nothingcosmos さんのやつとかぶってるような、かぶってないようなという感じだったので、書き気も起きなくなったので省略。

HITCON CTF 2015

今回は fuzzi3 というなんか卑怯な人の多さのチームに混ぜてもらいました。用事もあったのでゆるく参加するつもりが割と頑張ったけ。けどゆるく参加しても貢献度変わらなかったんじゃね?という感じの成果でした。

チームがものすごいので4位。他人の成果に乗っかってなんかするのができるのはラクだなーというのと、ちゃんと自分の成果を後に引きつげるように考えるべきだなーとか思った。

https://ctf2015.hitcon.org/scoreboard

hard to say

なんか shell 呼べる Ruby で記号しばり 10B で cat flag しろ、という問題。4つにわかれてる問題で、1つ目とかは結構サイズ制限ゆるい。とりあえず任意コードの記号化プログラム的なものが手元にあるのでそれで1問目終了。全チーム最初の1以上の得点だったと思う。

2,3とうーんどっちかというとshell芸だなーと思いつつ m4 flag とかしてこなす感じで。4つ目は全然わからなくて、1つ目で環境見て遊んでる最中に書き込める領域見つけたのでそこに cat flag て内容のファイル書いて実行して終わり。

https://gist.github.com/shinh/4999b3d154aafc774b91

本当は $0 が /bin/sh だからそれ使えば良かったらしい。

おでかけに行く。

babyfirst

出先で docs を見ると、空白と改行と \w+ だけで shell のコード適当に動かせるよ、 wget で色んなファイル置けるよ、ただファイル名指定できない、ということをチームの人が発見していた。ファイル名指定できないと index.html になるので、 . が使えないと実行できない。うんテザリングであれこれ試すにはちょうどいいくらいかな…とあれこれ試す。

まー redirect っしょーと色々やるも、どうもファイル名が index.html になる。 ftp どうよ、と ftp://.../robots.txt にリダイレクトすると robots.txt という名前で落ちてくる。ああこれいけるなーと思ったけど anonymous write できる ftp わからんかったし ftp サーバ出先で上げるのは困難な気がしたので(よく考えると VPS 使えばできたが)、チャットで言ったら他の人が終わらせてくれた。

risky

酒飲んで帰って、適当に見てると risky というのが x86 でない謎アーキテクチャで僕好み。見る。適当に PLT とか見て命令推測…とか少しやってたけどよく docs 見ると RISC-V であることはわかってたぽいので、ツールチェインインストール…とかやってたけどよく見ると他の人が objdump の結果はってた。

適当にアセンブリを方程式にするコード書いて、でこれどうやって解くかな、と思いつつ結果を docs に貼ってたら、他の人が z3 にかけて終わらせてくれた。なるほどこういう時に使うのかーと。これは解析的にも解けたと思うけど、まああきらかに便利そうだから使いかた覚えよう。。

https://gist.github.com/shinh/c67ed9379ef8e5ef4740

unreadable

他の問題解いてる間に足されてて、 xxd したら終わった。

http://shinh.skr.jp/tmp/unreadable.png

moonglow

心を砕いた問題。フラグのファイル名を知らないプロセスから、同じUIDで動いてる、フラグを知ってるプロセスにフラグの内容を送って、あってるか間違ってるかをチェックしてもらう、って問題。

chroot の中にいるので /proc/*/cmdline とか見てコマンドラインに渡されてるフラグの名前を調べたりできないし、フラグの置かれてるディレクトリは x しか立ってないので中を見ることはできない。 ptrace は yama に殺されている。

なんかマイナーなシステムコールを使うか、チェックにかかった時間を計測するしかないかなーとあれこれする。 prlimit がなにかに使えそうだなーとコアを吐くように変更してみたりするもダメ。 Ubuntu だと apport かなんかに投げるのと、あとそもそも core て chroot されてると出ないらしい。知らなかった。

あと試したのは inotify syslog などなど。時間はかる方も、なんかの間違いで wait4 できねーかなーとやってみたり少し待ってから kill 送って死んだか死んでないかで分離できないか一応チェックしたり。まあ無理に決まっていた。

答えは perf_event_open だったらしい。なるほど perf かー。思いついても良いレベルだった気がするなあ…と悔しい。

waterleaf

なんか波ぽいノイズが乗った動画の中の steganography 。画像化とかはぷよAIの時のがあるのでさっくりできるし、まあ画像系は苦手ではない…ということで見てた。ヒントが出て、波の方向とかにデータ乗ってる、としか思えなくて脊髄反射的に波の方向を調べてどうこうするプログラム書いたり、波の方向目で見てフラグぽい文字列が出ないか…と頑張ってた。

答えは FFT しろってことだった。ヒントも波の形もあきらかにそういう感じでした。。。物理やってたんなら見覚えあるだろうに。。

kati について

https://github.com/google/kati

kati について、ドキュメント書こう…と思っていたのですがなかなか進まないので、とりあえず日本語で書いてみることにしました。何書くかがあまり明確じゃないテーマなので、何書くか考えるのと英語考えるのを両方同時にやるのが少し大変で。

動機

kati は GNU make のクローンです。いずれ完全なコンパチになると嬉しいですが、なかなか難しいだろうと個人的には諦めています。用途に対して実用的ならば良いかなと。

動機としては、 Android platform のビルドシステムが、なかなかシュールな GNU make 黒魔術で構成されていて、 make が実際になんかしはじめるまでが遅かったので、そこを高速化したいというものでした。

ビルドシステムが遅いという時、まずだいたいヌルビルドとフルビルドの2点を考えます。ヌルビルドてのは生成物が全てできている時の、 make て叩いてから Nothing to be done て出るまでの時間で、フルビルドは生成物が全くない時の時間です。現実のビルドは両者の間になりますが、大雑把に言って、ヌルビルドは開発中に .c を一ついじった時のターンアラウンド時間などに強く影響し、フルビルドは新しいリビジョンをチェックアウトした時や多くのファイルから include されてるファイルをいじった時などのビルド時間に影響します。

Android の場合、結構立派なワークステーション+SSDで、ヌルビルドが100秒ほど、フルビルドが30分てとこでした。30分はまぁ良いんです、30000ターゲット以上あるので。ヌルビルド100秒の方が終わっています。 C ファイル一個書き換えて、コンパイルが通るかがわかるのが100秒後というのは結構つらい。 kati 使うと Makefile 書き換えや Java ファイル追加が無い時のヌルビルドは5秒、ある場合で50秒てとこです。

というわけで GNU make クローン作ってみるのはどうよ、ということで始めたプロジェクトでした。最初はコマンド実行もやる予定でしたが、 ninja 出力する方が色々都合が良い部分もあり今はそうなっています。 20% 的に始めたプロジェクトでしたが、気がついたら本職になってて、 AOSP で make て叩くと勝手に kati+ninja でビルドされるようになりました。

最初は色んなものキャッシュするとかちゃんとやれば十分パフォーマンスが出るだろう、という見通しで ukai さんと Go で書き始めたのですが、主に GC のせいかすごく遅かったので一人で C++ で書き直しました。とはいえそれまでにわかった知見やテストケースが生かせたり、ほぼ Go のコードを単純作業で C++ に翻訳すれば良い部分も多かったことから、 Go バージョンも無駄ではなかった、という感じでした。

構成

大雑把に言って、 kati には以下のようなコンポーネントがあります:

  • パーサ
  • 評価するとこ
  • 依存グラフ作るとこ
  • 依存グラフを実行するとこ
  • 依存グラフから ninja ファイルを作るとこ

GNU make ではパーサと評価するとこはひっついてると思われます(GNU make のコードは OSS トップクラスの俺的可読性の低さなので、ほとんど読んでないです)。最後の実行するところと ninja ファイルを作るところはモードによって切り替えることになります。

パーサと評価器は文のパース/評価と式のパース/評価にそれぞれ2つあります。 make は行指向なので文は1行と対応し、一つの文は種類によって0個以上の式を持ちます。

普通の make ユーザは、評価器をあまり意識しないんでないかと思います。ただ実は関数とか作れてこの部分はチューリング完全です。また、 Android のヌルビルドのほとんどの時間がここに使われています。グラフ作るところとか stat してまわるのが相対的にたいしたことないのは、たぶん Android 固有の話です。要は黒魔術駆使しすぎてるので評価器の負担がすごい。

評価が終わって得られるのはルールのリストと、変数テーブルです。ルールを次々と適用させていくと依存グラフが完成します。このフェーズで変数テーブルは基本使わないです。

実行する時か ninja ファイルを生成する時にコマンドに対する式の評価が行なわれます。この時に再度変数テーブルが必要です。

以下では個々のコンポーネントを見ていきます。 CS の常識が通用しない、 GNU make の狂気が散見される予定です。

文のパーサ

既に書いた通り、文のパーサと式のパーサがあります。あと実はルールのパーサというのもありますがそれは後で。前述の通り GNU make ではおそらくパーサと評価器はひっついてますが、 kati で分けた理由は速度のためです。普通は分けなくて良いと思うのですが、 Android のビルドでは同じファイルや式を何度も参照します。どのくらいかというと3千ファイルを5万回を越える回数読みます。最も何度も読まれるファイルは5千回以上読まれます。こんなものを毎度パースするのは完全にムダです。 Android の場合、文のパース結果を記憶しておくのをやめると、パースと評価の部分が倍程度遅くなります。

文の種類は

  • ルール
  • 代入
  • コマンド
  • ディレクティブ

の4つです。ディレクティブには if 系、 include 系、 export/unexport がありますが重要でないです。あと内部的にはパースエラー文というのがあります。というのは GNU make の if 系は実行しないところのパースエラーはエラーにならないので、もしここが評価されたらエラーよろしくー、という文が必要なのでした。

残りの3つは

VAR := yay!      # これは代入
all:             # これはルール
	echo $(VAR)  # これはコマンド

となっています。 make でルールと言うと普通コマンドも含む気がしますが、まぁこう呼んでいます。

文のパーサで厄介なのは、コンテキストに依存してパース結果を変えないといけないことです。一つ目として、式を評価しないと文の種類がわからないケースがあることがあります。例えばこの2行目はなんでしょうか

$(VAR)
	X=hoge echo $${X}

この2行目は、 $(VAR) の展開結果がルールならコマンド文で、そうでなければ代入文です。もし手前に

VAR := all:

なんてのがあるとコマンドになるわけですね。コロンを変数に入れて展開してもルールとして認識される、というのが一つ驚きなのではないかと思います。式を評価してみないとルールの中身のパースに入れないということなので。では必ず式の評価をしてから文の種類判定が行なわれるかというと、そうではなく、ルールだけが式評価の後に中身チェックがなされます。変数に X=Y のような代入文や include ディレクティブなどを入れても、それは単にルールと解釈されます。例えば

ASSIGN := A=B
$(ASSIGN):
	echo $@

は A に B: を代入するのではなく、 A=B というターゲットに対するビルドルールです。

こういった理由から、 kati ではタブで始まってる行はとりあえずコマンドとして解釈しつつ元の文字列を覚えておき、もし間違ってたらパースしなおす、ということをしています。

また、2つ目の厄介な点として、普通のプログラム言語ではパースの前に \ による行連結や # によるコメントの処理が行なわれるのではないかと思いますが、 GNU make ではこれらの処理の仕方が3種類あるので、この段階でまだできないということがあります。例えば

VAR := has\
space
all:
	echo $(VAR)
	echo has\
nospace

の結果は has space と hasnospace 、です。

式のパーサ

A := B

のような代入文だと、 A と B がそれぞれ式です。式は一般的には他の式の配列で木構造をなしていて、リーフは、リテラル、変数参照、変数少しいじる参照、関数、となってます。

式のパースで狂っているのは、前述の通りこのタイミングで \ や # による行連結とコメントに対応しないといけないこと、対応が取れてない括弧の組がエラーにならない場合があること、あとは関数によってパースの仕方が少し違うものがあること、です。

まず対応が取れない括弧は、 $($(foo) などという式がある場合、 "$(foo" という名前の変数を参照する、というのが GNU make の挙動になっています。これを意図的に利用する Makefile は無いでしょうけど、 GNU make が許す以上括弧の対応が取れてない式は世の中に存在するので、これに対して適切にカラ文字列を出す必要があります。

GNU make には関数があります。だいたいの人は $(wildcard ...) や $(subst ...) あたりしか使わないやつです。でもこれは実は $(if ...) や $(call ...) などがあって、条件分岐と関数呼び出しができるので、 GNU make の評価器はチューリング完全になってます。パースでここが問題になるのは $(if ...) 、 $(and ...) 、 $(or ...) の3つだけ、空白の処理の違いかたが少し違う、ということです。

ルールのパーサ

代入でもコマンドでもディレクティブでも無いものはルール文としてあるわけですが、このルール文は評価の後、実際には4つのものになりえます。

  • ルール
  • target specific variable
  • 空白だけの場合何もしない
  • コロンが無い空白以外の文字列だとエラー

ここでいうルールというのが all: hello.exe みたいなやつです。 target specific variable というのは、ほとんどの人が知らないと思います。これを使うと特定のターゲットだけで有効な変数を定義することができて、例えば

VAR := X
target1: VAR := Y
target1:
	echo $(VAR)
target2:
	echo $(VAR)

などとあると、 target1 は Y と表示し、 target2 は X を表示します。感覚としては namespace のような動きをするものです。詳しくは後述しますが、 GNU make での変数は2種類あって、 := の右辺は即座に評価、 = の右辺の評価は展開時に遅延される、という特徴があります。つまり target specific variable のあるルール文の右辺の評価は遅延しないといけないため、「ルール文全体をとりあえず評価してからパースする」というような簡単なやり方は通用しません。このへんものすごいコーナーケースがたくさんあるので、たぶん ckati は厳密に実装してなかった記憶があります。

同様に、ルールにも実行が遅延されなければならない部分があって、ほとんどの人が知らないであろうセミコロンというのがあって、

target:
	echo hi!

という普通の Makefile

target: ; echo hi!

は等価ということになっています。これのせいで話が2つの意味でややこしくなります。1つ目はさっきの = と同じで、 ; から先の評価はコマンド実行時まで遅延させないといけない、ということ。あと = と ; が両方ある場合は基本的には左にある方が勝つ…はずです。

2つ目は ; が存在した場合、その後のパースの仕方が変わる、ということです。具体的には前述の \ と # の処理が、コマンドとして処理する時のモードになります。

あとおまけとして、おそらく普通知らないであろう、 double colon rule やら order only dependency なんてものも GNU make にはあったりします…がここでは詳しいことは書きません。

文の評価

文の種類は

  • ルール
  • 代入
  • コマンド
  • ディレクティブ

という話でした。ディレクティブは全て比較的常識的な挙動をし、コマンドは単に一個手前のルールにコマンドの式を追加していくだけのことです。ルール文は前述のルールパーサがだいたいの処理をすることになります。

意外と欝陶しいのは代入です。先に少し書いた通り GNU make の変数には2種類あります。 recursive と simple と呼ばれています。 $(info ...) という print みたいな GNU make 関数があるのですが、それを使って

A = $(info world!)
B := $(info Hello,)
$(A)
$(B)

というコードを書いたと思います。これだと world! が先に出ちゃうんじゃ…と思うかもしれませんが、これはちゃんと Hello, と出力してから world! と出力します。なぜそうなるかというと、 A は = で代入されててこっちは評価が遅延される recursive という種類の変数定義で、 B は := なので即時評価なのです。よって A の方は $(A) とした時に始めて出力が行なわれているのですが、 B の方は代入時に評価されています。

少しややこしいのは += と ?= です。これらは前回の代入文の形式を引き継ぎます。まだ定義されてない変数に += や ?= を使った場合は recursive 、つまり = の方の代入となります。

さらに target specific variable と += が組み合わさると、結構複雑なことになります。詳しくは説明しませんが、 http://shinh.skr.jp/slide/make/014.html に一例があります。

式の評価

なにぶん関数の種類が多くて実装の量は多い部分ですが、この部分は結構普通です。コマンド行が評価される場合は $@ などの特殊変数が定義されるなどもありますが、まぁとにかく常識的な動きをします。

少しヘンな部分は $(wildcard ...) くらいで、これは GNU make の最適化が入ってるためか、コマンド行にあっても評価タイミングが遅延されないという特徴があります。というか評価タイミングがヘン。これについては kati は Go の方も C++ の方も別の意味で実装が正しくないと思ってます…が実害は普通ないです。

ninja ファイルを出力する場合、コマンドに対してこの部分が少しややこしくなるのですが、それは後述。

AndroidMakefile には $(shell find ...) というパターンが大量に出てきます。これは同じところ何度も find するのですごく遅いです。 kati では高速化のために find コマンドのエミュレータを書いてあって、最初にコード全域を find しておいて、その結果を使って $(shell find ...) を高速実行する、ということをしています。 find コマンドってそれなりにややこしいので結構めんどくさかった部分です。

依存グラフを作る

さてこれまでは GNU make のカジュアルユーザーにとっては異世界の話だったと思うのですが、ここは馴染みのある話、というか make というと思い浮かべるのはこの部分かと思います。

この部分もそれなりに複雑なのですが、理不尽な挙動はしません。 GNU make のルールには3種類のものがあります。

  • explicit rule
  • implicit rule
  • suffix rule

explicit rule はターゲット名が明確なルールで、 implicit rule はターゲット名に % が含まれていて、色んなものにマッチするルールです。 suffix rule はサフィックスでマッチする歴史のある書き方。要はこの3つ

all: hoge.o
hoge.o:
	echo explicit
%.o:
	echo implicit
.c.o:
	echo suffix

優先順位は suffix が最弱で、後は % にマッチした量が少ないものが優先されます。要はより厳密な方なので、 explicit rule は優先度最大です。このマッチングを、ルールにコマンドがついていて、前提条件を満たせるものが見つかるまで行ないます。

Android には % のついてるルールが1000以上あってコマンドのついてないルールが万単位であるため、このマッチングをナイーブにやると100万回単位のループが回って、遅いです。よってここは trie 使って速くなるようにしてあります。

見つかったルールは色々とマージされたりするので色々ややこしかったりします。このへんは kati は厳密にはおかしい実装になってるはず…です。

コマンド実行する

コマンドの式は実行する直前に、特殊変数 $@ などと、 target specific variable を仕込んだ後に評価されます。ここもまぁおおむね普通です。

実装自体は普通なのですが、 target specific variable が大変筋の悪いものであることを説明しておきたいと思います。 target specific variable は、その target specific variable が仕込んであるターゲットから起爆されたターゲットにも適用されます。どういうことかというと、

VAR:=global
all: VAR:=target specific
all: hoge
hoge:
	echo $(VAR)

は "target specific" と出力します。 all に仕込んである target specific variable がターゲット hoge にも影響を及ぼしているわけです。で、何が問題かというと、これ "make hoge" としてターゲット hoge をターゲット指定すると、ターゲット all から起動されたわけじゃないので、 "global" と出力しちゃうんですね。 make って途中のターゲット指定した場合も同じものができる、ってのは当たり前の期待だと思うので、これは本当に筋が悪いと思います。

ninja ファイルを生成する

GNU make の話はここで終了です。 exec するかわりに同じ情報を ninja ファイルに書き込めば終わり…なのですが、なかなかそれが難しい。

まず GNU make は一つのルールにコマンドが複数くっついてたりしますが、これは ninja には無いので、 (cmd1) && (cmd2) && ... などと混ぜたり、色んなものをエスケープしたり、 $(SHELL) に対応したり…など、割と色々やることがあります。

本気で厄介なのは、コマンド内にある副作用のある make 関数、具体的に $(shell ...) です。これの実行は ninja 実行時にしなければならないので、適切な shell script に変換しないといけませんが、 $(if $(shell ...),X,Y) などと $(shell ...) の結果がまた GNU make 関数に渡されると詰みます。

これの正しい対処は試験的なパッチも書いてみたのですが、めんどくさすぎるので放棄していて、 Android に出てくるパターンだけ対処できれば良いや、というアドホックなコードが入っています。

ninja ファイルの再生成が必要かどうかを判断する

kati は AndroidMakefile に対して、 GNU make より3倍程度速く Makefile を評価しますが、 ninja を生成している時間も含めると2倍程度しか高速でなく、毎度実行してると Android のヌルビルドは結局50秒程度になってしまいます。

これだと別にあまり嬉しくないので、 ckati には以前生成した ninja ファイルが再生成必要かどうかを判断する機能がついています。これは結構難しい問題で、一番自明なのは Makefile が書き変わった時で、これは単にパースした全ての Makefile のタイムスタンプを記録しておけば必要な時に再生成ができます。現状、 ckati がチェックしている項目は5つあって

  • ckati 起動時のコマンドライン
  • パースした全ての Makefile のタイムスタンプ
  • 全ての評価に用いられた環境変数、全ての最初の評価時に未定義だった変数。後者は定義されてなかった環境変数が定義されることによって Makefile の挙動が変化する可能性があるからです
  • $(wildcard ...) の結果
  • $(shell ...) の結果

最後のがめんどくさいです。というのは前述の通り、 $(shell find ...) は遅いので、これを毎度チェックすると遅くなってしまうからです。幸い ckati は Android の find のほとんどに対応できる find エミュレータを持っているので、 find エミュレータで評価した場合、チェックしたディレクトリのタイムスタンプを全て覚えておいて、それらが1つでも変化した時にだけ find を走らせる、などということをしています。

あとは $(shell date) みたいな毎回変わるのが当然でかつ本質的に重要でないもの、 $(shell echo foo > file) などというファイル書き込みに使われてるもの、あたりは再実行する必要がないのでしてません。

そんなこんなでこのステップは1秒ほどで終わります。 ninja の読み込みに4秒ほどかかるので、トータルで5秒のヌルビルドになってます。

goma サポート

Google 内では goma という、主に Chrome で使ってるビルドインフラとうまいこと組み合わせれるようになってて、フルビルドも2倍くらい速くなってる…はずです。まぁ外からは見えてなくて意味ないので詳しく説明しませんが、これ ninja だから組み合わせやすい感じになっていて、 GNU make だと実現が困難だったりします。

goma についてはGoogle Developer Day 2011 Japan: 「Chromium 開発を支えるクラウドの技術Googleクラウドコンパイラ」 - Google Developer Japan Blogをどうぞ。 goma の言い出しっぺでもあったので、このスライド出た時は私も goma チームにいたような気がします、たしか。

TODO

デカい TODO としては $(MAKE) で起動される sub-make と呼ばれてるもの、があります。これは ninja 経由で正しくやるのは結構大変…で考え中。

まとめ

始める前から GNU make が複雑なものだってのは知ってたつもりだったのですが、クローンを書いてみると想像以上に大変でした。おおむね楽しく作業してましたが、たまにつらくもありました。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h