Obserwując pobieranie starych instrukcji na x86 z samo modyfikującym się kodem
Zostałem poinformowany i przeczytałem z podręczników Intela, że możliwe jest zapisanie instrukcji w pamięci, ale kolejka pobierania instrukcji już pobrała przestarzałe instrukcje i wykona te stare instrukcje. Nie udało mi się zaobserwować tego zachowania. Moja metodologia jest następująca.
Podręcznik rozwoju oprogramowania firmy Intel stanowi w sekcji 11.6
Zapis do lokalizacji pamięci w segmencie kodu, który jest aktualnie buforowany w procesorze, powoduje unieważnienie powiązanej linii (lub linii) pamięci podręcznej. To sprawdzenie opiera się na fizycznym adresie instrukcji.Ponadto rodziny procesorów P6 i Pentium sprawdzają, czy zapis do segmentu kodu może modyfikować instrukcję, która została wstępnie pobrana do wykonania. Jeśli zapis wpływa na wstępnie pobraną instrukcję, kolejka pobierania wstępnego jest unieważniana. Ta ostatnia kontrola opiera się na adresie liniowym instrukcji.
Wygląda więc na to, że jeśli mam nadzieję wykonać stare instrukcje, muszę mieć dwa różne adresy liniowe odnoszące się do tej samej strony fizycznej. Tak więc pamięć mapuje plik na dwa różne adresy.
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);
Mam funkcję zespołu, która pobiera pojedynczy argument, wskaźnik do instrukcji, którą chcę zmienić.
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
W C kopiuję kod do pliku odwzorowanego w pamięci. Wywoływam funkcję z adresu liniowegoa1
, ale przekazuję wskaźnik doa2
jako cel modyfikacji kodu.
#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);
Jeśli CPU odebrał zmodyfikowany kod, val == 1. W przeciwnym razie, jeśli stare instrukcje zostały wykonane (dwa nopy), val == 0.
Uruchomiłem to na Intel Core i5 1,7 GHz (2011 macbook air) i CPU Intel Xeon X3460 @ 2,80 GHz. Za każdym razem jednak widzę, że val == 1 wskazuje, że CPU zawsze zauważa nową instrukcję.
Czy ktoś doświadczył zachowania, które chcę obserwować? Czy moje rozumowanie jest prawidłowe? Trochę się mylę z instrukcją dotyczącą procesorów P6 i Pentium, a co z brakiem wzmianki o moim procesorze Core i5. Być może dzieje się coś innego, co powoduje, że procesor opróżnia kolejkę pobierania wstępnego instrukcji? Każdy wgląd byłby bardzo pomocny!