Compare commits

..

44 Commits

Author SHA1 Message Date
ddff443183 docs: добавил README с основной информацией по использованию 2025-04-12 20:00:52 +03:00
99512fa611 chore: немного лучше описал причину грязной структуры 2025-04-12 19:37:25 +03:00
89608dcb22 chore: сменил лицензию на более официальную 2025-04-12 19:35:06 +03:00
0707a981e1 Merge branch 'hotfix/broken-encoding'
fix: исправлена ошибка кодировки при чтении файл
2025-04-12 19:34:40 +03:00
9a3399a975 fix: исправлена ошибка при чтении и записи файлов 2025-04-12 19:33:57 +03:00
7b6b3f73cd sync: тут мелкие изменения, которые я делал в процессе дебага
История разработки превращается в кашу, но что поделать, этот репозиторий поддерживается на скорую руку
2025-04-03 22:15:59 +03:00
6a4a02ffb9 sync: Пытаюсь наладить точки останова 2025-04-03 09:14:24 +03:00
1aade33b48 chore: добавил пермессивную лицензию 2025-04-02 10:15:21 +03:00
94160414aa chore: заменил импорты внутри пакета на относительные 2025-04-02 01:29:22 +03:00
4f1c9286a2 chore: переместил проект в свой пакет, чтобы избежать конфликтов с системными пакетами 2025-04-02 01:13:48 +03:00
afbe0a5dfe fix: починил ошибку в регулярном выражении для регистров 2025-04-02 00:49:37 +03:00
8e719f7f6e chore: починил возможность запускать дебаггер как исполняемый скрипт 2025-04-02 00:47:06 +03:00
4236ad59b4 chore: указал новые зависимости в pyproject.toml 2025-04-02 00:39:56 +03:00
82eb4e96ff feat: исправлено поведение консоли при переполнении сделана обработка исключений 2025-04-02 00:39:24 +03:00
eae2846925 feat: написал нормальное регулярное выражение для парсинга номеров регистров 2025-04-02 00:38:18 +03:00
a9bedc18cf chore: переместил функцию получения текущей строкиисходного кода 2025-04-02 00:37:34 +03:00
90040c9823 feat: добавлена команда сброса виртуальной машины 2025-04-02 00:17:47 +03:00
69234aac2d feat: добавил статусы виртуальной машине 2025-04-02 00:15:21 +03:00
922670be47 feat: рабочая версия дебаггера
скорее всего конкретно в TUI ничего уже меняться не будет
2025-04-01 16:50:31 +03:00
8f63f4d09f feat: заменен фреймворк для написания TUI, написана первая рабочая версия TUI 2025-04-01 16:24:56 +03:00
13f244d118 sync: в процессе разработки TUI 2025-03-31 22:03:49 +03:00
942f3c73fd feat: добавил сокращенные команды в пул и возможность получения текущей строки исходного кода 2025-03-31 22:03:12 +03:00
c556b67fbc feat: добавлена фукнция подкрашивания текста 2025-03-31 13:21:43 +03:00
aeaede0097 feat: реализована вспомогательная функция получения строк исходного кода 2025-03-31 12:58:37 +03:00
d8a51cc1cd fix: вернул обратно нормальные пути запуска скриптов 2025-03-31 12:35:10 +03:00
d4c2bb0dc7 sync: добавил файл реализации tui 2025-03-31 12:34:39 +03:00
fe8276e9da feat: написал парсинг параметров для дебаггера 2025-03-31 12:33:54 +03:00
e56ff2d847 chore: исправил обращение к пакету 2025-03-31 12:33:22 +03:00
5962a73341 mess: вернул все назад, потому что это привело к хаосу с зависимостями
Теперь репозиторий очень грязный, но мне правда уже не хочется мучиться с зависимостиями, поэтому пусть будет так
2025-03-31 11:36:05 +03:00
d72bbb9ee2 chore: обновил версию проекта 2025-03-31 03:05:00 +03:00
d3f06a6795 feat: написано первое приближение дебаггера
Сейчас он не запускаестя из-за проблем с модулями
2025-03-31 03:03:50 +03:00
44f3e622c8 feat: добавил возможность писать сообщения об ошибках к исключениям виратуальной машинки 2025-03-31 03:02:56 +03:00
6b2d5fa31e chore: теперь пакет можно установить как исполняемый скрипт 2025-03-30 20:37:03 +03:00
436d4e501f chore: удалил ненужынй файл 2025-03-30 20:32:39 +03:00
9392326337 fix: полностью переработана система изменения счетчика
Выяснилось, что в DP32 счетчик команд ведет себя совершенно не так, как у меня в виртуальной машине, поэтому логика его работы была полностью переписана
2025-03-30 11:54:50 +03:00
ca2e19ae0a fix[?]: починил поведение после условного перехода
Не уверен, что это поможет на самом деле, но это временно решает проблему одного конкретного перехода
2025-03-30 02:37:25 +03:00
12a662d8bb chore: удалил ненужные строки, где делал мелкие проверки 2025-03-30 02:35:49 +03:00
5afadaa654 fix: починил опокод одной операции 2025-03-29 22:51:03 +03:00
621eb13692 fix: исправил условия перехода 2025-03-29 22:40:24 +03:00
88a34b87ab fix: исправил проверку выхода за границы памяти 2025-03-29 22:26:57 +03:00
6e3216d16c feat: добавлено выбрасывание исключений в некоторые места
так странно написано потому что я уверен, что будут еще другие исключительные случаи и корнеркейсы, которые я забыл
2025-03-29 22:26:57 +03:00
bdfdfe06da chore: убрал отладочный print в main.py 2025-03-29 22:26:57 +03:00
430db1c2ac fix: всем математические операции помечены как не имеющие доп поля 2025-03-29 22:26:57 +03:00
3c4e8f258a fix: убрал упоминания AFTER_BRANCH из конструкций ветвления 2025-03-29 21:49:38 +03:00
15 changed files with 796 additions and 67 deletions

10
LICENSE Normal file
View File

@ -0,0 +1,10 @@
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,54 @@
# DP32-proto
Прототип эмулятора процессора DP32
## Описание
Это проект программной эмуляции и отладки процессора DP32 на языке python. В этом проекте можно найти 2 компонента - отладчик dp32dbg и эмулятор dp32emu. Первый полностью опирается на отладочную информацию в следующем формате. Это очень простой отладчик и очень нестабильный, поскольку писался с упором на скорость, а не на качество.
```json
{
"src": "absolute/path/to/source"
"labels" : {
"label1": 1
"label2": 2
...
},
"instructions" : {
"<offset-in-words>" : {
"lenght": 2,
"srcline": 3
}
...
}
}
```
## Установка
Стяните данный репозиторий и выполните в корне проекта команду `pip install .`
## Использование
После установки у вас появятся 2 программы:
- dp32emu - программный эмулятор процессора dp32. Принимает на вход бинарный файл с командами dp32, выполняет их и в результате своей работы выдает дамп памяти процессора после завершения работы
- dp32dbg - отладчик, опирающийся на вышеуказанный эмулятор. Позволяет выполнять действия пошаговой отладки и предоставляет базовую функциональность точек останова (но с ньюансами)
Посмотреть инструкцию по запуску проще всего передав флаг `-h` каждой из этих программ
## Использование отладчика
help не встроен в отладчик, поскольку до этого его функционал не был документирован. Приведу здесь основной набор команд отладчика
- `step/s` - выполняет одну инструкцию, на которую сейчас указывает регистр pc виртуальной машины
- `breakpoint <lineno>` - устанавливает точку останова на определенной строке исходного кода. С точками останова 2 ньюанса:
1. Они они не могут быть установлены на комментарии или пустые строки. Если на строке больше одной операции, точка будет установлена на самую раннюю
2. Когда виртуальная машина встретит точку останова, временно она не сможет выполнять команду continue, поскольку не предусмотрено ротации точек останова. Если нужно продолжить выполнение программы дальше - выполните одну команду step
- `continue/c` - выполняет программу до ближайшей точки останова
- `reset` - сбрасывает состояние виртуальной машины и ее память, но не точки останова
- `run` - сбрасывает состояние виртуальной машины и запускает выполнение кода
- `cbp` - очищает все точки останова
- `print/p <reg>` - печатает содержимое регистров. Регистры записываются в формате `r1-r255`, позволяет также посмотреть адреса меток (указав название меток в качестве параметра) и системные регистры: pc, cc
- `inspect/i <size> <amount> <place>` - печатает содержимое памяти по определенному адресу в определенном формате (строчная - знаковые, заглавная - беззнаковые): b/B - байт, h/H - полуслова - 16 бит, w/W - слова - 32 бита. `amount` - количество подряд идущих ячеек, содержимое которых нужно прочитать. `place` - место, с которого следует начать печать. Можеть представлять собой либо имя метки, либо полный адрес в десятичной или 16-ричной системе счисления

View File

@ -1,7 +1,17 @@
[project]
name = "dp32-proto"
version = "0.1.0"
version = "0.1.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
dependencies = [
"py-cui>=0.1.6",
]
[project.scripts]
dp32emu = "dp32proto.main:main"
dp32dbg = "dp32proto.dbg_main:main"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

3
setup.py Normal file
View File

@ -0,0 +1,3 @@
import setuptools
setuptools.setup()

4
src/dp32proto/README.md Normal file
View File

@ -0,0 +1,4 @@
# Почему такая некрасивая структура
Этот репозиторий нужен был для того, чтобы отладить программу для процессора DP32. Структура проекта сделана такой для того, чтобы сократить время, необходимое на структуризацию и разрешение зависимостей.

View File

98
src/dp32proto/dbg.json Normal file
View File

@ -0,0 +1,98 @@
{
"labels": {
"_start": 0,
"_start_return": 9,
"f_fibo": 11,
"f_fibo_loop": 14,
"return": 27,
"result": 28,
"end": 29
},
"instructions": {
"0": {
"length": 1,
"srcline": 6
},
"1": {
"length": 1,
"srcline": 7
},
"2": {
"length": 2,
"srcline": 8
},
"4": {
"length": 2,
"srcline": 9
},
"6": {
"length": 2,
"srcline": 10
},
"8": {
"length": 1,
"srcline": 11
},
"9": {
"length": 2,
"srcline": 13
},
"11": {
"length": 1,
"srcline": 16
},
"12": {
"length": 1,
"srcline": 17
},
"13": {
"length": 1,
"srcline": 18
},
"14": {
"length": 1,
"srcline": 20
},
"15": {
"length": 1,
"srcline": 21
},
"16": {
"length": 1,
"srcline": 22
},
"17": {
"length": 1,
"srcline": 23
},
"18": {
"length": 1,
"srcline": 24
},
"19": {
"length": 2,
"srcline": 25
},
"21": {
"length": 2,
"srcline": 27
},
"23": {
"length": 2,
"srcline": 28
},
"25": {
"length": 2,
"srcline": 29
},
"27": {
"length": 1,
"srcline": 31
},
"28": {
"length": 1,
"srcline": 32
}
},
"src": "C:\\Users\\etdia\\code\\dp32-proto\\src\\test.dasm"
}

60
src/dp32proto/dbg_main.py Normal file
View File

@ -0,0 +1,60 @@
"""
Это очень грязное решение, но из-за страданий с пакетированием всех скриптов
оказалось тупо проще засунуть дебаггер сразу в эту папку и не мучить себя
"""
from typing import cast
from .dpdebugger import Debugger, DbgDict, DbgInstrDesc
from argparse import ArgumentParser
import json
from . import dbg_tui
def parse_dbg(dbg_json_dict: dict) -> DbgDict:
"""
При сериализации json всегда переводит ключи в формат строк и с этим
ничего поделать нельзя. Но для работы отладчика мне нужно, чтобы
ключами были числа, поэтому эта функция пытается привести к int
все ключи, лежащие в "instructions"
"""
instrs: dict[str | str, DbgInstrDesc] = dbg_json_dict["instructions"]
result: dict[int, DbgInstrDesc] = {
int(k) : instrs[k] for k in instrs
}
dbg_copy = dbg_json_dict.copy()
dbg_copy["instructions"] = result
dbg_copy = cast(DbgDict, dbg_copy)
return dbg_copy
def main():
parser = ArgumentParser(
prog="dp32dbg",
description="Bad debugger for DP32 processor"
)
parser.add_argument(
"-m",
"--memory",
required=True,
help="Initial memory for virtual machine"
)
parser.add_argument(
"-d",
"--debug-file",
required=True,
help="JSON with debug information. by default named dbg.json"
)
args = parser.parse_args()
with open(args.memory, "rb") as f:
mem = bytearray(f.read())
with open(args.debug_file, 'r') as f:
dbg_dict: DbgDict = parse_dbg(json.load(f))
dbg = Debugger(mem, dbg_dict)
with open (dbg_dict["src"], 'r', encoding="utf8") as f:
src = f.read()
dbg_tui.run_tui(dbg, src)
if __name__ == "__main__":
main()

164
src/dp32proto/dbg_tui.py Normal file
View File

@ -0,0 +1,164 @@
from .dpdebugger import Debugger
from .vm import VMException, VMExceptionType, VMStatus
from dataclasses import dataclass
from typing import cast
import math
import py_cui
import py_cui.keys
from collections import deque
@dataclass
class SourceLines:
active: int
begining_number: int
lines: list[str]
def _get_source_lines(
srcline: int,
lines: list[str],
max_height: int
) -> SourceLines:
"""
Собирает выборку из строк исходного кода так, чтобы она
подходила по количесту строчек
"""
if srcline > len(lines):
raise IndexError(f"Cant take line number {srcline} from passed lines, "
"index is out of range")
start_bound = srcline - math.floor(max_height//2)
end_bound = srcline + math.ceil(max_height//2)
lines_len = len(lines)
if start_bound < 1:
end_bound += abs(start_bound) + 1
if end_bound > lines_len:
start_bound -= end_bound - lines_len
start_bound = max(start_bound, 1)
end_bound = min(end_bound, lines_len)
return SourceLines(
active=srcline,
begining_number=start_bound,
lines=list(lines[start_bound-1:end_bound])
)
class DebuggerUI:
ACTIVE_LINE_POINTER = "~> "
def __init__(self, master: py_cui.PyCUI, debugger: Debugger, src: str):
self.master = master
self.dbg = debugger
self.src = src
self.srclines = src.split('\n')[:-1]
self.source_panel = self.master.add_text_block(
"Source code", 0, 0
)
self.prompt_panel = self.master.add_text_box("Prompt",
1, 0,
row_span=1
)
self.prompt_panel.add_key_command(py_cui.keys.KEY_ENTER, self._process_prompt)
self.history_index = 0
self.prompt_panel.add_key_command(py_cui.keys.KEY_UP_ARROW, self._history_up)
self.prompt_panel.add_key_command(py_cui.keys.KEY_DOWN_ARROW, self._history_down)
self.terminal_panel = self.master.add_text_block("Debugger Console",
2, 0,
initial_text="to exit type in \"exit\""
)
self.master.move_focus(self.prompt_panel)
self.master.add_key_command(
py_cui.keys.KEY_ENTER,
lambda: self.master.move_focus(self.prompt_panel)
)
self.commands_history: deque[str] = deque(maxlen=10)
self._display_source_lines()
def _process_prompt(self):
user_input = self.prompt_panel.get()
self.prompt_panel.clear()
if user_input == "exit":
exit()
if user_input == "clear":
self.terminal_panel.clear()
return
if user_input.strip() == "":
if len(self.commands_history) < 1:
return
user_input = self.commands_history[0]
self.history_index = 0
self.commands_history.appendleft("")
try:
dbg_output = self.dbg.do_command(user_input.split())
except VMException as vme:
self._terminal_append(
"VM Exception: " + vme.message
)
return
# self._terminal_append(dbg_output)
self._terminal_append(dbg_output)
self._display_source_lines()
self.commands_history[0] = user_input
def _terminal_append(self, text: str):
self.terminal_panel.write(text)
viewport_height = self.terminal_panel.get_viewport_height()
contents = self.terminal_panel.get().split('\n')[-viewport_height:]
self.terminal_panel.set_text("\n".join(contents))
def _display_source_lines(self):
if self.dbg._vm.status == VMStatus.FINISHED:
return
try:
active_line = self.dbg.get_current_source_line_number()
except KeyError:
self._terminal_append(
"Cant find source line your are on. "
"May be you reached end of program. "
)
self._terminal_append(
"Please type \"reset\" to reset vm "
"it's initial state"
)
return
lines = _get_source_lines(
srcline=active_line,
lines=self.srclines,
max_height=self.source_panel.get_viewport_height()
)
# emplace line lumbers
linenos = [lines.begining_number + i
for i in range(len(lines.lines))]
lines_with_linenos: list[str] =[]
# длина самого большого числа для вычисления выравнивания
align_with = len(str(linenos[-1]))
for lineno, line in zip(linenos, lines.lines):
if lineno == lines.active:
padding = len(line) - len(stripped := line.lstrip(" "))
line = self.ACTIVE_LINE_POINTER.rjust(padding) + stripped
lines_with_linenos.append(
f"{str(lineno).rjust(align_with)}. {line}"
)
self.source_panel.set_text("\n".join(lines_with_linenos))
def _history_up(self):
if len(self.commands_history) == 0:
return
self.history_index += 1
self.history_index %= len(self.commands_history)
self.prompt_panel.set_text(self.commands_history[self.history_index])
def _history_down(self):
if len(self.commands_history) == 0:
return
self.history_index -= 1
self.history_index %= len(self.commands_history)
self.prompt_panel.set_text(self.commands_history[self.history_index])
def run_tui(debugger: Debugger, srcfile: str):
root = py_cui.PyCUI(3, 1)
root.set_title("Stupid DP32 debugger")
root.set_status_bar_text("")
DebuggerUI(root, debugger, srcfile)
root.start()

212
src/dp32proto/dpdebugger.py Normal file
View File

@ -0,0 +1,212 @@
"""
Отладчик принимает на вход довольно много всего и пытается вам
хоть как-то помочь
"""
from .vm import VM, VMException, Breakpoint, Condition, WORD_SIZE
from dataclasses import dataclass
from typing import TypedDict, Callable, cast
import re
import struct
class DbgInstrDesc(TypedDict):
"""
Описание отдельно взятой инструкции по определенному оффсету
"""
length: int
srcline: int
class DbgDict(TypedDict):
"""
Урезанная версия полной отладочной информации, предоставляемой
в файле dbg.json. Содержит ту информацию, которую может
предоставить ассемблер
"""
src: str
labels: dict[str, int]
instructions: dict[int, DbgInstrDesc]
@dataclass
class Debugger:
_vm: VM
_dbg_dict: DbgDict
_callbacks_table: dict[str, Callable[[list[str]], str]]
_breakpoints: set[int]
_source_lines: list[str]
# will be nessesary in _inspect
UNPACK_SYMBOL_TABLE = {
"b": "b",
"B": "B",
"h": "h",
"H": "H",
"w": "i",
"W": "I"
}
SIZE_SYMBOL_TABLE = {
"b": 1,
"B": 1,
"h": 2,
"H": 2,
"w": 4,
"W": 4
}
def __init__(self, mem: bytearray, dbg_dict: DbgDict):
with open(dbg_dict["src"], 'r', encoding="utf8") as f:
self._source_lines = f.readlines()
self._vm = VM(mem)
self._dbg_dict = dbg_dict
self._breakpoints = set()
self.__init_callbacks__()
self.last_breakpoint: Breakpoint | None = None
def __init_callbacks__(self):
self._callbacks_table = {
"step": self._step,
"s": self._step,
"continue": self._continue,
"c": self._continue,
"breakpoint": self._breakpoint,
"b": self._breakpoint,
"clearbreakpoints": self._breakpointsclear,
"cbp": self._breakpointsclear,
"print": self._print,
"p": self._print,
"run": self._run,
"r": self._run,
"inspect": self._inspect,
"i": self._inspect,
"reset": self._reset
}
def do_command(self, command: list[str]) -> str:
callback_name = command[0]
if not callback_name in self._callbacks_table:
return "Unknown command"
return self._callbacks_table[callback_name](command[1:])
def get_current_source_line_number(self) -> int:
return self._dbg_dict["instructions"][self._vm.pc.value]['srcline']
def _reset(self, args: list[str]) -> str:
self._vm.reset()
return "VM reseted"
def _step(self, args: list[str]) -> str:
try:
self._vm.step()
except VMException as e:
# TODO: тут необходимо выкорчевать из
# исключения текст ошибки
return f"Virtual machine exception: {e.message}"
return ""
def _continue(self, args: list[str]) -> str:
try:
self._vm.continue_()
except Breakpoint:
return ""
return "Program finished"
def _breakpoint(self, args: list[str]) -> str:
if len(args) == 0:
addr = self._vm.pc.value
self._breakpoints.add(addr)
src_line = self._dbg_dict["instructions"][addr]["srcline"]
return f"Breakpoint succesfully set on line {src_line}"
desired_addr: int = int(args[0])
addr = -1
# находим за линейное время. Плохая практика, но так как
# сходные коды вряд ли будут длиннее 1000 строк - приемлемо
for addr, desc in self._dbg_dict["instructions"].items():
desc = cast(DbgInstrDesc, desc)
if not desc["srcline"] == desired_addr:
continue
self._breakpoints.add(int(addr))
self._vm.breakpoints.add(int(addr))
return f"Breakpoint succesfully set on line {desired_addr}"
return f"Couldn't place breakpoint on src line {desired_addr}"
def _breakpointsclear(self, args: list[str]):
self._breakpoints.clear()
self._vm.breakpoints.clear()
return "Cleared all breakpoints"
def _print(self, args: list[str]) -> str:
to_print = args[0]
if to_print == "pc":
return "pc: " + str(self._vm.pc.value)
elif to_print == "cc":
flags = Condition(self._vm.cc.value)
return (
f"v: {flags.v}\n"
f"n: {flags.n}\n"
f"z: {flags.z}\n"
)
elif re.fullmatch(
# r0-r255
r'r([0-9]|[1-9][0-9]|1[0-9][0-9]|2[1-4][1-9]|25[1-5])',
to_print):
index = int(to_print[1:])
return f"{to_print}: {self._vm.registers[index].value}"
elif to_print in self._dbg_dict["labels"]:
return f'{to_print}: {self._dbg_dict["labels"][to_print]}'
else:
return "Can't print required value"
def _run(self, args: list[str]) -> str:
try:
self._vm.run()
except Breakpoint as b:
return (f'breakpoint on line '
f'{self._dbg_dict["instructions"][b.address]["srcline"]}')
return "Program finished"
def _inspect(self, args: list[str]) -> str:
size_arg = args.pop(0)
amount = int(args.pop(0))
mem_location = args.pop(0)
offset = -1
if mem_location in self._dbg_dict["labels"]:
offset = self._dbg_dict["labels"][mem_location]
elif mem_location.isdigit():
offset = int(mem_location) * WORD_SIZE
elif mem_location.startswith("0x"):
offset = int(mem_location, 16)
else:
return ("You passed wrong offset parameter. It should be either "
"name of some label or explicit decimal or hexdecimal "
"number, in the second case it should start with 0x")
offset = self._vm._to_raw_bytes_offset(offset)
try:
sym = self.UNPACK_SYMBOL_TABLE[size_arg]
size = self.SIZE_SYMBOL_TABLE[size_arg]
except KeyError:
return ("Size Error. Please select one of the following "
"options as size: b/B - byte, h/H - 2 bytes, "
"w/W - 4 bytes. Big - unsigned, small - signed")
contents = struct.unpack(
">"+sym*amount,
self._vm.mem[offset:offset+size*amount]
)
return f"{mem_location}:" + " ".join(map(str, contents))
def _recover_from_breakpoint(self, cur_bp: Breakpoint):
"""
Когда виртуальная машина сталкивается с точкой останова, она на
ней зависает навечно. Для продолжения работы надо текущую точку
останова убрать, сделать шаг, а потом добавить. Предыдущая точка
остановка как раз хранится в дебаггере
"""
if not self.last_breakpoint:
return
if cur_bp.address != self.last_breakpoint.address:
return
self._vm.breakpoints.remove(cur_bp.address)

View File

@ -1,5 +1,5 @@
from argparse import ArgumentParser
from vm import VM
from .vm import VM
def main():
parser = ArgumentParser(
@ -15,7 +15,6 @@ def main():
default="out.mem"
)
args = parser.parse_args()
print(args)
with open(args.mem_file, 'rb') as f:
mem = bytearray(f.read())
vm = VM(mem)

View File

@ -43,13 +43,13 @@ OpD = OpcodeDescription
OPCODES = {
# block 1
0x00: OpD(OpF(0), OpL.MATH, OpA.ADD),
0x00: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.ADD),
0x10: OpD(OpF.QUICK, OpL.MATH, OpA.ADD),
0x01: OpD(OpF(0), OpL.MATH, OpA.SUB),
0x01: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.SUB),
0x11: OpD(OpF.QUICK, OpL.MATH, OpA.SUB),
0x02: OpD(OpF(0), OpL.MATH, OpA.MUL),
0x02: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.MUL),
0x12: OpD(OpF.QUICK, OpL.MATH, OpA.MUL),
0x03: OpD(OpF(0), OpL.MATH, OpA.DIV),
0x03: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.DIV),
0x13: OpD(OpF.QUICK, OpL.MATH, OpA.DIV),
0x04: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.AND),
0x05: OpD(OpF.UNEXPANDED, OpL.MATH, OpA.OR),
@ -63,9 +63,6 @@ OPCODES = {
# block 3
0x40: OpD(OpF(0), OpL.BRANCH, OpA.BRANCH),
0x50: OpD(OpF.QUICK, OpL.BRANCH, OpA.BRANCH),
0x51: OpD(OpF(0), OpL.BRANCH, OpA.IND_BRANCH),
0x41: OpD(OpF(0), OpL.BRANCH, OpA.IND_BRANCH),
}
if __name__ == "__main__":
print(hex(12))
print(~12)

View File

@ -1,13 +1,16 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import ClassVar, Callable
from ctypes import c_uint32, c_int32
import struct
from optable import OPCODES, OpcodeDescription, OpL, OpA, OpF, OpD
from .optable import OPCODES, OpcodeDescription, OpL, OpA, OpF, OpD
from enum import IntFlag, Enum, auto
WORD_SIZE: int = 4
class VMFlags(IntFlag):
# if last instruction is big, then you should
# skip more memory on step
AFTER_BRANCH = auto()
EXPANDED_INSTR = auto()
class Condition:
@ -22,6 +25,11 @@ class VMCC(IntFlag):
NEGATIVE = 1 << 1
ZERO = 1 << 0
class VMStatus(Enum):
RUNNING = auto()
INITED = auto()
FINISHED = auto()
@dataclass
class Breakpoint(Exception):
address: int
@ -34,6 +42,7 @@ class VMExceptionType(Enum):
class VMException(Exception):
cause: VMExceptionType
pc: int
message: str = ""
@dataclass
class VM:
@ -44,48 +53,46 @@ class VM:
registers: list[c_int32]
breakpoints: set[int]
_vm_flags: VMFlags
status: VMStatus
def __init__(self, mem: bytearray):
self.mem: bytearray = mem
self.cc: VMCC = VMCC(0)
self.pc: c_uint32 = c_uint32(0)
self.registers: list[c_int32] = [c_int32(0) for _ in range(256)]
self.breakpoints: set[int] = set()
self._vm_flags: VMFlags = VMFlags(0)
self._initial_mem = mem.copy()
self.__init_callbacks__()
self.reset()
self.breakpoints: set[int] = set()
def __init_callbacks__(self):
VM.instr_callbacks = {
# ariphmetic
OpD(OpF(0), OpL.MATH, OpA.ADD):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.ADD):
self._math_callback_gen(lambda lhs, rhs: lhs + rhs),
OpD(OpF.QUICK, OpL.MATH, OpA.ADD):
self._math_quick_callback_gen(lambda lhs, rhs: lhs + rhs),
OpD(OpF(0), OpL.MATH, OpA.SUB):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.SUB):
self._math_callback_gen(lambda lhs, rhs: lhs - rhs),
OpD(OpF.QUICK, OpL.MATH, OpA.SUB):
self._math_quick_callback_gen(lambda lhs, rhs: lhs - rhs),
OpD(OpF(0), OpL.MATH, OpA.MUL):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.MUL):
self._math_callback_gen(lambda lhs, rhs: lhs * rhs),
OpD(OpF.QUICK, OpL.MATH, OpA.MUL):
self._math_quick_callback_gen(lambda lhs, rhs: lhs * rhs),
OpD(OpF(0), OpL.MATH, OpA.DIV):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.DIV):
self._math_callback_gen(lambda lhs, rhs: lhs // rhs),
OpD(OpF.QUICK, OpL.MATH, OpA.DIV):
self._math_quick_callback_gen(lambda lhs, rhs: lhs // rhs),
# logical ops
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.AND):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.AND):
self._math_callback_gen(lambda lhs, rhs: lhs & rhs),
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.OR):
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.OR):
self._math_callback_gen(lambda lhs, rhs: lhs | rhs),
OpD(OpF.UNEXPANDED, OpL.MATH, OpA.XOR):
@ -115,49 +122,95 @@ class VM:
self._branch_indexed_callback
}
def reset(self):
self.mem: bytearray = self._initial_mem.copy()
self.cc: VMCC = VMCC(0)
self.pc: c_uint32 = c_uint32(0)
self.registers: list[c_int32] = [c_int32(0) for _ in range(256)]
self._vm_flags: VMFlags = VMFlags(0)
self.status = VMStatus.INITED
def step(self) -> None:
"""
Make one step (only step into)
"""
opcode = self.mem[self.pc.value * 4]
opdesc = self._fetch_opcode_desc(opcode)
args = self._parse_arguments(opdesc)
# По какой-то причине адрессация работает
# так, будто мы на 1 слово впереди опкода
self.pc = c_uint32(self.pc.value + 1)
if self._to_raw_bytes_offset(self.pc) > len(self.mem) - WORD_SIZE:
self.status = VMStatus.FINISHED
raise VMException(
VMExceptionType.END_OF_MEM,
self.pc.value,
"couldn't perform step because end of memory occured"
)
self.status = VMStatus.RUNNING
opcode, *_ = instr = self._fetch_instr()
opdesc = self._get_opcode_desc(opcode)
args: tuple[int, ...] = self._parse_instr_fields(bytes(instr))
if not (OpF.UNEXPANDED in opdesc.flags or
OpF.QUICK in opdesc.flags):
extra = struct.unpack(">i", self._fetch_instr())[0]
args = (*args, extra)
self._run_callback(opdesc, args)
if VMFlags.EXPANDED_INSTR in self._vm_flags:
self.pc = c_uint32(self.pc.value + 1)
self._vm_flags &= ~(VMFlags.EXPANDED_INSTR)
self._vm_flags &= ~(VMFlags.AFTER_BRANCH)
def continue_(self) -> None:
"""
Continue from current breakpoint
"""
while self.pc.value < len(self.mem):
while self._to_raw_bytes_offset(self.pc.value) < len(self.mem):
if self.pc.value in self.breakpoints:
raise Breakpoint(self.pc.value)
self.step()
self.status = VMStatus.FINISHED
def run(self) -> None:
"""
Run from very beginning
"""
self.pc = c_uint32(0)
while (self.pc.value < len(self.mem) // 4):
self.reset()
while (self._to_raw_bytes_offset(self.pc.value) < len(self.mem)):
self.continue_()
self.status = VMStatus.FINISHED
def _fetch_opcode_desc(self, opcode: int):
def _fetch_instr(self) -> bytearray:
"""
Берет из памяти следущее слово, возвращает его
ВАЖНО: инкрементирует счетчик команд на каждом вызове
"""
addr = self._to_raw_bytes_offset(self.pc)
instr = self.mem[addr:addr+WORD_SIZE]
self._increment_pc()
return instr
def _get_opcode_desc(self, opcode: int):
"""
Обертка над обращением к таблице опкодов из файлика optable.py.
Нужна чтобы прокидывать исключения виртуальной машины, а не
KeyError
"""
if not opcode in OPCODES:
raise VMException(
VMExceptionType.INVALID_OPCODE,
self.pc.value,
f"Couldn't resolve an opcode {hex(opcode)}"
)
return OPCODES[opcode]
def _parse_arguments(self, opdesc: OpcodeDescription) -> tuple[int, ...]:
addr = self.pc.value * 4
main_part = struct.unpack(">BBBb", self.mem[addr:addr+4])
if OpF.UNEXPANDED in opdesc.flags or OpF.QUICK in opdesc.flags:
return main_part
upper_part = struct.unpack(">i", self.mem[addr+4:addr+8])
return (*main_part, *upper_part)
def _to_raw_bytes_offset(self, pointer: int | c_uint32 | c_int32) -> int:
"""
Переводит адресс из указателей в рамках процессора, которые
являются 32-битными, в формат смещения в сырых байтах в памяти
"""
if isinstance(pointer, int):
return pointer * WORD_SIZE
return pointer.value * WORD_SIZE
def _parse_instr_fields(
self,
instr: bytes
) -> tuple[int, int, int, int]:
return struct.unpack(">BBBb", instr)
def _run_callback(
self,
@ -169,7 +222,7 @@ class VM:
if opdesc.layout == OpL.MATH:
assert len(args) == 4
_, r3, r1, r2_or_i8 = args
# поскольку этот колбэк сгенерирован,
# поскольку этот колбэк сгенерирован,
# ему необходимо в явном виде указывать
# аргумент self
self.instr_callbacks[opdesc](
@ -209,6 +262,25 @@ class VM:
cond, disp
)
@staticmethod
def _perform_ariphmetic_operation(
lhs: int,
rhs: int,
op: Callable[[int, int], int]
) -> tuple[int, VMCC]:
cc = VMCC(0)
result = op(lhs, rhs)
if result < 0:
cc |= VMCC.NEGATIVE
elif result == 0:
cc |= VMCC.ZERO
# самая дорогая проверка на переполнение)
try:
struct.pack('i', result)
except struct.error:
cc |= VMCC.OVERFLOW
return result, cc
def _math_callback_gen(
self,
operation: Callable[[int, int], int]
@ -221,10 +293,16 @@ class VM:
def callback(self, r3: int, r1: int, r2: int):
lhs = self.registers[r1].value
rhs = self.registers[r2].value
self.registers[r3] = c_int32(operation(lhs, rhs))
result, cc = self._perform_ariphmetic_operation(
lhs,
rhs,
operation)
self.registers[r3] = c_int32(result)
self.cc = cc
return callback
def _math_quick_callback_gen(
self,
operation: Callable[[int, int], int]
@ -240,41 +318,45 @@ class VM:
def callback(self, r3: int, r1: int, i8: int) -> None:
self.cc = VMCC(0)
lhs = self.registers[r1].value
result = operation(lhs, i8)
if result < 0:
self.cc |= VMCC.NEGATIVE
elif result == 0:
self.cc |= VMCC.ZERO
# самая дорогая проверка на переполнение)
try:
struct.pack('i', result)
except struct.error:
self.cc |= VMCC.OVERFLOW
result, flags = self._perform_ariphmetic_operation(
lhs,
i8,
operation)
self.registers[r3] = c_int32(result)
self.cc = flags
return callback
def _load_callback(self, r3: int, r1: int, disp: int) -> None:
addr = (self.registers[r1].value + disp) * 4
addr = self._to_raw_bytes_offset(self.registers[r1].value + disp)
raw_bytes = self.mem[addr:addr+WORD_SIZE]
self.registers[r3] = c_int32(
struct.unpack(">i", self.mem[addr:addr+4])[0]
struct.unpack(
">i", raw_bytes
)[0]
)
def _store_callback(self, r3: int, r1: int, disp: int) -> None:
addr = (self.registers[r1].value + disp) * 4
self.mem[addr:addr+4] = struct.pack(">i", self.registers[r3].value)
addr = self._to_raw_bytes_offset(self.registers[r1].value + disp)
self.mem[addr:addr+WORD_SIZE] = struct.pack(
">i",
self.registers[r3].value
)
def _branch_callback(self, cond: int, disp: int) -> None:
c = Condition(cond)
vm_c = Condition(self.cc)
if (c.v & vm_c.v) & (c.n & vm_c.n) & (c.z & vm_c.z) == c.i:
self._vm_flags |= VMFlags.AFTER_BRANCH
if (c.v & vm_c.v) | (c.n & vm_c.n) | (c.z & vm_c.z) == c.i:
self._vm_flags = VMFlags.AFTER_BRANCH
self.pc = c_uint32(self.pc.value + disp)
def _branch_indexed_callback(self, cond: int, r1: int, disp: int) -> None:
c = Condition(cond)
vm_c = Condition(self.cc.value)
if (c.v & vm_c.v) & (c.n & vm_c.n) & (c.z & vm_c.z) == c.i:
self._vm_flags |= VMFlags.AFTER_BRANCH
if (c.v & vm_c.v) | (c.n & vm_c.n) | (c.z & vm_c.z) == c.i:
self._vm_flags = VMFlags.AFTER_BRANCH
addr = self.registers[r1].value + disp
self.pc = c_uint32(addr)
def _increment_pc(self):
self.pc = c_uint32(self.pc.value + 1)

36
uv.lock generated Normal file
View File

@ -0,0 +1,36 @@
version = 1
revision = 1
requires-python = ">=3.11"
[[package]]
name = "dp32-proto"
version = "0.1.1"
source = { editable = "." }
dependencies = [
{ name = "py-cui" },
]
[package.metadata]
requires-dist = [{ name = "py-cui", specifier = ">=0.1.6" }]
[[package]]
name = "py-cui"
version = "0.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "windows-curses", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/d8/bb92087dc36dc94a60993955133d0f6473237a6ce1a6145ad95dce3b2a21/py_cui-0.1.6.tar.gz", hash = "sha256:91186f33f26216cd82a676e6b02e85110a7b562d8ab9792359cfdde4ee6b9abc", size = 63009 }
[[package]]
name = "windows-curses"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/b3/46a2508fff83b5affb5a311797c584c494b4b0c4c8796a1afa2c1b1e03ac/windows_curses-2.4.1-cp311-cp311-win32.whl", hash = "sha256:4fa1a176bfcf098d0c9bb7bc03dce6e83a4257fc0c66ad721f5745ebf0c00746", size = 71129 },
{ url = "https://files.pythonhosted.org/packages/50/29/fd7c0c80177d8df55f841504a5f332b95b0002c80f82055913b6caac94e6/windows_curses-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fd7d7a9cf6c1758f46ed76b8c67f608bc5fcd5f0ca91f1580fd2d84cf41c7f4f", size = 81431 },
{ url = "https://files.pythonhosted.org/packages/f4/99/60f34e51514c82631aa5c6d21eab88ce1701562de9844608411e39462a46/windows_curses-2.4.1-cp312-cp312-win32.whl", hash = "sha256:bdbe7d58747408aef8a9128b2654acf6fbd11c821b91224b9a046faba8c6b6ca", size = 71489 },
{ url = "https://files.pythonhosted.org/packages/62/8f/d908bcab1954375b156a9300fa86ceccb110f39457cd6fd59c72777626ab/windows_curses-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c9c2635faf171a229caca80e1dd760ab00db078e2a285ba2f667bbfcc31777c", size = 81777 },
{ url = "https://files.pythonhosted.org/packages/25/a0/e8d074f013117633f6b502ca123ecfc377fe0bd36818fe65e8935c91ca9c/windows_curses-2.4.1-cp313-cp313-win32.whl", hash = "sha256:05d1ca01e5199a435ccb6c8c2978df4a169cdff1ec99ab15f11ded9de8e5be26", size = 71390 },
{ url = "https://files.pythonhosted.org/packages/2b/4b/2838a829b074a68c570d54ae0ae8539979657d3e619a4dc5a4b03eb69745/windows_curses-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8cf653f8928af19c103ae11cfed38124f418dcdd92643c4cd17239c0cec2f9da", size = 81636 },
]