На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Перед отправкой сообщения внимательно прочтите правила раздела!!!
1. Запрещается обсуждать написание вирусов, троянов и других вредоносных программ!
2. Помните, что у нас есть FAQ раздела Assembler и Полезные ссылки. Посмотрите, возможно, там уже имеется решение вашего вопроса.

3. Настоятельно рекомендуем обратить особое внимание на правила форума, которые нарушаются чаще всего:
  3.1. Заголовок темы должен кратко отражать её суть. Темы с заголовками типа "Срочно помогите!" или "Ассемблер" будут отправляться в Корзину для мусора.
  3.2. Исходники программ обязательно выделяйте тегами [code]...[/code] (одиночные инструкции можно не выделять).
  3.3. Нежелательно поднимать старые темы (не обновлявшиеся более года) без веской на то причины.

Не забывайте также про главные Правила форума!

Добро пожаловать и приятного вам общения!!! ;)
 
Модераторы: Jin X, Qraizer
  
> MMX и x87 FPU - общие регистры и переключение режимов
    MMX и x87 FPU - общие регистры и переключение режимов

    MMX-регистры (MM0–MM7) используют те же физические регистры, что и FPU-регистры (ST0–ST7) в сопроцессоре x87. Это означает, что при выполнении MMX-инструкций данные в регистрах FPU могут быть перезаписаны – и наоборот. Ниже разберём, как именно организовано такое перекрытие, какие возникают конфликты при смешанном использовании, и как правильно переключаться между режимами с помощью инструкций FINIT и EMMS.

    user posted image

    Восьмеричный стек 80-битных регистров x87 FPU (ST0–ST7) с отображением регистров MMX (MM0–MM7) на их младшие 64 бита. Верхние 16 бит каждого FPU-регистра (область экспоненты) при работе MMX помечаются как 1, формируя специальное значение (NaN/∞)

    Общий регистровый файл (MMX поверх FPU)

    Архитектурно восемь 64-разрядных регистров MMX являются алиасами (логическими копиями) восьми 80-разрядных регистров FPU.
    Процессор не вводит отдельного физического хранилища для MMX: вместо этого нижние 64 бита каждого регистра сопроцессора x87 (часть мантиссы) используются для хранения данных MMX, а верхние 16 бит остаются зарезервированы (и устанавливаются равными 1 при записи MMX-данных).Таким образом, запись значения в регистр MMX автоматически заносит его в соответствующий FPU-регистр, и наоборот – загрузка числа в стек FPU приводит к появлению этого значения (его 64-битной части) в связном MMX-регистре. Такое решение было принято Intel по соображениям совместимости: новые MMX-регистры “наложили” на существующие FPU-регистры, чтобы не нарушить механизмы сохранения/восстановления контекста в операционных системах. Проще говоря, раз ОС уже умела сохранять состояние FPU (например, при переключении задач), то дополнительного кода под MMX не потребовалось – состояние MMX сохраняется и загружается вместе с FPU благодаря общим физическим регистрам.

    Конфликт при смешанном использовании MMX и FPU

    Из-за общих регистров невозможно одновременно полноценно пользоваться инструкциями x87 FPU и MMX. Любая операция с плавающим стеком FPU может затереть содержимое MMX-регистров, и наоборот – выполнение MMX-инструкций меняет состояние тех же регистров, что ожидаются FPU​. Когда выполняются MMX-инструкции, FPU “видит” что его регистры заняты нестандартными данными. Аппаратно это реализовано так: при любой операции MMX сопроцессор помечает все восемь FPU-регистров как занятые действительными значениями (их тэги устанавливаются в 00b)​. Более того, верхние биты (экспоненты) каждого затронутого регистра устанавливаются в 1 (получается битовая маска, соответствующая NaN или бесконечности)​. В результате, если сразу после MMX попытаться выполнить обычную FPU-инструкцию без очистки, сопроцессор воспримет содержимое как некорректные числа с плавающей точкой. Это может привести к ошибкам, неожиданным результатам или исключениям (наподобие #MF – FPU Floating-Point Error). Проще говоря, смешивание FPU и MMX без специальной очистки состояния приводит к конфликту из-за неверного интерпретирования данных. Практическое следствие: чтобы избежать постоянно возникающего конфликта, программы обычно разделяют использование MMX и x87 на разные участки времени. Например, сначала выполняются все необходимые вычисления в формате с плавающей запятой, затем результат преобразуется и обрабатывается MMX-инструкциями (или наоборот)​. Одновременное чередование инструкций FPU и MMX нежелательно: во-первых, оно требует постоянного переключения контекста (что само по себе имеет накладные расходы по времени)​; во-вторых, повышает риск ошибки при неверной последовательности команд.

    Инструкции FINIT и EMMS для очистки состояния

    Чтобы корректно переключиться между использованием MMX и FPU, архитектура x86 предоставляет специальные инструкции для сброса состояния:

    • EMMS (Empty MMX State) – инструкция, освобождающая MMX-регистры. Она помечает все восемь регистров FPU как пустые (устанавливает их тэги в 11b)​, тем самым сообщая сопроцессору, что данные в них более не используются в формате MMX. После выполнения EMMS процессор «возвращается» в режим FPU – последующие операции с плавающей запятой будут нормально работать, не обнаруживая конфликтующих остатков данных MMX​. Обычно EMMS вызывается сразу после блока MMX-инструкций, до возобновления каких-либо вычислений на FPU или вызова функций, которые могут использовать FPU.
    • FINIT (Floating-point INITialize) – инструкция, которая инициализирует FPU до известного начального состояния. Она сбрасывает регистровый стек FPU, очищает флаги состояния, переустанавливает управляющее слово и т.д. Практически, FINIT также приведёт к очистке любых «мусорных» значений, остающихся от предыдущего использования FPU/MMX. Важно: FINIT не заменяет EMMS в обычном коде, поскольку FINIT не предназначен специально для MMX. Обычно её применяют при инициализации сопроцессора (например, при старте программы) или для экстренного сброса FPU при ошибке. В контексте MMX же, чаще достаточно выполнить EMMS. Однако, если по каким-то причинам требуется полностью сбросить и переинициализировать FPU после работы MMX, можно выполнить последовательность EMMS (чтобы очистить тэги) и затем FINIT (чтобы реинициализировать сопроцессор).

    Правильное чередование MMX и FPU: Всегда завершавайте блок MMX-кода инструкцией EMMS перед возвратом к вещественным вычислениям. Если до этого на FPU выполнялись вычисления и важны его настройки (режим округления, флаги и т.д.), может потребоваться FINIT или сохранение/восстановление состояния FPU. Но в большинстве случаев для переключения обратно на FPU достаточно выполнить EMMS, после чего можно безопасно использовать инструкции x87​. Пренебрежение этой инструкцией рано или поздно приводит к сбоям: например, стандартная библиотека языка Си, обнаружив неочищенное MMX-состояние при попытке выполнить printf с форматированием %f, может аварийно завершить программу.

    Производительность и современные альтернативы

    Стоит отметить, что переключение между режимами FPU и MMX влечёт за собой ощутимые накладные расходы. Выполнение EMMS/FINIT и связанная очистка тегов — сравнительно медленная операция​. По этой причине высокопроизводительное ПО старалось группировать однородные вычисления: например, все MMX-операции выполнять подряд, отложив использование FPU до тех пор, пока это возможно​. В современных x86-64 системах проблема смешанного использования частично утратила актуальность: начиная с SSE2, SIMD-расширения получили независимые регистровые файлы (XMM, YMM и т.д.), не связанные с x87 FPU​. Это позволяет свободно сочетать векторные операции с плавающей запятой без необходимости сбрасывать состояние сопроцессора после каждой серии команд. Однако понимание устройства MMX и x87 важно при работе с наследуемым кодом и низкоуровневыми оптимизациями.

    Вывод: Регистры MMX и x87 FPU фактически являются двумя представлениями одного и того же набора физических регистров​. Выполнение операций в одном режиме неизбежно воздействует на другой, поэтому необходимо явно разграничивать использование MMX и FPU. Инструкция EMMS служит основным инструментом для такого разграничения, освобождая регистры после MMX-вычислений, а FINIT при необходимости возвращает сопроцессор в начальное состояние. Соблюдение этих правил позволяет избегать ошибок и эффективно использовать возможности процессора.

    Основные моменты: MMX и x87 разделяют одни и те же регистры; смешанное использование без очистки приводит к конфликтам; после блока MMX-команд всегда нужно выполнять EMMS (а при необходимости — FINIT) перед возобновлением работы с FPU. Это гарантирует корректность результатов и стабильность работы приложения.​

    Пример кода на fasm

    ExpandedWrap disabled
      format PE64 console
      entry start
       
      include 'win64a.inc'
       
      section '.data' data readable writeable
          fp_value dq 3.14  ; Пример значения с плавающей точкой (double)
          mm_value dq 12345678h  ; Пример значения для MMX
       
      section '.code' code readable executable
      start:
          ; Инициализация FPU
          finit  ; Инициализируем FPU (сбрасываем регистры и настройки)
       
          ; Загружаем значение в ST0
          fld qword [fp_value]  ; ST0 = 3.14
       
          ; Выполняем операции с ST0
          fadd st0, st0  ; ST0 = ST0 + ST0 (3.14 + 3.14 = 6.28)
       
          ; Сохраняем ST0 в память, чтобы освободить регистр
          fstp qword [fp_value]  ; Сохраняем ST0 в память и освобождаем ST0
       
          ; Очистка состояния MMX перед использованием MMX-регистров
          emms  ; Очищаем состояние MMX (переводим FPU в обычный режим)
       
          ; Загружаем значение в MM0
          movq mm0, [mm_value]  ; MM0 = 12345678h
       
          ; Выполняем операции с MM0
          paddb mm0, mm0  ; MM0 = MM0 + MM0 (по байтам)
       
          ; Сохраняем MM0 в память, чтобы освободить регистр
          movq [mm_value], mm0  ; Сохраняем MM0 в память
       
          ; Очистка состояния MMX после использования MMX-регистров
          emms  ; Очищаем состояние MMX (переводим FPU в обычный режим)
       
          ; Восстанавливаем ST0 из памяти
          fld qword [fp_value]  ; ST0 = 6.28
       
          ; Выполняем еще операции с ST0
          fsqrt  ; ST0 = sqrt(ST0) (sqrt(6.28) ≈ 2.506)
       
          ; Сохраняем ST0 в память, чтобы освободить регистр
          fstp qword [fp_value]  ; Сохраняем ST0 в память и освобождаем ST0
       
          ; Очистка состояния MMX перед использованием MMX-регистров
          emms  ; Очищаем состояние MMX (переводим FPU в обычный режим)
       
          ; Восстанавливаем MM0 из памяти
          movq mm0, [mm_value]  ; MM0 = 12345678h + 12345678h (по байтам)
       
          ; Очистка состояния MMX после использования MMX-регистров
          emms  ; Очищаем состояние MMX (переводим FPU в обычный режим)
       
          ; Завершение программы
          invoke ExitProcess, 0
       
      section '.idata' import data readable writeable
          library kernel32, 'kernel32.dll'
          import kernel32, ExitProcess, 'ExitProcess'
      Перед использованием MMX никаких специфических действий не требуется. "Перевод" FPU в режим MMX осуществляется автоматически на первой же MMX инструкции. При этом тэги выставляются не обязательно в NaN/inf, нули распознаются корректно. Только лишь после использования MMX для перевода в "нормальный" режим нужна EMMS.
        Цитата Majestio @
        ExpandedWrap disabled
          start:
        ExpandedWrap disabled
          start: push rax ; чтобы выровнять стек на 16 после call из ntdll. Хотя тут это не критично, но может падать в ExitProcess без выравнивания стека.

        Цитата Majestio @
        ExpandedWrap disabled
          fld qword [fp_value]  ; ST0 = 3.14
        Для числа PI есть fldpi
          Qraizer и macomics, очень прошу привести пруфы, откуда ваша инфа! Это не прецедент "недоверия", мне нужно просто ознакомиться с тем что знаете вы, и чего не знаю я! ;)

          Цитата Qraizer @
          Перед использованием MMX никаких специфических действий не требуется. "Перевод" FPU в режим MMX осуществляется автоматически на первой же MMX инструкции. При этом тэги выставляются не обязательно в NaN/inf, нули распознаются корректно. Только лишь после использования MMX для перевода в "нормальный" режим нужна EMMS.

          Откуда инфа?

          Цитата macomics @
          Для числа PI есть fldpi

          Ну ... не очень существенно, но полезно. Записываю! ;)

          Другой вопрос...

          Цитата macomics @
          start: push rax ; чтобы выровнять стек на 16 после call из ntdll. Хотя тут это не критично, но может падать в ExitProcess без выравнивания стека.

          Немного прокачал тему. Зацени - всё ли правильно?

          Цитата
          В 64-битных системах (x86-64) существует требование к выравниванию стека на 16 байт перед вызовом функций. Это связано с соглашениями о вызовах (calling conventions), которые используются в Windows (Microsoft x64 calling convention) и других системах. Как оказалось потом и в Linux, MacOSX, FreeBSD (мне интересных, дальше не смотрел)

          Но рекомендуют чуть полнее, просто чтобы не забыть:
          ExpandedWrap disabled
            start:
                push rax ; Выравниваем стек на 16 байт
                ; Ваш код
                pop rax ; Восстанавливаем значение rax (если нужно)
                ; Тут код завершение программы

          Ну, т.е. ты забыл выкинуть из стека по выходу raх. Хотя, думаю, это не только не важно, но и не нужно - ибо по завершении процесса вся им аллоцируемая память уничтожается. Смысл тогда?
            Цитата Majestio @
            Ну, т.е. ты забыл выкинуть из стека по выходу raх. Хотя, думаю, это не только не важно, но и не нужно - ибо по завершении процесса вся им аллоцируемая память уничтожается. Смысл тогда?

            Нет. Не нужно ничего из стека вытаскивать.

            Тут все просто. Это инфа из MSDN
            Оттого, что Windows активно использует стековую память для работы с xmm регистрами и командами обращения с выровненной памятью, то перед вызовом функции значение в rsp должно быть выровнено на 16. После вызова оно опять не выровнено (команда call записала в стек 8 байт - адрес возврата), но после push rbp/mov rbp, rsp оно опять выровнено (записано еще 8 байт - значение rbp и всего это будет 16 байт). Так вот на метку start вашей программы из ntdll будет команда call rax (или видел в некоторых версиях call qword [var]). Это сбивает значение rsp на метке start до не выровненного на 16 и push rax просто все исправляет.

            Хотя при выделении локальных переменных все точно так же. Надо поддерживать значение в rsp кратное 16.
            Если этого не делать, тогда в винде есть куча movdqa, из-за которых возникает исключение обращения к не выровненному адресу. В случае с ExitProcess сообщения с ошибкой может и не успеть появиться, но вот звук его появления MB_ICONERROR вы услышите.

            Даже если заглянуть в макросы к fasm можно найти в PROC64.INC
            ExpandedWrap disabled
              macro prologuedef procname,flag,parmbytes,localbytes,reglist
               { local loc,fill,counter
                 loc = (localbytes+15) and (not 15) ; Поддержание выровненного значение в rsp
                 parmbase@proc equ rbp+16
                 localbase@proc equ rbp-loc
            Сообщение отредактировано: macomics -
              Цитата macomics @
              Нет. Не нужно ничего из стека вытаскивать.

              Ну тут бы я готов поверить... :lol: Но что-то чуйка меня "тормозит". Просто логика подсказывает: после того как "положил" - достань! Стек - это "алгоритм/механизм". Или ты должен 1) "сам достать" или это 2) "достают за тебя". Я предположил, что это "достают за тебя" (п.2) в следствии простого убиения процесса и аллоцируемой им памяти. С предоставленной тобой ссылкой ознакомлюсь завтро - сення не хочу портить себе настрой, если я не прав :lol:
                Цитата Majestio @
                Ну тут бы я готов поверить... Но что-то чуйка меня "тормозит". Просто логика подсказывает: после того как "положил" - достань! Стек - это "алгоритм/механизм". Или ты должен 1) "сам достать" или это 2) "достают за тебя". Я предположил, что это "достают за тебя" (п.2) в следствии простого убиения процесса и аллоцируемой им памяти. С предоставленной тобой ссылкой ознакомлюсь завтро - сення не хочу портить себе настрой, если я не прав

                Это логика работы объектов, а не процессора. У процессора стек это просто область памяти и указатель на вершину. Его можно двигать как напрямую через add/sub/lea так и спец командами push/pop. Иногда возникают ситуации, когда не нужно балансировать push и pop. Например
                ExpandedWrap disabled
                  proc:
                      push rbp      ; положили в стек 8 байт со значением из rbp
                      mov  rbp, rsp ; изменили значение в rbp на значение из rsp
                      sub  rsp, 32  ; изменили указатель стека еще на 32 байта, выделив место для локальных переменных
                   
                      fldpi
                      fstp qword [rbp - 10] ; обращение к адресу кратному не 8 для записи числа длиной 8 байт (для FPU это не критично)
                   
                      movdqa xmm3, [rbp - 20] ; а вот тут уже будет ошибка
                      movdqu [rbp - 21], xmm3 ; тут ошибки не будет
                   
                      leave          ; а вот хитрая команда. Он делает две инструкции разом: mov rsp, rbp и pop rbp (но это для однократной вложенности)
                      retn           ; после leave стек сбалансирован и не понадобилось запоминать количество байт для локальных переменных.


                Посмотрите как работают связка enter N,M и leave. Особенно интересно когда M > 0
                Сообщение отредактировано: macomics -
                  Цитата macomics @
                  Иногда возникают ситуации, когда не нужно балансировать push и pop.

                  Ну это triks за счет leave ;) Но, в общем случае, pop все же нужен.
                    Цитата Qraizer @
                    Перед использованием MMX никаких специфических действий не требуется. "Перевод" FPU в режим MMX осуществляется автоматически на первой же MMX инструкции. При этом тэги выставляются не обязательно в NaN/inf, нули распознаются корректно. Только лишь после использования MMX для перевода в "нормальный" режим нужна EMMS.

                    Попутно вопрос ... Допустим мы использовали инструкции MMХ, а затем мы решили использовать инструкции, использующие ST* (первый раз с начала исполнения).

                    Что мы должны сделать:

                    1. Хватит просто EMMS
                    2. Нужно только FINIT
                    3. Нужно EMMS, а потом FINIT
                    4. Что-то другое

                    Собственно, в этом вопрос.

                    Скрытый текст
                    Это было в моей статье:
                    Цитата
                    Основные моменты: MMX и x87 разделяют одни и те же регистры; смешанное использование без очистки приводит к конфликтам; после блока MMX-команд всегда нужно выполнять EMMS (а при необходимости — FINIT) перед возобновлением работы с FPU. Это гарантирует корректность результатов и стабильность работы приложения.​

                    Это правильно?
                      Цитата Majestio @
                      Попутно вопрос ... Допустим мы использовали инструкции MMХ, а затем мы решили использовать инструкции, использующие ST* (первый раз с начала исполнения).

                      Что мы должны сделать:

                      Хватит просто EMMS
                      Нужно только FINIT
                      Нужно EMMS, а потом FINIT
                      Что-то другое

                      Собственно, в этом вопрос.

                      Для ответа на это не надо читать других источников, кроме документации на процессор
                      Прикреплённый файлПрикреплённый файл__________________________20250320_115723.png (165,87 Кбайт, скачиваний: 30)

                      Добавлено
                      Достаточно EMMS
                        +1 :good:
                          Majestio, зачем тебе MMX? Чисто из исторического интереса?
                          Сейчас SSE2 есть ну прям везде. На компах 20-25-летней давности (начиная с Pentium 4).
                          Они шире в 2 раза, чем MMX, есть возможность работать с float'ами/double'ами (помимо int'ов).
                          Про AVX/FMA уж не буду, можно ещё встретить старые компы/ноуты/нетбуки (или какие-нибудь более-менее современные Atom/Celeron/Pentium'ы), в которых этого нет.

                          Добавлено
                          Цитата Majestio @
                          Это означает, что при выполнении MMX-инструкций (они же 3DNow!-инструкции)
                          3DNow! особо не имеет отношения к MMX, это типа AMD-шное расширение MMX'а, но это не MMX.
                          Это всё равно, что говорить, что расширение AES или SHA – это SSE или AVX, потому что они используют те же регистры :-?
                            Цитата Jin X @
                            Majestio, зачем тебе MMX? Чисто из исторического интереса?

                            Ага.
                            Цитата Jin X @
                            3DNow! особо не имеет отношения к MMX, это типа AMD-шное расширение MMX'а, но это не MMX.
                            Это всё равно, что говорить, что расширение AES или SHA – это SSE или AVX, потому что они используют те же регистры

                            Вычёркиваем! :lol:
                            1 пользователей читают эту тему (1 гостей и 0 скрытых пользователей)
                            0 пользователей:


                            Рейтинг@Mail.ru
                            [ Script execution time: 0,0507 ]   [ 16 queries used ]   [ Generated: 7.07.25, 05:58 GMT ]