Интерпретатор ассемблера на Python

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

Одним из способов понять архитектуру компьютера является изучение ассемблера. Написание программ на ассемблере, возможно, покажется более сложным, поскольку предстоит иметь дело непосредственно железом. Тем не менее это несет ряд преимуществ. Прежде всего ассемблер позволит писать более эффективный, компактный и производительный код, который лучше использует ресурсы компьютера. Причем даже если не планируется использовать ассемблер в повседневной разработке, то знание ассемблера все равно может обогатить арсенал разработчика, позволит писать более компактный и эффективный код на других языках более высокого уровня как Си или Python. Знание ассемблера может помочь при реверс-инжениринге, анализе вирусов и прочих вредоносных программ, а также при их создании и поиске уязвимостей. В конце концов поминание работы ассемблера является важным навыком в низкоуровневом программировании, например, при написании операционных систем и драйверов.

Однако это руководство не совсем по ассемблеру. На данном сайте metanit.com уже есть ряд руководств непосредственно по ассемблеру:

В этом же цикле статей мы рассмотрим некоторые базовые аспекты ассемблера без привязке к конкретной архитектуре с точки зрения реализации этих аспектов на языке Python. И создадим простейший симулятор ассемблера на Python.

Регистры процессора

Чтобы производить операции с данными, в большистве случаев их сначала надо загрузить в регистр процессора. Регистры - представляют ключевой компонент процессора. И в данном случае нас будут интересовать прежде всего регистры общего назначения, которые используются в прикладных программах для вычислений.

В зависимости от архитектуры набор регистров будет отличаться. В ARM64 (Aarch64) это 31 64-разрядный регистр, которые называются X0-X30. Их младшие 32 бита составляют 32-разрядные регистры W0-W30:

Регистры в архитектуре ARM64

На платформе Intel x86-64 это 16 64-битных регистров с именами RAX, RBX, RCX, RDX, RSP, RBP, RDI, RSI и регистры R8 - R15. Также можно обращаться к 32-, 16- и 8-битной части 64-разрядных регистров:

Регистры в архитектуре х64

Таким образом, в архитектуре Intel x86-64 мы можем использовать следующие регистры общего пользования:

  • Шестнадцать 64-разрядных регистров RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14 и R15

  • Шестнадцать 32-разрядных регистров EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, R8D, R9D, R10D, R11D, R12D, R13D, R14D и R15D

  • Шестнадцать 16-разрядных регистров AX, BX, CX, DX, SI, DI, BP, SP, R8W, R9W, R10W, R11W, R12W, R13W, R14W и R15W

  • Шестнадцать 8-разрядных регистров AL, AH, BL, BH, CL, CH, DL, DH, DIL, SIL, BPL, SPL, R8B, R9B, R10B, R11B, R12B, R13B, R14B и R15B

Эмуляция регистров

Для простой эмуляции регистров на языке Python мы можем использовать стандартный список или словарь. Например, в ARM64 31 регистр X0-X30:

regs=[0]*31         # 31 регистр X0-X30, в каждом из которых число 0
regs[0] = 8         # X0 = 8
print(regs[0])      # 8

Здесь переменная regs представляет 31 регистр X0-X30, в каждом из которых по умолчанию число 0. Поскольку регистры называются довольно просто X0-X30, то их можно сопоставить по индексу с определенным элементом списка regs. Однако названия регистров общего назначения в архитектуре Intel x86-64 несколько отличается. И для хранения их данных можно было бы использовать словарь:

regs = {"rax":0, "rbx": 0, "rcx": 0, "rdx": 0, "rsi": 0, "rdi": 0, "r8": 0, "r9": 0, "r10": 0, "r11": 0, "r12": 0, "r14":0, "r15":0}

regs["rax"] = 8
print(regs["rax"])

Словарь regs в качестве ключей применяет названия регистров, каждому из которых сопоставляется значение регистра - по умолчанию число 0. Используя ключ - название регистра, мы можем установить или получить его значение.

Одни регистры могут быть частью других регистров. Например, в ARM64 регистры W0-W30 занимают младшие 32 разряда 64-битных регистров X0-X30. Поскольку нумерация регистров аналогична, то фактически для хранения применяется один и тот же набор, только для 32-разрядных регистров извлекаются младшие 32 бита числа:

r=[0]*31         # 31 регистр X0-X30, в каждом из которых число 0
num = 0x0102030405060708
r[0] = num & 0xffffffffffffffff   # в X0 число 0x0102030405060708

x0 = r[0]          # получим обратно значение для X0
print(f"X0 = 0x{x0:0x}")      # X0 = 0x102030405060708

w0 = r[0] & 0xffffffff     # получим значение для W0
print(f"W0 = 0x{w0:0x}")  # w0= 0x5060708

Поскольку в списке r изначально хранятся значения 64-разрядных регистров (условно X0-X30), то перед сохранением в них значений с помощью операции логического умножения & применяем маску 0xffffffffffffffff:

r[0] = num & 0xffffffffffffffff

Таким образом, мы отсеиваем все биты кроме первых 64 разрядов.

Для получения условного значения 32-разрядного регистра W0 извлекаем из этого значения младшие 32 бита:

w0 = r[0] & 0xffffffff

В архитектуре Intel x86-64 это особенно показательно, например, 8-разрядный регистр AL является частью 16-разрядного регистра AX, который является частю 32-разрядного регистра EAX, который, в свою очередь, является частью 64-разрядного регистра RAX. В качестве одного решений мы могли бы использовать несколько словарей, каждый элемент которого будет указывать на одно и то же значение:

r = [0]*16      # значения 16 регистров
regs64 = {"rax":0, "rbx":1, "rcx":2, "rdx":3, "rsp":4, "rbp":5, "rsi":6, "rdi":7, "r8":8, "r9":9, "r10":10, "r11":11, "r12":12, "r12":13, "r14":14, "r15":15}
regs32 = {"eax":0, "ebx":1, "ecx":2, "edx":3, "esp":4, "ebp":5, "esi":6, "edi":7, "r8d":8, "r9d":9, "r10d":10, "r11d":11, "r12d":12, "r12d":13, "r14d":14, "r15d":15}
regs16 = {"ax":0, "bx":1, "cx":2, "dx":3, "sp":4, "bp":5, "si":6, "di":7, "r8w":8, "r9w":9, "r10w":10, "r11w":11, "r12w":12, "r13w":13, "r14w":14, "r15w":15}
regs8 = {"al":0, "bl":1, "cl":2, "dl":3, "r8b":8, "r9b":9, "r10b":10, "r11b":11, "r12b":12, "r13b":13, "r14b":14, "r15b":15}

num = 0x0102030405060708
raxIndex = regs64["rax"]
r[raxIndex] = num & 0xffffffffffffffff   # в rax число 0x0102030405060708

rax = r[regs64["rax"]]          # получим обратно значение для rax
print(f"rax = 0x{rax:0x}")      # rax = 0x012030405060708


eax = r[regs32["eax"]]  & 0xffffffff     # получим значение для eax
print(f"eax = 0x{eax:0x}")              # eax = 0x5060708

ax = r[regs16["ax"]]  & 0xffff      # получим значение для ax
print(f"ax = 0x{ax:0x}")            # ax = 0x708

al = r[regs8["al"]]  & 0xff     # получим значение для al
print(f"al = 0x{al:0x}")        # al = 0x8

Список r хранит значения для всех 16 регистров. Словари regs64/regs32/regs16/regs8 представляют названия регистров соответствующей разрядности, в котором значение указывает на индекс в списке r. Например, элементы с ключами "rax", "eax", "ax" и "al" указывают на 0-й индекс в списке r, то есть ссылаются на один и тот же элемент.

Для примера здесь в условный регистр RAX помещаем число 0x0102030405060708. Для большей наглядности число написано в 16-ричном формате.

Вначале получаем индекс регистра RAX в массие r:

raxIndex = regs64["rax"]

Затем помещаем по этому индексу в список r число num:

r[raxIndex] = num & 0xffffffffffffffff

Обратите внимание, что у нас регистр RAX - 64-разрядный. Передаваемое в него число может теоретически иметь большую разрядность, и с помощью маски 0xffffffffffffffff и операции логического умножения & убираем все старшие разряды, которые выходят за пределы 64 битов.

Далее мы можем получить значение регистра:

rax = r[regs64["rax"]]

Поскольку в списке r все значения представляют 64-разрядные числа, то нам не надо дополнительно применять маску, чтобы получить только первые 64 бита числа. Иначе обстоит дело с регистрами другой разрядности. Например, регистр EAX - 32-разрядной, поэтому применяем маску 0xffffffff, чтобы получить только первые 32 бита:

eax = r[regs32["eax"]]  & 0xffffffff

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

rax = 0x102030405060708
eax = 0x5060708
ax = 0x708
al = 0x8

В дальнейшем в качестве примеров мы будем использовать абстрактные 32-разрядные регистры r0, r1, r2... как в ARM.

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