コンピュータサイエンスにおいて、自己修正コード(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' <オープンされたアドレス>です OI SUBRTN+1,X'F0' はい、NOP を無条件分岐に変更します (47F0...) 初めてなので、入力ファイルを開いてください。 オープンしました。入力取得。ここから通常の処理が再開されます。 ...代替コードでは、毎回「フラグ」をテストするかもしれません。無条件分岐は比較命令よりもわずかに高速で、全体のパス長も短縮されます。保護されたストレージに格納されているプログラムの場合、後のオペレーティングシステムではこの手法は使用できず、代わりにサブルーチンへのポインタを変更することが使用されます。ポインタは動的ストレージに格納され、最初のパスの後で自由に変更して、 (直接分岐してサブルーチンにリンクする代わりに最初にポインタをロードする必要があるため、パス長にN 個OPENの命令が追加されますが、不要になった無条件分岐については、対応する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命令セットでは、レジスタで指定された入力ポートから1バイトを入力することはできません。入力ポートは、2バイト命令の2バイト目として、命令自体に静的にエンコードされています。自己書き換えコードを使用することで、レジスタの内容を命令の2バイト目に格納し、変更された命令を実行することで、目的の効果を得ることができます。
コンパイル言語の中には、明示的に自己修正コードを許可するものがあります。たとえば、COBOLALTERの動詞は、実行中に修正される分岐命令として実装できます。[ 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 ] [ nb 1 ]
:始める SHOWMENU.EXE
コマンドラインからMENU.BATが起動されると、SHOWMENUは画面上にメニューを表示し、ヘルプ情報、使用例などを表示します。最終的にユーザーがSOMENAMEコマンドの実行を必要とする選択を行うと、SHOWMENUはMENU.BATファイルを書き換えて終了します。
:始める SHOWMENU.EXE CALL SOMENAME.BAT スタートへ移動
コマンドインタープリタはスクリプト ファイルをコンパイルしてから実行したり、実行開始前にファイル全体をメモリに読み込んだり、レコード バッファの内容に依存したりしないため、SHOWMENU が終了すると、コマンドインタープリタは実行する新しいコマンドを見つけます (SHOWMENU が認識しているプロトコルを介してディレクトリの場所にあるスクリプト ファイルSOMENAMEを呼び出す)。そのコマンドが完了すると、スクリプト ファイルの先頭に戻り、次の選択に備えて SHOWMENU を再度アクティブ化します。メニューの選択が終了の場合、ファイルは元の状態に書き戻されます。この開始状態ではラベルは不要ですが、コマンドインタープリタは次のコマンドを開始する際に次のコマンドのバイト位置を記憶するため、ラベルまたは同等の量のテキストが必要です。したがって、書き戻されたファイルは、次のコマンドの開始点が実際に次のコマンドの開始点となるように、アライメントを維持する必要があります。
メニューシステム(および補助機能の可能性)の利便性とは別に、この方式では、選択されたコマンドがアクティブ化されたときに SHOWMENU.EXE システムがメモリ上に存在しないため、メモリが限られている場合に大きな利点となります。[ 4 ] [ 5 ]
制御テーブルインタープリタは、ある意味では、テーブルエントリから抽出されたデータ値によって「自己修正」されるものと考えることができます(条件文の形式で具体的に手動でコーディングされるのではなく)。IF inputx = 'yyy'
IBMの一部のアクセス方式では、従来、自己変更チャネルプログラムが使用されていました。これは、ディスクアドレスなどの値がチャネルプログラムによって参照される領域に読み込まれ、後続のチャネルコマンドによってディスクへのアクセスに使用されるというものです。
1948 年 1 月に実証されたIBM SSEC は、命令を変更したり、データとまったく同じように扱ったりする機能を持っていた。しかし、この機能は実際にはほとんど使用されなかった。[ 6 ]コンピュータの初期の頃は、限られたメモリの使用を減らしたり、パフォーマンスを向上させたり、あるいはその両方を行うために、自己修正コードがよく使われた。また、命令セットが制御フローを変化させるための単純な分岐命令やスキップ命令しか提供していなかった場合、サブルーチン呼び出しと戻りを実装するために使われることもあった。[ 7 ] [ 8 ]この使用法は、少なくとも理論的には、特定の超RISCアーキテクチャで今でも関連性がある。たとえば、1 命令セット コンピュータを参照のこと。ドナルド・クヌースのMIXアーキテクチャも、サブルーチン呼び出しを実装するために自己修正コードを使用していた。[ 9 ]
自己書き換えコードは、さまざまな目的に使用できます。
*a = **bの に相当するもの)を実行することはできません。擬似コードの例:
N回繰り返す STATEが1の場合 Aを1つ増やす それ以外 Aを1つ減らす Aを使って何かをする }
この場合、自己修正コードは、ループを次のように書き換えるだけで実現できます。
N回繰り返す{ A を 1増やす} Aを使って何かをする 状態を切り替える必要がある場合 上記の「増加」オペコードを減少させるオペコードに置き換えるか、またはその逆を行ってください。 } }オペコードの2状態置換は、「アドレスの変数を値でxorするopcodeOf(Inc) xor opcodeOf(dec)」と簡単に記述できることに注意してください。
この解決策を選択するかどうかは、 Nの値と状態変化の頻度によって決まるはずです。
大規模なデータセットに対して、平均、極値、極値の位置、標準偏差などの統計量を計算する必要があるとします。一般的には、データに重みを関連付けるオプションがあり、各x iにw iが関連付けられます。すべてのインデックス値で重みの存在をテストする代わりに、重みを使用する場合と使用しない場合の 2 つの計算バージョンを用意し、最初に 1 つのテストを実行することができます。さらに、各値に、その値をスキップするかどうかを示すブール値を関連付けるオプションを考えてみましょう。これは、順列ごとに 1 つずつ、合計 4 つのコードバッチを生成することで処理できますが、コードが肥大化します。あるいは、重み配列とスキップ配列を一時配列 (スキップする値の重みはゼロ) にマージすることもできますが、処理コストがかかり、やはり肥大化します。しかし、コードを修正することで、統計量を計算するテンプレートに、不要な値をスキップするコードと重みを適用するコードを適切に追加できます。オプションのテストは繰り返されず、データ配列へのアクセスは一度のみで、重み配列やスキップ配列(該当する場合)へのアクセスも一度のみとなる。
自己改変コードは標準コードよりも解析が複雑であるため、リバースエンジニアリングやソフトウェアクラッキングに対する防御策として利用できます。1980年代には、IBM PC互換機やApple IIなどのシステム向けディスクベースプログラムにおいて、コピープロテクト命令を隠すために自己改変コードが使用されていました。例えば、IBM PCでは、フロッピーディスクドライブへのアクセス命令はint 0x13実行可能プログラムのイメージには表示されず、プログラムの実行開始後に実行可能プログラムのメモリイメージに書き込まれるようになっていました。
自己改変コードは、コンピュータウイルスや一部のシェルコードなど、存在を隠したいプログラムによって使用されることもあります。自己改変コードを使用するウイルスやシェルコードは、多くの場合、ポリモーフィックコードと組み合わせて使用します。実行中のコードの一部を変更することは、バッファオーバーフローなどの特定の攻撃にも使用されます。
従来の機械学習システムは、パラメータを調整するための固定された事前プログラム済みの学習アルゴリズムを備えています。しかし、1980年代以降、ユルゲン・シュミットフーバーは、自身の学習アルゴリズムを変更できる自己修正システムをいくつか発表しています。これらのシステムは、自己修正がユーザー指定の適合度、誤差、または報酬関数に従って有用である場合にのみ存続するようにすることで、壊滅的な自己書き換えの危険を回避しています。[ 14 ]
Linuxカーネルは、自己修正コードを広く利用していることで知られています。これは、検出された特定のCPUモデルに応じて起動時にメモリ内のカーネルコードを適応させながら、主要なアーキテクチャ(IA-32、x86-64、32ビットARM、ARM64など)ごとに単一のバイナリイメージを配布できるようにするためです。たとえば、新しいCPU命令を利用したり、ハードウェアのバグを回避したりすることができます。[ 15 ] [ 16 ] DR-DOSカーネルも、程度は低いものの、基盤となるプロセッサ世代に応じてロード時に速度が重要なセクションを最適化します。[ 10 ] [ 11 ] [ nb 2 ]
いずれにせよ、メタレベルでは、プログラムは他の場所に保存されているデータを変更することによって(メタプログラミングを参照)、またはポリモーフィズムを使用することによって、自身の動作を変更することができます。
Alexia Massalinの博士論文[ 17 ] [ 18 ]で紹介されているSynthesisカーネルは、ファイルハンドルなどの個々のクォジェクトに対してコードが作成される、構造化された、あるいはオブジェクト指向的なアプローチで自己修正コードを作成する小さな Unixカーネルです。特定のタスクに対してコードを生成することで、Synthesis カーネルは (JIT インタプリタのように)定数畳み込みや共通部分式の削除などの最適化を適用できます。
Synthesisカーネルは非常に高速でしたが、完全にアセンブリ言語で記述されていました。そのため移植性が低く、マサリンの最適化アイデアはどの製品版カーネルにも採用されませんでした。しかし、その技術の構造から、既存の中級言語よりも複雑ではあるものの、より高水準の言語で表現できる可能性が示唆されます。そのような言語とコンパイラがあれば、より高速なオペレーティングシステムやアプリケーションの開発が可能になるでしょう。
ポール・ヘーバーリとブルース・カーシュは、開発コスト削減を優先して自己修正コードや最適化全般が「軽視」されていることに異議を唱えている。[ 19 ]
データキャッシュと命令キャッシュが結合されていないアーキテクチャ(例えば、一部のSPARC、ARM、MIPSコア)では、キャッシュ同期は変更コードによって明示的に実行されなければなりません(変更されたメモリ領域に対してデータキャッシュをフラッシュし、命令キャッシュを無効化します)。
場合によっては、自己書き換えコードの短いセクションは、最新のプロセッサでは実行速度が低下することがあります。これは、最新のプロセッサが通常、コードブロックをキャッシュメモリに保持しようとするためです。プログラムが自身の一部を書き換えるたびに、書き換えられた部分を再度キャッシュにロードする必要があり、書き換えられたコードレットが書き換え元のコードと同じキャッシュラインを共有している場合(書き換え元のコードのメモリアドレスから数バイト以内にある場合など)、わずかな遅延が発生します。
最新のプロセッサにおけるキャッシュ無効化の問題は、自己書き換えコードが依然として高速になるのは、内部ループ内の状態切り替えのように、書き換えがまれにしか発生しない場合に限られることを意味する。
現代のプロセッサのほとんどは、マシンコードを実行する前にロードします。つまり、命令ポインタに近すぎる命令が変更された場合、プロセッサはそれを検知せず、変更前のコードを実行します。プリフェッチ入力キュー(PIQ)を参照してください。PCプロセッサは、後方互換性のために自己変更コードを正しく処理する必要がありますが、その処理効率は決して高くありません。
自己改変コードにはセキュリティ上のリスクが伴うため、主要なオペレーティングシステムはすべて、そのような脆弱性が判明次第、慎重に修正している。懸念されるのは、プログラムが意図的に自己改変を行うことではなく、悪意のある攻撃によってプログラムが改変される可能性があることである。
悪意のあるコード変更を防ぐメカニズムの一つに、W^X(「書き込みまたは実行」の略)と呼ばれるオペレーティングシステムの機能があります。このメカニズムは、プログラムがメモリの任意のページを書き込み可能かつ実行可能にすることを禁止します。一部のシステムでは、書き込み権限が削除された場合でも、書き込み可能なページが実行可能に変更されることを防止します。また、別のシステムでは、メモリの複数のマッピングに異なる権限を設定できる一種の「バックドア」を提供しています。W^Xを回避する比較的移植性の高い方法は、すべての権限を持つファイルを作成し、そのファイルをメモリに2回マッピングすることです。Linuxでは、非公開のSysV共有メモリフラグを使用することで、ファイルを作成することなく実行可能な共有メモリを取得できます。
自己修正コードは、ソースプログラムリスト内の命令が必ずしも実行される命令ではないため、読みにくく保守しにくい。ただし、関数ポインタの置換による自己修正は、呼び出される関数名が後で識別される関数のプレースホルダーであることが明確であれば、それほど難解ではないかもしれない。
自己書き換えコードは、フラグをテストし、テスト結果に基づいて代替シーケンスに分岐するコードとして書き換えることができますが、自己書き換えコードは通常、より高速に実行されます。
自己改変コードはコードの認証と矛盾するため、システム上で実行されるすべてのコードに署名を義務付けるポリシーに例外を設ける必要がある場合がある。
変更されたコードは元のコードとは別に保存する必要があり、これは通常RAM内のコードを破棄し、必要に応じて実行ファイルから再読み込みするメモリ管理ソリューションと矛盾する。
命令パイプラインを備えた最新のプロセッサでは、頻繁に自身を修正するコードは、プロセッサが既にメモリからパイプラインに読み込んだ命令を修正する場合、実行速度が低下する可能性があります。このようなプロセッサの中には、修正された命令が正しく実行されることを保証する唯一の方法が、パイプラインをフラッシュして多くの命令を再読み込みすることであるものもあります。
自己書き換えコードは、以下のような環境では一切使用できません。
REP MOVSW以降では、カーネルのランタイム イメージ内の一部のデフォルトの 16 ビット (「コピー ワード」) 命令シーケンスを、REP MOVSDあるメモリ位置から別のメモリ位置にデータをコピーする際に 32 ビット (「コピー ダブル ワード」) 命令に動的に置き換えて、ディスク データ転送を高速化します。奇数カウントなどのエッジ ケースは処理されます。 [ 10 ] [ 11 ]SSEC は、自身の格納命令をデータとまったく同じように扱い、変更し、その結果に基づいて動作できる最初のオペレーティング コンピュータでした。
[…] 元々、
バイナリ書き換えは
、実行中にプログラムの一部を変更する必要性(例えば、 1960年代の
PDP-1
における実行時パッチ適用)から生まれたものでした[…]
(36ページ)
Z80
は命令のフェッチに加えて、
サイクルの半分をダイナミック RAM の更新に使用します
。
[
… ] Z80 は各
命令フェッチ
サイクルの半分を他のタスクの実行に費やす必要があるため、
命令バイト
をフェッチする時間はデータ バイトをフェッチする時間ほどありません。アクセスしているメモリ位置の
RAM チップ
のいずれか
が少し遅い場合、Z80 は命令をフェッチするときに間違ったビット パターンを取得する可能性がありますが、データを読み取るときには正しいビット パターンを取得します。 […] 内蔵のメモリ テストでは、この種の問題は検出されません […] これは厳密にはデータの読み書きテストです。テスト中、すべての命令フェッチはRAMからではなく
ROM
から行われるため、
H89
はメモリテストに合格するものの、一部のプログラムでは依然として不安定な動作を示す。 […] これは、RAM内を移動することでメモリをテストするプログラムである。その際、CPUはプログラムの現在のアドレスを
CRT
に表示し、そのアドレスの命令をフェッチする。そのアドレスのRAM ICが正常であれば、CPUはテストプログラムを次のメモリ位置に移動し、新しいアドレスを表示して、この手順を繰り返す。しかし、RAM ICのいずれかが十分に遅く、誤ったビットパターンを返すと、CPUは命令を誤って解釈し、予測不能な動作をする。ただし、ディスプレイは故障したICのアドレスを表示してロックアップする可能性が高い。これにより問題は8つのICに絞り込まれ、最大32個をチェックする必要があった場合と比べて改善されました。[…] このプログラムは、メモリの下位アドレスから最後の動作アドレスまでRST 7(RESTART 7)命令をプッシュすることでワームテストを実行します。プログラムの残りの部分は静止したままで、RST 7命令の現在の位置とその
再配置
の表示を処理します。ちなみに、このプログラムが
ワーム
テストと呼ばれるのは、RST 7命令がメモリを移動する際に、
NOP
(NO OPERATION)の
痕跡を
残すためです。[…]