Создание простейшего симулятора ассемблера

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

В прошлой теме была рассмотрена базовая обработка инструкций ассемблера на примере двух инструкций: 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 
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850