Наблюдение за устаревшими инструкциями на x86 с самоизменяющимся кодом
Мне сказали и прочитали из руководств Intel, что можно записывать инструкции в память, но очередь предварительной выборки инструкций уже извлекла устаревшие инструкции и выполнит эти старые инструкции. Мне не удалось наблюдать за этим поведением. Моя методология заключается в следующем.
В руководстве по разработке программного обеспечения Intel из раздела 11.6 говорится, что
Запись в область памяти в сегменте кода, который в настоящее время кэшируется в процессоре, приводит к тому, что соответствующая строка (или строки) кэша становится недействительной. Эта проверка основана на физическом адресе инструкции. Кроме того, процессоры семейства P6 и Pentium проверяют, может ли запись в сегмент кода изменить инструкцию, предварительно выбранную для выполнения. Если запись влияет на предварительно выбранную инструкцию, очередь предварительной выборки становится недействительной. Эта последняя проверка основана на линейном адресе инструкции.
Итак, похоже, что если я надеюсь выполнить устаревшие инструкции, мне нужно, чтобы два разных линейных адреса ссылались на одну и ту же физическую страницу. Итак, я отображаю в памяти файл на два разных адреса.
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);
У меня есть функция сборки, которая принимает один аргумент, указатель на инструкцию, которую я хочу изменить.
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
В C я копирую код в отображенный файл памяти. Я вызываю функцию с линейного адреса a1
но я передаю указатель на a2
как цель модификации кода.
#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);
Если процессор подобрал измененный код, val==1. В противном случае, если были выполнены устаревшие инструкции (два nops), val==0.
Я запускал его на Intel Core i5 с тактовой частотой 1,7 ГГц (MacBook Air 2011) и процессоре Intel® Xeon® X3460 с частотой 2,80 ГГц. Однако каждый раз, когда я вижу val == 1, процессор указывает на новую инструкцию.
Кто-нибудь сталкивался с поведением, которое я хочу наблюдать? Правильно ли мое рассуждение? Я немного запутался в руководстве, в котором упоминаются процессоры P6 и Pentium, а также в том, что не хватает упоминания моего процессора Core i5. Возможно, происходит что-то еще, что заставляет процессор сбрасывать свою очередь предварительной выборки команд? Любое понимание было бы очень полезно!
4 ответа
Я думаю, вы должны проверить MACHINE_CLEARS.SMC
счетчик производительности (частьMACHINE_CLEARS
событие) процессора (он доступен в Sandy Bridge 1, который используется в вашей Air powerbook; а также доступен на вашем Xeon, то есть Nehalem 2- поиск "smc"). Ты можешь использовать oprofile
,perf
или Intel Vtune
найти его значение:
Машина очищает
Метрическое описание
Некоторые события требуют, чтобы весь конвейер был очищен и перезапущен сразу после последней удаленной инструкции. Этот показатель измеряет три таких события: нарушения порядка памяти, самоизменяющийся код и определенные загрузки в недопустимые диапазоны адресов.
Возможные проблемы
Значительная часть времени выполнения тратится на обработку станков. Изучите события MACHINE_CLEARS, чтобы определить конкретную причину.
MACHINE_CLEARS Код события: 0xC3 Маска SMC: 0x04
Обнаружен самоизменяющийся код (SMC).
Количество обнаруженных автоматических модификаций кода
Intel также говорит о smc http://software.intel.com/en-us/forums/topic/345561(связано с таксономией Intel Performance Bottleneck Analyzer
Это событие возникает при обнаружении самоизменяющегося кода. Обычно это могут использовать люди, которые выполняют двоичное редактирование, чтобы заставить его выбрать определенный путь (например, хакеры). Это событие подсчитывает количество раз, которое программа записывает в раздел кода. Самомодифицирующийся код приводит к серьезным штрафам во всех процессорах Intel 64 и IA-32. Модифицированная строка кэша записывается обратно в кэши L2 и LLC. Кроме того, инструкции должны быть перезагружены, что приведет к снижению производительности.
Я думаю, вы увидите несколько таких событий. Если они есть, то CPU смог обнаружить акт самоизменения кода и вызвал "Machine Clear" - полный перезапуск конвейера. Первыми этапами являются Fetch, и они будут запрашивать кэш L2 для нового кода операции. Я очень заинтересован в точном подсчете количества событий SMC за выполнение вашего кода - это даст нам некоторую оценку задержек.. (SMC подсчитывается в некоторых единицах, где предполагается, что 1 единица составляет 1,5 такта процессора - B.6.2.6 из руководства по оптимизации Intel)
Мы видим, что Intel говорит, что "перезапущен сразу после последней удаленной инструкции", поэтому я думаю, что последняя удаленная инструкция будетmov
; и ваши nops уже находятся в стадии разработки. Но SMC будет повышен после ухода Мова, и это убьет все в трубопроводе, включая nops.
Этот перезапуск конвейера, вызванный SMC, недешев, у Агнера есть некоторые измерения в Optimizing_assembly.pdf- "17.10 Самомодифицирующийся код (Все процессоры)" (я думаю, что любой Core2/CoreiX подобен PM здесь):
Штраф за выполнение фрагмента кода сразу после его изменения составляет примерно 19 часов для P1, 31 для PMMX и 150-300 для PPro, P2, P3, PM. P4 будет очищать весь кэш трассировки после самоизменения кода. Процессоры 80486 и более ранние версии требуют перехода между модифицирующим и модифицированным кодом для очистки кеша кода....
Самомодифицирующийся код не считается хорошей практикой программирования. Его следует использовать только в том случае, если выигрыш в скорости является значительным, а модифицированный код выполняется так много раз, что преимущество перевешивает штрафы за использование самоизменяющегося кода.
Рекомендовано использовать разные линейные адреса для сбоя детектора SMC: /questions/5013424/kak-sinhroniziruetsya-kesh-instruktsij-x86/5013442#5013442- Я попытаюсь найти актуальную документацию Intel... Не могу ответить на ваш настоящий вопрос сейчас.
Здесь могут быть некоторые советы: Руководство по оптимизации, 248966-026, апрель 2012 г. "3.6.9 Смешивание кода и данных":
Размещение записываемых данных в сегменте кода может быть невозможно отличить от самоизменяющегося кода. Записываемые данные в сегменте кода могут подвергаться тому же снижению производительности, что и самоизменяющийся код.
и следующий раздел
Программное обеспечение должно избегать записи в кодовую страницу на той же самой подстранице объемом 1 КБ, которая выполняется, или извлекать код на той же самой подстранице размером 2 КБ, которая записывается. Кроме того, совместное использование страницы, содержащей непосредственно или спекулятивно выполненный код, с другим процессором в качестве страницы данных может вызвать условие SMC, которое приводит к очистке всего конвейера машины и кэша трассировки. Это связано с самоизменяющимся условием кода.
Таким образом, возможно, есть некоторые схемы, которые управляют пересечениями записываемых и исполняемых подстраниц.
Вы можете попытаться выполнить изменение из другого потока (кросс-модифицирующий код) - но необходима очень осторожная синхронизация потока и очистка конвейера (вы можете захотеть включить некоторые грубые задержки в потоке записи; CPUID сразу после синхронизации желательно). Но вы должны знать, что ОНИ уже исправили это, используя "ядерное оружие" - проверьте патент US6857064.
Я немного запутался в руководстве, в котором упоминаются процессоры P6 и Pentium
Это возможно, если вы загрузили, расшифровали и выполнили устаревшую версию инструкции по эксплуатации Intel. Вы можете сбросить конвейер и проверить эту версию: Номер заказа: 325462-047US, июнь 2013 г. "11.6 КОД САМОДИФИЦИРУЮЩЕГОСЯ". Эта версия по-прежнему ничего не говорит о более новых процессорах, но упоминает, что при изменении с использованием разных виртуальных адресов поведение может быть несовместимым между микроархитектурами (оно может работать на вашем Nehalem/Sandy Bridge и может не работать на.. Skymont)
11.6 КОД САМОДИФИЦИРОВАНИЯ Запись в ячейку памяти в сегменте кода, который в данный момент кэшируется в процессоре, приводит к тому, что соответствующая строка (или строки) кэша становится недействительной. Эта проверка основана на физическом адресе инструкции. Кроме того, процессоры семейства P6 и Pentium проверяют, может ли запись в сегмент кода изменить инструкцию, предварительно выбранную для выполнения. Если запись влияет на предварительно выбранную инструкцию, очередь предварительной выборки становится недействительной. Эта последняя проверка основана на линейном адресе инструкции. Для процессоров Pentium 4 и Intel Xeon запись или отслеживание инструкции в сегменте кода, где целевая инструкция уже декодирована и находится в кэше трассировки, делает недействительным весь кэш трассировки. Последнее поведение означает, что программы, которые самостоятельно изменяют код, могут вызвать значительное снижение производительности при работе на процессорах Pentium 4 и Intel Xeon.
На практике проверка линейных адресов не должна создавать проблем совместимости между процессорами IA-32. Приложения, которые включают самоизменяющийся код, используют один и тот же линейный адрес для изменения и извлечения инструкции.
Системное программное обеспечение, такое как отладчик, которое может изменить инструкцию, используя линейный адрес, отличный от того, который использовался для извлечения инструкции, выполнит операцию сериализации, такую как инструкция CPUID, перед выполнением модифицированной инструкции, которая автоматически выполнит повторную синхронизацию кеш инструкций и очередь предвыборок. (См. Раздел 8.1.3, "Обработка кода с самомодификацией и кросс-модификацией", для получения дополнительной информации об использовании кода с самоизменением.)
Для процессоров Intel486 запись в кеш-инструкцию изменит ее как в кеше, так и в памяти, но если инструкция была предварительно выбрана до записи, старая версия инструкции могла бы быть той, которая была выполнена. Чтобы предотвратить выполнение старой инструкции, очистите модуль предварительной выборки, кодируя инструкцию перехода сразу после любой записи, которая изменяет инструкцию
РЕАЛЬНОЕ обновление, для Google"Обнаружение SMC"(с кавычками) и есть некоторые подробности того, как современный Core2 / Core iX обнаруживает SMC, а также множество списков ошибок с Xeons и Pentiums, висящими в детекторе SMC:
http://www.google.com/patents/US6237088 Система и способ отслеживания инструкций в полете в трубопроводе @ 2001
DOI 10.1535 / itj.1203.03 (Google для него, есть бесплатная версия на citeseerx.ist.psu.edu) - в Penryn был добавлен "INCLUSION FILTER" для уменьшения количества ложных обнаружений SMC; "существующий механизм обнаружения включения" изображен на рисунке 9
http://www.google.com/patents/US6405307- более старый патент на логику обнаружения SMC
Согласно патенту US6237088 (фиг.5, краткое изложение) существует "буфер линейного адреса" (со многими линейными адресами по одному адресу на каждую извлеченную инструкцию - или, другими словами, буфер, заполненный извлеченными IP-адресами с точностью строки кеша). Каждое хранилище или более точная фаза "адреса магазина" каждого хранилища будет передаваться в параллельный компаратор для проверки, будут ли храниться пересечения с любой из выполняющихся в настоящее время инструкций или нет.
В обоих патентах четко не сказано, будут ли они использовать физический или логический адрес в логике SMC... L1i в Sandy bridge - это VIPT ( виртуально индексируемый, физически помеченный, виртуальный адрес для индекса и физический адрес в теге.) Согласно http://nick-black.com/dankwiki/index.php/Sandy_Bridge поэтому у нас есть физический адрес в тот момент, когда кэш L1 возвращает данные. Я думаю, что Intel может использовать физические адреса в логике обнаружения SMC.
Более того, http://www.google.com/patents/US6594734@ 1999 (опубликовано в 2003 году, просто помните, что цикл разработки ЦП составляет около 3-5 лет), в разделе "Сводка" говорится, что SMC сейчас находится в TLB и использует физические адреса (или, другими словами - пожалуйста, не пытайтесь обмануть детектор SMC):
Самоизменяющийся код обнаруживается с использованием буфера быстрого преобразования. [Который ] хранит адреса физических страниц, по которым можно выполнять отслеживание с использованием адреса физической памяти хранилища в памяти.... Чтобы обеспечить более высокую степень детализации, чем страница адресов, биты FINE HIT включены в каждую запись в кэше, связывая информацию в кэше с частями страницы в памяти.
(часть страницы, называемая в патенте US 6594734 квадрантами, звучит как 1K подстраниц, не так ли?)
Тогда они говорят
Следовательно, отслеживание, запускаемое хранением инструкций в памяти, может выполнять обнаружение SMC путем сравнения физического адреса всех инструкций, хранящихся в кэше инструкций, с адресом всех инструкций, хранящихся на соответствующей странице или страницах памяти. Если есть совпадение адресов, это означает, что ячейка памяти была изменена. В случае совпадения адреса, указывающего условие SMC, кэш команд и конвейер команд сбрасываются блоком вывода, и новые команды выбираются из памяти для хранения в кэш команд.
Поскольку отслеживание для обнаружения SMC является физическим, и ITLB обычно принимает в качестве входных данных линейный адрес для преобразования в физический адрес, ITLB дополнительно формируется в виде адресуемой памяти содержимого на физических адресах и включает в себя дополнительный порт сравнения ввода (упоминаемый как в качестве порта отслеживания или порта обратного перевода)
- Таким образом, чтобы обнаружить SMC, они вынуждают хранилища пересылать физический адрес обратно в буфер инструкций через snoop (аналогичные отслеживания будут доставляться из других ядер / процессоров или из записей DMA в наши кэши....), если физика snoop. адрес конфликтует со строками кэша, хранящимися в буфере команд, мы перезапустим конвейер через сигнал SMC, доставленный из iTLB в модуль вывода. Можно представить, сколько процессорных часов будет потрачено в таком цикле отслеживания от dTLB через iTLB до модуля вывода (он не может отменить следующую инструкцию "nop", хотя она была выполнена раньше, чем mov и не имеет побочных эффектов). Но ЧТО? ITLB имеет физический адресный ввод и вторую CAM (большую и горячую) только для поддержки и защиты от сумасшедшего и изменчивого самоизменяющегося кода.
PS: А что если мы будем работать с огромными страницами (4M или 1G)? L1TLB имеет огромные записи страницы, и может быть много ложных обнаружений SMC для 1/4 из 4 МБ страницы...
PPS: Существует вариант, что ошибочная обработка SMC с разными линейными адресами присутствовала только в ранних версиях P6/Ppro/P2...
Мне сказали и прочитали из руководств Intel, что можно записывать инструкции в память, но очередь предварительной выборки команд [возможно, уже] извлекла устаревшие инструкции и [может] выполнить эти старые инструкции. Мне не удалось наблюдать за этим поведением.
Да, ты был бы.
Все или почти все современные процессоры Intel строже, чем руководство:
Они отслеживают конвейер на основе физического адреса, а не только линейного.
Реализации процессора могут быть более строгими, чем руководства.
Они могут выбрать так, потому что они столкнулись с кодом, который не соответствует правилам в руководствах, что они не хотят нарушать.
Или... потому что самый простой способ придерживаться архитектурной спецификации (которая в случае SMC была официально "до следующей инструкции сериализации", но на практике для унаследованного кода была "до следующей принятой ветви, которая больше, чем??? байтов ") может быть более строгим.
Семейство Sandybridge (по крайней мере Skylake) по-прежнему ведет себя таким же образом, по-видимому, отслеживая физический адрес.
Ваш тест несколько сложнее. Я не вижу смысла в дальнем прыжке, и если вы соберете (и скомпонуете при необходимости) функцию SMC в плоский двоичный файл, вы можете просто открыть + mmap дважды. Делать a1
а также a2
указатели на функции return a1(a2)
после картирования.
Вот простой тестовый набор на тот случай, если кто-то захочет попробовать на своей машине: (Блок open/assert/mmap был скопирован из вопроса, спасибо за отправную точку.)
(С другой стороны, вы должны каждый раз перестраивать плоский двоичный файл SMC, потому что отображаете его MAP_SHARED
на самом деле изменяет это. IDK, как получить два сопоставления одной и той же физической страницы, которые не изменят базовый файл; запись в MAP_PRIVATE будет COW его на другой физической странице. Итак, запись машинного кода в файл и их отображение имеет смысл, когда я это понимаю. Но мой ассм все еще намного проще.)
// smc-stale.c
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
typedef int (*intfunc_t)(void *); // __attribute__((sysv_abi)) // in case you're on Windows.
int main() {
int fd = open("smc-func", O_RDWR);
assert(fd>=0);
intfunc_t a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
intfunc_t a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
assert(a1 != a2);
return a1(a2);
}
Источник NASM для тестовой функции:
(См. Как создать простые двоичные файлы, такие как nasm -f bin, с помощью GNU GAS ассемблера? as
+ ld
Альтернативой nasm -f
)
;;build with nasm smc-func.asm -fbin is the default.
bits 64
entry: ; rdi = another mapping of the same page that's executing
mov byte [rdi+dummy-entry], 0xcc ; trigger any copy-on-write page fault now
mov r8, rbx ; CPUID steps on call-preserved RBX
cpuid ; serialize for good measure
mov rbx, r8
; mfence
; lfence
mov dword [rdi + retmov+1 - entry], 0 ; return 0 for snooping
retmov:
mov eax, 1 ; opcode + imm32 ; return 1 for stale
ret
dummy: dd 0xcccccccc
На i7-6700k под управлением Linux 4.20.3-arch1-1-ARCH мы не наблюдаем устаревшую выборку кода. mov
что переписало немедленное 1
с 0
действительно изменил эту инструкцию, прежде чем она запустилась.
peter@volta:~/src/experiments$ gcc -Og -g smc-stale.c
peter@volta:~/src/experiments$ nasm smc-func.asm && ./a.out; echo $?
0
# remember to rebuild smc-func every time, because MAP_SHARED modifies it
Он нацелен на гораздо более старый процессор (Intel 8088), но 4-канальный музыкальный проигрыватель в конце демонстрации 8088 MPH не только выполняет устаревшие инструкции, но и зависит от их устаревания! https://www.reenigne.org/blog/8088-pc-speaker-mod-player-how-its-done/