Вопрос по c, assembly, cpu-cache, instructions, self-modifying – Как синхронизируется кэш инструкций x86?

22

Мне нравятся примеры, поэтому я написал немного самоизменяющегося кода на 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

Но, честно говоря, я неexpect это работать на всех. Я ожидал инструкции, содержащейc[2] = 0 быть кэшированным при первом вызовеcпосле чего все последовательные звонкиc будет игнорировать повторные изменения, внесенные вc (если я каким-то образом явно не сделал кэш недействительным). К счастью, мой процессор оказался умнее этого.

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

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

Строго говоря, макрос тестирования функций (обычно указывается в виде-D_POSIX_C_SOURCE=200809L или же-D_XOPEN_SOURCE=700) необходим для получения интерфейсов POSIX. R..
@WillBuddha: mmap полностью отсутствует как в стандартах c11, так и в стандартах gnu - это часть POSIX, которая является полностью независимым стандартом. Если ваша система поддерживает POSIX, она будет поддерживать mmap независимо от того, какие флаги компилятора вы используете. Если он не поддерживает POSIX, mmap (вероятно) не будет работать, независимо от того, какой флаг -std вы используете. Chris Dodd
mmap это чистый POSIX, но0b... все это выглядело как какая-то старая версия компилятора DOS ... Я понятия не имел, что у GCC это было R..
аналогичныйstackoverflow.com/questions/1756825/… Вы должны работать над чистой сборкой вместо C, чтобы лучше понять часть x86. Ciro Santilli 新疆改造中心 六四事件 法轮功
Какая среда / компилятор у вас есть гдеmmap и странный0b... двоичный синтаксис (не действительный C) работает? R..

Ваш Ответ

5   ответов
24

То, что вы делаете, обычно называютself-modifying code, Платформы Intel (и, вероятно, AMD) также выполняют всю работу по поддержаниюi/d cache-coherency, как указано в руководстве (Руководство 3А, Системное программирование)

11.6 SELF-MODIFYING CODE

A write to a memory location in a code segment that is currently cached in the processor causes the associated cache line (or lines) to be invalidated.

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

Applications that include self-modifying code use the same linear address for modifying and fetching the instruction. Systems software, such as a debugger, that might possibly modify an instruction using a different linear address than that used to fetch the instruction, will execute a serializing operation, such as a CPUID instruction, before the modified instruction is executed, which will automatically resynchronize the instruction cache and prefetch queue.

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

3.3.1.2.1 Self-Modifying Code

When a processor modifies any memory location that can contain an instruction, software must ensure that the instruction cache is made consistent with data memory and that the modifications are made visible to the instruction fetching mechanism. This must be done even if the cache is disabled or if the page is marked caching-inhibited.

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

Код, который вы предложили, ненадежен на архитектурах безsnooping или продвинутыйcache-coherency объекты, и, следовательно, скорее всего, не сможет

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

2

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

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

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

  1. mmap() sets up your virtual address space and iTLB1, dTLB1 and TLB2s.
  2. "a[0]=0b01000000;" will actually Trap(H/W magic) into kernel code and your physical address will be setup and all Processor TLBs will be loaded by the kernel. Then, You will be back into user mode and your processor will actually Load 16 bytes(H/W magic a[0] to a[3]) into L1 dCache and L2 Cache. Processor will really go into Memory again, only when you refer a[4] and and so on(Ignore the prediction loading for now!). By the time you complete "a[7]=0b11000011;", Your processor had done 2 burst READs of 16 bytes each on the eternal Bus. Still no actual WRITEs into physical memory. All WRITEs are happening within L1 dCache(H/W magic, Processor knows) and L2 cache so for and the DIRTY bit is set for the Cache-line.
  3. "a[3]++;" will have STORE Instruction in the Assembly code, but the Processor will store that only in L1 dCache&L2 and it will not go to Physical Memory.
  4. Let's come to the function call "a()". Again the processor do the Instruction Fetch from L2 Cache into L1 iCache and so on.
  5. Result of this user mode program will be the same on any Linux under any processor, due to correct implementation of low level mmap() syscall and Cache coherency protocol!
  6. If You are writing this code under any embedded processor environment without OS assistance of mmap() syscall, you will find the problem you are expecting. This is because your are not using either H/W mechanism(TLBs) or software mechanism(memory barrier instructions).
Что такоеTLB2? Обычно это не отдельныеTLB записи для кода / данных; но я знаю, что x86 немного странный.TLB это отдельныйMMU кеш и не относится кdcache или жеicache.
«TLB существует для MMU и для всех уровней кэшей внутри и / или вне процессорных ядер». - на самом деле нет, не для большинства процессоров. Не для кешей, которые физически проиндексированы и физически помечены. Некоторые процессоры x86 могут иметь L2 TLB, но это не обязательно связано с кэшем L2. Насколько я знаю, у x86 нет L3 TLB. Тем не менее, одна из моих любимых реализаций помещает TLB L2 и унифицированный кэш I / D L2 в один и тот же физический массив - так что у вас есть структура sngle. // Возможно, вы думаете о графических процессорах, которые часто имеют виртуальные кэши, с TLB на каждом.
TLB2 = & gt; Я имею в виду TLB L2 Cache. TLB существует для MMU и для всех уровней кэшей внутри и / или вне процессорных ядер. Ядро должно правильно управлять всеми этими TLB, чтобы эффективно использовать процессор H / W. TLB кэша используются процессором H / W для обеспечения протокола когерентности кэша. MMU TLB используется модулем MMU для преобразования виртуального в физическое, когда процессор помещает виртуальный адрес в шину после того, как произошла ошибка кэширования на всех уровнях кэшей (обычно L1, L2. В некоторых случаях даже L3).
4

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

@ ugoren- В этом случае, однако, код еще не должен находиться в i-кеше, потому что он был заново создан (из-за того, что MAP_PRIVATE копируется при записи), и ничто никогда не пыталось его выполнить. Если это была попытка изменить существующий код, а не создавать новый, то да, могут потребоваться дополнительные меры предосторожности. Хотя ради здравомыслия и мобильности программиста, я бы надеялся, что «mmap»; и компилятор позаботится о вас, насколько это возможно.
Это не обязательно полностью автоматически. Для других процессоров, например ARM, вам может понадобиться вставить специальную инструкцию для аннулирования конвейера / кэша.
Это не так для кэша команд в процессорах Intel. Запись в сегмент кодаdoes not всегда лишать законной силы кэш кода L1 и iTLB. Особое внимание следует уделить написанию самоизменяющегося кода.
4

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

6

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

Правильнее было бы сказать, что «он запускает машинный нюк с самоизменяющимся кодом (он же конвейерный сброс)». Для этого у процессоров Intel есть счетчик перфокарт. (Что-то вродеmachine_nuke.smcIIRC). Кроме того, я думаю, что я помню, что читалcall или жеjmp инструкция, подобная коду OP, важна для гарантированного обнаружения SMC. Хранилище, которое изменяет следующую инструкцию после себя, может не иметь непосредственного влияния на некоторые процессоры.
@Leeor: Поскольку этот вопрос конкретно касается x86, я хотел бы добавить, что, насколько мне известно, автоматическая аннулирование кэша на процессорах Intel сопровождается глубоким сбросом, поэтому SMC просто работает (хотя на высокая стоимость исполнения).
Отменить его из-за icache почти никогда не достаточно, это может быть уже где-то вдоль каналов. Если ваша система резервирует относительно строгий порядок в памяти, вам также потребуется глубокий сброс для очистки любой старой копии этой строки кода и любых более младших зависимых вычислений (в основном, всего)

Похожие вопросы