Что не так с этой эмуляцией инструкции CMPXCHG16B?

Я пытаюсь запустить двоичную программу, которая использует инструкцию CMPXCHG16B в одном месте, к сожалению, мой Athlon 64 X2 3800+ не поддерживает ее. Это здорово, потому что я рассматриваю это как проблему программирования. Кажется, что инструкция не сложна для реализации с пещерным прыжком, так что то, что я сделал, но что-то не сработало, программа просто застыла в цикле. Может быть, кто-нибудь скажет мне, если я внедрил свой CMPXCHG16B неправильно?

Во-первых, фактический кусок машинного кода, который я пытаюсь подражать, заключается в следующем:

f0 49 0f c7 08                lock cmpxchg16b OWORD PTR [r8]

Отрывок из руководства Intel, описывающий CMPXCHG16B:

Сравните RDX: RAX с m128. Если он равен, установите ZF и загрузите RCX: RBX в m128. Вновь очистите ZF и загрузите m128 в RDX: RAX.

Сначала я заменяю все 5 байтов инструкции прыжком на кодовую пещеру с моей процедурой эмуляции, к счастью, прыжок занимает ровно 5 байтов! Скачок фактически является инструкцией call e8, но может быть jmp e9, оба работают.

e8 96 fb ff ff            call 0xfffffb96(-649)

Это относительный прыжок с 32-битным смещенным смещенным, закодированным в двух дополнениях, смещение указывает на кодовую пещеру относительно адреса следующей команды.

Затем код эмуляции, с которым я перехожу,:

PUSH R10
PUSH R11
MOV r10, QWORD PTR [r8]
MOV r11, QWORD PTR [r8+8]
TEST R10, RAX
JNE ELSE
TEST R11, RDX
JNE ELSE
MOV QWORD PTR [r8], RBX
MOV QWORD PTR [r8+8], RCX
JMP END
ELSE:
MOV RAX, r10
MOV RDX, r11
END:
POP R11
POP R10
RET

Лично я доволен этим, и я думаю, что он соответствует функциональной спецификации, приведенной в руководстве. Он восстанавливает стек и два регистра r10 и r11 в исходный порядок, а затем возобновляет выполнение. Увы, это не сработает! Это код работает, но программа действует так, как будто она ждет наконечника и сжигания электричества. Что указывает на то, что моя эмуляция не была идеальной, и я случайно побил ее. Вы видите что-то не так с этим?

Я замечаю, что это атомный вариант этого, владеющий префиксом lock. Я надеюсь, что это что-то еще, кроме того, что я сделал не так. Или есть способ эмулировать атомарность?

Ответ 1

Невозможно эмулировать lock cmpxchg16b. Это возможно, если все обращения к целевому адресу синхронизируются с отдельной блокировкой, но это включает в себя все другие инструкции, включая неатомные хранилища для любой половины объекта, и Atom-read-modify-write (например, xchg, lock cmpxchg, lock add, lock xadd) с половиной (или другой частью) 16-байтового объекта.

Вы можете эмулировать cmpxchg16b (без lock), как вы это делали, с исправлениями из ответа @Fifoernik. Это интересное учебное упражнение, но не очень полезно на практике, потому что реальный код, который использует cmpxchg16b, всегда использует его с префиксом lock.

Неатомная замена будет работать большую часть времени, потому что редкость для строки кэша недействительна из другого ядра, чтобы попасть в небольшое временное окно между двумя соседними инструкциями. Это не значит, что это безопасно, это просто означает, что он действительно трудно отлаживать, когда он иногда терпит неудачу.. Если вы просто хотите, чтобы игра работала для вашего собственного использования и может принимать случайные блокировки/ошибки, это может быть полезно. Для чего-либо, где важна правильность, вам не повезло.


Как насчет MFENCE? Кажется, это то, что мне нужно.

MFENCE до, после или между нагрузками и хранилищами не будет препятствовать тому, чтобы другой поток видел полузаписанное значение ( "разрывание" ) или изменял данные после того, как ваш код принял решение о том, что сравнение преуспел, но прежде чем он делает магазин. Он может сократить окно уязвимости, но он не может закрыть его, потому что MFENCE предотвращает перераспределение глобальной видимости наших собственных магазинов и загрузок. Он не может остановить магазин из другого ядра, чтобы он стал видимым для нас после наших нагрузок, но до наших магазинов. Для этого требуется цикл шины атомарного чтения-модификации-записи, для чего предназначены lock ed инструкции.

Выполнение двух 8-байтовых атомных обменов будет решать проблему уязвимости в окне, но только для каждой половины отдельно, оставляя проблему "разрыва".

Atomic 16B load/stores решает проблему разрыва, но не проблему атомарности между нагрузками и магазинами. возможно с SSE на каком-то оборудовании, но не гарантируется, что он будет атомарным с помощью x86 ISA способом 8B естественно выровненных нагрузок и магазинов.


эмуляция Xen lock cmpxchg16b:

В виртуальной машине Xen есть эмулятор x86, я думаю, для случая, когда виртуальная машина запускается на одном компьютере и переносится на менее работоспособное оборудование. Он эмулирует lock cmpxchg16b, принимая глобальный замок, потому что другого пути нет. Если бы был способ подражать "правильно", я уверен, что Xen сделает это.

Как обсуждалось в этот поток списка рассылки, решение Xen по-прежнему не работает, когда эмулированная версия на одном ядре обращается к одной и той же памяти как неэмулируемая инструкция на другом ядре. (Собственная версия не учитывает глобальную блокировку).

См. также этот патч в списке рассылки Xen, который изменяет эмуляцию lock cmpxchg8b для поддержки как lock cmpxchg8b, так и lock cmpxchg16b.

Я также обнаружил, что эмулятор KVM x86 не поддерживает cmpxchg16b либо в соответствии с результатами поиска для emulate cmpxchg16b.

Я думаю, что все это является хорошим доказательством того, что мой анализ правилен и что его невозможно безопасно эмулировать.

Ответ 2

Я вижу, что это неправильно с вашим кодом для эмуляции инструкции cmpxchg16b:

  • Для получения правильного сравнения вам нужно использовать cmp вместо test.

  • Вам нужно сохранить/восстановить все флаги, кроме ZF. В руководстве упоминается:

    Флаги CF, PF, AF, SF и OF не подвержены влиянию.


Руководство содержит следующее:

IF (64-Bit Mode and OperandSize = 64)
    THEN
         TEMP128 ← DEST
         IF (RDX:RAX = TEMP128)
              THEN
                    ZF ← 1;
                    DEST ← RCX:RBX;
              ELSE
                    ZF ← 0;
                    RDX:RAX ← TEMP128;
                    DEST ← TEMP128;
                    FI;
         FI

Итак, чтобы действительно написать код, который "соответствует функциональной спецификации, указанной в руководстве", требуется запись в m128. Хотя эта конкретная запись является частью заблокированной версии lock cmpxchg16b, она, конечно же, не принесет пользы атомности эмуляции! Таким образом, простая эмуляция lock cmpxchg16b невозможна. См. ответ @PeterCordes.

Эта команда может использоваться с префиксом LOCK, чтобы позволить инструкции выполняться атомарно. Чтобы упростить интерфейс к шине процессоров, операнд назначения получает цикл записи без учета результата сравнения

ELSE:
MOV RAX, r10
MOV RDX, r11
MOV QWORD PTR [r8], r10
MOV QWORD PTR [r8+8], r11
END: