Beobachten, wie veraltete Anweisungen auf x86 mit selbstmodifizierendem Code abgerufen werden
Ich habe von den Handbüchern von Intel erfahren und gelesen, dass es möglich ist, Anweisungen in den Speicher zu schreiben, aber die Anweisungsvorabrufwarteschlange hat die veralteten Anweisungen bereits abgerufen und wird diese alten Anweisungen ausführen. Es ist mir nicht gelungen, dieses Verhalten zu beobachten. Meine Methodik ist wie folgt.
Das Intel-Softwareentwicklungshandbuch besagt ab Abschnitt 11.6, dass
Ein Schreiben in eine Speicherstelle in einem Codesegment, das derzeit im Prozessor zwischengespeichert ist, führt dazu, dass die zugehörige (n) Cache-Zeile (n) ungültig wird (n). Diese Prüfung basiert auf der physikalischen Adresse des Befehls.Außerdem prüfen die Prozessoren der P6-Familie und von Pentium, ob ein Schreibvorgang in ein Codesegment eine Anweisung ändern kann, die vorab zur Ausführung abgerufen wurde. Wenn sich der Schreibvorgang auf einen vorabgerufenen Befehl auswirkt, wird die Vorabrufwarteschlange ungültig. Diese letztere Prüfung basiert auf der linearen Adresse des Befehls.
Wenn ich also veraltete Anweisungen ausführen möchte, müssen zwei verschiedene lineare Adressen auf dieselbe physische Seite verweisen. Ich speichere also eine Datei auf zwei verschiedene Adressen.
int fd = open("code_area", O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);
assert(fd>=0);
write(fd, zeros, 0x1000);
uint8_t *a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
uint8_t *a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
assert(a1 != a2);
Ich habe eine Assembly-Funktion, die ein einziges Argument, einen Zeiger auf die Anweisung, die ich ändern möchte, nimmt.
fun:
push %rbp
mov %rsp, %rbp
xorq %rax, %rax # Return value 0
# A far jump simulated with a far return
# Push the current code segment %cs, then the address we want to far jump to
xorq %rsi, %rsi
mov %cs, %rsi
pushq %rsi
leaq copy(%rip), %r15
pushq %r15
lretq
copy:
# Overwrite the two nops below with `inc %eax'. We will notice the change if the
# return value is 1, not zero. The passed in pointer at %rdi points to the same physical
# memory location of fun_ins, but the linear addresses will be different.
movw $0xc0ff, (%rdi)
fun_ins:
nop # Two NOPs gives enough space for the inc %eax (opcode FF C0)
nop
pop %rbp
ret
fun_end:
nop
In C kopiere ich den Code in die Speicherzuordnungsdatei. Ich rufe die Funktion von der linearen Adresse aufa1
, aber ich gebe einen Zeiger aufa2
als Ziel der Codeänderung.
#define DIFF(a, b) ((long)(b) - (long)(a))
long sz = DIFF(fun, fun_end);
memcpy(a1, fun, sz);
void *tochange = DIFF(fun, fun_ins);
int val = ((int (*)(void*))a1)(tochange);
Hat die CPU den geänderten Code erhalten, ist val == 1. Andernfalls, wenn die veralteten Anweisungen ausgeführt wurden (zwei Nops), ist val == 0.
Ich habe dies auf einem 1,7 GHz Intel Core i5 (2011 MacBook Air) und einer Intel (R) Xeon (R) CPU X3460 bei 2,80 GHz ausgeführt. Jedes Mal, wenn ich jedoch val == 1 sehe, bemerkt die CPU immer den neuen Befehl.
Hat jemand Erfahrung mit dem Verhalten, das ich beobachten möchte? Ist meine Argumentation richtig? Ich bin ein wenig verwirrt über das Handbuch, in dem P6- und Pentium-Prozessoren erwähnt werden, und über das Fehlen, meinen Core i5-Prozessor zu erwähnen. Vielleicht ist etwas anderes im Gange, das die CPU veranlasst, ihre Anweisungsvorabrufwarteschlange zu leeren? Jeder Einblick wäre sehr hilfreich!