В прошлой теме была рассмотрена базовая обработка инструкций ассемблера на примере двух инструкций: MOV (инструкция копирования) и ADD (инструкция сложения). Теперь добавим еще пару инструкций:
SUB: инструкция вычитания. Пусть в нашем случае она принимает три операнда:
SUB rD, rS1, rS2/lit // rD = rS1 - rS2/lit
Первый операнд - регистр, в который помещаем разность второго и третьего операнда. В качестве второго операнда также может выступать только регистр, а в качестве третьего - регистр или литерал
MUL: инструкция умножения. Пусть она имеет аналогичные три операнда:
MUL rD, rS1, rS2/lit // rD = rS1 * rS2/lit
Первый операнд-регистр помещается произведение второго и третьего операндов
AND: инструкция логического умножения (операция И). Она имеет аналогичные три операнда:
AND rD, rS1, rS2/lit // rD = rS1 & rS2/lit
Первый операнд-регистр помещается результат логического умножения второго и третьего операндов
ORR: инструкция логического сложения (операция ИЛИ). Также имеет три операнда:
ORR rD, rS1, rS2/lit // rD = rS1 | rS2/lit
Первый операнд-регистр помещается результат логического сложения второго и третьего операндов
Стоит отметить, что в данном случае мы определяем инструкции по аналогии с инструкциями ARM. К примеру в Intel x86-64 инструкция логического сложения обычно называется OR, а все аналогичные инструкции принимают по два операнда
SUB dest, source ; dest = dest - source IMUL dest, source ; dest = dest * source AND dest, source ; dest = dest & source OR dest, source ; dest = dest | source
Таким образом, у нас получается одна инструкция MOV с двумя операндами и 5 инструкций (ADD, SUB, MUL, AND, ORR) с тремя операндами. Теперь определим простейший симулятор инструкций:
instructions = [] # список инструкций r = [0]*4 # значения 4 регистров # карта сопоставления регистров и их индексов в списке r regs32 = {"r0":0, "r1":1, "r2":2, "r3":3} pc = 0 # указатель на следующую инструкцию # поддерживаемые инструкции и количество их операндов mnemonics = {"mov":2, "add": 3, "sub":3, "mul": 3, "and": 3, "orr": 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:<16}", end=" ") # выводим текущую инструкцию for reg in regs32: # выводим регистры rInd = regs32[reg] print(f"{reg}:{r[rInd]:<3}", end=" ") print() # цикл обработки инструкций while True: if pc >= len(instructions): break # если инструкции закончились, то выход из цикла tokens = instructions[pc] # получаем текущую инструкцию для выполнения pc = pc + 1 # увеличиваем указатель инструкций inst = " ".join(tokens) # текущая инструкция if tokens[0] not in mnemonics: # проверяем корректность инструкции print("Некорректная инструкция ", inst) break # получаем количество операндов для данной инструкции opCount = mnemonics[tokens[0]] if opCount!= len(tokens[1:]): # проверяем количество операндов print("Некорректное количество операндов для инструкции: ", inst) break # получаем операнды op2, op3 = 0, 0 # по умолчанию 2-й и 3-й операнд равны 0 op1=regs32[tokens[1]] # 1-й операнд всегда индекс регистра в regs32 # если инструкция с 2-мя операндами, то второй операнд может быть регистром или литералом if(opCount==2): if (tokens[2] in regs32): op2=r[regs32[tokens[2]]] else: op2 = int(tokens[2]) # если инструкция с 3-мя операндами, то второй операнд может быть регистром # а третий операнд может быть регистром или литералом if(opCount==3): op2=r[regs32[tokens[2]]] if (tokens[3] in regs32): op3=r[regs32[tokens[3]]] else: op3 = int(tokens[3]) match tokens[0]: case "mov": r[op1] = op2 case "add": r[op1] = op2 + op3 case "sub": r[op1] = op2 - op3 case "mul": r[op1] = op2 * op3 case "and": r[op1] = op2 & op3 case "orr": r[op1]= op2 | op3 print_state(inst) # логгируем состояние программы на консоль
Вначале идут базовые глобальные переменные, как список инструкций instructiions, для хранения значения 4 регистров определен список r, для сопоставления названий регистров с этим списком определен словарь regs32 и также определен указатель на номер следующей выполняемой инструкции - переменная pc.
Для обработки корректности данных по сравнению с прошлой темой здесь добавлен словарь mnemonics:
mnemonics = {"mov":2, "add": 3, "sub":3, "mul": 3}
В качестве ключей в нем выступают мнемоники всех поддерживаемых инструкций, и им сопоставляется количество операндов. Это позволит нам отследить корректности инструкции.
Далее идет считывание файла исходной программы, разбиение каждой строки на токены и их добавление в список инструкций, что уже рассматривалось в прошлых темах.
Основные изменения произошли в цикле обработки инструкций. Прежде всего перед выполнением инструкций проверяем, что инструкция корректна и корректно количество операндов
if tokens[0] not in mnemonics: # проверяем корректность инструкции print("Некорректная инструкция ", inst) break # получаем количество операндов для данной инструкции opCount = mnemonics[tokens[0]] if opCount != len(tokens[1:]): # проверяем количество операндов print("Некорректное количество операндов для инструкции: ", inst) break
Первый операнд инструкций всегда представляет регистр, куда помещается результат. Поэтому сразу считываем индекс целевого регистра и также определяем переменные для второго и третьего операнда инструкции:
op2, op3 = 0, 0 # для второго и третьго операндов op1 = regs32[tokens[1]] # получаем первый операнд - это всегда регистр
И затем получаем второй и третий операнды:
# если инструкция с 2-мя операндами, то второй операнд может быть регистром или литералом if(opCount==2): if (tokens[2] in regs32): op2=r[regs32[tokens[2]]] else: op2 = int(tokens[2]) # если инструкция с 3-мя операндами, то второй операнд может быть регистром # а третий операнд может быть регистром или литералом if(opCount==3): op2 = r[regs32[tokens[2]]] if (tokens[3] in regs32): op3=r[regs32[tokens[3]]] else: op3 = int(tokens[3])
В зависимости от того сколько операндов в инструкции, отличается принцип получения второго операнда. Так, для инструкции MOV второй операнд может быть регистром или числовым литералом, тогда как в трехоперандных инструкциях - второй операнд - регистр.
В данном случае для упрощения не применяется обработка ошибка на случай, если будет передан некорректный регистр или нечисловой литерал, но при желании дополнительно добавить проверку на корректность токенов и в случае некорректности токена выводить ошибку и выходить из цикла обработки. Далее мы рассмотрим один из способов валидации данных.
Затем выполняем над операндами действия:
match tokens[0]: case "mov": r[op1] = op2 case "add": r[op1] = op2 + op3 case "sub": r[op1] = op2 - op3 case "mul": r[op1] = op2 * op3 case "and": r[op1] = op2 & op3 case "orr": r[op1]= op2 | op3
Допустим файл с исходным кодом "hello.s" выглядит следующим образом:
// тестовая программа на ассемблере mov r1, 10 // помещаем в r1 число 10 mul r1, r1, 2 // умножаем r1 на 2 и помещаем результат в r1 mov r2, 12 // помещаем в r2 число 12 add r0, r1, r2 // складываем r1 и r2 и помещаем результат в r0 add r0, r0, 2 // складываем r0 и число 2 и помещаем результат в r0 mov r3, 4 // помещаем в r3 число 4 sub r0, r0, r3 // вычитаем из r0 число из r3 и помещаем результат в r0
В этом случае мы получим следующий консольный вывод:
pc:1 mov r1 10 r0:0 r1:10 r2:0 r3:0 pc:2 mul r1 r1 2 r0:0 r1:20 r2:0 r3:0 pc:3 mov r2 12 r0:0 r1:20 r2:12 r3:0 pc:4 add r0 r1 r2 r0:32 r1:20 r2:12 r3:0 pc:5 add r0 r0 2 r0:34 r1:20 r2:12 r3:0 pc:6 mov r3 4 r0:34 r1:20 r2:12 r3:4 pc:7 sub r0 r0 r3 r0:30 r1:20 r2:12 r3:4
Протестируем логические операции. Для этого в файле "hello.s" определим следующий код:
// тестовая программа на ассемблере mov r0, 10 // помещаем в r0 число 10 - 1010 в двоичной системе mov r1, 3 // помещаем в r1 число 3 - 0011 в двоичной системе and r2, r0, r1 // r2 = r0 & r1 = 1010 & 0011 = 0010 = 2 orr r3, r0, r1 // r3 = r0 | r1 = 1010 | 0011 = 1011 = 11
При выполнении этой программы мы получим следующий вывод:
pc:1 mov r0 10 r0:10 r1:0 r2:0 r3:0 pc:2 mov r1 3 r0:10 r1:3 r2:0 r3:0 pc:3 and r2 r0 r1 r0:10 r1:3 r2:2 r3:0 pc:4 orr r3 r0 r1 r0:10 r1:3 r2:2 r3:11