Флаги состояния

Последнее обновление: 01.03.2024

В процессе работы программы процессор может учитывать результат выполнения определенных инструкций и сохранять его в специальном регистре состояния. В Intel x86-64 это регистр RFLAFS, в ARM64 это регистр PSTATE. Этот регистр хранит флаги или биты состояния, которые устанавливаются в 1 или сбрасываются в 0 в зависимости от результата инструкций. Анализируя эти флаги, мы можем задать условия в программе на ассемблере.

В независимости от архитектуры этот регистр определяет прежде всего 4 флага:

  • Флаг знака: равен 1, если результат операции представляет отрицательное число. Равен 0, если результат - положительное число или 0.

  • Флаг нуля: равен 1, если результат операции равен нулю. Равен 0, если результат операции НЕ равен нулю.

  • Флаг переноса: установка этого флага зависит от констекста. Обычно он устанавливается, если при выполнении арифметической операции произошел перенос. Например, возьмем операцию сложения двух 32-разрядных чисел

    0xffffffff
    +
    0x00000003
    =
    0x00000002 (результат)
    1 (флаг переноса)
    

    Результат такого сложения 0xffffffff + 0x3 превышает размер 32 бит. Формально он будет равен 33-разрядному числу 0x100000002. Однако если мы используем 32-разрядные регистры, то они естественно не могут хранить 33-разрядные значения. Таким образом, фактический результат будет равен 0x00000002 (или просто 0x2), а старший бит переходит во флаг переноса.

    Но, проверив, флаг переноса, мы можем узнать, было ли переполнение результата или нет.

    При вычитании установка флага переноса может отличаться в зависимости от архитектуры. Так, на Intel x86-64 флаг устанавливается, если было заимствование. А на ARM64, наоборот, устанавливается, если не было заимствования.

  • Флаг переполнения: устанавливается, если при выполнении арифметической операции произошло переполнение со знаком (signed integer overflow). При сложении этот флаг устанавливается в следующих случаях:

    • Если складываются два положительных числа, а результат отрицательный

    • Если складываются два отрицательных числа, а результат положительный

    При вычитании флаг устанавливается в следующих случаях:

    • Если из положительного числа вычитается отрицательное, а результат отрицательный

    • Если из отрицательного числа вычитается положительное, а результат положительный

    Например, возьмем сложение двух 64-разрядных чисел со знаком. Число +9223372036854775807 в 16-ричной системе равно 0x7fffffffffffffff. Сложим это число с самим собой:

    0x7fffffffffffffff
    +
    0x7fffffffffffffff
    =
    0xfffffffffffffffe (результат)
    1 (флаг переполнения)
    

    Результатом сложения является число 0xfffffffffffffffe. Если мы рассматриваем данное сложение как сложение чисел со знаком, то десятичным результатом является число -2 - отрицательное число. И в принципе мы по десятичному результату можем увидеть, что знаковый бит операндов изменился на противоположный - с 0 на 1. Поэтому устанавливается флаг переполнения. Собственно поэтому в данном случае мы говорим о переполнении со знаком или переполнении знака, так как знак меняется.

На разных архитектурах подход к изменению флагов может отличаться. Например, на ARM есть отдельный вид инструкций, которые меняют флаги. Мнемоники таких инструкций оканчиваются на "S". Например, вычитание с установкой флагов - инструкция SUBS. Стандартная же инструкция вычитания - SUB не устанавливает флаги. Аналогично инструкция сложения ADDS устанавливает флаги, а инструкция ADD - нет. В Intel подобного разделения инструкций нет - флаги устанавливаются стандартными инструкциями как ADD или SUB

Кроме инструкций, которые устанавливает флаги, различные архитектуры могут иметь инструкции, которые используют флаги. Например, и на Intel, и на ARM64 есть такая инструкция как ADC, которая выполняет сложение с переносом. То есть если к моменту выполнения инструкции установлен флаг переноса (например, в результате прошлых операций), то инструкция ADC складывает два числа и дополнительно прибавляет к ним флаг переноса.

Аналогично на Intel и ARM есть инструция вычитания с заимствованием. На Intel она называется SBB, на ARM - SBC. Она вычитает из одного числа второе и дополнительно вычитает флаг переноса.

Добавим в симулятор ассемблера из прошлой темы поддержку флагов. Для простоты возьмем только три флага - флаг переноса, знака и нуля. Также добавим две дополнительные инструкции:

  • ADC: для сложения с битом переноса

  • SBC: для вычитания с битом переноса

Для установки флагов не будем определять отдельных инструкций как в ARM, а будем использовать уже имеющиеся инструкции. Определим следующую программу:

instructions = []       # список инструкций
r = [0]*4       # значения 4 регистров
# карта сопоставления регистров и их индексов в списке r    
regs32 = {"r0":0, "r1":1, "r2":2, "r3":3}
pc = 0  # указатель на следующую инструкцию
# флаги 
c = 0   # флаг переноса
n = 0   # флаг знака
z = 0   # флан нуля
# поддерживаемые инструкции и количество их операндов
mnemonics = {"mov":2, "add": 3, "sub":3, "mul": 3, "and": 3, "orr": 3, "adc": 3, "sbc":3}

# считываем файл hello.s в список инструкций
with open("hello.s", "r", encoding="utf8") as source:
    lines = source.readlines()
    # обрабатываем строки из файла
    for i in range(0,len(lines)):
        lines[i] = lines[i].split("//")[0] \
                    .replace(",", " ")  \
                    .strip().rstrip("\n") \
                    .lower()
        while "  " in lines[i]:             # заменяем несколько пробелов одним
            lines[i] = lines[i].replace("  ", " ")  
        if(lines[i]) == "": continue        # если получилась пустая строка, переходим к следующей строке

        tokens = lines[i].split(" ")      # разбиваем инструкцию на токены
        instructions.append(tokens)         # добавляем инструкцию в список instructions

# функций вывода состояния программы на консоль
def print_state(instruction):
    print(f"pc:{pc}", end="   ")        # выводим значение PC
    print(f"{instruction:<18}", end=" ")     # выводим текущую инструкцию
    for reg in regs32:                      # выводим регистры
        rInd = regs32[reg]
        print(f"{reg}:0x{r[rInd]:04x}", end="  ")
    # выводим флаги
    print("\n" + " "*26 + f"c: {c}   n: {n}   z: {z}")

# получаем индекс регистра
def get_register_index(token, show_error):
    if (token in regs32): return regs32[token]
    if show_error: print("Некорректный регистр", token)
    return None

# получаем значение регистра
def get_register(token, show_error):
    rInd = get_register_index(token, show_error)
    if (rInd != None): return r[rInd]
    return None

# получаем литерал
def get_literal(token):
    try:
        result = 0
        if (token[0:2]=="0x"): result = int(token[2:],16)     # если 16-ричное число
        elif (token[0:2]=="0b"): result = int(token[2:],2)     # если двоичное число
        else: result= int(token)
        return result & 0xffffffff      # нормализуем литерал до 32 разрядов
    except ValueError:
        print("Некорректный токен", token)
    return None

# получаем операнд, который может быть регистром или литералом
def get_register_or_literal(token):
    reg = get_register(token, False)
    if (reg != None): return reg
    return get_literal(token)

# получаем тип инструкции
def get_opCount(tokens):
    if tokens[0] not in mnemonics:          # проверяем корректность инструкции
        print("Некорректная инструкция ", " ".join(tokens))
        return None 
    # получаем количество операндов для данной инструкции
    count = mnemonics[tokens[0]] 
    if count!= len(tokens[1:]):     # проверяем количество операндов
        print("Некорректное количество операндов для инструкции: ", " ".join(tokens))
        return None
    return count

# цикл обработки инструкций
while True:
    if pc >= len(instructions): break  # если инструкции закончились, то выход из цикла
    tokens = instructions[pc]     # получаем текущую инструкцию для выполнения
    pc = pc + 1                 # увеличиваем указатель инструкций

    opCount = get_opCount(tokens)   # получаем количество операндов
    if(opCount == None): break

    # получаем операнды
    op2, op3 = 0, 0
    
    # первый операнд всегда регистр
    op1=get_register_index(tokens[1], True)
    
    # если инструкция с 2-мя операндами, то второй операнд может быть регистром или литералом
    if(opCount==2): op2 = get_register_or_literal(tokens[2])
        
    # если инструкция с 3-мя операндами, то второй операнд может быть регистром
    # а третий операнд может быть регистром или литералом
    if(opCount==3):
        op2 = get_register(tokens[2], True)
        op3 = get_register_or_literal(tokens[3])
        
    # если какой-то параметр не установлен, завершаем цикл
    if(None in [op1, op2, op3]): break
       
    result = 0
    match tokens[0]: 
        case "mov": 
            result = op2
        case "add": 
            result = op2 + op3
            # если сумма больше 32 разрядов, устанавливаем флаг переноса
            if(result > 0xffffffff): c = 1  
            else: c=0
        case "adc":     # сложение с переносом
            result = op2 + op3 + c
            c = 0
        case "sub": 
            result = op2 - op3
            # если результат отрицательный (идет заимствование), устанавливаем флаг переноса
            if(op2 < op3): c = 1 
            else: c=0
        case "sbc":     # вычитание с переносом
            result = op2 - op3 - c
            c = 0
        case "mul": 
            result = op2 * op3
        case "and": 
            result = op2 & op3
        case "orr": 
            result= op2 | op3
    r[op1]= result & 0xffffffff   # нормализуем число до 32 разрядов
    # для инструкции mov не надо устанавливать флаг нуля и знака
    if(tokens[0]!="mov"):
        # получаем флаг знака
        n = (r[op1] >> 31)
        # получаем флаг нуля
        z = 1 if r[op1] == 0 else 0
    
    print_state(" ".join(tokens))    # логгируем состояние программы на консоль

Итак, среди глобальных переменных добавлены флаги, которые по умолчанию сброшены, то есть равны нулю:

# флаги 
c = 0   # флаг переноса
n = 0   # флаг знака
z = 0   # флан нуля

В функции print_state дополнительно выводим значения этих флагов.

В список инструкций добавлены две дополнительные инструкции - adc и sbc, которые принимают по три параметра:

mnemonics = {"mov":2, "add": 3, "sub":3, "mul": 3, "and": 3, "orr": 3, "adc": 3, "sbc":3 }

При обработки сложения проверяем результат:

case "add": 
    result = op2 + op3
    if(result > 0xffffffff): c = 1  
    else: c=0

Если результат выходит за пределы 32 разрядов, значит, имеет место перенос, соответственно устанавливаем флаг переноса. Если переноса нет, флаг переноса сбрасываем.

Аналогично при вычитании устанавливаем флаг переноса, если есть заимствование (то есть первый операнд меньше чем второй):

case "sub": 
    result = op2 - op3
    if(op2 < op3): c = 1 # если разность меньше 0, устанавливаем флаг переноса
    else: c=0

Новая инструкция ADC складывает оба операнда с флагом переноса:

case "adc":     # сложение с переносом
    result = op2 + op3 + c
    c = 0

В принципе и здесь мы могли бы проверять результат и устанавливать флаг переноса, если результат занимает больше 32 битов. Но ограничимся более простым вариантом.

остальные флаги устанавливаются, если текущая инструкция не является инструкцией MOV:

if(tokens[0]!="mov"):
    n = (r[rd]  >> 31)    # получаем флаг знака
    z = 1 if r[rd] == 0 else 0  # получаем флаг нуля

Флагу знака n присваиваем самый старший бит результата. Для этого сдвигаем результат вправо на 31 бит, чтобы старший бит указался единственным установленным.

Флагу нуля просто присваиваем 1, если результат равен 0, в противном случае сбрасываем флаг z в ноль.

Для теста возьмем следующий файл "hello.s":

mov r3, 0xffffffff 
add r3, r3, 3
adc r3, r3, 2
sub r3, r3, 5
mov r3, 0xfffffffe
add r3, r3, 1

В итоге мы получим следующий вывод:

pc:1  mov r3 0xffffffff  r0:0x0000 r1:0x0000 r2:0x0000 r3:0xffffffff 
                         c: 0   n: 0   z: 0
pc:2  add r3 r3 3        r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0002 
                         c: 1   n: 0   z: 0
pc:3  adc r3 r3 2        r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0005 
                         c: 0   n: 0   z: 0
pc:4  sub r3 r3 5        r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 1
pc:5  mov r3 0xfffffffe  r0:0x0000 r1:0x0000 r2:0x0000 r3:0xfffffffe 
                         c: 0   n: 0   z: 1
pc:6  add r3 r3 1        r0:0x0000 r1:0x0000 r2:0x0000 r3:0xffffffff 
                         c: 0   n: 1   z: 0

Здесь мы видим, что вначале регистр r3 хранит число 0xffffffff. После прибавления к нему 3, устанавливается флаг переноса, так как результат - 0x1_000000002 занимает 33 бита. После этого r3 хранит число 2.

Сложение больших чисел

Операции со сложением/вычитанием с учетом флага переноса часто применяются при работе с большими числами, которые выходят за пределы доступной разрядности. Например, у нас числа ограничены 32 битами, но что, если мы хотим сложить числа большей разрядности? И одним из решений является сложение с флагом переноса.

Например, нам надо сложить 2 64-разрядных числа 0x1_ffffffff и 0x2_00000003. Стандартный алгоритм заключается в том, что бы в два регистра поместить младшие 32 бита чисел и сложить их. Поскольку их сумма может выйти за пределы 32 разрядов, то в этом случае устанавливаем бит переноса.

Затем старшие 32 бита числа помещаются в другие два регистра и складываются с учетом флага переноса. Рассмотрим на примере. Пусть в файле "hello.s" определена следующая программа на ассемблере:

// складываем 33-разрядные числа 0x1_ffffffff и 0x2_00000003
mov r0, 0xffffffff  // младшие 32 бита числа 0x1_ffffffff
mov r1, 0x1         // старшие 32 бита числа 0x1_ffffffff
mov r2, 0x3         // младшие 32 бита числа 0x2_00000003
mov r3, 0x2         // старшие 32 бита числа 0x2_00000003
add r0, r0, r2      // складываем младшие 32 бита двух чисел
adc r1, r1, r3      // складываем старшие 32 бита с учетом бита переноса

И в данном случае мы получим следующий результат:

pc:1  mov r0 0xffffffff  r0:0xffffffff  r1:0x0000  r2:0x0000  r3:0x0000 
                         c: 0   n: 0   z: 0
pc:2  mov r1 0x1         r0:0xffffffff  r1:0x0001  r2:0x0000  r3:0x0000 
                         c: 0   n: 0   z: 0
pc:3  mov r2 0x3         r0:0xffffffff  r1:0x0001  r2:0x0003  r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  mov r3 0x2         r0:0xffffffff  r1:0x0001  r2:0x0003  r3:0x0002 
                         c: 0   n: 0   z: 0
pc:5  add r0 r0 r2       r0:0x0002  r1:0x0001  r2:0x0003  r3:0x0002 
                         c: 1   n: 0   z: 0
pc:6  adc r1 r1 r3       r0:0x0002  r1:0x0004  r2:0x0003  r3:0x0002 
                         c: 0   n: 0   z: 0

Здесь видно, что после сложения младших 32 разрядов двух чисел (регистры r0 и r2), устанавливается флаг переноса. Это флаг переноса применяется на последней инструкции. В итоге регистр r1 будет хранить старшие 32 бита 64-разрядного числа, а регистр r0 - младшие 32 разряда. А само число будет равно 0x4_00000002

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850