自己書き換えコードは様々な目的で用いられる。
- 状態依存ループの最適化
- 実行時コード生成、実行時あるいはロード時にアルゴリズムを特化させる(これは、リアルタイムグラフィックスなどの領域で一般的である)。
- オブジェクトのインライン状態を変化させる。あるいはクロージャの高度な構造をシミュレートする。
- サブルーチンを呼び出す部分にパッチを当てる。一般にダイナミックリンクライブラリをロードするときに行われる。しかし、これを自己書き換えコードと呼ぶかどうかは場合による。
- ダイナミックリンクライブラリのロード時などにサブルーチンを呼び出すアドレスにパッチを当てる。これを自己書き換えコードと呼ぶかどうかは微妙である。
- 遺伝的プログラミングなど
- 逆アセンブラやデバッガを使ったリバースエンジニアリングを防ぐためにコードを隠す目的で行う。
- コンピュータウイルスやスパイウェアがアンチウイルスソフトウェアから逃れる目的で行う。
- メモリやディスク容量が限られている環境で、コードを圧縮しておき、実行時に解凍してから実行する。
- 命令セットが非常に小さい場合、自己書き換えコードを使う以外に機能を実現できない場合がある。例えば、「減算し、その結果が負であれば分岐する」 (subtract-and-branch-if-negative) という命令しかないコンピュータも原理的には可能だが、この場合C言語での "*a = **b" に相当するような間接コピーは自己書き換えコードを使わないと実行できない。
2と3はLISPのような高級言語でもよく使われる。
Linuxカーネルは起動時に環境に応じた自己書き換えを行ったり (alternative.c)、デバッグ用のコードを自己書き換えで挿入するようにしたり (jump labels) して、コードの最適化を図っている。また、自己書き換えによって任意の位置の性能解析をすることができる (perf events)。
状態依存ループを最適化する自己書き換えコード
編集
仮想コードの例は以下の通りである。
repeat N 回 {
if STATE == 1
A = A + 1
else
A = A - 1
A に関して処理をする
}
自己書き換えコードをこの場合に当てはめると、単純にループを以下のように書き換える。
repeat N 回 {
A = A + 1
A に関して処理をする
}
when STATEが変化した時 {
上記の + 命令を -命令に書き換える。
}
ふたつの状態に対応した命令コードの書き換えは、XOR交換アルゴリズムを使えば簡単に記述できる。
この手法をとるかどうかは N(ループ回数)が大きいかどうかと、状態変化が頻繁かどうかによる。
他にも有効な選択肢がある場合は自己書き換えコードはお勧めできないという人もいる。
というのは、自己書き換えコードは理解しにくいし、後でメンテナンスが困難になるからである。
また他の人は、自己書き換えコードは単にコーディング時にやっていることを実行時にやるだけじゃないかと言う。
自己書き換えコードは初期のコンピュータで限りあるメモリ空間を節約するために使われていた。
また、単純な分岐しかないシステムでサブルーチンを実装するために自己書き換えコードを使用する場合もあった。ドナルド・クヌースのMIXアーキテクチャでもサブルーチン呼び出しを実現するために自己書き換えコードを使用していた。
また、未来の高度に進化した人工知能は本質的に自己書き換えを行うはずだと主張する者もいる。未来のソフトウェアがユーザーとのやり取りから学習し、ほとんど無限のパーソナライゼーションを提供するだろうという見方もある。
自己書き換えコードは1980年代のMS-DOS上のゲームで、コピープロテクションを隠すのに使われた。
フロッピーディスクドライブアクセス命令「int 0x13」は、実行プログラムのイメージには存在しないのだが、実行プログラムがメモリにロードされると自己書き換えを実施し、コピープロテクションのためのフロッピーディスクアクセス命令が書き込まれるようになっていた。
自己書き換えコードは自身の存在を隠したいプログラムにも使われることがある。
すなわち、コンピュータウイルスなどである。
自己書き換えコードを使用するウイルスの多くは、同時にポリモルフィックコードを使っている。
ポリモルフィック(多様)なウイルスは、ある意味で自分を突然変異させるプログラムとも言える。
動作中のコードを書き換えることはある種の攻撃(たとえばバッファオーバーラン)にも使われる。
自己参照型機械学習システムでの自己書き換えコード
編集
機械学習システムは一般に学習アルゴリズムは事前に用意され固定であり、パラメータを変化させることで学習を行う。しかし、Jürgen Schmidhuber は1980年代から自己書き換え式の学習アルゴリズムを自ら変更できるシステムをいくつか発表している。機能不全に陥るような自己書き換えに陥らないように、ユーザー指定のフィットネス関数などを使って、有効な書き換えのみが生き残るようにしている。
オペレーティングシステムおよびバイナリファイルフォーマットと自己書き換えコード
編集
自己書き換えコードはセキュリティ上問題があるため、いくつかのオペレーティングシステムではそれを禁止している。懸念されているのは、そのプログラム自身が自分のコードを書き換えることではなく、他者が悪意を持ってコードを改変することである。たとえばOpenBSDの最近のバージョンは W^X(英語版)("write XOR execute")という機能を持っており、あるメモリページについてプログラムは書き込むことができるか「あるいは」実行することができるが、書き込んで実行することはできないというものである。W^X 機能を持った OpenBSDでは自己書き換えコードは通常は動作できない。自己書き換えが必要なプログラムは、mmap で PROT_EXEC | PROT_WRITE 属性でページをマッピングしてそこにコードを書き込まなければならない。
メタなレベルで考えれば、適切なデータ構造を使うことで振る舞いを変化させるプログラムは一種の自己書き換えとも言える(メタプログラミング参照)。
バイナリ実行ファイルのフォーマットのうち、Unix系OSにて広く用いられているELFにあっては、プログラムヘッダの記述に基づいて実行ファイルの各セグメントをメモリ上にロードないしは確保する。プログラムヘッダにはセグメントへのアクセス権限も含まれているため、コードセグメントのアクセス権限に書き込みと実行の両者を与えれば理論上はコードの自己書き換えが可能となる。ただし、ELFが用いられている実行環境ではメモリ保護によるコードの書き換え禁止を前提としていることが一般的なため、通常リンカはコードセグメントに書き込みと実行の両権限を与えないように設定する。
Javaなどのプログラミング言語には ジャストインタイムコンパイラがあり、小さなプログラムを機械語に変換して即座に実行する。
最近のプロセッサでは自己書き換えコードは実行速度が遅くなる。
実行コードを書き換えると、命令キャッシュに保持していた筈の命令が使えなくなるので、メモリからキャッシュにロードし直さなければならなくなり、遅くなるのである。
つまり自己書き換えコードで性能改善が図れるのは、書き換えがごくまれにしか発生しない、ループ内のスイッチ切り替え(前述の状態依存ループ)のような場合だけである。
コード書き換えは一瞬で終わるわけではないから、これは何も命令キャッシュに限った問題ではない。
最近のプロセッサは命令を実行前に内部に取り込むので、プログラムカウンタに近い箇所を書き換えるとプロセッサがそれに気づかない可能性があり、書き換え前のコードを実行してしまうことがある。これについては命令プリフェッチキューを参照されたい。
NASM文法の自己書き換えx86アセンブラコード:命令プリフェッチキューのサイズ測定
code_starts_here:
xor cx, cx ; レジスタ cx をゼロクリア
xor ax, ax ; レジスタ ax をゼロクリア
around:
cmp ax, 1 ; ax が変化したかチェック
je found_size
mov [nop_field+cx], 0x90 ; 0x90 = "nop" (NO oPeration)命令のオペコード
inc cx
jmp short flush_queue
flush_queue:
mov [nop_field+cx], 0x40 ; 0x40 = "inc ax" (INCrease ax)命令のオペコード
nop_field:
nop times 256
jmp around
found_size:
;
; これで、レジスタ cx は命令プリフェッチキューのサイズを保持している
;
このコードは処理の流れを変更して力ずくで命令プリフェッチキューの大きさを調べるものである。
コードを順次書き換えていき、どれだけ書き換えたらプロセッサが書き換え後の命令をフェッチするかを調べることでキューの長さがわかる。
これをプロテクトモードで実行する際にはコンテキストスイッチが発生しないようにしなければならない。
さもなくば、このプログラムは間違った値を返すだろう。