Как синхронизируется кэш инструкций x86?

мне нравятся примеры, поэтому я написал немного самомодифицирующегося кода на 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 (Если я что-то explicitedly недействительным кэша). К счастью, мой cpu, похоже, будь умнее.

Я думаю, что процессор сравнивает ОЗУ (предполагая c даже находится в ОЗУ) с кешем инструкций, когда указатель инструкции делает большой скачок (как и при вызове mmapped памяти выше), и аннулирует кэш, когда он не соответствует (все это?), но я надеюсь получить более точную информацию об этом. В частности, я хотел бы знать, можно ли считать это поведение предсказуемым (за исключением каких-либо различий в оборудовании и ОС) и полагаться о?

(Я, вероятно, должен обратиться к руководству Intel, но эта вещь составляет тысячи страниц, и я, как правило, теряюсь в ней...)

5 ответов


Что вы делаете обычно называют самомодифицирующийся код. Платформы Intel (и, вероятно, AMD тоже) выполняют за вас работу по поддержанию I/d кэш-когерентность, как указано в руководстве (руководство 3A, системное программирование)

11.6 САМОМОДИФИЦИРУЮЩИЙСЯ КОД

запись в ячейку памяти в сегменте кода, который в настоящее время кэшируется в процессор заставляет связанную строку кэша (или строки) быть лишать законной силы.

но это утверждение действительно, пока один и тот же линейный адрес используется для изменения и извлечения, что не относится к отладчики и бинарные погрузчики поскольку они не работают в одном адресном пространстве:

приложения, включающие самомодифицирующийся код, используют то же самое линейный адрес для изменения и извлечения инструкции. Системное программное обеспечение, например отладчик, который может изменить инструкция, использующая другой линейный адрес чем то, что используется для извлечения инструкции, выполнит операцию сериализации, такую как Инструкция CPUID, прежде чем измененная инструкция будет выполнена, которая будет автоматически повторная синхронизация кэша инструкций и очереди предварительной выборки.

например, операция сериализации всегда запрашивается многими другими архитектурами, такими как PowerPC, где это должно быть сделано явно (Е500 Ядра Руководство):

3.3.1.2.1 Самомодифицирующийся Код

когда процессор изменяет местоположение в памяти, которое может содержать инструкции, программное обеспечение должно убедитесь, что кэш инструкций согласован с памятью данных и что изменения сделаны видимыми для механизма извлечения инструкций. Это необходимо сделать, даже если кэш отключено или если страница помечена как кэширование-заблокировано.

интересно отметить этот PowerPC требует выпуска инструкции по синхронизации контекста, даже когда кэши отключены; я подозреваю, что он обеспечивает сброс более глубоких блоков обработки данных, таких как буферы загрузки/хранения.

предложенный вами код ненадежен на архитектурах без слежка или advanced кэш-когерентность объекты, и поэтому, вероятно, потерпят неудачу.

надеюсь, что это поможет.


Это довольно просто; запись на адрес, который находится в одной из строк кэша в кэше инструкций, делает его недействительным из кэша инструкций. Никакая "синхронизация".


CPU обрабатывает недействительность кэша автоматически, вам не нужно ничего делать вручную. Программное обеспечение не может предсказать, что будет или не будет в кэше процессора в любой момент времени, так что до оборудования, чтобы позаботиться об этом. Когда процессор увидел, что вы изменили данные, он соответствующим образом обновил свои различные кэши.


кстати, многие процессоры x86 (над которыми я работал) вынюхивают не только кэш инструкций, но и конвейер, окно инструкций - инструкции, которые в данный момент находятся в полете. Таким образом, самоизменяющийся код вступит в силу в следующей инструкции. Но вам рекомендуется использовать инструкцию сериализации, такую как CPUID, чтобы гарантировать, что ваш недавно написанный код будет выполнен.


Я только что достиг этой страницы в одном из моих поисков и хочу поделиться своими знаниями в этой области ядра Linux!

ваш код выполняется так, как ожидалось, и здесь для меня нет сюрпризов. Протокол согласованности кэша mmap() syscall и процессора делает этот трюк для вас. Флаги "PROT_READ|PROT_WRITE / PROT_EXEC" просят mmamp() правильно установить iTLB, dTLB кэша L1 и TLB кэша L2 этой физической страницы. Этот низкоуровневый архитектурный код ядра делает это по-разному в зависимости от архитектуры процессора (x86, AMD, ARM, SPARC и т. д...). Любая ошибка ядра здесь испортит вашу программу!

Это только для целей объяснения. Предположим, что ваша система не делает много, и нет никаких переключателей процессов между "a[0]=0b01000000;" и запуском "printf("\n"):"... Кроме того, предположим, что у вас есть 1K L1 iCache, 1k dCache в вашем процессоре и некоторый кэш L2 в ядре . (Сейчас это порядка нескольких MBs)

  1. mmap () настраивает виртуальное адресное пространство и iTLB1, dTLB1 и TLB2s.
  2. " a[0]=0b01000000;" фактически будет ловушкой(H/W magic) в код ядра, и ваш физический адрес будет настроен, и все процессорные TLBs будут загружены ядром. Затем вы вернетесь в пользовательский режим, и ваш процессор фактически загрузит 16 байтов(H/W magic a[0] to a[3]) в кэш L1 dCache и L2. Процессор действительно снова войдет в память, только когда вы обратитесь к[4] и так далее on (игнорировать загрузку прогноза на данный момент!). К тому времени, когда вы закончите "a[7]=0b11000011;", ваш процессор сделал 2 пакетных чтения по 16 байт каждый на вечной шине. По-прежнему никаких записей в физическую память. Все записи происходят в L1 dCache (H / W magic, процессор знает) и L2 cache, поэтому для и грязный бит установлен для кэш-строки.
  3. " a[3]++; " будет иметь инструкцию STORE в коде сборки, но процессор будет хранить это только в L1 dCache&L2, и он не будет физическая память.
  4. давайте перейдем к вызову функции " a ()". Снова процессор выполняет выборку инструкций из кэша L2 в L1 iCache и так далее.
  5. результат этой программы пользовательского режима будет одинаковым на любом Linux под любым процессором, благодаря правильной реализации низкоуровневого протокола согласованности mmap() и Кэша!
  6. если вы пишете этот код в любой встроенной среде процессора без помощи ОС mmap () syscall, вы найдете проблему вы ждете. Это связано с тем, что вы не используете механизм H/W(TLBs) или программный механизм(инструкции по барьеру памяти).