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!

Antworten auf die Frage(3)

Ihre Antwort auf die Frage