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

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

(Мне, вероятно, следует обратиться к руководству Intel, но оно состоит из тысяч страниц, и я часто теряюсь в нем ...)


person Will    schedule 12.06.2012    source источник
comment
Какая у вас среда / компилятор, где работает mmap и нечетный 0b... двоичный синтаксис (недействительный C)?   -  person R.. GitHub STOP HELPING ICE    schedule 12.06.2012
comment
mmap - это чистый POSIX, но 0b... материал выглядел как какой-то устаревший компилятор DOS ... Я понятия не имел, что у GCC он есть.   -  person R.. GitHub STOP HELPING ICE    schedule 12.06.2012
comment
@WillBuddha: mmap полностью отсутствует в стандартах c11 и gnu - это часть POSIX, который является полностью независимым стандартом. Если ваша система поддерживает POSIX, она будет поддерживать mmap независимо от того, какие флаги компилятора вы используете. Если он не поддерживает POSIX, mmap (вероятно) не будет работать, независимо от того, какой флаг -std вы используете.   -  person Chris Dodd    schedule 12.06.2012
comment
Строго говоря, макрос проверки функций (обычно указываемый в форме -D_POSIX_C_SOURCE=200809L или -D_XOPEN_SOURCE=700) необходим для получения интерфейсов POSIX.   -  person R.. GitHub STOP HELPING ICE    schedule 12.06.2012
comment
аналогичный stackoverflow.com / questions / 1756825 / Вы должны работать на чистой сборке вместо C, чтобы лучше понять часть x86.   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 08.11.2015


Ответы (5)


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

11.6 САМОИЗМЕНЯЕМЫЙ КОД

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

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

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

Например, операция сериализации всегда запрашивается многими другими архитектурами, такими как PowerPC, где она должна выполняться явно (Базовое руководство E500):

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

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

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

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

Надеюсь на эту помощь.

person Benoit    schedule 12.06.2012

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

person R.. GitHub STOP HELPING ICE    schedule 12.06.2012
comment
Недостаточно сделать его недействительным из icache, он уже может быть где-то по трубам. Если ваша система резервирует относительно строгий порядок памяти, вам также понадобится глубокая очистка, чтобы очистить любую старую копию этой строки кода и любые младшие зависимые вычисления (в основном все) - person Leeor; 26.06.2013
comment
@Leeor: Поскольку этот вопрос касается конкретно x86, я хотел бы добавить, что, насколько мне известно, автоматическое аннулирование кеша на процессорах Intel сопровождается глубокой очисткой, поэтому SMC просто работает (хотя и с высокой стоимостью к производительности). - person Nathan Fellman; 22.08.2013
comment
Правильнее было бы сказать, что он запускает ядерную бомбу с самомодифицирующимся кодом (также известную как очистка конвейера). В процессорах Intel для этого есть счетчик производительности. (Что-то вроде machine_nuke.smc, IIRC). Кроме того, я помню, как читал, что инструкция call или jmp, такая как код OP, необходима для гарантированного обнаружения SMC. Сохранение, которое изменяет следующую инструкцию после себя, может не иметь немедленного эффекта на некоторые процессоры. - person Peter Cordes; 11.07.2016

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

person bta    schedule 12.06.2012
comment
Это не обязательно полностью автоматически. Для других процессоров, например ARM, вам может потребоваться вставить специальную инструкцию, чтобы сделать конвейер / кеш недействительным. - person starblue; 12.06.2012
comment
Это не верно для кеша инструкций в процессорах Intel. Запись в сегмент кода не всегда делает недействительными кэш кода L1 и iTLB. При написании самомодифицирующегося кода следует проявлять особую осторожность. - person ugoren; 12.06.2012
comment
@ ugoren - В этом случае, однако, кода еще не должно быть в i-cache, потому что он был недавно создан (из-за того, что MAP_PRIVATE был копией при записи), и никто никогда не пытался его выполнить. Если это была попытка изменить существующий код, а не создать новый, тогда да, могут потребоваться дополнительные меры предосторожности. Хотя ради рассудка программиста и переносимости, я надеюсь, что mmap и компилятор позаботятся об этом как можно больше. - person bta; 12.06.2012

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

person Krazy Glew    schedule 22.08.2013

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

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

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

  1. mmap () настраивает ваше виртуальное адресное пространство и iTLB1, dTLB1 и TLB2.
  2. "а [0] = 0b01000000;" на самом деле будет перехватывать (H / W magic) код ядра, и ваш физический адрес будет установлен, и все TLB процессора будут загружены ядром. Затем вы вернетесь в пользовательский режим, и ваш процессор фактически загрузит 16 байтов (H / W magic a [0] to a [3]) в L1 dCache и L2 Cache. Процессор действительно снова войдет в память, только когда вы обратитесь к [4] и так далее (пока не обращайте внимания на загрузку прогнозов!). К тому времени, когда вы завершите «a [7] = 0b11000011;», ваш процессор выполнил 2 пакетных чтения по 16 байтов каждое на вечной шине. По-прежнему нет фактических ЗАПИСЕЙ в физическую память. Все ЗАПИСИ происходят в L1 dCache (H / W magic, Процессор знает) и в кэше L2, так что для и бит DIRTY установлен для строки кэша.
  3. "a [3] ++;" будет иметь инструкцию STORE в коде сборки, но процессор сохранит ее только в L1 dCache и L2, и она не перейдет в физическую память.
  4. Перейдем к вызову функции «a ()». Снова процессор выполняет выборку инструкций из кэша L2 в iCache L1 и так далее.
  5. Результат этой программы пользовательского режима будет одинаковым на любом Linux и на любом процессоре, благодаря правильной реализации низкоуровневого системного вызова mmap () и протокола согласованности кэша!
  6. Если вы пишете этот код в любой среде встроенного процессора без помощи ОС с помощью системного вызова mmap (), вы обнаружите ожидаемую проблему. Это связано с тем, что вы не используете ни аппаратный механизм (TLB), ни программный механизм (инструкции барьера памяти).
person sukumarst    schedule 19.06.2013
comment
Что такое TLB2? Обычно нет отдельных записей TLB для кода / данных; но я знаю, что x86 немного странный. TLB - это отдельный кэш MMU, не связанный с dcache или icache. - person artless noise; 20.06.2013
comment
TLB2 = ›Я имею в виду TLB L2 Cache. TLB существует для MMU и для всех уровней кэшей внутри и / или вне ядер процессора. Ядро должно правильно управлять всеми этими TLB, чтобы эффективно использовать аппаратное обеспечение процессора. TLB кэша используются H / W процессора, чтобы заботиться о протоколе когерентности кэша. MMU TLB используется блоком MMU для преобразования виртуального в физический, когда процессор помещает виртуальный адрес в шину после получения промаха кэша на всех уровнях кэшей (обычно L1, L2. В некоторых случаях даже L3). - person sukumarst; 21.06.2013
comment
TLB существует для MMU и для всех уровней кэшей внутри и / или вне ядер процессора. - на самом деле нет, не для большинства процессоров. Не для кешей, которые физически индексированы и физически помечены. Некоторые процессоры x86 могут иметь TLB L2, но это не обязательно связано с кешем L2. Насколько я знаю, на x86 нет L3 TLB. Однако одна из моих любимых реализаций помещает TLB L2 и унифицированный кеш ввода-вывода L2 в один и тот же физический массив - так что у вас есть структура sngle. // Возможно, вы думаете о графических процессорах, которые часто имеют виртуальные кеши, с TLB на каждом. - person Krazy Glew; 22.08.2013