Tail Recursion

末尾再帰の最適化でマシン語に久しぶりに触れてみようと思いました。マシン語を見ているとプログラムというのは、CPUに渡すデータである、とも感じとれます。
末尾再帰については、下記ブログで5年くらい前に取り上げました。

http://bitlife.me/archives/date/2011/11

これをLinuxでテストし、バイナリーエディットもしてみました。

環境: Ubuntu 16.04 LTS
recursion.c

cc -o recursion recursion.c
cc -o recursion_t -O2 recursion.c

recursion_tのように最適化すると、関数の再帰呼び出しがアドレスジャンプになります。

cc -S recursion.c
cc -O2 -S -o recursion_t.s recursion.c

recursion.s

recursion_t.s

末尾再帰が最適化されているものはprintfの後に関数呼び出し(call)がなくなっているのがわかります。通常の再帰だと実行時スタックがオーバーフローして以下のようにエラーとなります。

./recursion
…….(省略)
24 261725 261726 261727 261728 261729 261730 261731 261732 261733 261734 261735 261736 261737 261738 261739 261740 261741 261742 261743 261744 261745 261746 261747 261748 261749 261750 261751 261752 261753 261754 261755 261756 261757 261758 261759 261760 261761 261762 261763 261764 261765 261766 261767 261768 261769 261770 261771 261772 261773 261774 261775 261776 261777 261778 261Segmentation fault (コアダンプ)

最適化されるとアドレスジャンプとなり無限にループします。

./recursion_t
…….(省略)
1968219 1968220 1968221 1968222 1968223 1968224 1968225 1968226 1968227 1968228 1968229 1968230 1968231 1968232 1968233 1968234 1968235 1968236 1968237 1968238 1968239 1968240 1968241 1968242 1968243 1968244 1968245 1968246 1968247 1968248 1968249 1968250 1968251 1968252 1968253 1968254 1968255 1968256 1968257 1968258 1968259 1968260 1968261 1968262 1968263 1968264 ^C
(無限につづく)

さてここでちょっと遊んでコアダンプする原因となる関数呼び出しをバイナリエディットして無効にしてみたいと思います。

cp recursion recursion_e
objdump -D recursion > recursion.dmp
vi -b recursion_e
objdump -D recursion_e > recursion_e.dmp

recursion.dmp抜粋

このファイルより編集箇所を特定します。
40054d番地のcallqをnop(C9)で埋めることにします。
実行ファイルをviエディタのバイナリーモードで開き、コマンド
:%!xxd
でバイナリーモードに
:%!xxd -r
で、元にもどします。
:w
で保存します。
バイナリーモードのときに、e8d4 d4ff などの文字列で検索をして場所を特定します。
recur_01
e8から5バイトをc9で埋めます。nopというのはNo Operationの略で何もしない、という意味です。サイズを変えられないバイナリファイルの編集時によく使います。
(条件分岐などを操作すると結構いろいろとできますが要注意)

recursion_e.dmp抜粋

バイナリが書き換えられているかどうか確認できました。
これを実行すると、

./recursion_e
1

loopが一回しか呼ばれないので、1のみ表示して終了します。

今回バイナリーの話題を取り上げるきっかけとなったのは、最近必要性が増していような気がしているためです。
以前も下記で取り上げましたが、
http://decode.red/blog/20150913427/
FPGAやGPUプログラミングの必要性についても感じます。

コンピュータの進化がムーアの法則に従っているときはよかってのですが、そのような進化ができなくなり、かつ要求される処理の負荷が増大していることが起因しているからだと思います。
コンピュータリソースに余裕があるときは、人間にとってやさしい手法、生産性を高める手法が開発の現場でも用いられますが、今後は再びコンピュータリソースに余裕がなかった時代に戻る傾向にあるような気がします。(C言語は遅いからアセンブラで組め、などと言われた時代もあった)
難解な関数型言語が注目されるのもこの影響からだと思われます。
今回とりあげた末尾再帰についてもこの傾向によるところがあります。
マシン語のアドレスジャンプはBASICでいうGOTO文(C言語でもありますが)と同じで、構造化プログラミングでは使ってはいけないものとされていますが、再帰処理では欠かせないものになっています。

最近BASICから学ぶことが多い・・かも。。

About

Categories: 未分類 タグ: