私は例が好きなので、C言語で自己修正コードを少し書きました...
#include <stdio.h>
#include <sys/mman.h> // linux
int main(void) {
unsigned char *c = mmap(NULL, 7, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|
MAP_ANONYMOUS, -1, 0); // get executable memory
c[0] = 0b11000111; // mov (x86_64), immediate mode, full-sized (32 bits)
c[1] = 0b11000000; // to register rax (000) which holds the return value
// according to linux x86_64 calling convention
c[6] = 0b11000011; // return
for (c[2] = 0; c[2] < 30; c[2]++) { // incr immediate data after every run
// rest of immediate data (c[3:6]) are already set to 0 by MAP_ANONYMOUS
printf("%d ", ((int (*)(void)) c)()); // cast c to func ptr, call ptr
}
putchar('\n');
return 0;
}
...明らかに動作します:
>>> gcc -Wall -Wextra -std=c11 -D_GNU_SOURCE -o test test.c; ./test
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
しかし、正直なところ、私はそれがうまくいくとはまったく予想していませんでした。への最初の呼び出しで を含む命令c[2] = 0
がキャッシュされることを期待していましたc
。その後、への連続したすべての呼び出しは、へc
の繰り返しの変更を無視しますc
(何らかの方法で明示的にキャッシュを無効にしない限り)。幸いなことに、私の CPU はそれよりも優れているようです。
CPUc
は、命令ポインタが(上記のmmapされたメモリへの呼び出しと同様に)大規模なジャンプを行うたびにRAM(RAMに存在すると仮定しても)を命令キャッシュと比較し、一致しない場合はキャッシュを無効にすると思います(すべて?)、しかし、私はそれについてより正確な情報を得ることを望んでいます. 特に、この動作が予測可能 (ハードウェアと OS の違いを除いて) であると見なされ、信頼できるかどうかを知りたいですか?
(おそらく Intel のマニュアルを参照する必要がありますが、それは何千ページにもわたる長さであり、迷子になりがちです...)