Чтение CF, PF, ZF, SF, OF

Я пишу виртуальную машину для своего собственного языка ассемблера, я хочу иметь возможность устанавливать флаги переноса, четности, нуля, знака и переполнения, как они установлены в архитектуре x86-64, когда я выполняю такие операции, как сложение.

Примечания:

  • Я использую Microsoft Visual C++ 2015 и компилятор Intel C++ 16.0.
  • Я компилирую как приложение Win64.
  • Моя виртуальная машина (в настоящее время) выполняет арифметические операции только с 8-битными целыми числами.
  • Меня (в настоящее время) не интересуют никакие другие флаги (например, AF)

В моем текущем решении используется следующая функция:

void update_flags(uint16_t input)
{
    Registers::flags.carry = (input > UINT8_MAX);
    Registers::flags.zero = (input == 0);
    Registers::flags.sign = (input < 0);
    Registers::flags.overflow = (int16_t(input) > INT8_MAX || int16_t(input) < INT8_MIN);

    // I am assuming that overflow is handled by trunctation
    uint8_t input8 = uint8_t(input);
    // The parity flag
    int ones = 0;
    for (int i = 0; i < 8; ++i)
        if (input8 & (1 << i) != 0) ++ones;

    Registers::flags.parity = (ones % 2 == 0);
}

Что для добавления я бы использовал следующим образом:

uint8_t a, b;
update_flags(uint16_t(a) + uint16_t(b));
uint8_t c = a + b;

РЕДАКТИРОВАТЬ: Чтобы уточнить, я хочу знать, есть ли более эффективный/аккуратный способ сделать это (например, путем прямого доступа к RFLAGS). Также мой код может не работать для других операций (например, умножение)

EDIT 2 Теперь я обновил свой код следующим образом:

void update_flags(uint32_t result)
{
    Registers::flags.carry = (result > UINT8_MAX);
    Registers::flags.zero = (result == 0);
    Registers::flags.sign = (int32_t(result) < 0);
    Registers::flags.overflow = (int32_t(result) > INT8_MAX || int32_t(result) < INT8_MIN);
    Registers::flags.parity = (_mm_popcnt_u32(uint8_t(result)) % 2 == 0);
}

Еще один вопрос, будет ли правильно работать мой код для флага переноса? Я также хочу, чтобы он был правильно установлен для "заимствований", которые происходят во время вычитания.

Примечание. Язык ассемблера, который я виртуализирую, разработан мной, должен быть простым и основан на реализации Intel x86-64 (т.е. Intel64), и поэтому я хотел бы, чтобы эти флаги вели себя в основном одинаково.


person Isaac    schedule 26.03.2016    source источник
comment
В чем именно проблема, с которой вы столкнулись? Ваш код не работает?   -  person ApproachingDarknessFish    schedule 26.03.2016
comment
input < 0 никогда не будет истинным, потому что input не имеет знака, а OF будет зависеть от операндов, а не только от результата. Например, 8-битная операция 0x7f + 0x02 = 0x81 приведет к OF = 1, а 0x82 + 0xff = 0x81 приведет к OF = 0. Этот код неверен, поэтому некоторый код, который правильно устанавливает флаги, более аккуратен, чем этот способ.   -  person MikeCAT    schedule 26.03.2016


Ответы (2)


TL:DR: используйте ленивую оценку флага, см. ниже.


input - странное имя. Большинство ISA обновляют флаги на основе результата операции, а не входных данных. Вы смотрите на 16-битный результат 8-битной операции, что является интересным подходом. В C вы должны просто использовать unsigned int, что гарантированно будет как минимум uint16_t. Он будет компилироваться в лучший код на x86, где unsigned — 32-битный. 16-битные операции требуют дополнительного префикса и могут привести к замедлению частичного регистра.

Это может помочь с проблемой 8bx8b->16b mul, которую вы отметили, в зависимости от того, как вы хотите определить обновление флага для инструкции mul в архитектуре, которую вы эмулируете.

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


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

Эффективный эмулятор (особенно тот, который должен поддерживать флаг четности), вероятно, много выиграет от некоторой отложенной оценки флага. Сохраните значение, из которого вы можете вычислить флаги, если это необходимо, но на самом деле ничего не вычисляйте, пока не дойдете до инструкции, которая считывает флаги. Большинство инструкций только записывают флаги, не читая их, и они просто сохраняют результат uint16_t в ваше архитектурное состояние. Инструкции по чтению флагов могут либо вычислить только тот флаг, который им нужен, из сохраненного uint16_t, либо вычислить их все и каким-то образом сохранить.


Предполагая, что вы не можете заставить компилятор фактически прочитать PF из результата, вы можете попробовать _mm_popcnt_u32((uint8_t)x) & 1. Или горизонтально XOR всех битов вместе:

x  = (x&0b00001111) ^ (x>>4)
x  = (x&0b00000011) ^ (x>>2)
PF = (x&0b00000001) ^ (x>>1)   // tweaking this to produce better asm is probably possible

Я сомневаюсь, что какой-либо из основных компиляторов может оптимизировать кучу проверок результата в LAHF + SETO al или PUSHF. Компиляторы могут быть введены в использование условия флага для обнаружения целочисленного переполнения, например, для реализации сложения с насыщением. Но понять, что вам нужны все флаги, и фактически использовать LAHF вместо серии инструкций setcc, вероятно, невозможно. Компилятору понадобится распознаватель шаблонов, когда он сможет использовать LAHF, и, вероятно, никто не реализовал его, потому что варианты использования исчезающе редки.

В C/C++ нет способа прямого доступа к результатам операции флага, что делает C плохим выбором для реализации чего-то подобного. IDK, если какие-либо другие языки имеют результаты флага, кроме asm.

Я ожидаю, что вы могли бы значительно повысить производительность, написав части эмуляции на ассемблере, но это будет зависеть от платформы. Что еще более важно, это намного больше работы.

person Peter Cordes    schedule 26.03.2016
comment
Спасибо за это, я посмотрю на _mm_popcnt_u32. И имя ввода аргумента было плохим выбором. Пара вопросов: Что не так с моей проверкой переполнения? Я основывал это на том, что сказано в руководстве по архитектуре Intel. Флаг переполнения — устанавливается, если целочисленный результат является слишком большим положительным числом или слишком маленьким отрицательным числом (исключая бит знака), чтобы соответствовать операнду назначения; очищается в противном случае. Этот флаг указывает на состояние переполнения для арифметики с целым числом со знаком (дополнение до двух). Также ваша штука с XOR, как это работает? - person Isaac; 26.03.2016
comment
Кроме того, ленивая оценка, вероятно, окажется менее эффективной и, безусловно, более сложной, учитывая дизайн моей архитектуры. - person Isaac; 26.03.2016
comment
@Isaac: четность это операция xor всех битов. XOR добавляется без переноса. Мой предлагаемый метод похож на горизонтальную сумму SIMD, но побитовый и с использованием XOR. - person Peter Cordes; 26.03.2016
comment
@Isaac: ленивый eval действительно легко реализовать: вместо вызова функции обновления флага при реализации каждой операции, которая его обновляет, просто сохраните файл uint16_t. Используйте методы доступа для флагов. Везде, где вы читаете Registers::flags.carry, вместо этого читайте Registers::flags.carry(). - person Peter Cordes; 26.03.2016
comment
@Isaac: комментарий MikeCAT к вашему вопросу объясняет проблему с вашим флагом переполнения. Проблема в том, что int16_t, которое вы получаете от uint16_t, никогда не будет отрицательным. Старшие 7 бит (выше возможного 9-битного результата сложения двух 8-битных чисел) дополняются нулями, а не знаками. - person Peter Cordes; 26.03.2016
comment
Спасибо, будет работать update_flags(uint32(int32(a) + int32(b))? Также проблема с ленивой оценкой заключается в том, что флаги просто хранятся в блоке памяти, и инструкции могут обращаться к нему, как и к любому другому блоку памяти, они могут даже обращаться к непрерывному потоку памяти, который где-то содержит флаги. - person Isaac; 26.03.2016
comment
@Isaac: вы пишете это на С++, поэтому очень легко изменить свой дизайн, чтобы использовать функции доступа, как я предложил. Любой другой способ реализации ленивой проверки флагов подойдет, если вы эмулируете x86-подобную ISA, где запись флага встречается гораздо чаще, чем чтение флага. Если вы хотите, чтобы это было быстро, написание эмулятора на ассемблере (или языке высокого уровня с флагами), вероятно, является вашим единственным другим вариантом. По крайней мере, используйте ленивый eval для PF, так как он почти никогда не используется. ИДК, сработает ли ваша int32 идея. Подумайте об этом самостоятельно для ввода с переполнением и без переполнения. - person Peter Cordes; 26.03.2016
comment
Если вы серьезно относитесь к написанию эмулятора, эта статья описывает, как Bochs и другие эмуляторы производительности обрабатывают флаги. - person Raymond Chen; 26.03.2016
comment
@RaymondChen: Спасибо за эту ссылку. Я не знал, что настоящие эмуляторы используют ленивое вычисление флагов, но не был удивлен, что я не был первым, кто до этого додумался. : P Интересно, что они могут хранить только результат и перенос (что делает OP, выполняя операции с двойной шириной, но это нецелесообразно для эмуляции 32-битного ЦП на 32-битном ЦП). - person Peter Cordes; 26.03.2016
comment
Быстрее, чем сохранить результат и выполнить ленивую оценку флагов, вообще ничего не сохраняя! - это то, что эмуляторы JIT (wikiwand.com/en/Just-in-time_compilation) часто делают, они проводят анализ живучести (потока данных) (wikiwand.com/en/Live_variable_analysis), чтобы определить, какие флаги могут понадобиться, и выполнить ленивую оценку только для них. Например, за ADD x86 следует CMP, флаги, сгенерированные ADD, перезаписываются CMP... поэтому ADD не нужно ничего делать с флагами. Вот патент 28-летней давности на этот google.com/patents/US4951195. - person amdn; 28.03.2016

Кажется, я решил проблему, разделив аргументы для обновления флагов на неподписанный и подписанный результат следующим образом:

void update_flags(int16_t unsigned_result, int16_t signed_result)
{
    Registers::flags.zero = unsigned_result == 0;
    Registers::flags.sign = signed_result < 0;
    Registers::flags.carry = unsigned_result < 0 || unsigned_result > UINT8_MAX;
    Registers::flags.overflow = signed_result < INT8_MIN || signed_result > INT8_MAX
}

Для добавления (которое должно давать правильный результат как для подписанных, так и для неподписанных входов) я бы сделал следующее:

int8_t a, b;
int16_t signed_result = int16_t(a) + int16_t(b);
int16_t unsigned_result = int16_t(uint8_t(a)) + int16_t(uint8_t(b));
update_flags(unsigned_result, signed_result);
int8_t c = a + b;

И знаковое умножение я бы сделал следующее:

int8_t a, b;
int16_t result = int16_t(a) * int16_t(b);
update_flags(result, result);
int8_t c = a * b;

И так далее для других операций, обновляющих флаги.

Примечание. Я предполагаю, что здесь расширяется знак int16_t(a), а расширяется ноль int16_t(uint8_t(a)).

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

P.S. Спасибо всем, кто откликнулся, было очень полезно. Также, если кто-то может обнаружить какие-либо ошибки в моем коде, это будет оценено.

person Isaac    schedule 26.03.2016
comment
Когда добавляются два отрицательных значения int8_t, так что сумма не может быть представлена ​​(signed_result ‹ INT8_MIN), возникает переполнение, и код правильно устанавливает OF=1, но затем указывает, что результат отрицательный, SF=1, что не имеет место в случае архитектура x86-64. В x86 SF отражает самый значащий бит результата. Например, если a=-120 и b=-9 результатом 8-битного сложения будет 127, положительное число (SF=0). - person amdn; 28.03.2016
comment
Поскольку этот интерпретатор является интерпретатором моего собственного языка ассемблера, я определил SF для указания знака «математически правильного» результата (при условии, что входные операнды являются числами со знаком), а не знака результата, представленного в 8-битном формате. (Я знаю, что в x86 SF — это просто самый значащий бит результата, усеченный до целевого размера) - person Isaac; 28.03.2016
comment
Это не то, о чем говорит ваше первое предложение в вопросе. Я хочу иметь возможность устанавливать флаги переноса, четности, нуля, знака и переполнения, как они установлены в архитектуре x86-64, когда я выполняю такие операции, как сложение. - person amdn; 28.03.2016
comment
@amdn: я тоже нашел этот вопрос неясным. Я не был уверен, изобретал ли ОП новую ISA или нет, учитывая это предложение. Поэтому я не был уверен, что мое предложение полностью исключить PF было полезным (поскольку вычисления на C обходятся дорого). - person Peter Cordes; 28.03.2016
comment
Установлен ли флаг нуля после добавления в значение true тогда и только тогда, когда оба операнда равны нулю? - person amdn; 28.03.2016
comment
@amdn Хм, хорошее замечание, ну, моя ISA устанавливает его в 1 тогда и только тогда, когда математически правильный результат равен 0, для таких инструкций, как ADD и SUB, которые работают одинаково для чисел со знаком и без знака, это математический результат, если предполагаются операнды быть без подписи. Итак, чтобы ответить на ваш вопрос, да для инструкций ADD и MUL, но не для других. (возможно, вместо этого мне следует иметь отдельные подписанные и неподписанные версии ADD и SUB?) - person Isaac; 28.03.2016
comment
Рассмотрим эту функцию C: int8_t div( int8_t a, int8_t b, int8_t c ) { int8_t sum = b + c; if (sum == 0) return 0; else return a / sum; } Теперь, что должен сгенерировать компилятор для этой функции с вашей ISA в качестве цели? - person amdn; 28.03.2016
comment
@amdn Технически, в соответствии со стандартом c, если b + c не вписывается в int8_t, это неопределенное поведение, но я предполагаю, что вы не хотите, чтобы я просто усекал, в этом случае: Pop r3; r3 = c; Pop r2; r2 = b; Pop r1; r1 = a; Add r2, r3; b += c; Compare r2, 0; This updates the flags as if computing 'r2 - 0'; If-NotEqual; If the zero flag is not set; Jump LABEL1;; Push 0;; Return; I.e. 'return 0'; LABEL: Divide r1, r2; r1 /= r2; Push r1;; Return; Return r1; Примечание: комментарии начинаются с ; и заканчиваются на конец строки или другое ; - person Isaac; 28.03.2016
comment
Я думаю, вы имели в виду поведение, определяемое реализацией, а не неопределенное поведение. Выражение b + c оценивается путем преобразования обоих операндов в целое число (гарантировано, что оно не менее 16 бит), а затем преобразования в int8_t для выполнения присваивания sum. При добавлении переполнение невозможно, а преобразование из int в int8_t определяется реализацией. - person amdn; 29.03.2016
comment
@amdn: на самом деле подписанное переполнение действительно является неопределенным поведением. Компиляторы могут значительно улучшить код, предполагая, что этого не происходит. (например, представьте, что счетчик циклов является полным регистром, поэтому его можно использовать для индексации массива.) Этот пост блога объясняет более подробно: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html - person Peter Cordes; 30.03.2016
comment
@PeterCordes: обычно переполнение может произойти во время преобразования (поведение, определенное реализацией) или во время оценки (неопределенное поведение). В этом случае у нас сначала есть два преобразования (расширение до int операндов сложения int8_t) и переполнения там явно нет. Затем выполняется суммирование без переполнения, потому что сумма двух 8-битных величин со знаком умещается в 9 битах, а int гарантированно будет не менее 16 бит. Последнее преобразование — это присвоение результата суммы (int) int8_t — это преобразование может переполниться, а поведение определяется реализацией. - person amdn; 31.03.2016
comment
@amdn: ой, я забыл о преобразовании целых чисел в int, даже если оба операнда имеют один и тот же тип, и просмотрел эту часть вашего предыдущего комментария, xD. Согласен с вашим выводом. - person Peter Cordes; 31.03.2016
comment
Целочисленное продвижение @PeterCordes и другие подразумеваемые преобразования, а также то, что реализация определена по сравнению с неопределенной, - это область, в которой я должен продолжать возвращаться к стандарту... время от времени я все еще иду что? как здесь (где я определенно был сбит с толку): stackoverflow.com/questions/24795651/ - person amdn; 31.03.2016