pyliu 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyliu-0.1.0/MANIFEST.in +5 -0
- pyliu-0.1.0/PKG-INFO +113 -0
- pyliu-0.1.0/Pyliu/__init__.py +27 -0
- pyliu-0.1.0/Pyliu/__main__.py +8 -0
- pyliu-0.1.0/Pyliu/verification.py +854 -0
- pyliu-0.1.0/README.md +99 -0
- pyliu-0.1.0/pyliu.egg-info/PKG-INFO +113 -0
- pyliu-0.1.0/pyliu.egg-info/SOURCES.txt +14 -0
- pyliu-0.1.0/pyliu.egg-info/dependency_links.txt +1 -0
- pyliu-0.1.0/pyliu.egg-info/entry_points.txt +2 -0
- pyliu-0.1.0/pyliu.egg-info/requires.txt +1 -0
- pyliu-0.1.0/pyliu.egg-info/top_level.txt +1 -0
- pyliu-0.1.0/pyproject.toml +15 -0
- pyliu-0.1.0/setup.cfg +4 -0
- pyliu-0.1.0/setup.py +26 -0
- pyliu-0.1.0/tests/test_qemu_optional.py +23 -0
pyliu-0.1.0/MANIFEST.in
ADDED
pyliu-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyliu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight assembly experimentation framework for Python
|
|
5
|
+
Home-page: https://github.com/aa425-aarohcharne/pyliu
|
|
6
|
+
Author: Aaroh
|
|
7
|
+
Author-email: Aaroh <aaroh.charne@gmail.com>
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: keystone-engine
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: home-page
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# Pyliu
|
|
16
|
+
|
|
17
|
+
Pyliu is a lightweight Python assembly experimentation framework for building, compiling, and executing simple assembly-like programs. It combines a small CPU instruction DSL, a compiler layer, and multiple runtime backends so you can explore low-level execution patterns from Python.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- A simple CPU instruction abstraction layer
|
|
22
|
+
- A compiler pipeline for assembling programs into machine code
|
|
23
|
+
- An in-process JIT-style runtime
|
|
24
|
+
- A bare-metal QEMU runtime for isolated execution
|
|
25
|
+
- An optional CLI for installing QEMU explicitly
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install from the project directory:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or, if you are using the local virtual environment:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
.venv\Scripts\python.exe -m pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Optional QEMU dependency
|
|
42
|
+
|
|
43
|
+
QEMU is treated as an optional dependency for Pyliu.
|
|
44
|
+
|
|
45
|
+
By default, Pyliu does not try to download or install anything automatically. If you want to use the QEMU-backed runtime, install it explicitly:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m Pyliu install-qemu
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or, if you are using the project virtual environment:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
.venv\Scripts\python.exe -m Pyliu install-qemu
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from Pyliu.verification import Program
|
|
61
|
+
|
|
62
|
+
program = Program()
|
|
63
|
+
program.begin()
|
|
64
|
+
program.mov("eax", "1")
|
|
65
|
+
program.ret()
|
|
66
|
+
program.end()
|
|
67
|
+
|
|
68
|
+
print(program.compile())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Using the QEMU runtime
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from Pyliu.verification import Program, QEMURuntime
|
|
75
|
+
|
|
76
|
+
program = Program()
|
|
77
|
+
program.begin()
|
|
78
|
+
program.mov("eax", "1")
|
|
79
|
+
program.ret()
|
|
80
|
+
program.end()
|
|
81
|
+
|
|
82
|
+
runtime = QEMURuntime(timeout=5)
|
|
83
|
+
print(runtime.run(program))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## CLI
|
|
87
|
+
|
|
88
|
+
Pyliu exposes a small CLI for optional QEMU installation:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m Pyliu --help
|
|
92
|
+
python -m Pyliu install-qemu
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Project structure
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
Pyliu/
|
|
99
|
+
├── __init__.py
|
|
100
|
+
├── __main__.py
|
|
101
|
+
├── verification.py
|
|
102
|
+
└── tests/
|
|
103
|
+
└── test_qemu_optional.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Notes
|
|
107
|
+
|
|
108
|
+
- The QEMU runtime is intended for experimentation and research-oriented workflows.
|
|
109
|
+
- The package is designed to remain safe and non-invasive for PyPI-style packaging by avoiding automatic system modifications during import or normal runtime use.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is provided as-is for educational and research use.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Pyliu package entry point."""
|
|
2
|
+
|
|
3
|
+
from .verification import (
|
|
4
|
+
CPU,
|
|
5
|
+
Compiler,
|
|
6
|
+
Memory,
|
|
7
|
+
OS,
|
|
8
|
+
Playground,
|
|
9
|
+
Program,
|
|
10
|
+
QEMURuntime,
|
|
11
|
+
Runtime,
|
|
12
|
+
ensure_qemu_installed,
|
|
13
|
+
install_qemu,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CPU",
|
|
18
|
+
"Compiler",
|
|
19
|
+
"Memory",
|
|
20
|
+
"OS",
|
|
21
|
+
"Playground",
|
|
22
|
+
"Program",
|
|
23
|
+
"QEMURuntime",
|
|
24
|
+
"Runtime",
|
|
25
|
+
"ensure_qemu_installed",
|
|
26
|
+
"install_qemu",
|
|
27
|
+
]
|
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import struct
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
from keystone import Ks, KS_ARCH_X86, KS_MODE_16, KS_MODE_32, KS_MODE_64
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _install_qemu_windows_portable(verbose=True):
|
|
14
|
+
"""
|
|
15
|
+
Downloads the official QEMU Windows installer (the same builds
|
|
16
|
+
linked from https://www.qemu.org/download/#windows, hosted at
|
|
17
|
+
qemu.weilnetz.de) and runs it silently (/S) into a user-local
|
|
18
|
+
folder under %LOCALAPPDATA% -- no admin rights, no install
|
|
19
|
+
wizard, no clicking anything.
|
|
20
|
+
|
|
21
|
+
NOTE: this path is implemented carefully but has not been run
|
|
22
|
+
against a real Windows machine -- verify it on yours and report
|
|
23
|
+
back if the installer's silent-mode flags behave differently
|
|
24
|
+
than documented for the version you get.
|
|
25
|
+
"""
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
install_dir = os.path.join(
|
|
29
|
+
os.environ.get("LOCALAPPDATA", tempfile.gettempdir()),
|
|
30
|
+
"asmlib_qemu", "qemu",
|
|
31
|
+
)
|
|
32
|
+
exe_path = os.path.join(install_dir, "qemu-system-x86_64.exe")
|
|
33
|
+
if os.path.exists(exe_path):
|
|
34
|
+
return exe_path
|
|
35
|
+
|
|
36
|
+
def _log(msg):
|
|
37
|
+
if verbose:
|
|
38
|
+
print(f"[ensure_qemu_installed] {msg}")
|
|
39
|
+
|
|
40
|
+
_log("locating the latest official QEMU Windows build...")
|
|
41
|
+
listing_url = "https://qemu.weilnetz.de/w64/"
|
|
42
|
+
try:
|
|
43
|
+
with urllib.request.urlopen(listing_url, timeout=15) as resp:
|
|
44
|
+
html = resp.read().decode(errors="ignore")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
f"Could not reach {listing_url} to find a QEMU build "
|
|
48
|
+
f"({e}). Download and install QEMU manually from "
|
|
49
|
+
f"https://www.qemu.org/download/#windows, then try again."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
candidates = sorted(set(re.findall(r'href="(qemu-w64-setup-[^"]+\.exe)"', html)))
|
|
53
|
+
if not candidates:
|
|
54
|
+
raise RuntimeError(
|
|
55
|
+
f"Found the page at {listing_url} but no installer link "
|
|
56
|
+
f"matched the expected pattern -- the site layout may have "
|
|
57
|
+
f"changed. Download QEMU manually from "
|
|
58
|
+
f"https://www.qemu.org/download/#windows"
|
|
59
|
+
)
|
|
60
|
+
installer_name = candidates[-1] # filenames are date-stamped -> lexicographic sort = latest
|
|
61
|
+
installer_url = listing_url + installer_name
|
|
62
|
+
|
|
63
|
+
_log(f"downloading {installer_name} ...")
|
|
64
|
+
tmp_installer = os.path.join(tempfile.gettempdir(), installer_name)
|
|
65
|
+
try:
|
|
66
|
+
urllib.request.urlretrieve(installer_url, tmp_installer)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise RuntimeError(f"Download of {installer_url} failed: {e}")
|
|
69
|
+
|
|
70
|
+
os.makedirs(install_dir, exist_ok=True)
|
|
71
|
+
# NSIS silent-install flags: /S = silent, /D=<dir> = install directory.
|
|
72
|
+
# /D must be the LAST argument and must not be quoted, even if the
|
|
73
|
+
# path contains spaces (an NSIS requirement, not a Python one).
|
|
74
|
+
install_args = ["/S", f"/D={install_dir}"]
|
|
75
|
+
|
|
76
|
+
_log(f"installing silently to {install_dir}...")
|
|
77
|
+
try:
|
|
78
|
+
subprocess.run([tmp_installer] + install_args, capture_output=True, timeout=180)
|
|
79
|
+
except OSError as e:
|
|
80
|
+
# WinError 740 = "The requested operation requires elevation".
|
|
81
|
+
# The official QEMU installer's manifest requires admin no
|
|
82
|
+
# matter which folder it targets -- subprocess.run can't grant
|
|
83
|
+
# that. ShellExecuteW with the "runas" verb triggers the one
|
|
84
|
+
# UAC prompt Windows actually requires here; after you click
|
|
85
|
+
# "Yes" once, /S still keeps everything else silent.
|
|
86
|
+
if getattr(e, "winerror", None) == 740:
|
|
87
|
+
_log(
|
|
88
|
+
"the installer needs admin rights -- a UAC prompt should "
|
|
89
|
+
"appear now. Click \"Yes\" to continue (this is the only "
|
|
90
|
+
"prompt you'll see)."
|
|
91
|
+
)
|
|
92
|
+
import ctypes as _ctypes
|
|
93
|
+
_ctypes.windll.shell32.ShellExecuteW(
|
|
94
|
+
None, "runas", tmp_installer, " ".join(install_args), None, 1
|
|
95
|
+
)
|
|
96
|
+
# ShellExecuteW doesn't block, so poll for the install to finish
|
|
97
|
+
waited = 0
|
|
98
|
+
while not os.path.exists(exe_path) and waited < 120:
|
|
99
|
+
import time as _time
|
|
100
|
+
_time.sleep(2)
|
|
101
|
+
waited += 2
|
|
102
|
+
else:
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
if not os.path.exists(exe_path):
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
f"Installer ran but {exe_path} wasn't created. Either the "
|
|
108
|
+
f"UAC prompt wasn't accepted, or this build's silent flags "
|
|
109
|
+
f"or directory layout differ -- try running "
|
|
110
|
+
f"'{tmp_installer}' manually to install with the UI, "
|
|
111
|
+
f"or install via https://www.qemu.org/download/#windows"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_log(f"QEMU ready at {exe_path}")
|
|
115
|
+
return exe_path
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ensure_qemu_installed(qemu_binary="qemu-system-x86_64", auto_install=False, verbose=True):
|
|
119
|
+
"""
|
|
120
|
+
Check whether a QEMU x86-64 system emulator is available on PATH.
|
|
121
|
+
|
|
122
|
+
QEMU is treated as an optional runtime dependency. By default this
|
|
123
|
+
function does not install anything automatically; it only raises a
|
|
124
|
+
clear error that tells the user how to opt in explicitly.
|
|
125
|
+
|
|
126
|
+
If auto_install is True, it tries to install QEMU using whatever
|
|
127
|
+
package manager fits the current platform:
|
|
128
|
+
|
|
129
|
+
- Linux: apt, dnf, or pacman (whichever is present), via sudo
|
|
130
|
+
- macOS: Homebrew (brew)
|
|
131
|
+
- Windows: winget, falling back to a manual download link
|
|
132
|
+
|
|
133
|
+
Returns the resolved path to the qemu-system-x86_64 binary.
|
|
134
|
+
Raises RuntimeError with manual install instructions if it can't
|
|
135
|
+
find or install QEMU automatically.
|
|
136
|
+
"""
|
|
137
|
+
existing = shutil.which(qemu_binary)
|
|
138
|
+
if existing:
|
|
139
|
+
return existing
|
|
140
|
+
|
|
141
|
+
if not auto_install:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"{qemu_binary} not found on PATH. QEMU is an optional dependency "
|
|
144
|
+
f"for this package, so installation is disabled by default. "
|
|
145
|
+
f"Install it manually or run: python -m Pyliu install-qemu"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
system = platform.system()
|
|
149
|
+
|
|
150
|
+
def _log(msg):
|
|
151
|
+
if verbose:
|
|
152
|
+
print(f"[ensure_qemu_installed] {msg}")
|
|
153
|
+
|
|
154
|
+
def _try(cmd, timeout=None):
|
|
155
|
+
_log(f"running: {' '.join(cmd)}")
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
158
|
+
return result.returncode == 0
|
|
159
|
+
except subprocess.TimeoutExpired:
|
|
160
|
+
_log(
|
|
161
|
+
f"'{cmd[0]}' didn't finish within {timeout}s -- it's likely "
|
|
162
|
+
f"waiting on a UAC/consent dialog that isn't visible to this "
|
|
163
|
+
f"script. Giving up on this method and trying the next one."
|
|
164
|
+
)
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def _maybe_sudo(cmd):
|
|
168
|
+
# root (e.g. inside containers) has no sudo binary and doesn't
|
|
169
|
+
# need one; everyone else does
|
|
170
|
+
needs_privilege = os.name == "posix" and hasattr(os, "geteuid") and os.geteuid() != 0
|
|
171
|
+
if needs_privilege and shutil.which("sudo"):
|
|
172
|
+
return ["sudo"] + cmd
|
|
173
|
+
return cmd
|
|
174
|
+
|
|
175
|
+
if system == "Linux":
|
|
176
|
+
if shutil.which("apt-get"):
|
|
177
|
+
_log("detected apt -- installing qemu-system-x86")
|
|
178
|
+
# apt-get update can return nonzero due to unrelated broken
|
|
179
|
+
# third-party repos even when the repos we need are fine,
|
|
180
|
+
# so don't gate the install attempt on its exit code --
|
|
181
|
+
# only the final "is the binary on PATH" check matters.
|
|
182
|
+
_try(_maybe_sudo(["apt-get", "update"]), timeout=120)
|
|
183
|
+
ok = _try(_maybe_sudo(["apt-get", "install", "-y", "qemu-system-x86"]), timeout=180)
|
|
184
|
+
elif shutil.which("dnf"):
|
|
185
|
+
_log("detected dnf -- installing qemu-system-x86")
|
|
186
|
+
ok = _try(_maybe_sudo(["dnf", "install", "-y", "qemu-system-x86"]), timeout=180)
|
|
187
|
+
elif shutil.which("pacman"):
|
|
188
|
+
_log("detected pacman -- installing qemu")
|
|
189
|
+
ok = _try(_maybe_sudo(["pacman", "-Sy", "--noconfirm", "qemu-system-x86"]), timeout=180)
|
|
190
|
+
else:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"No supported package manager found (apt/dnf/pacman). "
|
|
193
|
+
"Install QEMU manually: https://www.qemu.org/download/#linux"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif system == "Darwin":
|
|
197
|
+
if not shutil.which("brew"):
|
|
198
|
+
raise RuntimeError(
|
|
199
|
+
"Homebrew not found. Install it from https://brew.sh, "
|
|
200
|
+
"then run: brew install qemu"
|
|
201
|
+
)
|
|
202
|
+
_log("detected Homebrew -- installing qemu")
|
|
203
|
+
ok = _try(["brew", "install", "qemu"], timeout=300)
|
|
204
|
+
|
|
205
|
+
elif system == "Windows":
|
|
206
|
+
raise RuntimeError(
|
|
207
|
+
"QEMU not found. Install manually or run: python -m Pyliu install-qemu"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
else:
|
|
211
|
+
raise RuntimeError(f"Unsupported platform for auto-install: {system}")
|
|
212
|
+
|
|
213
|
+
resolved = shutil.which(qemu_binary)
|
|
214
|
+
if not resolved:
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
f"Install attempted but {qemu_binary} still isn't on PATH. "
|
|
217
|
+
f"You may need to open a new shell/terminal for PATH changes "
|
|
218
|
+
f"to take effect, or install QEMU manually: "
|
|
219
|
+
f"https://www.qemu.org/download/"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def install_qemu(qemu_binary="qemu-system-x86_64", verbose=True):
|
|
224
|
+
"""Install QEMU explicitly when the user requests it."""
|
|
225
|
+
return ensure_qemu_installed(qemu_binary=qemu_binary, auto_install=True, verbose=verbose)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main(argv=None):
|
|
229
|
+
"""Small CLI entry point for optional QEMU installation."""
|
|
230
|
+
import argparse
|
|
231
|
+
|
|
232
|
+
parser = argparse.ArgumentParser(prog="python -m Pyliu")
|
|
233
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
234
|
+
|
|
235
|
+
install_parser = subparsers.add_parser("install-qemu", help="Install QEMU explicitly")
|
|
236
|
+
install_parser.add_argument("--qemu-binary", default="qemu-system-x86_64")
|
|
237
|
+
install_parser.add_argument("--verbose", action="store_true")
|
|
238
|
+
|
|
239
|
+
args = parser.parse_args(argv)
|
|
240
|
+
if args.command == "install-qemu":
|
|
241
|
+
install_qemu(qemu_binary=args.qemu_binary, verbose=args.verbose)
|
|
242
|
+
print("QEMU installation completed or was accepted by the runtime.")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
parser.print_help()
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class CPU:
|
|
250
|
+
def __init__(self):
|
|
251
|
+
self.instructions = []
|
|
252
|
+
|
|
253
|
+
def emit(self, instruction):
|
|
254
|
+
self.instructions.append(instruction)
|
|
255
|
+
|
|
256
|
+
def _mem_operand(self, operand):
|
|
257
|
+
operand = str(operand).strip()
|
|
258
|
+
if operand.startswith("[") and operand.endswith("]"):
|
|
259
|
+
return operand
|
|
260
|
+
return f"[{operand}]"
|
|
261
|
+
|
|
262
|
+
def mov(self, dest, src):
|
|
263
|
+
self.emit(f"mov {dest}, {src}")
|
|
264
|
+
|
|
265
|
+
def lea(self, dest, src):
|
|
266
|
+
self.emit(f"lea {dest}, {src}")
|
|
267
|
+
|
|
268
|
+
def xchg(self, a, b):
|
|
269
|
+
self.emit(f"xchg {a}, {b}")
|
|
270
|
+
|
|
271
|
+
def movzx(self, dest, src):
|
|
272
|
+
self.emit(f"movzx {dest}, {src}")
|
|
273
|
+
|
|
274
|
+
def movsx(self, dest, src):
|
|
275
|
+
self.emit(f"movsx {dest}, {src}")
|
|
276
|
+
|
|
277
|
+
def add(self, dest, src):
|
|
278
|
+
self.emit(f"add {dest}, {src}")
|
|
279
|
+
|
|
280
|
+
def sub(self, dest, src):
|
|
281
|
+
self.emit(f"sub {dest}, {src}")
|
|
282
|
+
|
|
283
|
+
def inc(self, dest):
|
|
284
|
+
self.emit(f"inc {dest}")
|
|
285
|
+
|
|
286
|
+
def dec(self, dest):
|
|
287
|
+
self.emit(f"dec {dest}")
|
|
288
|
+
|
|
289
|
+
def imul(self, dest, src):
|
|
290
|
+
self.emit(f"imul {dest}, {src}")
|
|
291
|
+
|
|
292
|
+
def idiv(self, src):
|
|
293
|
+
self.emit(f"idiv {src}")
|
|
294
|
+
|
|
295
|
+
def neg(self, dest):
|
|
296
|
+
self.emit(f"neg {dest}")
|
|
297
|
+
|
|
298
|
+
def and_(self, dest, src):
|
|
299
|
+
self.emit(f"and {dest}, {src}")
|
|
300
|
+
|
|
301
|
+
def or_(self, dest, src):
|
|
302
|
+
self.emit(f"or {dest}, {src}")
|
|
303
|
+
|
|
304
|
+
def xor(self, dest, src):
|
|
305
|
+
self.emit(f"xor {dest}, {src}")
|
|
306
|
+
|
|
307
|
+
def not_(self, dest):
|
|
308
|
+
self.emit(f"not {dest}")
|
|
309
|
+
|
|
310
|
+
def test(self, a, b):
|
|
311
|
+
self.emit(f"test {a}, {b}")
|
|
312
|
+
|
|
313
|
+
def shl(self, dest, count):
|
|
314
|
+
self.emit(f"shl {dest}, {count}")
|
|
315
|
+
|
|
316
|
+
def shr(self, dest, count):
|
|
317
|
+
self.emit(f"shr {dest}, {count}")
|
|
318
|
+
|
|
319
|
+
def sar(self, dest, count):
|
|
320
|
+
self.emit(f"sar {dest}, {count}")
|
|
321
|
+
|
|
322
|
+
def rol(self, dest, count):
|
|
323
|
+
self.emit(f"rol {dest}, {count}")
|
|
324
|
+
|
|
325
|
+
def ror(self, dest, count):
|
|
326
|
+
self.emit(f"ror {dest}, {count}")
|
|
327
|
+
|
|
328
|
+
def cmp(self, a, b):
|
|
329
|
+
self.emit(f"cmp {a}, {b}")
|
|
330
|
+
|
|
331
|
+
def jmp(self, target):
|
|
332
|
+
self.emit(f"jmp {target}")
|
|
333
|
+
|
|
334
|
+
def je(self, target):
|
|
335
|
+
self.emit(f"je {target}")
|
|
336
|
+
|
|
337
|
+
def jz(self, target):
|
|
338
|
+
self.emit(f"jz {target}")
|
|
339
|
+
|
|
340
|
+
def jne(self, target):
|
|
341
|
+
self.emit(f"jne {target}")
|
|
342
|
+
|
|
343
|
+
def jnz(self, target):
|
|
344
|
+
self.emit(f"jnz {target}")
|
|
345
|
+
|
|
346
|
+
def jg(self, target):
|
|
347
|
+
self.emit(f"jg {target}")
|
|
348
|
+
|
|
349
|
+
def jge(self, target):
|
|
350
|
+
self.emit(f"jge {target}")
|
|
351
|
+
|
|
352
|
+
def jl(self, target):
|
|
353
|
+
self.emit(f"jl {target}")
|
|
354
|
+
|
|
355
|
+
def jle(self, target):
|
|
356
|
+
self.emit(f"jle {target}")
|
|
357
|
+
|
|
358
|
+
def ja(self, target):
|
|
359
|
+
self.emit(f"ja {target}")
|
|
360
|
+
|
|
361
|
+
def jb(self, target):
|
|
362
|
+
self.emit(f"jb {target}")
|
|
363
|
+
|
|
364
|
+
def call(self, target):
|
|
365
|
+
self.emit(f"call {target}")
|
|
366
|
+
|
|
367
|
+
def ret(self):
|
|
368
|
+
self.emit("ret")
|
|
369
|
+
|
|
370
|
+
def push(self, src):
|
|
371
|
+
self.emit(f"push {src}")
|
|
372
|
+
|
|
373
|
+
def pop(self, dest):
|
|
374
|
+
self.emit(f"pop {dest}")
|
|
375
|
+
|
|
376
|
+
def syscall(self):
|
|
377
|
+
self.emit("syscall")
|
|
378
|
+
|
|
379
|
+
def int_(self, value):
|
|
380
|
+
self.emit(f"int {value}")
|
|
381
|
+
|
|
382
|
+
def nop(self):
|
|
383
|
+
self.emit("nop")
|
|
384
|
+
|
|
385
|
+
def hlt(self):
|
|
386
|
+
self.emit("hlt")
|
|
387
|
+
|
|
388
|
+
def cpuid(self):
|
|
389
|
+
self.emit("cpuid")
|
|
390
|
+
|
|
391
|
+
def rdtsc(self):
|
|
392
|
+
self.emit("rdtsc")
|
|
393
|
+
|
|
394
|
+
def clc(self):
|
|
395
|
+
self.emit("clc")
|
|
396
|
+
|
|
397
|
+
def stc(self):
|
|
398
|
+
self.emit("stc")
|
|
399
|
+
|
|
400
|
+
def cmc(self):
|
|
401
|
+
self.emit("cmc")
|
|
402
|
+
|
|
403
|
+
def cli(self):
|
|
404
|
+
self.emit("cli")
|
|
405
|
+
|
|
406
|
+
def sti(self):
|
|
407
|
+
self.emit("sti")
|
|
408
|
+
|
|
409
|
+
def load(self, dest, addr):
|
|
410
|
+
self.emit(f"mov {dest}, {self._mem_operand(addr)}")
|
|
411
|
+
|
|
412
|
+
def store(self, addr, src):
|
|
413
|
+
self.emit(f"mov {self._mem_operand(addr)}, {src}")
|
|
414
|
+
|
|
415
|
+
def deref(self, dest, src):
|
|
416
|
+
self.emit(f"mov {dest}, {self._mem_operand(src)}")
|
|
417
|
+
|
|
418
|
+
def addr(self, dest, src):
|
|
419
|
+
self.emit(f"lea {dest}, {self._mem_operand(src)}")
|
|
420
|
+
|
|
421
|
+
def label(self, name):
|
|
422
|
+
self.emit(f"{name}:")
|
|
423
|
+
|
|
424
|
+
def goto(self, target):
|
|
425
|
+
self.emit(f"jmp {target}")
|
|
426
|
+
|
|
427
|
+
def if_true(self, reg, target):
|
|
428
|
+
self.emit(f"test {reg}, {reg}")
|
|
429
|
+
self.emit(f"jnz {target}")
|
|
430
|
+
|
|
431
|
+
def if_false(self, reg, target):
|
|
432
|
+
self.emit(f"test {reg}, {reg}")
|
|
433
|
+
self.emit(f"jz {target}")
|
|
434
|
+
|
|
435
|
+
def loop(self, label_name):
|
|
436
|
+
self.emit(f"jmp {label_name}")
|
|
437
|
+
|
|
438
|
+
# --- bare-metal helpers (used by QEMURuntime) ---
|
|
439
|
+
|
|
440
|
+
def out_serial_char(self, char_reg="al"):
|
|
441
|
+
"""Write one byte (from the given 8-bit register) to the
|
|
442
|
+
standard PC serial port (COM1, 0x3F8), so QEMU's
|
|
443
|
+
-serial stdio can capture it as real program output."""
|
|
444
|
+
self.emit("mov dx, 0x3f8")
|
|
445
|
+
self.emit(f"out dx, {char_reg}")
|
|
446
|
+
|
|
447
|
+
def halt_forever(self):
|
|
448
|
+
"""Stop execution cleanly instead of falling into whatever
|
|
449
|
+
garbage bytes come after the program in memory."""
|
|
450
|
+
self.emit("cli")
|
|
451
|
+
self.label("__halt_loop")
|
|
452
|
+
self.emit("hlt")
|
|
453
|
+
self.emit("jmp __halt_loop")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class Memory:
|
|
457
|
+
def __init__(self):
|
|
458
|
+
self.vars = {}
|
|
459
|
+
self.stack_offset = 0
|
|
460
|
+
self.counter = 0
|
|
461
|
+
self.frame_size = 0
|
|
462
|
+
self.free_slots = []
|
|
463
|
+
|
|
464
|
+
def _align(self, value, alignment=8):
|
|
465
|
+
return ((value + alignment - 1) // alignment) * alignment
|
|
466
|
+
|
|
467
|
+
def alloc(self, name, size=8):
|
|
468
|
+
if name in self.vars:
|
|
469
|
+
return self.vars[name]["address"]
|
|
470
|
+
|
|
471
|
+
size = max(int(size), 8)
|
|
472
|
+
for idx, (slot_offset, slot_size) in enumerate(self.free_slots):
|
|
473
|
+
if slot_size >= size:
|
|
474
|
+
self.free_slots.pop(idx)
|
|
475
|
+
offset = slot_offset
|
|
476
|
+
self.vars[name] = {"address": f"[rbp-{offset}]", "size": size, "offset": offset}
|
|
477
|
+
self.frame_size = max(self.frame_size, offset)
|
|
478
|
+
return self.vars[name]["address"]
|
|
479
|
+
|
|
480
|
+
self.stack_offset = self._align(self.stack_offset + size, 8)
|
|
481
|
+
offset = self.stack_offset
|
|
482
|
+
self.frame_size = max(self.frame_size, offset)
|
|
483
|
+
self.vars[name] = {"address": f"[rbp-{offset}]", "size": size, "offset": offset}
|
|
484
|
+
return self.vars[name]["address"]
|
|
485
|
+
|
|
486
|
+
def free(self, name):
|
|
487
|
+
entry = self.vars.pop(name, None)
|
|
488
|
+
if entry is not None:
|
|
489
|
+
self.free_slots.append((entry["offset"], entry["size"]))
|
|
490
|
+
|
|
491
|
+
def get(self, name):
|
|
492
|
+
return self.vars[name]["address"]
|
|
493
|
+
|
|
494
|
+
def string(self, value):
|
|
495
|
+
return str(value)
|
|
496
|
+
|
|
497
|
+
def array(self, name, values):
|
|
498
|
+
return [self.alloc(f"{name}_{i}") for i in range(len(values))]
|
|
499
|
+
|
|
500
|
+
def pointer(self, name, target):
|
|
501
|
+
return self.alloc(name)
|
|
502
|
+
|
|
503
|
+
def struct(self, name, fields):
|
|
504
|
+
return {field: self.alloc(f"{name}_{field}") for field in fields}
|
|
505
|
+
|
|
506
|
+
def snapshot(self):
|
|
507
|
+
return {k: v["address"] for k, v in self.vars.items()}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class Compiler:
|
|
511
|
+
"""Assembles for the JIT (VirtualAlloc) path. Defaults to 64-bit,
|
|
512
|
+
matching the in-process Runtime below."""
|
|
513
|
+
|
|
514
|
+
def __init__(self, mode=KS_MODE_64):
|
|
515
|
+
self.ks = Ks(KS_ARCH_X86, mode)
|
|
516
|
+
self.last_asm = ""
|
|
517
|
+
self.last_machine_code = b""
|
|
518
|
+
|
|
519
|
+
def _validate(self, program):
|
|
520
|
+
for instruction in program.cpu.instructions:
|
|
521
|
+
if not instruction or instruction.endswith(":"):
|
|
522
|
+
continue
|
|
523
|
+
if instruction.startswith(("mov", "add", "sub", "cmp", "test", "and", "or", "xor", "shl", "shr", "sar", "rol", "ror")):
|
|
524
|
+
parts = [p.strip() for p in instruction.split(" ", 1)[1].split(",")]
|
|
525
|
+
if len(parts) != 2:
|
|
526
|
+
raise ValueError(f"Invalid operands for {instruction}")
|
|
527
|
+
left, right = parts
|
|
528
|
+
if left.startswith("[") and left.endswith("]"):
|
|
529
|
+
left = left[1:-1]
|
|
530
|
+
if right.startswith("[") and right.endswith("]"):
|
|
531
|
+
right = right[1:-1]
|
|
532
|
+
if left in {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}:
|
|
533
|
+
raise ValueError(f"Invalid destination operand for {instruction}")
|
|
534
|
+
if right in {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} and left.startswith(("[", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9")):
|
|
535
|
+
raise ValueError(f"Invalid immediate operand for {instruction}")
|
|
536
|
+
|
|
537
|
+
def compile(self, program):
|
|
538
|
+
self._validate(program)
|
|
539
|
+
self.last_asm = "\n".join(program.cpu.instructions)
|
|
540
|
+
encoding, _ = self.ks.asm(self.last_asm)
|
|
541
|
+
self.last_machine_code = bytes(encoding)
|
|
542
|
+
program.asm = self.last_asm
|
|
543
|
+
program.machine_code = self.last_machine_code
|
|
544
|
+
return self.last_machine_code
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class Runtime:
|
|
548
|
+
"""In-process JIT execution via VirtualAlloc (Windows only)."""
|
|
549
|
+
|
|
550
|
+
def __init__(self):
|
|
551
|
+
self.kernel32 = ctypes.windll.kernel32
|
|
552
|
+
self.kernel32.VirtualAlloc.restype = ctypes.c_void_p
|
|
553
|
+
self.MEM_COMMIT = 0x1000
|
|
554
|
+
self.MEM_RESERVE = 0x2000
|
|
555
|
+
self.PAGE_EXECUTE_READWRITE = 0x40
|
|
556
|
+
|
|
557
|
+
def execute(self, machine_code):
|
|
558
|
+
addr = self.kernel32.VirtualAlloc(
|
|
559
|
+
None,
|
|
560
|
+
len(machine_code),
|
|
561
|
+
self.MEM_COMMIT | self.MEM_RESERVE,
|
|
562
|
+
self.PAGE_EXECUTE_READWRITE,
|
|
563
|
+
)
|
|
564
|
+
ctypes.memmove(addr, machine_code, len(machine_code))
|
|
565
|
+
func = ctypes.CFUNCTYPE(ctypes.c_int)(addr)
|
|
566
|
+
return func()
|
|
567
|
+
|
|
568
|
+
def run(self, program):
|
|
569
|
+
program.compile()
|
|
570
|
+
return self.execute(program.machine_code)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class QEMURuntime:
|
|
574
|
+
"""
|
|
575
|
+
Boots machine code as a real bare-metal boot sector inside an
|
|
576
|
+
isolated QEMU virtual machine. No OS, no bootloader, no disk
|
|
577
|
+
image beyond the single 512-byte sector this class builds.
|
|
578
|
+
|
|
579
|
+
A crash, bad memory access, or infinite loop in your code is
|
|
580
|
+
contained entirely inside the QEMU subprocess -- your actual
|
|
581
|
+
Python process is never touched, unlike the in-process
|
|
582
|
+
VirtualAlloc Runtime above.
|
|
583
|
+
|
|
584
|
+
Runs your code in 32-bit protected mode by default. A fixed
|
|
585
|
+
16-bit real-mode stub handles the switch into protected mode
|
|
586
|
+
before jumping into your compiled bytes.
|
|
587
|
+
|
|
588
|
+
LIMITATION: this does not set up long mode (64-bit). Full
|
|
589
|
+
64-bit support needs paging (page tables + PAE + EFER.LME +
|
|
590
|
+
CR0.PG), which is real follow-up infrastructure, not a small
|
|
591
|
+
addition. Assemble your program body in 32-bit mode for this
|
|
592
|
+
runtime (use QEMURuntime.compile_32, not the 64-bit Compiler
|
|
593
|
+
above).
|
|
594
|
+
|
|
595
|
+
Output: there's no return value the way a JIT'd function has
|
|
596
|
+
one. Instead, write bytes to the serial port with
|
|
597
|
+
CPU.out_serial_char() -- QEMU's -serial stdio captures it as
|
|
598
|
+
real captured output from the guest machine.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
ORIGIN = 0x7C00
|
|
602
|
+
SECTOR_SIZE = 512
|
|
603
|
+
|
|
604
|
+
def __init__(self, timeout=5, qemu_binary="qemu-system-x86_64", auto_install=False):
|
|
605
|
+
self.timeout = timeout
|
|
606
|
+
self.qemu_binary = ensure_qemu_installed(qemu_binary, auto_install=auto_install)
|
|
607
|
+
self.last_output = ""
|
|
608
|
+
self.last_image_path = None
|
|
609
|
+
|
|
610
|
+
# ---- assembling the user's program body ----
|
|
611
|
+
|
|
612
|
+
def compile_32(self, program):
|
|
613
|
+
ks = Ks(KS_ARCH_X86, KS_MODE_32)
|
|
614
|
+
asm_text = "\n".join(program.cpu.instructions)
|
|
615
|
+
encoding, _ = ks.asm(asm_text)
|
|
616
|
+
machine_code = bytes(encoding)
|
|
617
|
+
program.asm = asm_text
|
|
618
|
+
program.machine_code = machine_code
|
|
619
|
+
return machine_code
|
|
620
|
+
|
|
621
|
+
# ---- GDT, built as raw bytes rather than via the assembler,
|
|
622
|
+
# since Keystone's directive/data-table support (dq/db, label
|
|
623
|
+
# arithmetic like `gdt_end - gdt_start`) isn't reliable enough
|
|
624
|
+
# to trust for a boot sector ----
|
|
625
|
+
|
|
626
|
+
def _build_flat_gdt(self):
|
|
627
|
+
null_entry = struct.pack("<Q", 0)
|
|
628
|
+
# base=0, limit=0xFFFFF, 4KB granularity, 32-bit, present, ring0
|
|
629
|
+
code_entry = struct.pack("<HHBBBB", 0xFFFF, 0x0000, 0x00, 0b10011010, 0b11001111, 0x00)
|
|
630
|
+
data_entry = struct.pack("<HHBBBB", 0xFFFF, 0x0000, 0x00, 0b10010010, 0b11001111, 0x00)
|
|
631
|
+
gdt = null_entry + code_entry + data_entry
|
|
632
|
+
return gdt, len(gdt) - 1
|
|
633
|
+
|
|
634
|
+
def _assemble_real_stub(self, gdt_descriptor_offset, pm_entry_offset):
|
|
635
|
+
ks16 = Ks(KS_ARCH_X86, KS_MODE_16)
|
|
636
|
+
asm = f"""
|
|
637
|
+
cli
|
|
638
|
+
xor ax, ax
|
|
639
|
+
mov ds, ax
|
|
640
|
+
mov es, ax
|
|
641
|
+
mov ss, ax
|
|
642
|
+
mov sp, 0x7c00
|
|
643
|
+
lgdt [0x7c00 + {gdt_descriptor_offset}]
|
|
644
|
+
mov eax, cr0
|
|
645
|
+
or eax, 1
|
|
646
|
+
mov cr0, eax
|
|
647
|
+
ljmp 0x08:0x7c00 + {pm_entry_offset}
|
|
648
|
+
"""
|
|
649
|
+
encoding, _ = ks16.asm(asm)
|
|
650
|
+
return bytes(encoding)
|
|
651
|
+
|
|
652
|
+
def _assemble_pm_entry(self):
|
|
653
|
+
ks32 = Ks(KS_ARCH_X86, KS_MODE_32)
|
|
654
|
+
asm = """
|
|
655
|
+
mov ax, 0x10
|
|
656
|
+
mov ds, ax
|
|
657
|
+
mov es, ax
|
|
658
|
+
mov fs, ax
|
|
659
|
+
mov gs, ax
|
|
660
|
+
mov ss, ax
|
|
661
|
+
mov esp, 0x90000
|
|
662
|
+
"""
|
|
663
|
+
encoding, _ = ks32.asm(asm)
|
|
664
|
+
return bytes(encoding)
|
|
665
|
+
|
|
666
|
+
def build_boot_sector(self, user_code):
|
|
667
|
+
"""
|
|
668
|
+
Lays out one 512-byte sector:
|
|
669
|
+
[0x7C00] 16-bit real-mode stub -> switches to protected mode
|
|
670
|
+
[...] flat GDT (3 entries: null, code, data)
|
|
671
|
+
[...] GDT descriptor (limit + linear base address)
|
|
672
|
+
[...] 32-bit protected-mode entry stub
|
|
673
|
+
[...] your compiled 32-bit user code
|
|
674
|
+
[0x7DFE] boot signature 0x55AA
|
|
675
|
+
"""
|
|
676
|
+
# pass 1: assemble the real-mode stub with placeholder offsets,
|
|
677
|
+
# purely to measure its length (Keystone has no multi-pass
|
|
678
|
+
# linker, so offsets are computed by hand here)
|
|
679
|
+
stub_probe = self._assemble_real_stub(0, 0)
|
|
680
|
+
stub_len = len(stub_probe)
|
|
681
|
+
|
|
682
|
+
gdt_bytes, gdt_limit = self._build_flat_gdt()
|
|
683
|
+
gdt_offset = stub_len
|
|
684
|
+
gdt_descriptor_offset = gdt_offset + len(gdt_bytes)
|
|
685
|
+
gdt_descriptor = struct.pack("<HI", gdt_limit, self.ORIGIN + gdt_offset)
|
|
686
|
+
pm_entry_offset = gdt_descriptor_offset + len(gdt_descriptor)
|
|
687
|
+
|
|
688
|
+
# pass 2: reassemble the stub now that real offsets are known
|
|
689
|
+
real_stub = self._assemble_real_stub(gdt_descriptor_offset, pm_entry_offset)
|
|
690
|
+
if len(real_stub) != stub_len:
|
|
691
|
+
raise RuntimeError(
|
|
692
|
+
"Real-mode stub changed size between assembly passes "
|
|
693
|
+
"(offset-dependent encoding shifted length) -- offsets "
|
|
694
|
+
"are no longer valid. This is an internal bug in "
|
|
695
|
+
"build_boot_sector, not your program."
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
pm_entry = self._assemble_pm_entry()
|
|
699
|
+
|
|
700
|
+
sector = bytearray(self.SECTOR_SIZE)
|
|
701
|
+
sector[0:len(real_stub)] = real_stub
|
|
702
|
+
sector[gdt_offset:gdt_offset + len(gdt_bytes)] = gdt_bytes
|
|
703
|
+
sector[gdt_descriptor_offset:gdt_descriptor_offset + len(gdt_descriptor)] = gdt_descriptor
|
|
704
|
+
sector[pm_entry_offset:pm_entry_offset + len(pm_entry)] = pm_entry
|
|
705
|
+
|
|
706
|
+
code_offset = pm_entry_offset + len(pm_entry)
|
|
707
|
+
end_offset = code_offset + len(user_code)
|
|
708
|
+
if end_offset > self.SECTOR_SIZE - 2:
|
|
709
|
+
raise ValueError(
|
|
710
|
+
f"Boot sector overflow: stub + GDT + entry + your code = "
|
|
711
|
+
f"{end_offset} bytes, max is {self.SECTOR_SIZE - 2}. "
|
|
712
|
+
f"Multi-sector loading isn't implemented yet -- shrink "
|
|
713
|
+
f"the program or ask to add sector-spanning support."
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
sector[code_offset:code_offset + len(user_code)] = user_code
|
|
717
|
+
sector[self.SECTOR_SIZE - 2:self.SECTOR_SIZE] = b"\x55\xAA"
|
|
718
|
+
return bytes(sector)
|
|
719
|
+
|
|
720
|
+
# ---- execution ----
|
|
721
|
+
|
|
722
|
+
def execute(self, machine_code):
|
|
723
|
+
image = self.build_boot_sector(machine_code)
|
|
724
|
+
|
|
725
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".img", delete=False)
|
|
726
|
+
tmp.write(image)
|
|
727
|
+
tmp.close()
|
|
728
|
+
self.last_image_path = tmp.name
|
|
729
|
+
|
|
730
|
+
cmd = [
|
|
731
|
+
self.qemu_binary,
|
|
732
|
+
"-drive", f"format=raw,file={tmp.name}",
|
|
733
|
+
"-nographic",
|
|
734
|
+
"-serial", "stdio",
|
|
735
|
+
"-no-reboot",
|
|
736
|
+
"-display", "none",
|
|
737
|
+
"-monitor", "none",
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
try:
|
|
741
|
+
result = subprocess.run(
|
|
742
|
+
cmd, capture_output=True, timeout=self.timeout, text=True
|
|
743
|
+
)
|
|
744
|
+
self.last_output = result.stdout
|
|
745
|
+
except subprocess.TimeoutExpired as e:
|
|
746
|
+
# Expected for a program that halts/loops forever (e.g. via
|
|
747
|
+
# halt_forever()) rather than triggering a triple-fault --
|
|
748
|
+
# QEMU just keeps running until this timeout kills it.
|
|
749
|
+
out = e.stdout
|
|
750
|
+
if isinstance(out, bytes):
|
|
751
|
+
out = out.decode(errors="replace")
|
|
752
|
+
self.last_output = out or ""
|
|
753
|
+
return self.last_output
|
|
754
|
+
|
|
755
|
+
def run(self, program):
|
|
756
|
+
machine_code = self.compile_32(program)
|
|
757
|
+
return self.execute(machine_code)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class Program:
|
|
761
|
+
def __init__(self, cpu=None, memory=None, compiler=None, runtime=None):
|
|
762
|
+
self.cpu = cpu or CPU()
|
|
763
|
+
self.memory = memory or Memory()
|
|
764
|
+
self.compiler = compiler or Compiler()
|
|
765
|
+
# lazy: Runtime() touches ctypes.windll (Windows-only), so it
|
|
766
|
+
# isn't constructed until .run() is actually called -- this
|
|
767
|
+
# keeps run_on_qemu() usable cross-platform without ever
|
|
768
|
+
# needing the JIT runtime at all.
|
|
769
|
+
self._runtime_override = runtime
|
|
770
|
+
self._runtime = None
|
|
771
|
+
self.asm = ""
|
|
772
|
+
self.machine_code = b""
|
|
773
|
+
self.result = None
|
|
774
|
+
|
|
775
|
+
def reset(self):
|
|
776
|
+
self.cpu.instructions = []
|
|
777
|
+
self.asm = ""
|
|
778
|
+
self.machine_code = b""
|
|
779
|
+
self.result = None
|
|
780
|
+
|
|
781
|
+
def _ensure_frame(self):
|
|
782
|
+
frame_size = max(0, self.memory.frame_size)
|
|
783
|
+
if frame_size <= 0:
|
|
784
|
+
return
|
|
785
|
+
aligned = max(0x20, ((frame_size + 15) // 16) * 16)
|
|
786
|
+
if any(instr.startswith("sub rsp") for instr in self.cpu.instructions):
|
|
787
|
+
return
|
|
788
|
+
if len(self.cpu.instructions) >= 2 and self.cpu.instructions[1] == "mov rbp, rsp":
|
|
789
|
+
self.cpu.instructions.insert(2, f"sub rsp, {aligned}")
|
|
790
|
+
|
|
791
|
+
def begin(self):
|
|
792
|
+
self.reset()
|
|
793
|
+
self.cpu.push("rbp")
|
|
794
|
+
self.cpu.mov("rbp", "rsp")
|
|
795
|
+
|
|
796
|
+
def end(self):
|
|
797
|
+
self.cpu.mov("rsp", "rbp")
|
|
798
|
+
self.cpu.pop("rbp")
|
|
799
|
+
self.cpu.ret()
|
|
800
|
+
|
|
801
|
+
def compile(self):
|
|
802
|
+
self._ensure_frame()
|
|
803
|
+
self.compiler.compile(self)
|
|
804
|
+
return self.machine_code
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def runtime(self):
|
|
808
|
+
if self._runtime is None:
|
|
809
|
+
self._runtime = self._runtime_override or Runtime()
|
|
810
|
+
return self._runtime
|
|
811
|
+
|
|
812
|
+
def run(self):
|
|
813
|
+
self.result = self.runtime.run(self)
|
|
814
|
+
return self.result
|
|
815
|
+
|
|
816
|
+
def run_on_qemu(self, timeout=5):
|
|
817
|
+
"""Run this program's instructions on a bare-metal QEMU VM
|
|
818
|
+
instead of the default in-process JIT runtime. Program bodies
|
|
819
|
+
for this path should be written in 32-bit terms (eax/ebx/...
|
|
820
|
+
registers), since QEMURuntime assembles in 32-bit mode."""
|
|
821
|
+
qemu_runtime = QEMURuntime(timeout=timeout)
|
|
822
|
+
self.result = qemu_runtime.run(self)
|
|
823
|
+
self.machine_code = getattr(self, "machine_code", b"")
|
|
824
|
+
return self.result
|
|
825
|
+
|
|
826
|
+
def __getattr__(self, name):
|
|
827
|
+
if hasattr(self.cpu, name):
|
|
828
|
+
return getattr(self.cpu, name)
|
|
829
|
+
if hasattr(self.memory, name):
|
|
830
|
+
return getattr(self.memory, name)
|
|
831
|
+
raise AttributeError(name)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class OS:
|
|
835
|
+
def boot(self, program):
|
|
836
|
+
return program.run()
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class Playground:
|
|
840
|
+
def __init__(self):
|
|
841
|
+
self.program = Program()
|
|
842
|
+
|
|
843
|
+
def run(self):
|
|
844
|
+
return self.program.run()
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class Debugger:
|
|
848
|
+
def inspect(self, program):
|
|
849
|
+
return {
|
|
850
|
+
"asm": program.asm,
|
|
851
|
+
"machine_code": program.machine_code.hex(),
|
|
852
|
+
"memory": program.memory.snapshot(),
|
|
853
|
+
"result": program.result,
|
|
854
|
+
}
|
pyliu-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Pyliu
|
|
2
|
+
|
|
3
|
+
Pyliu is a lightweight Python assembly experimentation framework for building, compiling, and executing simple assembly-like programs. It combines a small CPU instruction DSL, a compiler layer, and multiple runtime backends so you can explore low-level execution patterns from Python.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- A simple CPU instruction abstraction layer
|
|
8
|
+
- A compiler pipeline for assembling programs into machine code
|
|
9
|
+
- An in-process JIT-style runtime
|
|
10
|
+
- A bare-metal QEMU runtime for isolated execution
|
|
11
|
+
- An optional CLI for installing QEMU explicitly
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install from the project directory:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or, if you are using the local virtual environment:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
.venv\Scripts\python.exe -m pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Optional QEMU dependency
|
|
28
|
+
|
|
29
|
+
QEMU is treated as an optional dependency for Pyliu.
|
|
30
|
+
|
|
31
|
+
By default, Pyliu does not try to download or install anything automatically. If you want to use the QEMU-backed runtime, install it explicitly:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m Pyliu install-qemu
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or, if you are using the project virtual environment:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
.venv\Scripts\python.exe -m Pyliu install-qemu
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from Pyliu.verification import Program
|
|
47
|
+
|
|
48
|
+
program = Program()
|
|
49
|
+
program.begin()
|
|
50
|
+
program.mov("eax", "1")
|
|
51
|
+
program.ret()
|
|
52
|
+
program.end()
|
|
53
|
+
|
|
54
|
+
print(program.compile())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Using the QEMU runtime
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from Pyliu.verification import Program, QEMURuntime
|
|
61
|
+
|
|
62
|
+
program = Program()
|
|
63
|
+
program.begin()
|
|
64
|
+
program.mov("eax", "1")
|
|
65
|
+
program.ret()
|
|
66
|
+
program.end()
|
|
67
|
+
|
|
68
|
+
runtime = QEMURuntime(timeout=5)
|
|
69
|
+
print(runtime.run(program))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## CLI
|
|
73
|
+
|
|
74
|
+
Pyliu exposes a small CLI for optional QEMU installation:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python -m Pyliu --help
|
|
78
|
+
python -m Pyliu install-qemu
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Project structure
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
Pyliu/
|
|
85
|
+
├── __init__.py
|
|
86
|
+
├── __main__.py
|
|
87
|
+
├── verification.py
|
|
88
|
+
└── tests/
|
|
89
|
+
└── test_qemu_optional.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- The QEMU runtime is intended for experimentation and research-oriented workflows.
|
|
95
|
+
- The package is designed to remain safe and non-invasive for PyPI-style packaging by avoiding automatic system modifications during import or normal runtime use.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
This project is provided as-is for educational and research use.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyliu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight assembly experimentation framework for Python
|
|
5
|
+
Home-page: https://github.com/aa425-aarohcharne/pyliu
|
|
6
|
+
Author: Aaroh
|
|
7
|
+
Author-email: Aaroh <aaroh.charne@gmail.com>
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: keystone-engine
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: home-page
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# Pyliu
|
|
16
|
+
|
|
17
|
+
Pyliu is a lightweight Python assembly experimentation framework for building, compiling, and executing simple assembly-like programs. It combines a small CPU instruction DSL, a compiler layer, and multiple runtime backends so you can explore low-level execution patterns from Python.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- A simple CPU instruction abstraction layer
|
|
22
|
+
- A compiler pipeline for assembling programs into machine code
|
|
23
|
+
- An in-process JIT-style runtime
|
|
24
|
+
- A bare-metal QEMU runtime for isolated execution
|
|
25
|
+
- An optional CLI for installing QEMU explicitly
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install from the project directory:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or, if you are using the local virtual environment:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
.venv\Scripts\python.exe -m pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Optional QEMU dependency
|
|
42
|
+
|
|
43
|
+
QEMU is treated as an optional dependency for Pyliu.
|
|
44
|
+
|
|
45
|
+
By default, Pyliu does not try to download or install anything automatically. If you want to use the QEMU-backed runtime, install it explicitly:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m Pyliu install-qemu
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or, if you are using the project virtual environment:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
.venv\Scripts\python.exe -m Pyliu install-qemu
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from Pyliu.verification import Program
|
|
61
|
+
|
|
62
|
+
program = Program()
|
|
63
|
+
program.begin()
|
|
64
|
+
program.mov("eax", "1")
|
|
65
|
+
program.ret()
|
|
66
|
+
program.end()
|
|
67
|
+
|
|
68
|
+
print(program.compile())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Using the QEMU runtime
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from Pyliu.verification import Program, QEMURuntime
|
|
75
|
+
|
|
76
|
+
program = Program()
|
|
77
|
+
program.begin()
|
|
78
|
+
program.mov("eax", "1")
|
|
79
|
+
program.ret()
|
|
80
|
+
program.end()
|
|
81
|
+
|
|
82
|
+
runtime = QEMURuntime(timeout=5)
|
|
83
|
+
print(runtime.run(program))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## CLI
|
|
87
|
+
|
|
88
|
+
Pyliu exposes a small CLI for optional QEMU installation:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m Pyliu --help
|
|
92
|
+
python -m Pyliu install-qemu
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Project structure
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
Pyliu/
|
|
99
|
+
├── __init__.py
|
|
100
|
+
├── __main__.py
|
|
101
|
+
├── verification.py
|
|
102
|
+
└── tests/
|
|
103
|
+
└── test_qemu_optional.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Notes
|
|
107
|
+
|
|
108
|
+
- The QEMU runtime is intended for experimentation and research-oriented workflows.
|
|
109
|
+
- The package is designed to remain safe and non-invasive for PyPI-style packaging by avoiding automatic system modifications during import or normal runtime use.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is provided as-is for educational and research use.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
Pyliu/__init__.py
|
|
6
|
+
Pyliu/__main__.py
|
|
7
|
+
Pyliu/verification.py
|
|
8
|
+
pyliu.egg-info/PKG-INFO
|
|
9
|
+
pyliu.egg-info/SOURCES.txt
|
|
10
|
+
pyliu.egg-info/dependency_links.txt
|
|
11
|
+
pyliu.egg-info/entry_points.txt
|
|
12
|
+
pyliu.egg-info/requires.txt
|
|
13
|
+
pyliu.egg-info/top_level.txt
|
|
14
|
+
tests/test_qemu_optional.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keystone-engine
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pyliu
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyliu"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight assembly experimentation framework for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["keystone-engine"]
|
|
12
|
+
authors = [{ name = "Aaroh", email = "aaroh.charne@gmail.com" }]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
pyliu = "Pyliu.__main__:main"
|
pyliu-0.1.0/setup.cfg
ADDED
pyliu-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='pyliu',
|
|
5
|
+
version='0.1.0',
|
|
6
|
+
description='A lightweight assembly experimentation framework for Python',
|
|
7
|
+
long_description=open('README.md', encoding='utf-8').read(),
|
|
8
|
+
long_description_content_type='text/markdown',
|
|
9
|
+
author='Aaroh',
|
|
10
|
+
author_email='aaroh.charne@gmail.com',
|
|
11
|
+
url='https://github.com/aa425-aarohcharne/pyliu',
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
'keystone-engine',
|
|
15
|
+
],
|
|
16
|
+
classifiers=[
|
|
17
|
+
'Programming Language :: Python :: 3',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Operating System :: OS Independent',
|
|
20
|
+
'Development Status :: 3 - Alpha',
|
|
21
|
+
'Intended Audience :: Developers',
|
|
22
|
+
'Natural Language :: English',
|
|
23
|
+
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
24
|
+
],
|
|
25
|
+
python_requires='>=3.9',
|
|
26
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
8
|
+
|
|
9
|
+
from Pyliu import verification
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_qemu_install_is_not_triggered_on_import():
|
|
13
|
+
assert verification.ensure_qemu_installed.__name__ == "ensure_qemu_installed"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_qemu_auto_install_is_disabled_by_default():
|
|
17
|
+
params = inspect.signature(verification.ensure_qemu_installed).parameters
|
|
18
|
+
assert params["auto_install"].default is False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_qemu_runtime_requires_explicit_install():
|
|
22
|
+
with pytest.raises(RuntimeError):
|
|
23
|
+
verification.ensure_qemu_installed(qemu_binary="qemu-system-x86_64", auto_install=False)
|