Современные процессоры имеют весьма внушительный набор команд. Общее их кол-во можно подсчитать открыв том(2) Интеловского мануала
Под капотом..
1. Основная идея
При включении машины, системный BIOS/EFI должен каким-либо образом опознать установленный на материнской плате CPU, чтобы настроить под него чипсет и всё остальное. Нужно сказать, что на первом витке эволюции вплоть до древних 16-битных процессоров i80286 в этом не было необходимости, поскольку архитектура компьютера была примитивной, с довольно устойчивым состоянием. Но тут пришёл 32-битный процессор нового поколения i80386, который мог работать как в 16, так и в 32-битном защищённом режиме. Здесь-то и всплыла проблема. Инженеры Intel с присущим им оптимизмом решили её просто – они жёстко запрограммировали проц, чтобы на старте он возвращал свою "половую" принадлежность в регистровой паре EСX:EDX. Но дальше-больше..
Инструкцию CPUID принёс с собой следующий процессор i80486, когда благодаря механизму PAE (Physical Address Extension, расширение физического адреса) 32-битному CPU стало доступно сразу три ступени памяти, разрядностью 16, 32 и 36-бит. Теперь, биосу требовался паспорт процессора, иначе он просто не мог должным образом настроить ответственные за распределение ОЗУ регистры чипсета. Так возникла необходимость в идентификации CPU, что вынудило Intel включить в набор команд инструкцию CPUID. Первая его спецификация от Intel датируется 1993-годом.
Если говорить об инструкциях в целом, то каждая из них является совокупностью микрокоманд. Когда инструкция простая (типа INC, ADD, SUB и т.д.), для неё достаточно одной микрокоманды, а если сложная – она собирается из нескольких таких команд. На этапе изготовления процессора, производитель зашивает в специально предназначенную для этих целей встроенную ROM-память весь поддерживаемый набор микрокоманд, из которых в конечном счёте и собираются разнообразные инструкции. Блок процессора под названием "микро-секвенсер" (microsequencer) считывает микрокоманды из хранилища ROM, и по-требованию передаёт их в декодер инструкций. Таким образом, чтобы включить в набор команд процессора какую-нибудь новую инструкцию, в большинстве случаях производителю достаточно добавить лишь пару ключевых микрокоманд.
На этапе производства процессора отследить все его ошибки невозможно, что влечёт за собой различные хард-глюки. По этой причине, производитель предусматривает обновление микро-кодов уже при эксплуатации готового продукта. Обновы всегда кратны 2Кб и их можно найти на сайтах Intel и AMD. Включённый в пакет прошивальщик сначала идентифицирует бортовой CPU, и если его модель/степпинг/ревизия совпадает с прошивкой, то заливает обновлённые микро-коды не в ROM процессора, а в системный BIOS, от куда проц потом их и стягивает. Intel давно уже практикует рассылку своих обновлений всем производителям биосов, чтобы они включали их в свой код. В конечном итоге, поддержка биосами современных процессоров определяется в первую очередь наличием соответствующей прошивки.
На уровне-же микроархитектуры CPUID устроена так, что помимо прочего, в постоянную память ROM производитель закладывает порядка 30-ти основных листов с детальной информацией о своём продукте, и некоторое кол-во (под)листов. В документации эти листы назвали "Leaf", а подлисты "Subleaf". Перед тем-как вызвать инструкцию CPUID, мы должны предварительно указать номер конкретного листа в регистре EAX, и если этот лист имеет вложенный (под)лист, то его номер занести ещё и в регистр ECX. На программном уровне, запрос каждого из листов представляет собой отдельную функцию, о чём прямым текстом и сообщается в спецификации CPUID.
В таблице ниже я перечислил все доступные мне функции и их назначения. К сожалению, в наличии оказалась только спека от 2012-года, и более свежих данных мне так и не удалось выудить из сети (если кто поделится, буду благодарен). Причём действительна представленная табличка только для процессоров Intel, поскольку у AMD нумерация функций совсем иная. Как по мне, так двум этим гигантам пора-бы уже не выносить нам мозг, а "выпить за мировую", чтобы согласовывать между собой хотя-бы такие элементарные вещи. Ан-нет.. нужно чтобы всё было через известное место. В силу того, что на обеих моих машинах (бук и стационар) установлены процессоры Intel, всё нижесказанное будет относиться исключительно к продуктам Intel.
Обратите внимание, что вложенные подлисты имеют только функции EAX=04,07,0В и 0Dh (выделены красным). Например если мы запросим инструкцию
2. Стандартные функции
Начнём по-порядку и рассмотрим на конкретных примерах, какого рода информацию возвращают все функции CPUID. В некоторых случаях, это поможет осуществить направленные на конкретный процессор атаки, ну или просто идентифицировать его, чтобы пустить код по нужной ветке. К примеру если мы написали приложение с использованием современных инструкций SSE4 или AVX, то это приложение может попасть на платформу, процессор которой вообще не поддерживает данный тип инструкций. Значит перед запуском софта нужно обязательно позвать CPUID и проверить в нём нужные биты. В любом случае знания всегда идут на шаг впереди их отсутствия, а потому профит по-любому будет.
2.0. Функция EAX=0. (Vendor-ID, and Largest Standard Function)
Данная функция возвращает в EAX общее число поддерживаемых процессором стандартных функций, и в остальные три регистра сбрасывает строку с именем производителя. Для Intel и AMD эта строка будет иметь вид "GenuineIntel" и "AuthenticAMD" соответственно. На процессоре своего стационара я получил всего 14 указанных в таблице выше функций, при этом
2.1. Функция EAX=1. (Feature Information)
Очередная функция возвращает характерные особенности бортового процессора, где можно будет найти его: Family/Model/Stepping, идентификатор текущего контролёра прерываний APIC, макс.кол-во поддерживаемых ядер (не путать с реальным кол-вом), размер линейки кэш-памяти, и в 64-битах регистровой пары ECX:EDX разнообразные свойства, типа поддержка инструкций SSE4/AVX, гипер-трейдинг HT, x2APIC, RDRAND и многое другое (см.спецификацию). Чтобы вывести все эти 64 типа информации на консоль, придётся чекать их биты по отдельности например инструкцией
Как видно из листа "FeatureFlags", мой бородатый проц не знаком с инструкциями SSE4/AVX, а лишь поддерживает макс.SSSE3. Имеет встроенный датчик температуры "TermalMonitor2" (TM2), в режиме энергосбережения может сохранять в специальной область памяти значения всех регистров CPU/FPU (xsave/fxsave соответственно), при переходе в ядро вместо устаревшего
2.2. Функция EAX=2. (Cache Descriptors)
Каждый дескриптор кэша имеет размер 1-байт, и является закодированным значением определённой строки. На всякий-пожарный, я оформил все эти строки в виде таблички, и сохранил их в инклуд (см.скрепку в подвале статьи). Поскольку результат мы получим в четырёх 4-байтных регистрах EAX,EBX,ECX,EDX, итого получается 4х4=16 байт. Тогда общее кол-во дескрипторов будет 15, без учёта регистра-счётчика(AL). Если дескриптор имеет значение нуль, значит он пустой и не используется.
Теперь, для разбора дескрипторов последовательно перемещаемся от самого старшего байта всех регистров вниз, к младшему их байту (ну или наоборот). Код реализации парсинга дескрипторов и фрагмент таблицы из спецификации представлены ниже:
Проясним немного ситуацию с устройством кэша TLB процессора..
Не секрет, что процессоры заранее кэшируют нужную информацию, как-только им предоставляется малейшая возможность обращения к оперативной памяти ОЗУ. При этом контролёр памяти старается оттяпать из неё как-можно больше данных, для чего предусмотрен пакетный режим чтения. Значение "Burst-Length" (длина пакета) выставляется чипсетом на макс. =8, в результате чего контролёр повторяет операцию чтения по 8-байтной шине памяти 8-раз. Итого за одно обращение к ОЗУ контролёр читает про запас как-минимум пакетами по 64-байта данных, которые помещает в кэш-линейки "64 byte line size" (см.скрин выше). Кэши L1 бывают двух типов – отдельно для инструкций, отдельно для данных. А вот кэши L2 и L3 уже общие, без разделения на инструкции и данные.
Однако кроме кэша данных, в процессоре имеется ещё и кэш для хранения адресов страниц виртуальной памяти, к которым он обращался последний раз. Этот кэш назвали TLB, или "Translation Lookaside Buffer" (буфер ассоциативной трансляции). Проблема в том, что для доступа к памяти ОЗУ, транслятор контролёра памяти (в процессоре) сначала должен преобразовать текущий адрес из виртуального в физический, на что может уйти до 100-тактов процессора. По современным меркам, это чистой воды расточительство, поэтому один раз преобразовав вирт.адрес, процессор запоминает его в своём кэше TLB. Как показала практика, такой алго даёт существенный прирост скорости при чтении данных из оперативной памяти.
2.3. Функция EAX=3. (Processor Serial Number)
Если процессор фирменный и благополучно прошёл все надзорные инстанции, производитель может записать в него серийный номер продукта. Но в наше время это скорее исключение, чем правило, и лично мне такие пока не встречались.
2.4. Функция EAX=4. (Deterministic Cache Parameters)
Функция
Вызов этой функции, в регистре EBX возвращает информацию об ассоциативности кэшей, кол-ве блоков "Way", и числу записей "Sets". Используя эти данные можно вычислить и размер кэша, который не возвращает функция
Ниже представлен возможный вариант вывода результата этой функции на консоль. Здесь я при первом вызове получаю кол-во ядер, и дальше в цикле зову эту функцию с инкрементом счётчика в ECX, чтобы получить инфу о кэшах всех уровней:
Здесь стоит отметить, что для хранения информации о кэшах, современные процессоры 64-бит используют только данную функцию
3. Расширенные функции
Остальные стандартные функции не представляют особого интереса, и при желании вы можете по-экспериментировать с ними сами. А вот рассмотреть набор расширенных функций CPUID вполне себе стоит. Во времена своего младенчества, Intel и AMD разделили инструкцию CPUID пополам. Чтобы хоть как-то отличаться от Intel, инженеры AMD забрали себе старшую половину функций, начиная с 0x80000000. Однако позже всё утряслось и теперь обоим производителям принадлежит весь диапазон, хотя и нумерация по назначению абсолютно разная.
3.0. Функция EAX=8000_0000h (Largest Extended Function)
Эта функция особо не напрягается, и возвращает в единственный регистр EAX общее число поддерживаемых расширенных функций. На обоих своих процессорах я получаю значение EAX=8.
3.1. Функция EAX=8000_0001h (Extended Feature Bits)
Очередная функция возвращает в регистр EDX флаги расширенных возможностей процессора.
Посмотрим на флаг "XD bit".
В документации AMD этот бит называется NX, от слова "No eXecution", а Intel обозвала его XD – "eXecution Disable". Это самый старший бит(64) в записях виртуальных страниц PTE (Page Table Entry), который предотвращает исполнение данных. Для более детального ознакомления с ним отправляю вас к статье "DEP и ASLR – игра без правил", где ему была посвящена одна глава. Как видно из скрина, мой древний проц поддерживает и его, и архитектуру Intel-64, хотя в упор не видит гигантских виртуальных страниц размером 1Gb (по-умолчанию 4Кб), и не знаком с современной инструкций генератора рандома
3.2. Функции EAX=8000_0002/3/4 (Processor Brand String)
Цепочка этих трёх функций возвращает имя процессора, как его окрестил производитель. Под строку брэнда производитель резервирует 48 байт во-встроенной памяти ROM, поэтому чтобы получить её полностью, нужно вызвать эти функции последовательно 3-раза. При каждом запросе в регистры EAX,EBX,ECX,EDX будет возвращаться новая информация, поэтому не забываем сохранять её в выделенный буфер. В примере ниже я поместил счётчик цикла в переменную "count", и на каждой итерации увеличиваю номер функции в EAX:
Видимо хреновый контроль ведёт Intel над своими подчинёнными, о чём свидетельствует оформление строки. Зачем вставлять лишние пробелы в середину строки, когда можно дополнить её до 48-байт этими-же пробелами в конце? Здесь явно что-то не чисто..
3.3. Функция EAX=8000_0006h (Extended L2 Cache Features)
Ещё одна функция для сбора информации о кэше, только теперь исключительно об общем L2. Прямым текстом сообщает его размер и длину линейки, и в закодированном виде ассоциативность (мне лень было декодировать). Бьёт без промаха точно в цель на любых процессорах Intel, хоть 32, хоть 64-бит.
3.4. Функция EAX=8000_0008h (Virtual & Physical Address Sizes)
Это последняя функция из списка расширенных и возвращает способность процессора адресовать физическую и виртуальную память ОЗУ. Обратите внимание, что хоть процессор и 64-битный, память он адресует максимум 48-бит. Если вогнать это значение в степень двойки (зовите на помощь калькулятор), то получим макс.поддерживаемую современными процессорами память =
4. Пример кода для сбора информации CPUID
В заключении приведу готовый пример кода, который соберёт всю представленную выше информацию.
5. Постфикс
Опыты выше показали, что CPUID изначально не способен возвратить некоторую информацию. К примеру в нём нет данных типа: кодовое имя ядра, порог рабочих напряжений, тип сокета, потребляемая мощность, и значение производственного тех.процесса. Судя по всему, софт на подобии CPU-Z хранит это всё в своих таблицах, которые программисты скурпулёзно собирают ручками. Базы подобного рода пишутся исходя из значений "Family/Model/Stepping", предоставляемые нам функцией
В скрепке можно найти инклуд с описанием дескрипторов кэшей, и исполняемый файл для тестов. Всем удачи, пока!
Ссылка скрыта от гостей
. На различных площадках народ сильно преувеличивает утверждая, будто число опкодов давно перевалило за тысячу. Чтобы-уж наверняка и ради собственного интереса, я скопировал из оглавления указанной доки в блокнот весь перечень, и к своему удивлению на выходе получил всего 632 строки текста, в каждой из которых одна инструкция. Судя по-всему, на данный момент именно такой объём полностью удовлетворяет нашим требованиям, иначе монархи из Intel не упустили-бы возможности записать на свой счёт очередной новый опкод. Из всего этого листа, в данной статье предлагаю рассмотреть одну из интересных инструкций под названием CPUID, которая призвана идентифицировать бортовой процессор CPU.Под капотом..
1. Основная идея;
2. Стандартные функции CPUID;
3. Расширенные функции;
4. Практика;
5. Постфикс.
--------------------------------------------------------1. Основная идея
При включении машины, системный BIOS/EFI должен каким-либо образом опознать установленный на материнской плате CPU, чтобы настроить под него чипсет и всё остальное. Нужно сказать, что на первом витке эволюции вплоть до древних 16-битных процессоров i80286 в этом не было необходимости, поскольку архитектура компьютера была примитивной, с довольно устойчивым состоянием. Но тут пришёл 32-битный процессор нового поколения i80386, который мог работать как в 16, так и в 32-битном защищённом режиме. Здесь-то и всплыла проблема. Инженеры Intel с присущим им оптимизмом решили её просто – они жёстко запрограммировали проц, чтобы на старте он возвращал свою "половую" принадлежность в регистровой паре EСX:EDX. Но дальше-больше..
Инструкцию CPUID принёс с собой следующий процессор i80486, когда благодаря механизму PAE (Physical Address Extension, расширение физического адреса) 32-битному CPU стало доступно сразу три ступени памяти, разрядностью 16, 32 и 36-бит. Теперь, биосу требовался паспорт процессора, иначе он просто не мог должным образом настроить ответственные за распределение ОЗУ регистры чипсета. Так возникла необходимость в идентификации CPU, что вынудило Intel включить в набор команд инструкцию CPUID. Первая его спецификация от Intel датируется 1993-годом.
Если говорить об инструкциях в целом, то каждая из них является совокупностью микрокоманд. Когда инструкция простая (типа INC, ADD, SUB и т.д.), для неё достаточно одной микрокоманды, а если сложная – она собирается из нескольких таких команд. На этапе изготовления процессора, производитель зашивает в специально предназначенную для этих целей встроенную ROM-память весь поддерживаемый набор микрокоманд, из которых в конечном счёте и собираются разнообразные инструкции. Блок процессора под названием "микро-секвенсер" (microsequencer) считывает микрокоманды из хранилища ROM, и по-требованию передаёт их в декодер инструкций. Таким образом, чтобы включить в набор команд процессора какую-нибудь новую инструкцию, в большинстве случаях производителю достаточно добавить лишь пару ключевых микрокоманд.
На этапе производства процессора отследить все его ошибки невозможно, что влечёт за собой различные хард-глюки. По этой причине, производитель предусматривает обновление микро-кодов уже при эксплуатации готового продукта. Обновы всегда кратны 2Кб и их можно найти на сайтах Intel и AMD. Включённый в пакет прошивальщик сначала идентифицирует бортовой CPU, и если его модель/степпинг/ревизия совпадает с прошивкой, то заливает обновлённые микро-коды не в ROM процессора, а в системный BIOS, от куда проц потом их и стягивает. Intel давно уже практикует рассылку своих обновлений всем производителям биосов, чтобы они включали их в свой код. В конечном итоге, поддержка биосами современных процессоров определяется в первую очередь наличием соответствующей прошивки.
На уровне-же микроархитектуры CPUID устроена так, что помимо прочего, в постоянную память ROM производитель закладывает порядка 30-ти основных листов с детальной информацией о своём продукте, и некоторое кол-во (под)листов. В документации эти листы назвали "Leaf", а подлисты "Subleaf". Перед тем-как вызвать инструкцию CPUID, мы должны предварительно указать номер конкретного листа в регистре EAX, и если этот лист имеет вложенный (под)лист, то его номер занести ещё и в регистр ECX. На программном уровне, запрос каждого из листов представляет собой отдельную функцию, о чём прямым текстом и сообщается в спецификации CPUID.
В таблице ниже я перечислил все доступные мне функции и их назначения. К сожалению, в наличии оказалась только спека от 2012-года, и более свежих данных мне так и не удалось выудить из сети (если кто поделится, буду благодарен). Причём действительна представленная табличка только для процессоров Intel, поскольку у AMD нумерация функций совсем иная. Как по мне, так двум этим гигантам пора-бы уже не выносить нам мозг, а "выпить за мировую", чтобы согласовывать между собой хотя-бы такие элементарные вещи. Ан-нет.. нужно чтобы всё было через известное место. В силу того, что на обеих моих машинах (бук и стационар) установлены процессоры Intel, всё нижесказанное будет относиться исключительно к продуктам Intel.
Обратите внимание, что вложенные подлисты имеют только функции EAX=04,07,0В и 0Dh (выделены красным). Например если мы запросим инструкцию
CPUID.EAX=4:ECX=0
, то в регистрах EAX,EBX,ECX,EDX процессор вернёт нам информацию о своём кэше L1. Теперь, чтобы получить характеристики кэша L2, мы должны повторно вызвать CPUID.EAX=4
, только на этот раз сменить номер (под)листа на ECX=1. Соответственно для кэша L3 будет уже EAX=4:ECX=2
, и т.д. в том-же духе. Вне зависимости от разрядности процессора 32 или 64-бит, CPUID всегда возвращает информацию в первые четыре регистра общего назначения EAX,EBX,ECX,EDX. Надеюсь суть вы уловили, и вооружившись спекой можно теперь смело пробираться вглубь.
Ссылка скрыта от гостей
Ссылка скрыта от гостей
2. Стандартные функции
Начнём по-порядку и рассмотрим на конкретных примерах, какого рода информацию возвращают все функции CPUID. В некоторых случаях, это поможет осуществить направленные на конкретный процессор атаки, ну или просто идентифицировать его, чтобы пустить код по нужной ветке. К примеру если мы написали приложение с использованием современных инструкций SSE4 или AVX, то это приложение может попасть на платформу, процессор которой вообще не поддерживает данный тип инструкций. Значит перед запуском софта нужно обязательно позвать CPUID и проверить в нём нужные биты. В любом случае знания всегда идут на шаг впереди их отсутствия, а потому профит по-любому будет.
2.0. Функция EAX=0. (Vendor-ID, and Largest Standard Function)
Данная функция возвращает в EAX общее число поддерживаемых процессором стандартных функций, и в остальные три регистра сбрасывает строку с именем производителя. Для Intel и AMD эта строка будет иметь вид "GenuineIntel" и "AuthenticAMD" соответственно. На процессоре своего стационара я получил всего 14 указанных в таблице выше функций, при этом
0x08
и 0x0C
находятся в резерве. А вот на буке с 64-битным процессором счётчик показывает уже 25 поддерживаемых функций, но как говорилось выше спеки на него нет. Пример вызова функции выглядит так:
C-подобный:
xor eax,eax ;//<--- функция(0)
cpuid
mov dword[buff+0],ebx ;// кидаем производителя в буфер
mov dword[buff+4],edx
mov dword[buff+8],ecx
cinvoke printf,<10,' ***** EAX=0 ***************',\
10,' Func supported.: %d',\
10,' Vendor.........: %s',10,0>,eax,buff
2.1. Функция EAX=1. (Feature Information)
Очередная функция возвращает характерные особенности бортового процессора, где можно будет найти его: Family/Model/Stepping, идентификатор текущего контролёра прерываний APIC, макс.кол-во поддерживаемых ядер (не путать с реальным кол-вом), размер линейки кэш-памяти, и в 64-битах регистровой пары ECX:EDX разнообразные свойства, типа поддержка инструкций SSE4/AVX, гипер-трейдинг HT, x2APIC, RDRAND и многое другое (см.спецификацию). Чтобы вывести все эти 64 типа информации на консоль, придётся чекать их биты по отдельности например инструкцией
BT
(bit-test), как в примере ниже. Если тестируемый бит взведён, инструкция взводит флаг процессора(CF):
C-подобный:
mov eax,1
cpuid
mov [feature1],ecx ;//<--- запомнить свойства в переменных
mov [feature2],edx
movzx ecx,bl ;// Brand index
movzx edx,bh ;// Cache line size
shl edx,3 ;// ^^^^^
shr ebx,16 ;//
movzx esi,bl ;//
movzx edi,bh ;//
cinvoke printf,<10,' ***** EAX=1 ****************',\
10,' Family & Model.: 0x%x',\
10,' Brand index....: 0x%02x',\
10,' Cache line.....: %d byte',\
10,' CPU cores......: %d',\
10,' APIC id........: %d',\
10,' Feature flags..: ',0>, eax,ecx,edx,esi,edi
bt [feature2],25 ;// проверить бит(25) в регистре EDX
jnc @f ;// пропустить, если он сброшен
cinvoke printf,<'SSE,'> ;// иначе: есть поддержка инструкций SSE
@@: bt [feature2],26 ;// проверить на SSE2..
jnc @f ;//
cinvoke printf,<'SSE2,'> ;// есть поддержка
@@: bt [feature1],0 ;// и т.д..
Как видно из листа "FeatureFlags", мой бородатый проц не знаком с инструкциями SSE4/AVX, а лишь поддерживает макс.SSSE3. Имеет встроенный датчик температуры "TermalMonitor2" (TM2), в режиме энергосбережения может сохранять в специальной область памяти значения всех регистров CPU/FPU (xsave/fxsave соответственно), при переходе в ядро вместо устаревшего
SYSCALL
использует SYSENTER
, способен очищать линейки кэша L1 при помощи инструкции CLFLUSH
, и прочее. Описание всех этих аббревиатур имеется в спецификации, так-что курите её.2.2. Функция EAX=2. (Cache Descriptors)
CPUID.EAX=2
возвращает дескрипторы кэша и TLB. На выходе, младшие 8-бит регистра EAX (AL) содержат значение, которое определяет, сколько раз нужно потянуть за CPUID.EAX=2
, чтобы получить полный образ подсистемы кэш процессора. В большинстве случаях процессоры возвращают в AL значение(1), значит достаточно только одного вызова инструкции CPUID.Каждый дескриптор кэша имеет размер 1-байт, и является закодированным значением определённой строки. На всякий-пожарный, я оформил все эти строки в виде таблички, и сохранил их в инклуд (см.скрепку в подвале статьи). Поскольку результат мы получим в четырёх 4-байтных регистрах EAX,EBX,ECX,EDX, итого получается 4х4=16 байт. Тогда общее кол-во дескрипторов будет 15, без учёта регистра-счётчика(AL). Если дескриптор имеет значение нуль, значит он пустой и не используется.
Теперь, для разбора дескрипторов последовательно перемещаемся от самого старшего байта всех регистров вниз, к младшему их байту (ну или наоборот). Код реализации парсинга дескрипторов и фрагмент таблицы из спецификации представлены ниже:
C-подобный:
cinvoke printf,<10,10,' ***** EAX=2 ****************',0>
@@: mov eax,2
cpuid
dec al ;// повторить CPUID.EAX=2 по значению в AL
jnz @b
push ebx ecx edx
call printCpuid2 ;// распечатать дескрипторы из EAX
pop eax ;//<--- из EDX
call printCpuid2
pop eax ;//<--- из ECX
call printCpuid2
pop eax ;//<--- из EBX
call printCpuid2
;//-----------------------------
printCpuid2:
mov ecx,4 ;// байт в регистре
@next: xor ebx,ebx ;// под очередной дескриптор
mov bl,al ;// взять мл.байт из источника
shr eax,8 ;// удалить этот байт в источнике
or ebx,ebx ;// нулевой дескриптор?
jz @f ;//
call printCacheDescriptor ;// если нет..
@@: loop @next ;//
ret
printCacheDescriptor:
push eax ecx ;//
mov ecx,[tSize] ;// размер таблицы в инклуде
mov esi,cacheTable ;// её адрес
@@: cmp [esi],ebx ;// поиск соответствия
jnz @fuck ;// промах!
add esi,4 ;// иначе: берём адрес соответствующей строки
cinvoke printf,<10,'%s',0>,dword[esi]
jmp @f ;// ..и на выход.
@fuck: add esi,8 ;// если промах, смещаемся к сл.дескриптору в таблице..
loop @b ;//
@@: pop ecx eax ;//
ret
Проясним немного ситуацию с устройством кэша TLB процессора..
Не секрет, что процессоры заранее кэшируют нужную информацию, как-только им предоставляется малейшая возможность обращения к оперативной памяти ОЗУ. При этом контролёр памяти старается оттяпать из неё как-можно больше данных, для чего предусмотрен пакетный режим чтения. Значение "Burst-Length" (длина пакета) выставляется чипсетом на макс. =8, в результате чего контролёр повторяет операцию чтения по 8-байтной шине памяти 8-раз. Итого за одно обращение к ОЗУ контролёр читает про запас как-минимум пакетами по 64-байта данных, которые помещает в кэш-линейки "64 byte line size" (см.скрин выше). Кэши L1 бывают двух типов – отдельно для инструкций, отдельно для данных. А вот кэши L2 и L3 уже общие, без разделения на инструкции и данные.
Однако кроме кэша данных, в процессоре имеется ещё и кэш для хранения адресов страниц виртуальной памяти, к которым он обращался последний раз. Этот кэш назвали TLB, или "Translation Lookaside Buffer" (буфер ассоциативной трансляции). Проблема в том, что для доступа к памяти ОЗУ, транслятор контролёра памяти (в процессоре) сначала должен преобразовать текущий адрес из виртуального в физический, на что может уйти до 100-тактов процессора. По современным меркам, это чистой воды расточительство, поэтому один раз преобразовав вирт.адрес, процессор запоминает его в своём кэше TLB. Как показала практика, такой алго даёт существенный прирост скорости при чтении данных из оперативной памяти.
2.3. Функция EAX=3. (Processor Serial Number)
Если процессор фирменный и благополучно прошёл все надзорные инстанции, производитель может записать в него серийный номер продукта. Но в наше время это скорее исключение, чем правило, и лично мне такие пока не встречались.
2.4. Функция EAX=4. (Deterministic Cache Parameters)
Функция
CPUID.EAX=4
имеет подлист, поэтому вызывать её нужно с регистром ECX. Как и функция EAX=2, она возвращает информацию о кэшах процессора L1,2,3, только без кэшей TLB. Индекс в регистре ECX указывает, о каком кэше следует возвращать информацию. Чтобы получить информацию о кэшах всех уровней, мы должны вызывать её в цикле, пока в битах EAX[4:0] не получим нуль. Порядок возврата кэшей не регламентируется и может быть изменен по усмотрению Intel. Это означает, что при вызове функции с ECX=0 мы можем получить инфу например о кэше L3 и т.д. Бонусом, индекс(0) в регистре ECX возвращает ещё и кол-во ядер процессора, в старших битах EAX[31:26].Вызов этой функции, в регистре EBX возвращает информацию об ассоциативности кэшей, кол-ве блоков "Way", и числу записей "Sets". Используя эти данные можно вычислить и размер кэша, который не возвращает функция
CPUID.EAX=4
в явном виде. Спецификация предлагает нам такую формулу для этих целей:
C-подобный:
Cache Size in Bytes = (Ways +1) x (Partitions +1) x (Line Size +1) x (Sets +1)
= (EBX[31:22]+1) x (EBX[21:12]+1) x (EBX[11:0]+1 x (ECX +1)
Ниже представлен возможный вариант вывода результата этой функции на консоль. Здесь я при первом вызове получаю кол-во ядер, и дальше в цикле зову эту функцию с инкрементом счётчика в ECX, чтобы получить инфу о кэшах всех уровней:
C-подобный:
mov eax,4 ;// лист(4)
xor ecx,ecx ;// подлист(0)
cpuid
shr eax,26 ;//<--- оставить в EAX только биты 31:26
inc eax
cinvoke printf,<10,\
10,' ***** EAX=4 ****************',\
10,' CPU cores......: %d',0>,eax
;//----------------------------
@@: mov eax,4
mov ecx,[count] ;// счётчик в переменной
cpuid
push ecx ebx ebx ebx eax
and eax,11111b ;// оставить 5 мл.бит в EAX
or eax,eax ;// проверить на окончание цикла
jz @stop04 ;// да – на выход
mov esi,cacheType ;// иначе: ESI линк на таблицу в инклуде
dec eax ;// делаем из EAX смещение в таблице
shl eax,2 ;// ^^^^
add esi,eax ;// ESI = указатель на строку с именем кэша
pop ebx ;// EBX = уровень кэша L1,2,3
shr ebx,5 ;//
and ebx,111b ;//
cinvoke printf,<10,' Cache..........: L%d %-12s',0>,ebx,dword[esi]
pop eax ebx ecx edx
and eax,0xfff ;//<----- считаем размер кэша по формуле..
inc eax
shr ebx,12
and ebx,0x3ff
inc ebx
shr ecx,22
and ecx,0x3ff
inc ecx
imul eax,ebx
imul eax,ecx
inc edx
imul eax,edx
shr eax,10
cinvoke printf,<' %dK',0>,eax
inc [count] ;// ECX +1
jmp @b ;// уйти на повтор цикла..
@stop04: add esp,4*5
Здесь стоит отметить, что для хранения информации о кэшах, современные процессоры 64-бит используют только данную функцию
CPUID.EAX=4
, полностью игнорируя ранее рассмотренную CPUID.EAX=2
, которая возвращает дескрипторы кэшей. В этом случае, при вызове EAX=2 на выходе получаем 0xFF
вместо дескрипторов, что означает см.функцию EAX=4. Вот фрагмент из описания функции CPUID.EAX=2
с дескриптором 0xFF
:3. Расширенные функции
Остальные стандартные функции не представляют особого интереса, и при желании вы можете по-экспериментировать с ними сами. А вот рассмотреть набор расширенных функций CPUID вполне себе стоит. Во времена своего младенчества, Intel и AMD разделили инструкцию CPUID пополам. Чтобы хоть как-то отличаться от Intel, инженеры AMD забрали себе старшую половину функций, начиная с 0x80000000. Однако позже всё утряслось и теперь обоим производителям принадлежит весь диапазон, хотя и нумерация по назначению абсолютно разная.
3.0. Функция EAX=8000_0000h (Largest Extended Function)
Эта функция особо не напрягается, и возвращает в единственный регистр EAX общее число поддерживаемых расширенных функций. На обоих своих процессорах я получаю значение EAX=8.
3.1. Функция EAX=8000_0001h (Extended Feature Bits)
Очередная функция возвращает в регистр EDX флаги расширенных возможностей процессора.
C-подобный:
mov eax,0x80000001
cpuid
mov [feature1],edx ;//<--- флаги в переменную
cinvoke printf,<10,10,' ***** EAX=8000_0001h ********',\
10,' Ext features....: ',0>
bt [feature1],11
jnc @f
cinvoke printf,<'SYSCALL,'> ;//<--- чекаем биты EDX..
@@: bt [feature1],20
jnc @f
cinvoke printf,<'NX, '>
@@: bt [feature1],26
jnc @f
cinvoke printf,<'1Gb-Pages, '>
@@: bt [feature1],27
jnc @f
cinvoke printf,<'RDTSCP, '>
@@: bt [feature1],29
jnc @f
cinvoke printf,<'Intel64, '>
@@:
Посмотрим на флаг "XD bit".
В документации AMD этот бит называется NX, от слова "No eXecution", а Intel обозвала его XD – "eXecution Disable". Это самый старший бит(64) в записях виртуальных страниц PTE (Page Table Entry), который предотвращает исполнение данных. Для более детального ознакомления с ним отправляю вас к статье "DEP и ASLR – игра без правил", где ему была посвящена одна глава. Как видно из скрина, мой древний проц поддерживает и его, и архитектуру Intel-64, хотя в упор не видит гигантских виртуальных страниц размером 1Gb (по-умолчанию 4Кб), и не знаком с современной инструкций генератора рандома
RDTSCP
. Для перехода в ядро при вызовах API-функций инструкция SYSCALL
уже давно не юзается даже процессорами Pentium, и ей на смену пришла свежая SYSENTER
.3.2. Функции EAX=8000_0002/3/4 (Processor Brand String)
Цепочка этих трёх функций возвращает имя процессора, как его окрестил производитель. Под строку брэнда производитель резервирует 48 байт во-встроенной памяти ROM, поэтому чтобы получить её полностью, нужно вызвать эти функции последовательно 3-раза. При каждом запросе в регистры EAX,EBX,ECX,EDX будет возвращаться новая информация, поэтому не забываем сохранять её в выделенный буфер. В примере ниже я поместил счётчик цикла в переменную "count", и на каждой итерации увеличиваю номер функции в EAX:
C-подобный:
mov edi,buff ;// адрес приёмника для STOSD
mov eax,0x80000002 ;//<--- начинаем цикл с функции(2)
@@: push eax
cpuid ;//<--- очередной вызов CPUID
nop
stosd ;//<--- сохраняю все регистры по адресу EDI
mov eax,ebx
stosd
mov eax,ecx
stosd
mov eax,edx
stosd
pop eax
inc eax ;//<--- сл.функция..
dec [count] ;//<--- счётчик -1
jnz @b
cinvoke printf,<10,' ***** EAX=8000_0002/3/4 ******',\
10,' Name...........: %s',10,0>,buff
Видимо хреновый контроль ведёт Intel над своими подчинёнными, о чём свидетельствует оформление строки. Зачем вставлять лишние пробелы в середину строки, когда можно дополнить её до 48-байт этими-же пробелами в конце? Здесь явно что-то не чисто..
3.3. Функция EAX=8000_0006h (Extended L2 Cache Features)
Ещё одна функция для сбора информации о кэше, только теперь исключительно об общем L2. Прямым текстом сообщает его размер и длину линейки, и в закодированном виде ассоциативность (мне лень было декодировать). Бьёт без промаха точно в цель на любых процессорах Intel, хоть 32, хоть 64-бит.
C-подобный:
mov eax,0x80000006
cpuid
push ecx
xchg eax,ecx
and eax,0xff ;//<--- cache-line
pop ebx
shr ebx,16 ;//<--- cache-size
cinvoke printf,<10,10,' ***** EAX=8000_0006h ********',\
10,' L2 cache size...: %d Kb',\
10,' L2 line size...: %d byte',0>,ebx,eax
3.4. Функция EAX=8000_0008h (Virtual & Physical Address Sizes)
Это последняя функция из списка расширенных и возвращает способность процессора адресовать физическую и виртуальную память ОЗУ. Обратите внимание, что хоть процессор и 64-битный, память он адресует максимум 48-бит. Если вогнать это значение в степень двойки (зовите на помощь калькулятор), то получим макс.поддерживаемую современными процессорами память =
281.474.976.710.656
байт:
C-подобный:
mov eax,0x80000008
cpuid
push eax
and eax,0xFF ;//<--- EAX = физ.память
pop ebx
shr ebx,8 ;//<--- EBX = вирт.память
cinvoke printf,<10,10,' ***** EAX=8000_0008h ********',\
10,' Physical addr...: %d bit',\
10,' Virtual addr...: %d bit',0>,eax,ebx
4. Пример кода для сбора информации CPUID
В заключении приведу готовый пример кода, который соберёт всю представленную выше информацию.
C-подобный:
format pe console
entry start
;//-----------
section '.inc' data readable
include 'win32ax.inc'
include 'equates/cpuid.inc'
;---------
.data
cacheType dd szInst,szData,szUni
szInst db 'Instruction',0
szData db 'Data',0
szUni db 'Unified',0
cpu db 128 dup(0)
count dd 3
feature1 dd 0
feature2 dd 0
buff db 0
;---------
.code
start: invoke SetConsoleTitle,<'*** CPUID example ***',0>
;//*************************************
xor eax,eax
cpuid
mov dword[buff+0],ebx ;кидаем производителя в буфер
mov dword[buff+4],edx
mov dword[buff+8],ecx
cinvoke printf,<10,' ***** EAX=0 ***************',\
10,' Func supported.: %d',\
10,' Vendor.........: %s',10,0>,eax,buff
;//*************************************
mov edi,buff
mov eax,0x80000002
@@: push eax
cpuid
nop
stosd
mov eax,ebx
stosd
mov eax,ecx
stosd
mov eax,edx
stosd
pop eax
inc eax
dec [count]
jnz @b
cinvoke printf,<10,' ***** EAX=8000_0002/3/4 ******',\
10,' Name...........: %s',10,0>,buff
;//*************************************
mov eax,1
cpuid
mov [feature1],ecx
mov [feature2],edx
movzx ecx,bl ;// Brand index
movzx edx,bh ;// Cache line size
shl edx,3 ;// ^^^^^
shr ebx,16 ;//
movzx esi,bl ;//
movzx edi,bh ;//
cinvoke printf,<10,' ***** EAX=1 ****************',\
10,' Family & Model.: 0x%x',\
10,' Brand index....: 0x%02x',\
10,' Cache line.....: %d byte',\
10,' CPU cores......: %d',\
10,' APIC id........: %d',\
10,' Feature flags..: ',0>,\
eax,ecx,edx,esi,edi
bt [feature2],25
jnc @f
cinvoke printf,<'SSE,'>
@@: bt [feature2],26
jnc @f
cinvoke printf,<'SSE2,'>
@@: bt [feature1],0
jnc @f
cinvoke printf,<'SSE3,'>
@@: bt [feature1],9
jnc @f
cinvoke printf,<'SSSE3,'>
@@: bt [feature1],19
jnc @f
cinvoke printf,<'SSE4.1,'>
@@: bt [feature1],20
jnc @f
cinvoke printf,<'SSE4.2,'>
@@: bt [feature1],28
jnc @f
cinvoke printf,<'AVX,'>
@@: bt [feature2],23
jnc @f
cinvoke printf,<'MMX,'>
@@: bt [feature2],28
jnc @f
cinvoke printf,<'HT,'>
@@: bt [feature1],2
jnc @f
cinvoke printf,<'DTES64,'>
@@: bt [feature1],3
jnc @f
cinvoke printf,<'MWAIT,'>
@@: bt [feature1],5
jnc @f
cinvoke printf,<'VMX,'>
@@: bt [feature1],6
jnc @f
cinvoke printf,<'SMX,'>
@@: bt [feature1],7
jnc @f
cinvoke printf,<'EIST,'>
@@: bt [feature1],8
jnc @f
cinvoke printf,<'TM2,'>
@@: bt [feature1],13
jnc @f
cinvoke printf,<'CMPXCHG16,'>
@@: bt [feature1],18
jnc @f
cinvoke printf,<'DCA,'>
@@: bt [feature1],21
jnc @f
cinvoke printf,<'x2APIC,'>
@@: bt [feature1],22
jnc @f
cinvoke printf,<'MOVBE,'>
@@: bt [feature1],25
jnc @f
cinvoke printf,<'AES,'>
@@: bt [feature1],26
jnc @f
cinvoke printf,<'XSAVE,'>
@@: bt [feature1],29
jnc @f
cinvoke printf,<'F16C,'>
@@: bt [feature1],30
jnc @f
cinvoke printf,<'RDRAND,'>
@@: bt [feature2],1
jnc @f
cinvoke printf,<'VME,'>
@@: bt [feature2],2
jnc @f
cinvoke printf,<'DE,'>
@@: bt [feature2],3
jnc @f
cinvoke printf,<'PCE,'>
@@: bt [feature2],4
jnc @f
cinvoke printf,<'TSC,'>
@@: bt [feature2],6
jnc @f
cinvoke printf,<'PAE,'>
@@: bt [feature2],9
jnc @f
cinvoke printf,<'APIC,'>
@@: bt [feature2],11
jnc @f
cinvoke printf,<'SYSENTER,'>
@@: bt [feature2],17
jnc @f
cinvoke printf,<'PSE-36,'>
@@: bt [feature2],19
jnc @f
cinvoke printf,<'CLFLUSH,'>
@@: bt [feature2],24
jnc @f
cinvoke printf,<'FXSAVE,'>
@@:
;//*************************************
cinvoke printf,<10,10,' ***** EAX=2 ****************',0>
@@: mov eax,2
cpuid
dec al
jnz @b
push ebx ecx edx
call printCpuid2
pop eax
call printCpuid2
pop eax
call printCpuid2
pop eax
call printCpuid2
;//************************************
xor ecx,ecx
mov eax,4
cpuid
shr eax,26
inc eax
cinvoke printf,<10,\
10,' ***** EAX=4 ****************',\
10,' CPU cores......: %d',0>,eax
@@: mov eax,4
mov ecx,[count]
cpuid
push ecx ebx ebx ebx eax
and eax,11111b
or eax,eax
jz @stop04
mov esi,cacheType
dec eax
shl eax,2
add esi,eax
pop ebx
shr ebx,5
and ebx,111b
cinvoke printf,<10,' Cache..........: L%d %-12s',0>,ebx,dword[esi]
pop eax ebx ecx edx
and eax,0xfff ;// line size
inc eax
shr ebx,12
and ebx,0x3ff ;// partition
inc ebx
shr ecx,22
and ecx,0x3ff
inc ecx
imul eax,ebx
imul eax,ecx
inc edx
imul eax,edx
shr eax,10
cinvoke printf,<' %dK',0>,eax
inc [count]
jmp @b
@stop04: add esp,4*5
;//*************************************
mov eax,0x80000000
cpuid
sub eax,0x80000000
cinvoke printf,<10,10,' ***** EAX=8000_0000h *************************',\
10,' Func supported..: %d',0>,eax
mov eax,0x80000001
cpuid
mov [feature1],edx
cinvoke printf,<10,10,' ***** EAX=8000_0001h ********',\
10,' Ext features....: ',0>
bt [feature1],11
jnc @f
cinvoke printf,<'SYSCALL,'>
@@: bt [feature1],20
jnc @f
cinvoke printf,<'NX, '>
@@: bt [feature1],26
jnc @f
cinvoke printf,<'1Gb-Pages, '>
@@: bt [feature1],27
jnc @f
cinvoke printf,<'RDTSCP, '>
@@: bt [feature1],29
jnc @f
cinvoke printf,<'Intel64, '>
@@:
;//************************************
mov eax,0x80000006
cpuid
push ecx
xchg eax,ecx
and eax,0xff ;// line
pop ebx
shr ebx,16
cinvoke printf,<10,10,' ***** EAX=8000_0006h ********',\
10,' L2 cache size...: %d Kb',\
10,' L2 line size...: %d byte',0>,ebx,eax
;//************************************
mov eax,0x80000008
cpuid
push eax
and eax,0xFF
pop ebx
shr ebx,8
cinvoke printf,<10,10,' ***** EAX=8000_0008h ********',\
10,' Physical addr...: %d bit',\
10,' Virtual addr...: %d bit',0>,eax,ebx
@exit: cinvoke _getch
cinvoke exit,0
;//---------------------
printCpuid2:
mov ecx,4
@next: xor ebx,ebx
mov bl,al
shr eax,8
or ebx,ebx
jz @f
call printCacheDescriptor
@@: loop @next
ret
printCacheDescriptor:
push eax ecx
mov ecx,[tSize]
mov esi,cacheTable
@@: cmp [esi],ebx
jnz @fuck
add esi,4
cinvoke printf,<10,'%s',0>,dword[esi]
jmp @f
@fuck: add esi,8
loop @b
@@: pop ecx eax
ret
;//---------------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
5. Постфикс
Опыты выше показали, что CPUID изначально не способен возвратить некоторую информацию. К примеру в нём нет данных типа: кодовое имя ядра, порог рабочих напряжений, тип сокета, потребляемая мощность, и значение производственного тех.процесса. Судя по всему, софт на подобии CPU-Z хранит это всё в своих таблицах, которые программисты скурпулёзно собирают ручками. Базы подобного рода пишутся исходя из значений "Family/Model/Stepping", предоставляемые нам функцией
CPUID.EAX=1
. В любом случае, теперь мы знаем, на что способен "зверь" под названием CPUID, и будем делать соответствующие выводы.В скрепке можно найти инклуд с описанием дескрипторов кэшей, и исполняемый файл для тестов. Всем удачи, пока!