Как се синхронизира кешът на инструкциите 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 (освен ако по някакъв начин изрично съм обезсилил кеша). За щастие процесорът ми изглежда по-умен от това.

Предполагам, че процесорът сравнява RAM (ако приемем, че c дори се намира в RAM) с кеша на инструкциите всеки път, когато указателят на инструкциите направи голям скок (както при извикването на 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 кохерентност на кеша, както се посочва в ръководството (Ръководство 3A, Системно програмиране)

11.6 САМОМОДИФИЦИРАЩ СЕ КОД

Запис в място в паметта в кодов сегмент, който в момента е кеширан в процесора, води до невалидност на свързания кеш ред (или редове).

Но това твърдение е валидно, докато се използва един и същ линеен адрес за модифициране и извличане, което не е случаят с дебъгерите и двоичните зареждащи устройства, тъй като те не работят в същото адресно пространство:

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

Например операцията за сериализиране винаги се изисква от много други архитектури като PowerPC, където трябва да се направи изрично (E500 Core Manual):

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"):"... Освен това приемете, че имате 1K L1 iCache, 1K dCache във вашия процесор и малко L2 кеш в ядрото, . (Сега дни те са от порядъка на няколко MB)

  1. mmap() настройва вашето виртуално адресно пространство и iTLB1, dTLB1 и TLB2s.
  2. "a[0]=0b01000000;" всъщност ще прихване (H/W магия) в кода на ядрото и вашият физически адрес ще бъде настроен и всички TLB на процесора ще бъдат заредени от ядрото. След това ще се върнете в потребителски режим и вашият процесор действително ще зареди 16 байта (H/W магия от [0] до [3]) в L1 dCache и L2 Cache. Процесорът наистина ще влезе в паметта отново, само когато посочите [4] и така нататък (Засега игнорирайте зареждането на прогноза!). Докато завършите "a[7]=0b11000011;", вашият процесор е направил 2 пакетни четения от 16 байта всеки във вечната шина. Все още няма действителни WRITE във физическа памет. Всички WRITE се случват в L1 dCache (H/W магия, процесорът знае) и L2 кеш, така че битът DIRTY е зададен за Cache-line.
  3. "a[3]++;" ще има инструкция STORE в кода на асемблирането, но процесорът ще я съхранява само в L1 dCache&L2 и няма да отиде във физическата памет.
  4. Нека стигнем до извикването на функцията "a()". Отново процесорът извършва Извличане на инструкции от L2 Cache в L1 iCache и т.н.
  5. Резултатът от тази програма за потребителски режим ще бъде един и същ на всеки Linux под всеки процесор, поради правилното внедряване на mmap() syscall от ниско ниво и протокола за кохерентност на кеша!
  6. Ако пишете този код под каквато и да е вградена процесорна среда без помощ от операционната система на mmap() syscall, ще откриете проблема, който очаквате. Това е така, защото не използвате нито H/W механизъм (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, за да използва ефективно H/W на процесора. Кеш TLB се използват от H/W процесора, за да се грижат за протокола за кохерентност на кеша. MMU TLB се използва от MMU модула за виртуална към физическа транслация, когато процесорът поставя виртуалния адрес в шината, след като е направил пропуск на кеша във всички нива на кеш паметта (обикновено L1, L2. В някои случаи дори L3). - person sukumarst; 21.06.2013
comment
TLB съществува за MMU и за всички нива на кеш вътре и/или извън процесорните ядра. - всъщност не, не за повечето процесори. Не за кешове, които са физически индексирани и физически маркирани. Някои x86 CPU може да имат L2 TLB, но това не е задължително да има нещо общо с L2 кеша. Доколкото знам, никой x86 няма L3 TLB. Една от любимите ми реализации обаче поставя L2 TLB и L2 унифицирания I/D кеш в един и същи физически масив - така че да имате една структура. // Може би си мислите за GPU, които често имат виртуални кешове, с TLB на всеки. - person Krazy Glew; 22.08.2013