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

Пиша виртуална машина за моя собствен асемблер, искам да мога да задам флаговете за пренасяне, паритет, нула, знак и препълване, както са зададени в архитектурата x86-64, когато извършвам операции като добавяне.

Бележки:

  • Използвам Microsoft Visual C++ 2015 & Intel C++ Compiler 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) Също така моят код може да не работи за други операции (напр. умножение)

РЕДАКТИРАНЕ 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 tag wiki за това как се прави.


Това вероятно няма да се компилира до много бърз код, особено флага за паритет. Имате ли нужда 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.

Очаквам, че можете да спечелите много производителност, като напишете части от емулацията в asm, но това ще бъде специфично за платформата. По-важното е, че това е много повече работа.

person Peter Cordes    schedule 26.03.2016
comment
Благодаря ви за това, ще потърся _mm_popcnt_u32. И името на входния аргумент беше лош избор. Няколко последващи въпроса: Какво не е наред с моята проверка за препълване? Базирах го на това, което се казва в ръководството за архитектура на Intel. Флаг за препълване — Задайте, ако целочисленият резултат е твърде голямо положително число или твърде малко отрицателно число (с изключение на знаковия бит), за да се побере в целевия операнд; изчистени по друг начин. Този флаг показва условие за препълване за аритметика на цяло число със знак (допълнение от две). Също така вашето нещо с XOR'ing, как работи това? - 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: lazy 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: пишете това на c++, така че е изключително лесно да промените дизайна си, за да използвате функции за достъп, както предложих. Всеки друг начин за внедряване на lazy flag eval е добре, ако приемем, че емулирате x86-подобен ISA, където писането на флаг е много по-често срещано от четенето на флаг. Ако искате да бъде бърз, писането на емулатора на asm (или език от високо ниво с флагове) вероятно е единствената ви друга опция. Поне използвайте lazy eval за PF, тъй като почти никога не се използва. Разберете дали вашата int32 идея ще проработи. Помислете сами за препълващ и непреливен вход. - person Peter Cordes; 26.03.2016
comment
Ако сте сериозни за писането на емулатор, този документ описва как Bochs и други високи -емулаторите на производителност обработват флагове. - person Raymond Chen; 26.03.2016
comment
@RaymondChen: Благодаря за тази връзка. Не знаех, че истинските емулатори използват lazy flag eval, но не бях изненадан, че не бях първият, който се сети за това. :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), за да определите кои флагове може да са необходими и да направите мързелива оценка само за тях. Например x86 ADD, последван от 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: И аз намерих този въпрос за неясен. Не бях сигурен дали OP изобретява нова ISA или не, предвид това изречение. Така че не бях сигурен дали предложението ми да пропусна изцяло PF е полезно (тъй като е скъпо да се изчислява в C). - person Peter Cordes; 28.03.2016
comment
Дали флагът Нула след добавяне е зададен на истина, ако и само ако и двата операнда са нула? - 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