ニコニコ動画のタグネットワークをリアルタイムに可視化する

Posted on

巨大なグラフ1を階層的に纏め上げてくれるLouvain Method(Blondel, et al. [2008])というアルゴリズムの論文と、これをリアルタイム可視化に用いている(Praneenararat et al.[2011])論文を発見して、面白そうだなぁと思ったので(小学生並みの感想)、ニコニコデータセットに適応してやってみました。

明日、日曜日(4/26)に「ニコニコ超会議」内の「ニコつく」でサークルの一つとして展示します。I19です。

デモサイトはこちら

(2018/04/24: メモリの圧がキツいので終了しました)

ネットワーク・クラスタリング

ニコニコ動画へのタグって、ふつう「アニメ, 遊戯王, この虫野郎」みたいな感じで相互に関連するタグが付けられますよね。

このタグ同士の関連をネットワークとして可視化したら、ニコニコ動画の中でどんな概念同士がリンクしてるのかなー、とか、詳しくないジャンル2でどんなワードが流行っているのかなー、とかが一目で把握できそうです。

というわけで、今回はタグを頂点、辺を 「同じ動画に付けられている回数(共起回数)」としたグラフ(ネットワーク図)を作り、可視化してみました。

20140424_02

それだけ?って感じかもしれませんが、実はグラフをそのまま表示しただけでは頂点も辺も数が多すぎて人間には解釈不能な「Hair-ball graph(髪の毛もじゃもじゃグラフ)」になってしまいます。たとえば、ノードが1万4000のグラフを可視化した結果がこんな感じだそうです

20150425_01

うーむ、とてもじゃないけど、人間には読めない図です。今回は15万タグを対象としたので、この10倍ほどの頂点とエッジがあります。

この問題に対処するために、可視化する前にタグを階層的にまとめるクラスタリングを行います。たとえば、「ぽいぽい教」や「金剛」タグをまとめて「艦隊これくしょん」クラスタを作り、さらに「艦隊これくしょん」と「ご注文はうさぎですか?」や「きんいろモザイク」とまとめて「アニメ」クラスタを作るわけです。その状態で可視化を行えば、先ほどの図のように「ゲーム」「音楽」「アニメ」みたいなざっくりとした関係の図が出来上がります。

もちろん、その纏めた「アニメ」クラスターの中身もダブルクリックすれば見ることができます。ニコニコデータセットはそこまで最近のデータはないので、例として2011年データから「IS(インフィニットストラトス)」のクラスタを持ってきました。

20150424_03

わたし自身はこのアニメを見てないのであまり詳しくないのですが、一時期エースコンバットとのMADが流行っていたのと、(キャラ名)党っていうタグでお気に入りのキャラクタを示す文化があるのがわかります。

今回の売りは、この集計を「その場」で行っていることです。従来でもさいころ [2013], tasukuchan [2009]など可視化は行われているのですが、基本的には「事前に」集計しておいて、それを可視化しています。しかし今回のシステムでは、表示するたびに毎回、15万ノード+200万エッジの大規模なグラフを、クラスタリングしつつその場で可視化してしまいます。かかる時間はさくらのVPS2Gのような非力なマシン1台でも2秒以下です。

今回これをやった本心としては「データの分析をして知識をマイニングしたい!」とかよりは「この超高速なクラスタリングアルゴリズムを使ってみたかった」という感じなので、実際にこのソフトウェアから有益な知識が得られるかどうかは微妙です。客観的評価とかもとくにしてないし、まぁそのへんはご愛嬌。

タグネットワークで時間旅行

今回のシステムのもう一つの売りは、高速性を活かしてインタラクティブに集計期間を変更して好きな時期のデータをその場で可視化できることです。

上のバーを左右に動かすことで、時間を行き来しながらネットワークの様子を可視化できます3

初音ミクの居ない頃のニコニコ動画

たとえば、初音ミクはニコニコ動画文化を象徴するものだと言っていいと思うのですが、できた当時の2007年すぐには初音ミクはありません。なので、2007年の最初期のデータで可視化すると初音ミクがなかったころのニコニコ動画が見えます。

20150424_04

最近はもう見なくなったものもちらほらありますが4、今でも人気な作品も多いですね。「涼宮ハルヒの憂鬱」はアニメなのにアニメクラスタに入っていませんが、このような人気な作品はそれ自体が大きなコミュニティを形成しているので、独立して一個のクラスタになることがよくあります。また「ヘタリア」みたいな腐女子向け作品がちょくちょく独立してますが、腐女子向け作品は「アニメ」「ゲーム」タグと同時につかない動画が多いのが原因みたいです。

2011年3月

時間を行き来しながら見ると、時事ニュースによるその時の流行タグも自然と浮上してきます。たとえば、2011年4月の画面を見てみましょう。

20150424_05

右下のほうに「東北地方太平洋沖地震」とありますね。一ヶ月弱前にあった地震を扱った動画がたくさん投稿されていたことがわかります。ちなみに中はほとんど政治的な動画で、みんな大好き「あいさつの魔法。」は「エンターテインメント」の中に入ってます。ただいマンボウ。

20150424_06

 

昔の流行の思い出に浸るもよし、新規ジャンル開拓に使うもよし、まぁなんか暇つぶしにご活用ください。

「超大規模なグラフをその場で可視化して仮説発見に役立てる」というのはPraneenararat et al.[2011]. が元ネタです。彼らはバイオインフォマティクスで日々生まれる膨大なデータを素早く可視化するために使っていますが、これをニコニコ動画に適応したらどうなるかなーと考えた所、こんな感じになりました。

ソースコード

ソースコードはこちらに置いてあります。

結果を再現するためのプログラムが全部置いてあるので、あなたも自分のサーバで動かすことができます。データはNIIのサイトからダウンロードしてきてください。

また、クラスタリングアルゴリズムであるLouvain Methodの実装はゼロから書き起こした物を使っています。元の実装よりも少し制限がキツい(エッジの重みがintのみ)いのですが、その分だけ頑張って高速化して、だいたい3倍ぐらい速いです。

現状の問題点

  • 多くのタグを一度にまとめてしまって、部分的に一部のクラスタ以下がHair-ballになって破綻することが稀によくあります。
  • 実装がマルチスレッドになってないので本当に何人も同時にアクセスするとサーバが沈黙します。優しくしてあげてください。

  • !データがいまいち古い!
    ドワンゴ氏〜〜〜〜
    最新のデータくだされ〜〜〜〜〜〜〜〜〜

他のデータでもやってみたい?

もしも、「ニコニコデータセットじゃなくてウチのデータセットでもやってみたいんだけど…」という方が居た場合はご相談ください。本当に有益な情報をマイニングしようとするとここから更に何段階か努力しないといけないとおもいますが…。

  1. 頂点と辺からなる方 []
  2. わたしは鉄道とか全くわからない []
  3. 集計に使う長さは約二週間分 []
  4. ひぐらしのなく頃にとかね… []

JSによるcrypt関数の実装

Posted on

だいぶ前にJSで実装したcrypt関数を紹介します。crypt関数はパスワードを暗号化するための関数です。

char *crypt(const char *key, const char *salt);

Perlではビルトインで使えます。実際に使ってみるとこんな感じです。keyは8文字(8 bytes)まで、saltには2文字(2 bytes)までしか使えません。

% perl -e "print(crypt('Hello', 'wl'))"
 wlCoUbJ6h11vY

saltとkey、どちらが変わっても出力結果が変わります。パスワード認証をするときはユーザーのパスワードをそのまま保存するのではなく、このような関数でハッシュにしてから保存しなさいというのはよく言われていますが、さらにその際にユーザーの入力をそのままハッシュに掛けるとハッシュ表が盗まれた時にレインボー攻撃と呼ばれる攻撃ができてしまうので、keyにユーザーごとに異なるsalt(塩)も加えてからハッシュにかける…というのが典型的な使い方です(受け売りです)。

2chのトリップはこのcrypt関数を使っており、この(ledyba.org)サイトに設置してあったCGI「2chトリップ生成システム」をCGIからJSへ移植するために必要になったので書いたというわけです。

Apacheから流行りのnginxに移行したのですが、nginxではCGIを動かすのがかなり面倒臭くなっており、JSで書きなおすのが一番楽そうだったのでそうしました。Public DomainのcryptのCでの実装があったので、それをほぼそのまま移植しています。

ただし、暗号は理論だけでなく実装も注意深く行う必要があり、たとえばキーによって処理時間が変わるような実装だとその差を使ったタイミング攻撃ができたりします。 この実装は私が適当に作ったものなので、2chのトリップのようなどうでもいいアプリならともかく、実際に暗号に用いる場合はもっと有名で枯れた実装を使うのをお勧めします。

ソーシャル地球儀: ツイートをソラから見下ろそう

Posted on

大昔に作った「ソーシャル地球儀」っていうWebアプリを公開してないことに気がついたので記事を書いておきます。Twitterの位置情報付きのPublicStreamをリアルタイムでGoogleMaps上に表示するWebアプリです。

20150403
(実運用サービスは終了しました…のでその時のキャプチャーです)

大したことはないソフトウェアですが、リアルタイムで「いま世界中で呟かれているさま」を眺めていると「おお、地球上にはたくさんのニンゲンおり、皆それぞれに生きておるのじゃなぁ」というまるで10万10歳のロリババアかなにかのような達観を得られるのでオススメです(?)

もうちょっと人間くさいことをいうと、アフリカはやっぱりあんまりツイートないんだなとか、やっぱり今昼の時間帯のところで活発にツイートされてるんだなとか、色々気づきがあります。疲れたときの暇つぶしにどうぞ。

フレームワークにはScalatraを、JSとの連携にはAtmosphereを使っています。ユーザーごとの状態とかはとくに持ってないのでそんなに複雑なソースではないです。

ソースはこちらです。

Fortranを書こう (書くとは言ってない)

Posted on

とあるイベントでのLT。

[全画面/表示されない場合]

なんでFortranせなあかんねんと。その時間を使ってね、バイナリおじさんをする。そういう考え方もできると思うんですよ。だってスパコン向けでもないのにFortran書いてもこころぴょんぴょんしないでしょ。

画像を指定サイズぴったりに印刷する「ぴったり印刷くん」

Posted on

Haskellで書いたウェブサービスの習作です。ウェブサービス、習作ばっかりつくってる気がしてならない。
20140206

画像をぴったりサイズ指定して印刷する

コミケのサークルカットを印刷するのには毎回毎回苦労しています。申し込むためには、このサークルカットを4cm x 5.7cmに印刷して所定の用紙に貼り付けて申し込む必要がありますが、そのサイズを指定して印刷するのが結構めんどくさい。

プリンタへUSBで接続したプリンタへ、直接印刷できるときはGIMPを使えばサイズを指定して印刷できます。しかし、コンビニのプリンタなどのプリンタの場合はそのような指定を行う事が全くできません。画像を印刷すると固定の解像度で出力されたり、拡大されて出力されたり…。この問題を解決しない限りコミケに申し込むのが億劫なので、いい加減システムで解決することにしました。

このサービスを使うと、画像を指定したサイズに印刷するためのpdfを生成してくれます。このpdfを共用プリンタで「等倍指定」で印刷すると、ちょうどぴったりのサイズで印刷してくれます。少なくともセブンイレブンの印刷機ではうまくいくのを確かめました。

コミケの締め切り(8/20)までに公開できれば一番よかったのですが、結局間に合わなくてネットワーク申し込みにしたので、わたし自身には結局必要なくなってしまいましたw

せっかくなのでHaskellで書いた

やってる事自体は「証明写真作成工房」と全く同じことなので、今回は言語を変えてHaskellで書いてみました。意外とIOモナドは辛くないですね…!!

JavaScript:float32による小数演算最適化

Posted on

また翻訳です!


コンピュータ上で浮動小数点を表現する方法はいくつか存在する。だいたいのアーキテクチャではIEEE754標準を使っている。この標準では、倍精度浮動小数点(a.k.a. double or float64)を64bitで表し、単精度浮動小数点(a.k.a. float or float32)を32bitで表している。名前が示す通り、float64はfloat32よりもっと正確なんだ。だからパフォーマンスがあんまり重要視されないときは大体こっちを使おうねってアドバイスされる。

でもね、float32を使うことにもこんな利点がある:

  • float32は計算操作をするのにfloat64よりCPUサイクルがしばしば少ない。精度が低いからだ。
  • float64の計算をfloat32に対して行うには、いくらかオーバーヘッドが存在する。なぜなら、一旦float64に変換しないといけないから。これは、Float32arrayを使ってデータを格納しているときにしばしば発生する。WebGLを使ったコードや、EmscriptenでコンパイルされてC++のfloatがfloat32として格納されいるけど計算はJavaScriptのfloat64として行っている場合ではよくあることだ。
  • float32は半分のメモリしか使わない。プログラムのメモリ総使用量を減らすし、メモリーがボトルネックになるような(memory-bound)計算では速度も速くなるだろう。
  • float32に特化した標準C関数は、我々の調査したいくつかのライブラリではfloat64のバージョンより速かった。

JavaScriptにおいて、Number値はfloat64として定義されていて、すべての計算もfloat64として行うことが定義されている。さて、JS処理系がいままで長い間高速化のために利用していたfloat64の特性が、float64の値が十分小さい整数のとき、float64での計算結果は整数での計算結果と全くおなじ、ということだ(桁溢れが発生しなければ!)。JS処理系は、この特性を、整数の値を(floatでなく)生の整数として保持しておいて、整数演算で計算することで(そして桁溢れをチェックすることで)利用してきた。実際、次の少なくとも3種類の表現が実際に使われている:31bit整数、int32_t、uint32_t(くわしくはAndy Wingoの書いたこの値の表現に関する記事を見てみてね!)。

これらを全部踏まえると、一ついい感じの疑問が湧いてくる:似たような事を、float32で出来ないかな!?

答えは「しばしば可能」で、ES6で追加される新しいMath.fround関数を使うとプログラマ側がいつfloat32演算を使うかをコントロールすることが出来る。

いつ安全にfloat64演算の代わりにfloat32演算が使える?

Samuel A. Figueroaが“When is double rounding innocuous?” (SIGNUM Newsl. 30, 3, July 1995, 21-26)で書いているんだけど、float32とfloat64で交換がなりたつ恒等式がいくつかある。入力がfloat32であるかぎり、float32での演算とfloat64での演算、どちらを使ってもよくて、同じ結果を得られるんだ。もっと正確に言うなら、{+,-,*,/}のそれぞれの演算について、float32版の演算を行う関数であるop_f32と、float64版の演算を行う関数であるop_f64があったとすると、次の恒等式が成り立つんだ。C++で表現すると、こんな感じ:

  float op_f32(float x, float y);
  double op_f32(double x, double y);
  assert(op_f32(x,y) == (float) op_f64( (double)x, (double)y ));

似たような恒等式が、sqrtとかその他の数学関数のような単項演算にも成り立つ。

ただし、この性質は、それぞれの演算の前後でfloat32にcastした場合に成り立っている。たとえば、x=1024でy=0.0001で、さらにz=1024であるときに、(x+y)+zはfloat32の二回の足し算として見た時とfloat64の二回の足し算とみた場合では、もはや同じ結果にはならない。

この恒等式は、コンパイラがfloat64の演算のかわりにfloat32の演算を使って最適化を行うための、前提条件を与える。実際に、gccはこの恒等式を利用していて、さっきの恒等式の右辺のような式(float64のコード)があったら、左辺のような式(float32の式)を生成する。

でも、いつJavaScriptはfloat32の値を持ちうるんだろう?WebGLとそれに付随するTypedArray APIが使えるHTML5では、答えはこうなる:Float32Arrayから読む時と、書く時、だ。

たとえば、このコードを見てみよう:

  var f32 = new Float32Array(1000);
  for(var i = 0; i < 1000; ++i)
    f32[i] = f32[i] + 1;

ループの中の足し算のコードは、たしかにさっきの恒等式と一致してる:f32配列からfloat32の値を持ってきて、float641に変換され、float64同士の足し算演算が行われ、そしてf32に書き戻すためにfloat32に再度キャストされる。(足している”1″というリテラルも、float32からfloat64にキャストしていると考えた。なぜなら、float32で1は正確に表現できるからだ)

でも、もっと複雑な式を書きたいときはどうしよう?不要なFloat32Arrayへの出し入れを式のそれぞれの式の一部分に追加して、計算した式の一部分の結果がつねにfloat32にキャストされるようにすることも出来る。でも、これらの追加の(不要な)出し入れはプログラムを遅くするし、我々の目標はプログラムを速くすることなので、それでは意味がない。

うん、十分に頭のいいコンパイラーさんなら確かに不要なこの手の出し入れを除去することができる。でも、性能の予測可能性は重要だから、もっとこの最適化が行われることを確実にしたい。かわりに、わたしたちは小さな組み込み関数を提案し、Ecma Script 6規格で取り入れられた:Math.froundだ。

Math.fround

Math.froundは来るべきES6標準で提案されている、新しい数学関数だ。この関数は入力された値をいちばん近いfloat32の値にまるめて、そのfloat32をNumber値として返す。つまり、Math.froundは意味としては2つぎのplyfill3と同値だ:

 

  if (!Math.fround) {
    Math.fround = (function() {
      var temp = new Float32Array(1);
      return function fround(x) {
        temp[0] = +x;
        return temp[0];
      }
    })();
  }

いくつかのブラウザではTypedArraysに対応していないけど、そういう時はまた別の複雑なpolyfillがあるよ。でも、TypedArraysを使わなくても、SpiderMonkey(FirefoxのJSエンジン)とJavaScriptCore(SafariのJSエンジン)の両方ですでにMath.froundが実装されているよ。V8(ChromeのJSエンジン)でも実装する計画があって、このissueでその状況が確認できる(訳注 すでに実装されてるっぽい)。

結果として、float32の演算として演算を行い続けるには、単に途中の結果をMath.froundで包めばいい:

  var f32 = new Float32Array(1000);
  for(var i = 0; i < 999; ++i)
    f32[i] = Math.fround(f32[i] + f32[i+1]) + 1;

プログラマに速いコードを書くのを許すだけではなく、EmscriptenみたいなJSコンパイラにも、float32のコードをよりbetterにすることを許す。たとえば、Emscriptenは現在C++のfloat演算をJSのNumberを用いたコードにコンパイルする。技術的には、Emscriptenは原理的にはFloat32Arrayへの出し入れを全ての演算ごとに行うことで余計なfloat64精度を取り除くことは出来るけど、とても遅くなってしまうだろう。だから、忠実性はパフォーマンスのために犠牲にされた。

この違いが何かを壊すことは非常にレアなんだけど(もし壊れてしまうなら、そのプログラムは「未定義動作」に依存してるということだ)、この違いがバグを起こした所を実際に見たことがあって、しかもそういうバグを追跡するのは全く楽しくない。Math.froundを使えば、Emscriptenはもっと効率的で、もっと元のC++コードに忠実なコードを生成できるんだ!

Float32 in IonMonkey

わたしのインターンシップでのプロジェクトは、Firefoxにこれらの最適化を組み込むことだった。まず最初のステップとして、一般的なFloat32の演算のサポートをIonMonkey JITバックエンドに追加した。つぎに、Math.froundを一般的な(最適化されていない)ビルトインとしてJavaScriptエンジンに追加した。最後に、Float32Array/Math.froundを使っているのを認識してfloat32演算を出来る限り使うように最適化する最適化パスをコンパイラに追加した。これらの最適化は、Firefox27から使うことが出来る(今はAuroraリリースチャンネルに入ってるよ→訳注 もうStableでもVersion30ですね)

で、どれくらい早くなるんだろう?ちいさなベンチマークでは(C++とJavaScript両方で)、50%くらいの大きなスピードアップを示した。でもこういうちいさなベンチマークはしばしばミスリーディングを引き起こすから、わたしは次のようなもっと現実的なベンチマークを書いて、float32の大量の演算がどれくらい速くなるか、実際にありそうな問題で調べた:

  • 逆行列計算:このベンチマークでは、いくつか行列を作って、その逆行列を計算し、float64とfloat32でどれくらい精度が失われるかを比べるために、もとの行列と掛け算した。このベンチマークには、gl-matrixの改造したバージョンを利用した。gl-matrixは、WebGLの行列計算を使ったフレームワークで、実世界のアプリケーションでも使われている。float32バージョンを作るためには、Math.froundの呼び出しだけを追加した。
  • 画像処理行列計算:このベンチマークでも行列をいくつか生成して、画像処理でよく使われる行列の計算を行う。移動・回転・スケーリング、などなど。このベンチではたくさんの基本操作や複雑な操作(例えばMath.cosやMath.sinを回転操作で呼び出す)をおこなう。で、 float32の演算を最適化したとき、このベンチではすごい改善が見られた。このベンチマークも、gl-matrixの改造したバージョンを使っている。
  • 指数関数:このベンチマークでは、大きなFloat32Arrayを予測可能な値で埋めて、つぎにそれぞれの要素の指数関数を、テイラー展開をつかって求めた。このベンチマークの主な目的は、足し算と掛け算の重さを測ることだ。
  • 高速離散フーリエ変換:このベンチマークでは、適当なニセのバッファを作って、高速離散フーリエ変換(FFT)を行った。このベンチマークには、他にも基本操作と数回のMath.sqrtへの呼び出しが含まれている。FFTのコードは、既存のライブラリ、dsp.jsから取ってきた。

次の表が違うデバイスで最新のFirefox Nightlyを実行したときの結果を示している。それぞれの数値が、Math.froundの呼び出しを追加してfloat32最適化を許すことで得られたスピードアップを示している(だから、高大きい方がより良いことを示している)。

使っているデスクトップマシンは、「ThinkPad Lenovo W530 (Intel(R) Core(TM) i7-3820QM CPU @ 2.70GHz, 8 cores, 16 GB RAM)」だ。電話もしくはタブレットと書いてあるときは、Firefox NightlyのAndroidバージョンの事をしめしている。

この結果をひと目みたら、自分でもベンチマークを取ってみたいって思うんじゃない?ここで自分で実行できるよ!(あ、でも忘れないで。Firefox 27かそれ以降を使うんだ)

ベンチマークのソースコードはgithubで見れるよ(gh-pagesブランチにある)

DeviceMatrix InversionsMatrix GraphicsExponentialFFT
Desktop (x86)33%60%30%16%
Google Nexus 10 (ARM)12%38%33%25%
Google Nexus 4 (ARM)42%26%38%5%
Samsung Galaxy S3 (ARM)38%38%24%33%

Polyfilling Math.fround

すべてのJSエンジンでMath.froundが使えて最適化されるようになるまで、どうすればいいんだろう?上で紹介した忠実な方法ではなくて、こんな単純な恒等関数を使えばいい:

  var fround = Math.fround || function(x) { return x }

これがさっきのベンチマークが使っていて、かつ、上で示したように、殆どのコードで4違いをうまない。

すべてのモダンなJS処理系はだいたい小さな関数をハイエンドなJITでインライン化するので、このplyfillはパフォーマンス上のペナルティになることはない。それを確かめるために、例えばChromeの開発版で実行することもできる。しかしながら、いくつかの巨大なソースコードではインライン化が実行されず、plyfillのせいでパフォーマンスが悪くなったこともあった(たぶん、インライン化の深さ制限か、関数が何かしらの理由でJITコンパイルされなかったんだろう)。

まぁ簡単に言うと、「試してみる価値はある。そしてテストしよう」。

結論

結果は私達を元気づけてくれるものだった。Math.froundはES6の標準にはいるから、ほかのJS処理系も同じ最適化を行うようになってくれることが期待される。Webをゲームプラットフォームにしたいという観点からすると、低レイヤの今回のような最適化はより重要性を増してきていて、Webをよりネイティブ性能に近づけてくれると言えるだろう。ぜひFirefoxのNightlyAurolaでこの最適化をテストして、何かバグを見つけたらぜひ教えてね5

今回のことに参加してくれたすべての人に感謝したい:Jon CoppeardとDouglas CrosherにはARMバージョンを実装してくれたことを、Luke Wagner、 Alon ZakaiとDave Herman には校正とフィードバックを、そして、JavaScriptチームのみんなにヘルプとサポートを。


 感想

JSの最適化もここまで来たかーって感じがします。今後はこういう「プログラマと処理系が手を組んで行う最適化」が増えてくるのでしょうかね。そういう最適化はたぶんまだ沢山できて効果がありそうな一方、プログラムを書いている人が知ってるか知ってないかでかなり速度が変わってくるわけで、JSを書くのがどんどん大変になっていきそうな…。やっぱJSはアセンブリなんや…(何)。

  1. 訳注 JavaScriptのNumberはfloat64と定義されているのでした []
  2. 訳註 速度を除いて []
  3. なんとWeb時代に生まれた単語で、古いブラウザで新しいブラウザと同じ機能を提供するための方法のこと []
  4. 訳註 一度他の関数に代入することによる? []
  5. 訳註 もう30なので、製品版コードに入っています []

ドワンゴのC++勉強会でLTしてきたっ!

Posted on

土曜日に、ドワンゴC++勉強会 #1に行って発表してきましたっ!

[全画面/表示されない場合]

タイムシフトがまだ見れます


最初、「コンパイルの話ばっかりなのかなぁ、わたし、そこまでコンパイル時処理は書いてないんだけど…」と思って普通に聞く側で行こうかと思っていました。

行きたいけど、でも定員の倍くらい既に埋まってて絶望的だなぁ…と思っていたら、「Cocos2dxの闇 @ponkotuy」というのが見えて、「そうかー、実行時の話してもいいんだ…!そういえば去年お仕事で無限にcocos2d-xとバトルしてpull requestとか送ってたなあ結構いろいろ…」と思ったので、その時に活躍したvalgrind(ヴァルちゃん)の話をしてLT枠で参加することにしました!ぶっちゃけcocos2d-xがC++製だったのを忘れていたのは秘密だ

valgrindも色々制限があったり(たとえばリークの検出は、ちゃんと最後まで実行できないとうまくいかない)、遅かったりと万能ではないのですが、それでも最終手段として使ってみるのはアリだと思います。その後に知ったのですがLLVMにも同じようにメモリのアクセスをチェックしてくれる機能があるらしいです。

cocos2d-xは…もう何も言うまい。UnityもUnityで☆闇☆ですけどね…。


格好は五月祭のときにつくったコンちゃんの衣装で してみました。コンパイル時の話が多かったので(?)

家からドワンゴに行くまでもこの格好で行ったのですが、ガン見する人とさらっと流す人が明確に分かれてたので面白かったです。ちなみに人からジロジロ見られる率ですが、

白衣>>>(職質の差)>>>コスプレ>>>>>>>>>>>>> 異性装

みたいな感じです。白衣着てるのがこの社会では一番変な人扱い、らしい。


その後は妖怪ハウスで江添さんのピザを頂いたりしました。シーフードピザ、美味しかったです!ありがとうございました!(小学生の日記みたいな文章だ)

Fortranが書きたくないなら機械語を埋め込めばいいじゃないっ!

Posted on

Fortranが書きたくない!!

わたしは大学で天気とか地震とか海流といった「地球物理」と呼ばれるジャンルを学んでいるのですが、このジャンルではコンピュータをガンガン回して計算しまくります。気象庁が毎日やってる天気予報もそうですし、「地球シミュレータ」でエルニーニョや地球温暖化の予測とかをやっているのもそうです。

で、そういう学科なので、コンピュータの実習があるのですが、なぜか今でもFortranを教えています。

たしかにFortranには、LAPACKのような優れた数値計算ライブラリがあったり、スパコンで書く時はコンパイラのサポートが充実してたりします。なので、そういう特定の用途には悪くない言語だと思うのですが、デスクトップPCで十分扱えるようなデータサイズで、そもそも計算速度はさして重要ではなくてデータ解析手法を学ぶ実習で使うのには辛すぎます。陽には書きたくありません。1でも書かないと単位くれないんだってさ!

なので、Fortranを書かずにFortranを書く方法を検討して、実際に実装しました。今回のソースコードも、すべてgithub上に上げています。

Fortranを書かずにFortranを書くには?

最初、Fortran上で小さなVMのインタプリタを実装して、そのVMの仮想機械語にコンパイルされる言語のコンパイラを実装しようと思いました。

が、VMを書くのが普通にしんどいのと、言語とコンパイラをこのためだけに実装するのもかなりつらいので、もっとラクな方法は無いか考えてみました。

冷静に考えると、別にVMのインタプリタを実装しなくとも、コンパイルされたFortranのコードは既にCPUという名前のx86機械語のインタプリタ機械(?)で実行されています。このインタプリタ(?)を利用できないでしょうか。利用できれば自力でVMのインタプリタを実装しなくていいので手間を省略できますし、コンパイラもgccとかclang/llvmとかすごいやつがいっぱい使えます!

具体的にはそのためにどうすれば良いのかというと、Fortranのinteger配列にC言語のソースをコンパイルしたx86の機械語の数字を沢山並べて入れて、そのinteger配列のポインタをC言語の関数ポインタに無理やりキャストして、そして実行すればいいのですっ!!

module a;interface;subroutine f (z) bind(c);use iso_c_binding;type(c_ptr), value :: z;
end subroutine;end interface;type G;procedure(f), pointer, nopass :: x;end type;type J;integer, pointer :: x;
end type;end module;program b;use iso_c_binding;use a;implicit none;type (J), pointer :: x(:);
real(8),pointer :: dbl(:);type(G), pointer :: p;integer, pointer :: d(:);integer :: i;integer :: n;allocate( x(1) );
allocate( dbl(10000000) );allocate( d(10000000) );dbl = 0;x(1)%x => d(645);d(1)=-443987883;d(2)=1213580125;d(3)=267576713;
d(4)=1223181585;d(5)=1223181709;d(6)=48087689;d(7)=450755289;d(8)=-128612024;d(9)=-398095544;d(10)=-532313784;
d(11)=1158680562;d(12)=1438866912;d(13)=1223002440;d(14)=1223705997;d(15)=-338050423;d(16)=-1991763235;
d(17)=-1958152107;d(18)=-1991708603;d(19)=267577413;d(20)=1575503120;d(21)=-1991748157;d(22)=286257893;
d(23)=-1924601787;d(24)=-1991710651;d(25)=-654123582;d(26)=1209720318;d(27)=1224234377;d(28)=1223181707;
d(29)=-220183159;d(30)=-532344817;d(31)=1213580125;d(32)=267576713;d(33)=1223181585;d(34)=1223181709;d(35)=48087689;
d(36)=450756569;d(37)=-128612024;d(38)=-398095544;d(39)=-532313784;d(40)=1158680562;d(41)=1438866912;
d(42)=-219838136;d(43)=-398126833;d(44)=-398095032;d(45)=-574453432;d(46)=-572401406;d(47)=1435060250;
d(48)=1166756088;d(49)=1166625000;d(50)=269480672;d(51)=-1017257915;d(52)=-443987883;d(53)=-394426040;
d(54)=-1192987255;d(55)=0;d(56)=-129660600;d(57)=16008647;d(58)=-352321536;d(59)=-196768982;d(60)=-1924622264;
d(61)=50452;d(62)=-1958215680;d(63)=21555269;d(64)=269480656;d(65)=269480448;d(66)=267581517;d(67)=267567448;
d(68)=-2080881391;d(69)=-1962806203;d(70)=1161557061;d(71)=1221491940;d(72)=1224230283;d(73)=-220707447;
d(74)=-666562545;d(75)=1213580125;d(76)=-1991711351;d(77)=-1991710595;d(78)=1435099253;d(79)=47324;d(80)=-1991770112;
d(81)=1170733125;d(82)=244;d(83)=-1958286592;d(84)=-1740049339;d(85)=-988508856;d(86)=0;d(87)=-398095544;
d(88)=-221249208;d(89)=-1962405873;d(90)=-1740049339;d(91)=-988508856;d(92)=0;d(93)=-532313272;d(94)=-221249208;
d(95)=-234876913;d(96)=-222209777;d(97)=-129167345;d(98)=-1051193358;d(99)=1158746098;d(100)=-196770824;
d(101)=-196769023;d(102)=2094810427;d(103)=1166756018;d(104)=1166625016;d(105)=269480656;d(106)=-1017262011;

(略)

d(761)=-800748728;d(762)=-389576376;d(763)=-1168;d(764)=1223710091;d(765)=1220572555;d(766)=-1178057333;d(767)=20;
d(768)=-389576376;d(769)=-683;d(770)=-1980742261;d(771)=535478722;d(772)=-120467455;d(773)=-223591031;
d(774)=-1404753393;d(775)=-2008708280;d(776)=1118194;d(777)=16008647;d(778)=-352321536;d(779)=-196768970;
d(780)=-2092394424;d(781)=-1924660800;d(782)=50452;d(783)=-1958215680;d(784)=21530693;d(785)=-196768830;
d(786)=-1924622264;d(787)=50444;d(788)=-1958215680;d(789)=21545029;d(790)=9128136;d(791)=-2096985784;
d(792)=-1962806203;d(793)=1161557061;d(794)=-1195213652;d(795)=0;d(796)=1213580233;d(797)=-1017256567;
call c_f_pointer(C_LOC( x(1) ), p);i=0;read (*,*), n;dbl(1) = real(n);do i=2,n+1;read (*,*), dbl(i);end do;call p%x( c_loc(dbl(1)) );
n = int(dbl(1));do i=2, n+1;write (*,*), i, dbl(i);end do;deallocate ( d );deallocate ( x );deallocate ( dbl );
contains;end program;

Fortran Side

Fortranでのキャストテクニック

今回はintegerの配列のポインタをCの関数へのポインタだと思わせたいため、(Cで言うと)int*をvoid (*ptr)(double*)のポインタにキャストする必要があります。しかし、Fortranは割りと型に厳しく、Cのように簡単にキャストしたり出来ません。なので、ちょっと撚(ひね)る必要があります。

!! 関数の型定義。
!! typedef void(*f)(void*);
interface
	subroutine f (z) bind(c)
		use iso_c_binding
		type(c_ptr), value :: z
	end subroutine
end interface
!! struct FPointer { f fptr; };
type FPointer
	procedure(f), pointer, nopass :: fptr
end type
!! struct IPointer { int* iptr; };
type IPointer
	integer, pointer :: iptr
end type

このように2つ構造体(IPointer,FPointer)を作っておき、この2つの構造体のポインタをC言語のポインタを経由することで無理やりキャストします。

type (IPointer), pointer :: ip(:);       !! IPointer* ip;
real(8),pointer :: dbl(:);               !! double* dbl;
type(FPointer), pointer :: fp            !! FPointer* fp;
type(C_PTR) :: cptr                      !! void * cptr;
integer, pointer :: d(:) !命令列用の配列  !! int* d;
allocate( ip(1) )                        !! ip = malloc(sizeof(IPointer));
allocate( dbl(10000000) )                !! dbl = malloc(sizeof(double)*10000000);
allocate( d(10000000) )                  !! d = malloc(sizeof(int)*10000000);
!! 命令列をセット
d(1)=-443987883;
d(2)=1213580125;
!! 略
d(790)=-443987883;
d(791)=50013;
!! 645番目の要素がC言語側エントリポイント関数の頭なので、そこへのアドレスをセット。
ip(1)%iptr => d(645);
!! IPointerへのポインタを、C言語のポインタ、いわばvoid*に落とす
cptr = C_LOC( ip(1) );
!! そこからさらにFPointerへのポインタにキャストする(専用の関数を使います)
call c_f_pointer(cptr, fp)
!! 無理やり変換した関数ポインタ経由でバイナリコードにジャンプ
call fp%fptr( c_loc(dbl(1)) )

IPointer 構造体へのポインタを一度Cのポインタ(いわばvoid*)にキャストし、それを再度void*からFPointerのポインタにキャストしています。構造体の中身の型が「intへのポインタ」と「関数へのポインタ」で違うので、構造体を通して、その中身についてキャストすることができました。

なんでこんな七面倒臭いことをしているかというと、intのポインタと関数のポインタを直接キャストすることができないからです。intのポインタは type(C_PTR)、関数のポインタはtype(C_FUNPTR)なので互換性がなく、さらにC言語みたいにintに無理やり落としてもう一度ポイ ンタにするような操作もできません。しかし、一回構造体でくるんでしまえばどちらも値のポインタとして扱い、キャストすることができます!

さらに、現在のLinuxでは、通常データ領域をプログラムとして実行することはできない(DEP)ので、コンパイルする時にその制限をはずす必要もあります。これには、 -z execstackというオプションスイッチが使えます。

これで、任意のx86機械語の列をFortranから実行することがきました。あとは、その機械語の列をどうやって作るかという話になりますね。原理的には自分で書いてもよいのですが、一応課題をやっているコードなので、結構複雑です。なので、C言語で書いたコードをコンパイルして、その結果を利用したいところです。

Binary Side

コードのコンパイルにはGCCを使うことにしましょう。

この時問題になるのは、Fortranの動的に配置した配列の上に機械語を置くので、機械語の置かれるアドレスが実行時になるまでわからないことです。なので、機械語がどこに置かれるかがわかっていることが前提の通常のコードはうまく動いてくれません。

そのようなケースは実は珍しくなく、共有ライブラリもアドレス上のどこにロードされるかは分からないので、ちゃんとそのためのコードをコンパイラが出力します2。そのようなコードのことを、PIC(Position Indepentent Code)と呼びます!gccでは、-fPICというコンパイルオプションをつけると、PICとしてコンパイルしてくれます。

じゃ、-fPICを付けてコンパイルしたバイナリから機械語をもってくれば、目的は達成できるのでしょうか…?

Position Independent Code

ところがどっこい、これではうまく行きません。なんでかというと、一個前に翻訳した記事で解説されている、PLTとGOTがあるからです。

こんな非常に簡単なC言語コードを考えてみましょう。

void calledFunction(){
}

void function(){
  calledFunction();
}

これをコンパイルするためのMakefileはこんな感じで、

.PHONY: all

all:
  gcc -c -o test.o test.c -fPIC
  ld -shared -fPIC -o test.so test.o

コンパイルした結果が、こんな感じでーす。

test.so:     file format elf64-x86-64


Disassembly of section .plt:

0000000000000280 <calledFunction@plt-0x10>:
280:    ff 35 82 0d 20 00        push   QWORD PTR [rip+0x200d82]        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
286:    ff 25 84 0d 20 00        jmp    QWORD PTR [rip+0x200d84]        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
28c:    0f 1f 40 00              nop    DWORD PTR [rax+0x0]

0000000000000290 <calledFunction@plt>:
290:    ff 25 82 0d 20 00        jmp    QWORD PTR [rip+0x200d82]        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
296:    68 00 00 00 00           push   0x0
29b:    e9 e0 ff ff ff           jmp    280 <calledFunction@plt-0x10>

Disassembly of section .text:

00000000000002a0 <calledFunction>:
2a0:    55                       push   rbp
2a1:    48 89 e5                 mov    rbp,rsp
2a4:    5d                       pop    rbp
2a5:    c3                       ret

00000000000002a6 <function>:
2a6:    55                       push   rbp
2a7:    48 89 e5                 mov    rbp,rsp
2aa:    b8 00 00 00 00           mov    eax,0x0
2af:    e8 dc ff ff ff           call   290 <calledFunction@plt>
2b4:    5d                       pop    rbp
2b5:    c3                       ret

こんな感じで、ばっちりpltを経由して実行してしまいます。この機械語をFortran上に載せて強制的に呼び出したとしても、PLTがちゃんと解決されないとうまく動いてくれません。デフォルトのPLT解決処理は共有ライブラリとして実行されていることが前提になっているので、今回は利用することはできません。

integer配列のアドレスから関数のアドレスを解決することは原理的には可能なので、Fortran上でPLTのアドレスの所を埋めてちゃんと実行できるようにすること自体は可能(なはず)です。

しかし、かなり意味もなく複雑になってしまうので、できればPLTにジャンプするのではなくて、呼びたい関数に直接ジャンプするようなコードを出力して欲しいです。PLTを経由して関数を呼び出す時に使ってる0xe8命令は相対アドレスによるcall命令なので、可能なはずです。

リンカースクリプトを自分で書けばPLTとGOTを使わない…!?

リンク時にPLTを経由するようにリンクしているので、じゃあすごく単純なリンカースクリプト自分で書けばPLTみたいな便利機能使わなくなるんじゃない?

ということで、実際に書いてみました。こうなりました。

OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64","elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)

ENTRY(ep);
MEMORY
{
ROM(rxai) : ORIGIN = 0, LENGTH = 64k
}
SECTIONS
{
.text : {} > ROM
.rodata : {} > ROM
.data : {} > ROM
. = ALIGN(4);
__bss_start = .
; .bss : {} > ROM
__bss_end = . ;
}

Makefileは、

linkerscript:
    gcc -c -o test.o test.c -fPIC 
    ld test.so test.o --script ./linker.script

こんな感じにすると指定できます。結果は、

test-with-linkerscript:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <calledFunction>:
   0:    55                       push   rbp
   1:    48 89 e5                 mov    rbp,rsp
   4:    5d                       pop    rbp
   5:    c3                       ret    

0000000000000006 <function>:
   6:    55                       push   rbp
   7:    48 89 e5                 mov    rbp,rsp
   a:    b8 00 00 00 00           mov    eax,0x0
   f:    e8 ec ff ff ff           call   0 <calledFunction>
  14:    5d                       pop    rbp
  15:    c3                       ret

となって、見事、PLTを使わずに直接相対ジャンプするようになりました!

実は-pieだけで大丈夫

実をいうと、リンカースクリプトなんて書かなくても、-pieというオプションを付けて、通常の実行ファイルとしてコンパイルすればplt/gotを使わないコードを生成してくれます。

pie:
    gcc -c -o test.o test.c -pie
    ld -pie test.so test.o

-pieというオプションは、gcc –helpすると、

  -pie                     Create a position independent executable

ということで、実行時にどのアドレスに配置されても実行できる実行ファイルを生成してくれます。

実行ファイルは共有オブジェクトとは違って外部の他のプログラムからその中の関数が呼ばれるようなことはないので、内部のそれぞれの関数のアドレスを動的に解決する必要はありません。なので、PLTを経由せずに直接callするようなコードにしてくれる…のでしょう。

…でも冷静に考えると、共有ライブラリの中でならPLT経由せずに直接ジャンプでよくない?

全てが終わったあとに気づいたので時既に遅しという感じでしたが、一応書いておきます。

プログラムセクションを抽出する

残るところは、出来上がったファイルから機械語を抽出して、数字の列にするだけです。そのために、機械語が書いてある部分はファイルのなかでどこなのかを調べましょう。

セクションの一覧を出力するには、objdump -hが使えます。

% objdump -h test.so

test.so:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
0 .interp       0000000f  00000000000001c8  00000000000001c8  000001c8  2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .hash         00000028  00000000000001d8  00000000000001d8  000001d8  2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .dynsym       00000078  0000000000000200  0000000000000200  00000200  2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .dynstr       00000019  0000000000000278  0000000000000278  00000278  2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .text         00000016  0000000000000294  0000000000000294  00000294  2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .eh_frame     00000058  00000000000002b0  00000000000002b0  000002b0  2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynamic      000000c0  0000000000200f40  0000000000200f40  00000f40  2**3
CONTENTS, ALLOC, LOAD, DATA
7 .comment      0000002c  0000000000000000  0000000000000000  00001000  2**0
CONTENTS, READONLY

機械語本体が置かれているのは、.textというセクションです。この中のデータをFortranに書きましょう。File offの項目が、ファイルの先頭からの位置で、Sizeという項目がセクションのサイズで、今回の場合、要はプログラムコード全体のサイズです。

さらに、関数呼び出しするには、呼び出したい関数の、プログラムコード内での相対位置も必要です。これには、nmコマンドが使えます。

% nm test.so
0000000000201000 D __bss_start
0000000000000294 T calledFunction
0000000000200f40 d _DYNAMIC
0000000000201000 D _edata
0000000000201000 D _end
000000000000029a T function
0000000000000000 d _GLOBAL_OFFSET_TABLE_
                 U _start

セクションの所のVMAもしくはLMAというのがメモリ上での位置なので、機械語列の中での相対位置を調べるにはここでのアドレスから.textセクションのVMA/LMAの値を引けば求めることができます。

シェルスクリプトを使うともちろんこの作業は自動化できます。呼び出す関数名は”ep”としました。EntryPointの略ですね!

_start=$(objdump -h test.so | grep \\s.text\\s | awk '{ print $6 }')
_size=$(objdump -h test.so | grep \\s.text\\s | awk '{ print $3 }')
_ep=$(nm test.so | grep \\sep$ | awk '{ print $1 }')

Cのソースをコンパイルする所から、命令列を抽出してFortranのソースに変換する作業まで、これらの作業はすべて自動化しているので、よかったらgithubのソースを見てね!

今回書き込んだinteger型は4バイトなので、エントリポイントのオフセットが4の倍数でないとき(よくある)には、その整列条件を満たすために数バイトズラしたりしないといけません。そのコードとかも入ってます。

Programming Side

これで終わり…ではありません!もう一個、プログラムを書くという問題があります!!今回の環境では、

  • 外部にあるライブラリ関数などは呼び出すことができない

という、非常に大きな制限があります。実行時に決定されるライブラリ関数の実際のアドレスを知らないし、知るためにはライブラリ関数を呼び出さないといけないので、「服を買う服がない」状態なんです。もちろん、Fotranでなら調べられるので、調べた結果をC言語の関数に投げればいいんですけど、Fortran書きたくないし(ぉ)。

科学計算すればいいだけなので、sin/cosとsqrtくらいが使えればなんとかなります。sin/cos/sqrt/absは、FPU命令を使って計算できるので、ライブラリは必要ありません。

そのためには、インラインアセンブリで陽にFPU命令を実行する必要があります。

double fsin(double v)
{
	intptr_t d0;
	__asm__ (
		".intel_syntax noprefix;"
		"fld qword ptr [%0];"
		"fsin;"
		"fstp qword ptr [%0];"
		".att_syntax;"
		: "=&r"(d0)
		: "0"((intptr_t) &v)
		: "eax", "ecx"
	);
	return v;
}

こんな感じのを、FPUの命令ごとに実装していきます。

関数だけでなく、piもFPUを使って取得しています。なんでかというと、今回は機械語だけを埋め込んで、それとは別にある定数の領域をFortranのソースに含めていないからです!

Cのソースに円周率の値を直接書いていてもすこし複雑な処理になると定数領域に飛ばされてしまって、.textセクションに含まれなくなってしまい、結果としてプログラムがおかしくなってしまいました。定数領域もFortranに埋め込むだけでその問題は回避できるのですが、その改修がちょっとめんどくさかったのでやめて、FPU命令を毎回呼ぶことで回避しました。

入力と出力ですが、実際のデータの読み込みと結果の出力のみ、Fortranが担当することにしました。C言語は関数のシグネチャをvoid (*fun)(double *)とし、doubleの配列を処理してその結果をもとの配列の領域に書き込むことだけに専念していただきます。

他の言語でやってみても面白いかもね!

と思った。Fortranはなんだかんだ任意の機械語を実行する難易度は低い言語だったと思います。

ここまでやるならFortranで書いたほうが早くね?

ちなみに

書かなくて良くなった(╹◡╹)

ひさびさに色々バイナリが触れてたのしかったです。まぁ結果としては良かったのではないでしょうか!

  1. っていうか、いままでプログラム書いたことがない人にいきなりこれ教えるのってどうなんでしょう…。 []
  2. Windowsでは、64bit版からそうなりました。32bitでは固定だそうです。 []

ELFの再配置シンボルの解決

Posted on

この記事は、

Resolving ELF Relocation Name / Symbols

の翻訳です。認めてくれたLeaf SRの人ありがとう!訳はがんばりましたが、間違ってる所もあるかもしれません。そこはご了承ください…。


共有オブジェクトのELFファイル内関数へのcall命令(たとえば、puts関数の呼び出しとか)は、直接関数のアドレスへ飛ぶのではなくて、PLT(Procedure Linkage Table)に飛ぶよ。PLTを使うと、関数の実際のアドレスを実行時に解決することができる。言い換えると、共有オブジェクトが実際にどこにロードされるかは実行時にならないと分からないので、PLTを使って実際のアドレスを解決している。

次のELFのサンプルを見てみよう:

ここに、ELFのテキスト・セグメント1内にある、アドレス0x804833cへの呼び出し命令がある:

$ objdump -d example-elf | grep 804833c | grep call

804843f: e8 f8 fe ff ff call 804833c

それで、次がこのサンプルファイルのPLTだ。さっきの命令で呼んでたアドレス0x804833cに注意してね。ここには実際には単なる*0x8049684へのジャンプ命令が並んでいる。

$ objdump -d example-elf | grep “section .plt:” -A 31

Disassembly of section .plt:

080482fc <__gmon_start__@plt-0x10>:
80482fc:       ff 35 70 96 04 08       pushl  0x8049670
8048302:       ff 25 74 96 04 08       jmp    *0x8049674
8048308:       00 00                   add    %al,(%eax)
     ...

0804830c <__gmon_start__@plt>:
804830c:       ff 25 78 96 04 08       jmp    *0x8049678
8048312:       68 00 00 00 00          push   $0x0
8048317:       e9 e0 ff ff ff          jmp    80482fc <_init+0x18>

0804831c <__libc_start_main@plt>:
804831c:       ff 25 7c 96 04 08       jmp    *0x804967c
8048322:       68 08 00 00 00          push   $0x8
8048327:       e9 d0 ff ff ff          jmp    80482fc <_init+0x18>

0804832c <__stack_chk_fail@plt>:
804832c:       ff 25 80 96 04 08       jmp    *0x8049680
8048332:       68 10 00 00 00          push   $0x10
8048337:       e9 c0 ff ff ff          jmp    80482fc <_init+0x18>

0804833c :
804833c:       ff 25 84 96 04 08       jmp    *0x8049684
8048342:       68 18 00 00 00          push   $0x18
8048347:       e9 b0 ff ff ff          jmp    80482fc <_init+0x18>

0804834c :
804834c:       ff 25 88 96 04 08       jmp    *0x8049688
8048352:       68 20 00 00 00          push   $0x20
8048357:       e9 a0 ff ff ff          jmp    80482fc <_init+0x18>

(注意:*は、C言語のポインタ参照と同じで、0x8049684に書いてある値、の意味)

*0x8049684が実際には何なのかを知りたければ、Global Offset Table (GOT)を探せばいい。こんな感じだ:

$ objdump -s example-elf | grep got.plt -A3

Contents of section .got.plt:
804966c 98950408 00000000 00000000 12830408  ................
804967c 22830408 32830408 42830408 52830408  "...2...B...R..

0x8049684には、42830408(リトルエンディアンで0x08048342)が書いてあった。ここでPLTに戻ってもう一回0x08048342を見てみると、jmp 命令でこのアドレスに飛んだあと、”push $0x18″という命令を実行することが分かる。0x18=24っていうのは、再配置テーブルへのオフセットアドレスだ。このpush命令に続いて、PLTの最初のアドレスへのジャンプである、”jmp 80482fc”が実行される。

この80482fcに書いてある命令列は、PLTの他の部分と大分ちがっているのはすぐ分かるだろう。最初の2つの命令、”pushl 0x8049670″と”jmp *0x8049674″は結構重要だ。

最初push命令はアドレス0x8049670をスタックにpushしていて、これはGOTを指している。次の(*0x8049674)へのジャンプだけど、このアドレスもGOT内のアドレスだ。この2つのアドレスは、ELFファイルの中ではどちらも”0″が書いてある。

というのも、これらは実行時に動的に埋められるからだ。実行時には、最初のアドレスの値は特定のライブラリが使われているかを識別するための番号になり、次のアドレスにはリンカーのシンボル解決ルーチンのアドレスが入る。これらのルーチンは前にスタックにpushされた0x18を、正しい場所を解決するために使う。

このテクニックは「遅延リンク(lazy linking)」と呼ばれている。再配置された関数のアドレス解決が、実行時に、しかも必要な時に、実際に関数が呼ばれた時だけ行われるからだ。

一度リンカーによって解決されると、リンカーはGOTエントリを書き換えて、最初の jmp *(アドレス)が、それまでのpush $0x18をするコードのアドレスではなく、解決された関数アドレスに直接ジャンプするようになる。

それじゃあ、どうやって関数のアドレスを解決するためのシンボル名を取ってくるんだろう?関数のアドレスを解決するには、’0x8049688’ではなくて、’sprintf’という文字列が必要だ。

SHT_RELというタイプを持っているセクションには、次のような構造体が並んでいる:

 typedef struct
 {
      Elf32_Addr r_offset;    /* Address */
      Elf32_Word r_info;    /* Relocation type and symbol index */
 } Elf32_Rel;

さて、readelfでELFファイル内の再配置エントリを見てみよう2

$ readelf -r /testbins/sha1

 Relocation section '.rel.dyn' at offset 0x420 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
 0804b54c  00001106 R_386_GLOB_DAT    00000000   __gmon_start__
 0804b598  00000505 R_386_COPY        0804b598   stderr
 0804b59c  00000d05 R_386_COPY        0804b59c   stdin

 Relocation section '.rel.plt' at offset 0x438 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
 0804b55c  00000107 R_386_JUMP_SLOT   00000000   feof
 0804b560  00000207 R_386_JUMP_SLOT   00000000   putchar

これらの情報はいったい何処から来たんだろう!?Offset/Info/Type/Value/Nameのそれぞれの情報、さっきの構造体に無かったよね?えっとね、これらの値のうち、殆どは実はr_infoフィールドから直接もってこれるか、r_infoフィールドを間接的に使うともってこれるんだ。

offsetの値はPLTの中を指していて、ここに再配置エントリの実体が置かれる。バイナリを逆アセンブルすると、PLTの中の正しいオフセットにある、このアドレスへjmpする命令が見えるはずだ。

今注目したいのは、r_infoフィールドだ。このフィールドに関係する、2つの(ほんとの事をいうと3つあって、3つ目は1と2を組み合わせると得られる)重要なマクロがelf.hに存在する:

#define ELF32_R_SYM(val) ( (val) >> 8)
#define ELF32_R_TYPE(val) ( (val) & 0xff)

これらのマクロはr_infoからそれぞれ別の値を得るためのマクロだ。いっこずつ見てみよう。SHT_RELタイプをもつセクション(さっきからみてる、再配置セクション)は他にsh_linkっていうメンバを持っている。このsh_linkは重要で、これらの再配置情報のシンボル情報(関数名とかね)を持つ、ほかのセクションを指しているんだ。このsh_linkは、gccでは大抵「dynsym」というセクションを指している。

/bin/lsのdynsymを実際に呼んでみた結果がこれ(読みやすくするために、いくつか省略してるよ):

$ readelf -S /bin/ls

   There are 26 section headers, starting at offset 0x126d8:

 Section Headers:
 [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
 ...
 [ 4] .dynsym DYNSYM 080484a0 0004a0 0006b0 10 A 5 1 4
 ...
 [ 8] .rel.dyn REL 08049170 001170 000028 08 A 4 0 4
 [ 9] .rel.plt REL 08049198 001198 0002f0 08 A 4 11 4
 ...

セクション7と8のsh_linkメンバを見て。それぞれの4が、dynsym(dynamic symbol table)セクションを指している。この第4セクションが、再配置エントリに対応するシンボルネームが書いてあるセクションだ。ここまでやってきたことをまとめておくと、(1)SHT_RELタイプがついてるセクションを探して、(2)sh_linkメンバーの指し示すセクションをたどった。

よし、色々数字があるのは分かったから、あとはそれをどう組み合わせていけばいいんだろう。再配置テーブルのエントリを一個一個見ていって、r_infoフィールドの値を持ってきて、ELF32_R_SYM(val)マクロを使って値を取り出していけばいい。この数字はdynsymセクション(か、sh_linkの指している他のセクション)内のエントリに対応している。で、dynsymテーブルをパースしてエントリを調べれば、シンボルネームが解決できる。

他のマクロは何なんだろう?ELF32_R_TYPE(val)は再配置がどんなタイプなのかを教えてくれる。その値はelf.hにR_386_GOT32とかR_386_JMP_SLOTみたいなのが定義されている。この定義を使うと、再配置テーブルが関数のためのエントリなのかどうかとかが分かる(でも、rel.pltセクションの中のはだいたいそうだ3)。

ただし、ちょっと注意。全てのELFファイルがセクションヘッダを持っているわけじゃない。そういう時は、プログラムヘッダを使うと、動的なセグメントがどこかわかるし、再配置テーブルのアドレスとか、再配置テーブルのシンボルテーブルがどこかとかが分かる。

もしこのエントリに間違いがあったらぜひ教えてね!

  1. 訳注:テキストセグメントにはプログラムのコード本体が置かれている []
  2. 訳註:これから読むELFは、さっきまでのとは違うELFファイルだから、PLTのアドレスとかは違う []
  3. 訳註:PLTはProcedure Linkage Tableの略なのです []

32bit GCCのunordered_mapに下位32ビットが同じキーを送り続けると死ぬ

Posted on

前回の記事の最後で「unsigned long long intのハッシュは(size_t = 32ビットの環境で)下位32ビットしか見てない」みたいな事を書きました。

これが本当にそうだとすると、この挙動に依存しているunordered_mapに、下位32ビットが同じ値のキーを送り続けるとアルゴリズムの教科書通りの最悪ケースになって悲惨なことになるのでは…!という事が容易に予想されます。

ということで、やってみました。

実験環境

  • Windows 7 64bit
  • MinGW 32 g++ 4.8.1

ソースコードはこちら。

#include <unordered_map>
#include <cstdio>
#include <chrono>
unsigned long getT()
{
    return std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1);
}
int main()
{
    {
        std::unordered_map<unsigned long long int, int> map;
        unsigned long t = getT();
        for(int j=0;j<100;++j)
        for(unsigned long long int i=0;i<0x1000;++i){
            map.insert(std::make_pair(i << 17, 10));
        }
        printf("Case 1 Time: %d\n", static_cast<int>(getT() - t));
    fflush(stdout);
    }
    {
        std::unordered_map<unsigned long long int, int> map;
        unsigned long t = getT();
        for(int j=0;j<100;++j)
        for(unsigned long long int i=0;i<0x1000;++i){
            map.insert(std::make_pair(i << 33, 10));
        }
        printf("Case 2 Time: %d\n", static_cast<int>(getT() - t));
    fflush(stdout);
    }
}

シフトする値だけ少し変更することで、他の要素の影響を出来る限り減らしました。

結果

g++ -o test -std=gnu++11 test.cpp
./test
Case 1 Time: 120
Case 2 Time: 67825

というわけで500倍くらい遅かったので、本当に線形探索してるっぽいです。本当にありがとうごいました。

64bit環境なら問題ありません

64bit環境のwandboxで実行すると、64bit環境ではsize_tも64ビットになってくれるので、

Start

Case 1 Time: 287
Case 2 Time: 279

0

Finish

というわけでほぼ同じ時間で終わります。