コンピュータサイエンスにおいて、自己書き換えコード(SMCまたはSMoC)とは、実行中に自身の命令を変更するコードを指します。通常、命令パス長を短縮してパフォーマンスを向上させるため、あるいは単に類似した繰り返しコードを削減してメンテナンスを簡素化するために行われます。この用語は通常、自己書き換えが意図的なコードにのみ適用され、バッファオーバーフローなどのエラーによってコードが誤って自身を書き換える状況には適用されません。
自己変更コードでは、既存の命令を上書きしたり、実行時に新しいコードを生成してそのコードに制御を移したりすることがあります。
自己修正は、「フラグ設定」と条件付きプログラム分岐の方法の代替として使用でき、主に条件をテストする必要がある回数を減らすために使用されます。
この方法は、入出力サイクルごとに追加の計算オーバーヘッドを必要とせずに、条件付きでテスト/デバッグコードを呼び出すために頻繁に使用されます。
変更は次のように実行できます。
どちらの場合でも、新しい命令を既存の命令にオーバーレイすることで、マシン コード命令自体に直接変更を加えることができます(例: 比較と分岐を無条件分岐または ' NOP ' に変更する)。
IBM System/360 アーキテクチャー、およびz/Architectureまでの後継では、EXECUTE (EX) 命令は、ターゲット命令の 2 番目のバイトをレジスタ1の下位 8 ビットで論理的にオーバーレイします。これにより、ストレージ内の実際の命令は変更されませんが、自己変更の効果が得られます。
自己変更は、プログラミング言語とそのポインタのサポート、および/または動的コンパイラまたはインタープリタ「エンジン」へのアクセスに応じて、さまざまな方法で実現できます。
アセンブリ言語を使用すると、自己書き換えコードの実装は非常に簡単である。命令は、標準コンパイラがオブジェクトコードとして生成するシーケンスと同等のシーケンスで、メモリ内に動的に作成(または保護されていないプログラムストレージ内の既存のコードにオーバーレイ)される。 [1]最新のプロセッサでは、 CPUキャッシュに意図しない副作用が生じる可能性があり、これを考慮する必要がある。この手法は、適切にコメント付けされたIBM/360アセンブラの例のように、「初回」の状態をテストするために頻繁に使用されていた。これは、命令オーバーレイを使用して、命令パスの長さを(N×1)−1削減する。ここで、Nはファイル上のレコード数である(−1はオーバーレイを実行するためのオーバーヘッドである)。
SUBRTN NOP はここで初めて開きましたか?
* NOPはx'4700'<Address_of_opened>です
OI SUBRTN+1,X'F0' はい、NOP を無条件分岐に変更します (47F0...)
初めてなので、入力ファイルを開いて開きます。
オープンしました。入力を取得。通常の処理が再開されます。
...
代替コードでは、毎回「フラグ」をテストする必要があるかもしれません。無条件分岐は比較命令よりもわずかに高速で、パス全体の長さも短縮されます。後期のオペレーティングシステムでは、保護されたストレージに格納されるプログラムに対してこの手法は使用できず、代わりにサブルーチンへのポインタを変更する手法が採用されました。ポインタは動的ストレージに格納され、最初のパスの後にOPEN命令をバイパスするために任意に変更できるようになりました(直接分岐してサブルーチンにリンクする代わりに、最初にポインタをロードする必要があるため、パスの長さにN命令が追加されますが、無条件分岐が不要になるため、N命令が削減されます)。
以下はZilog Z80アセンブリ言語の例です。このコードはレジスタ「B」を範囲[0,5]でインクリメントします。「CP」比較命令はループごとに変更されます。
;==========
ORG 0H CALL FUNC00 HALT ;========== FUNC00: LD A 、6 LD HL 、label01 + 1 LD B 、( HL ) label00: INC B LD ( HL )、B label01: CP $ 0 JP NZ 、label00 RET ;===========
自己書き換えコードは、マシンの命令セットの制限を克服するために使用されることがあります。例えば、Intel 8080命令セットでは、レジスタで指定された入力ポートからバイトを入力することはできません。入力ポートは、2バイト命令の2番目のバイトとして、命令自体に静的にエンコードされています。自己書き換えコードを使用することで、レジスタの内容を命令の2番目のバイトに格納し、変更された命令を実行することで、目的の効果を得ることができます。
一部のコンパイル言語では、自己更新コードを明示的に許可しています。例えば、COBOLのALTER動詞は、実行中に変更される分岐命令として実装できます。[2]一部のバッチプログラミング手法では、自己更新コードが使用されます。ClipperとSPITBOLも、明示的な自己更新機能を提供しています。B6700システムのAlgolコンパイラは、オペレーティングシステムへのインターフェースを提供していました。これにより、実行コードはテキスト文字列または名前付きディスクファイルをAlgolコンパイラに渡し、新しいバージョンのプロシージャを呼び出すことができました。
インタプリタ型言語では、「マシンコード」はソーステキストであり、実行中に編集される可能性があります。SNOBOLでは、実行されるソース文はテキスト配列の要素です。PerlやPythonなどの他の言語では、プログラムは実行時に新しいコードを作成し、eval関数を使用して実行できますが、既存のコードを変更することはできません。変更されているように見せる(実際にはマシンコードが上書きされていないにもかかわらず)のは、関数ポインタを変更することによってです。次のJavaScriptの例をご覧ください。
var f = function ( x ) { return x + 1 };
// f に新しい定義を割り当てます:
f = new Function ( 'x' , 'return x + 2' );
Lisp マクロを使用すると、プログラム コードを含む文字列を解析せずに実行時コードを生成することもできます。
Pushプログラミング言語は、自己書き換えプログラムの作成を明示的に目的として設計された遺伝的プログラミングシステムです。高水準言語ではありませんが、アセンブリ言語ほど低水準ではありません。[3]
マルチウィンドウが登場する以前は、コマンドラインシステムは実行中のコマンドスクリプトを変更するためのメニューシステムを提供していました。例えば、MS-DOSのバッチファイルMENU.BATに次のような記述があるとします。[4] [注1]
:始める SHOWMENU.EXE
コマンドラインからMENU.BATを起動すると、SHOWMENUは画面上にメニューを表示し、ヘルプ情報や使用例などを表示します。最終的に、ユーザーはSOMENAMEコマンドの実行を必要とする選択を行います。SHOWMENUはMENU.BATファイルを書き換えて終了します。
:始める SHOWMENU.EXE誰かの名前 を呼ぶ.BAT GOTOスタート
コマンド インタープリタはスクリプト ファイルをコンパイルしてから実行したり、実行を開始する前にファイル全体をメモリに読み込んだり、レコード バッファの内容に依存したりしないため、SHOWMENU が終了すると、コマンド インタープリタは実行する新しいコマンド (ディレクトリ内のスクリプト ファイルSOMENAMEを SHOWMENU が認識しているプロトコルを使用して呼び出すコマンド) を見つけ、そのコマンドが完了するとスクリプト ファイルの先頭に戻り、次の選択に備えて SHOWMENU を再度アクティブ化します。メニュー選択が終了である場合、ファイルは元の状態に書き換えられます。この開始状態ではラベルは使用されませんが、コマンド インタープリタは次のコマンドを開始するときに次のコマンドのバイト位置を呼び出すため、ラベルまたは同等の量のテキストが必要になります。したがって、書き換えられたファイルは次のコマンドの開始点が実際に次のコマンドの開始となるように配置を維持する必要があります。
メニューシステム(および補助機能)の利便性とは別に、この方式は、選択されたコマンドがアクティブ化されたときにSHOWMENU.EXEシステムがメモリ内に存在しないことを意味し、メモリが限られている場合に大きな利点となります。[4] [5]
制御テーブル インタープリタは、ある意味では、テーブル エントリから抽出されたデータ値によって「自己変更」されるものと考えることができます ( 「IF inputx = 'yyy'」という形式の条件文で具体的に手動でコーディングされるのではなく)。
一部の IBMアクセス方式では、従来、自己変更チャネル プログラムが使用されていました。この方法では、ディスク アドレスなどの値がチャネル プログラムによって参照される領域に読み込まれ、その後のチャネル コマンドによってディスクにアクセスするために使用されていました。
1948年1月に実証されたIBM SSECは、命令を変更するか、あるいは命令をデータと全く同じように扱う能力を持っていた。しかし、この機能は実際にはほとんど使われなかった。[6]コンピュータの黎明期には、限られたメモリの使用を削減したり、性能を向上させたり、あるいはその両方を行うために自己書き換えコードがよく使われた。また、命令セットが制御フローを変化させるための単純な分岐やスキップ命令しか提供していなかった場合に、サブルーチン呼び出しと戻りを実装するために使われることもあった。[7] [8]この用途は、少なくとも理論的には、一部の超RISCアーキテクチャで今でも有効である。例えば、1命令セットコンピュータを参照のこと。ドナルド・クヌースのMIXアーキテクチャも、サブルーチン呼び出しを実装するために自己書き換えコードを使っていた。[9]
自己変更コードはさまざまな目的に使用できます。
擬似コードの例:
N回繰り返す{
STATEが1の場合
Aを1つ増やす
それ以外
Aを1つ減らす
Aで何かをする
}
この場合、自己変更コードは、ループを次のように書き直すだけです。
N回繰り返す{
Aを1つ増やす
Aで何かをする
STATEが切り替える必要がある場合{
上記の「増加」オペコードを「減少」オペコードに置き換えるか、その逆を行います。
}
}
オペコードの 2 状態置換は、「値が "opcodeOf(Inc) xor opcodeOf(dec)" であるアドレスの xor 変数」と簡単に記述できることに注意してください。
このソリューションの選択は、 Nの値と状態変化の頻度に応じて異なります。
大規模なデータセットに対して、平均、極値、極値の位置、標準偏差などの統計量を計算する場合を考えてみましょう。一般的な状況では、データに重みを関連付けるオプションがあり、各x iをaw iに関連付け、すべてのインデックス値で重みの有無をテストするのではなく、重みを使用するものと使用しないものの2つの計算バージョンを用意し、最初に1つのテストを実行するといった具合です。ここで、さらに別のオプションとして、各値に、その値をスキップするかどうかを示すブール値を関連付けるという方法を考えてみましょう。これは、各順列と結果ごとに1つずつ、合計4つのコードバッチを作成することで処理できます。あるいは、重み配列とスキップ配列を一時配列(スキップする値の重みは0)に統合することもできますが、処理コストが高く、それでもコードが肥大化します。しかし、コードを変更することで、統計量を計算するテンプレートに、不要な値をスキップするためのコードと重みを適用するためのコードを適宜追加できます。オプションを繰り返しテストする必要はなく、データ配列は 1 回アクセスされます。重み配列とスキップ配列も、関係する場合は 1 回アクセスされます。
自己書き換えコードは標準コードよりも解析が複雑であるため、リバースエンジニアリングやソフトウェアクラッキングに対する保護手段として使用できます。自己書き換えコードは、1980年代のIBM PC互換機やApple IIなどのシステム向けディスクベースプログラムにおいて、コピー防止命令を隠すために使用されました。例えば、IBM PCでは、フロッピーディスクドライブへのアクセス命令はint 0x13実行可能プログラムのイメージには表示されませんが、プログラムの実行開始後に実行可能ファイルのメモリイメージに書き込まれます。
自己書き換えコードは、コンピュータウイルスや一部のシェルコードなど、存在を明かしたくないプログラムによっても使用されることがあります。自己書き換えコードを使用するウイルスやシェルコードは、主にポリモーフィックコードと組み合わせて使用されます。実行中のコードの一部を変更することは、バッファオーバーフローなどの特定の攻撃にも使用されます。
従来の機械学習システムは、パラメータを調整するための固定された、事前にプログラムされた学習アルゴリズムを備えています。しかし、1980年代以降、ユルゲン・シュミットフーバーは、学習アルゴリズムを自ら変更できる自己修正システムをいくつか発表しました。これらのシステムは、ユーザーが指定した適応度、誤差、または報酬関数に基づいて有用な場合にのみ自己修正が維持されるようにすることで、壊滅的な自己書き換えの危険を回避しています。[14]
Linuxカーネルは自己書き換えコードを広く利用していることが特筆すべき点である。これは、主要なアーキテクチャ(IA-32、x86-64、32ビットARM、ARM64など)ごとに単一のバイナリイメージを配布しながら、起動時にメモリ内のカーネルコードを特定のCPUモデルに応じて適応させ、新しいCPU命令を利用したり、ハードウェアバグを回避したりできるようにするためである。[15] [16]程度は低いが、DR-DOSカーネルも、基盤となるプロセッサの世代に応じて、ロード時に速度が重要なセクションを最適化している。[10] [11] [注2]
いずれにしても、メタレベルでは、プログラムは他の場所に保存されているデータを変更したり (メタプログラミングを参照)、ポリモーフィズムを使用したりすることで、自身の動作を変更できます。
アレクシア・マサリン博士論文[17] [18]で発表されたSynthesisカーネルは、構造化、あるいはオブジェクト指向的なアプローチを採用した自己書き換え型コードであり、ファイルハンドルなどの個々のオブジェクトに対してコードが生成されます。特定のタスク用のコードを生成することで、Synthesisカーネルは(JITインタープリタと同様に)定数畳み込みや共通部分式の削除といった様々な最適化を適用できます。
Synthesisカーネルは非常に高速でしたが、完全にアセンブリ言語で記述されていました。その結果、移植性が欠如していたため、Massalinの最適化のアイデアは、実用カーネルには採用されていません。しかし、その手法の構造は、既存の中級言語よりも複雑ではあるものの、高級言語で実現可能であることを示唆しています。このような言語とコンパイラがあれば、より高速なオペレーティングシステムやアプリケーションの開発が可能になるかもしれません。
ポール・ヘーベルリとブルース・カーシュは、開発コストの削減を優先し、自己書き換えコードや最適化全般の「軽視」に反対している。[19]
データ キャッシュと命令キャッシュが結合されていないアーキテクチャ (一部のSPARC、ARM、およびMIPSコアなど) では、変更コードによってキャッシュ同期を明示的に実行する必要があります (データ キャッシュをフラッシュし、変更されたメモリ領域の命令キャッシュを無効化します)。
自己書き換えコードの短いセクションは、最新のプロセッサでは実行速度が遅くなる場合があります。これは、最新のプロセッサが通常、コードブロックをキャッシュメモリに保持しようとするためです。プログラムが自身の一部を書き換えるたびに、書き換えられた部分をキャッシュに再度ロードする必要があります。そのため、書き換えられたコードレットが書き換え元のコードと同じキャッシュラインを共有する場合、つまり書き換えられたメモリアドレスが書き換え元のコードのアドレスから数バイト以内にある場合など、わずかな遅延が発生します。
現代のプロセッサにおけるキャッシュ無効化の問題は、通常、内部ループ内での状態切り替えなど、変更がまれにしか発生しない場合にのみ、自己変更コードの方が高速になることを意味します。[引用が必要]
現代のプロセッサのほとんどは、マシンコードを実行する前にロードします。つまり、命令ポインタに非常に近い命令が変更されても、プロセッサはそれを認識せず、変更前のコードを実行します。プリフェッチ入力キュー(PIQ)を参照してください。PCプロセッサは、下位互換性のために自己書き換えコードを正しく処理する必要がありますが、その処理は効率的とは言えません。[要出典]
自己書き換えコードのセキュリティへの影響を考慮し、主要なオペレーティングシステムはすべて、そのような脆弱性が判明次第、慎重に排除しています。懸念されるのは、プログラムが意図的に自己書き換えを行うことではなく、エクスプロイトによって悪意を持って改変される可能性があることです。
悪意のあるコード改ざんを防ぐメカニズムの一つとして、 W^X(write xor execute)と呼ばれるオペレーティングシステムの機能があります。このメカニズムは、プログラムがメモリのどのページも書き込み可能かつ実行可能にすることを禁止します。一部のシステムでは、書き込み権限が削除された場合でも、書き込み可能なページが実行可能に変更されることを一切禁止しています。[要出典]また、ある種の「バックドア」を提供し、メモリページを複数の異なる権限でマッピングできるようにするシステムもあります。W^Xを回避する比較的移植性の高い方法は、すべての権限を持つファイルを作成し、そのファイルをメモリに2回マッピングすることです。Linuxでは、未文書化のSysV共有メモリフラグを使用することで、ファイルを作成せずに実行可能な共有メモリを取得できます。[要出典]
自己書き換えコードは、ソースプログラムリスト内の命令が必ずしも実行される命令とは限らないため、可読性と保守性がより困難になります。関数ポインタの置換による自己書き換えは、呼び出される関数名が、後で識別される関数のためのプレースホルダーであることが明確な場合は、それほど難解ではないかもしれません。
自己変更コードは、フラグをテストし、テストの結果に基づいて代替シーケンスに分岐するコードとして書き換えることができますが、通常、自己変更コードの方が実行速度が速くなります。
自己変更コードはコードの認証と競合し、システム上で実行されるすべてのコードに署名することを要求するポリシーの例外が必要になる場合があります。
変更されたコードは元の形式とは別に保存する必要があり、通常は RAM 内のコードを破棄し、必要に応じて実行可能ファイルから再ロードするメモリ管理ソリューションと競合します。
命令パイプラインを備えた最新のプロセッサでは、頻繁に自身を変更するコードが、プロセッサが既にメモリからパイプラインに読み込んだ命令を変更する場合、実行速度が低下する可能性があります。このようなプロセッサの中には、変更された命令が正しく実行されることを保証する唯一の方法が、パイプラインをフラッシュし、多くの命令を再読み込みすることであるものもあります。
次のような一部の環境では、自己変更コードをまったく使用できません。
REP MOVSWカーネルのランタイムイメージ内の16ビット(「コピーワード」)命令のデフォルトシーケンスの一部を32ビットREP MOVSD(「コピーダブルワード」)命令に動的に置き換えます(必要な繰り返し回数の半分)。奇数カウントなどのエッジケースも考慮されます。[10] [11]
SSECは、自身に格納された命令をデータと全く同じように扱い、変更し、その結果に基づいて動作することができる最初の実用コンピュータでした。
[…] もともと、
バイナリ書き換えは
、実行中にプログラムの一部を変更する必要性から始まりました(例: 1960年代の
PDP-1
の実行時パッチ適用) […]
(36ページ)
Z80
は命令フェッチに加えて、
サイクルの半分をダイナミックRAMのリフレッシュに使用します
。
[
…
] Z80は各
命令フェッチサイクルの半分を他の処理に費やす必要があるため、
命令バイト
のフェッチ時間はデータバイトのフェッチ時間ほど長くありません。アクセス対象のメモリ位置にある
RAMチップ
の1つが
少しでも遅い場合、Z80は命令フェッチ時に誤ったビットパターンを取得する可能性がありますが、データ読み取り時には正しいビットパターンを取得する可能性があります。[…] 内蔵メモリテストではこの種の問題は検出されません[…] これは厳密にはデータの読み書きテストです。テスト中、すべての命令フェッチはRAMではなく
ROM
から行われます[…] その結果、
H89
はメモリテストに合格しますが、一部のプログラムでは依然として不安定な動作をします。[…] これは、RAMを介して自身を再配置することでメモリをテストするプログラムです。その際、CPUはプログラムの現在のアドレスを
CRT
に表示し、そのアドレスにある命令をフェッチします。そのアドレスのRAM ICに問題がなければ、CPUはテストプログラムを次のメモリ位置に再配置し、新しいアドレスを表示して、この手順を繰り返します。しかし、RAM ICの1つが低速で誤ったビットパターンを返す場合、CPUは命令を誤って解釈し、予期しない動作をします。ただし、ディスプレイがロックアップし、故障したICのアドレスが表示される可能性があります。これにより、問題となるICを8個に絞り込むことができ、最大32個ものICをチェックする必要があった場合に比べて改善されます。[…] この[…]プログラムは、RST 7(RESTART 7)命令をメモリの下位から最後の作業アドレスまでプッシュすることでワームテストを実行します。プログラムの残りの部分は静止したまま、RST 7コマンドの現在位置の表示と
再配置を処理します。ちなみに、このプログラムが
ワーム
テストと呼ばれるのは、RST 7命令がメモリを上位に移動する際に、
NOP
(NO OPERATION)命令の
痕跡
が残るためです
。[…]