Skip to content

Commit 1e0df0b

Browse files
authored
Merge pull request #1662 from xwings/dev
refine ARCHITECTURE file
2 parents 19d33b6 + f912b7f commit 1e0df0b

13 files changed

Lines changed: 267 additions & 298 deletions

File tree

ARCHITECTURE.md

Lines changed: 171 additions & 221 deletions
Large diffs are not rendered by default.

ARCHITECTURE/arch.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Arch — CPU architecture layer
@@ -14,9 +14,9 @@ exceptions below). No roadmap milestone applies; maturity-based status.
1414

1515
## Status
1616

17-
`done` — all ten architectures are exercised by the CI suites; CPU model
18-
selection is covered by `tests/test_cpu_models.py` (observed:
19-
`Ran 7 tests … OK`).
17+
`done` — all ten architectures are exercised by the CI suites; the CPU
18+
model enums are checked against Unicorn's constants by
19+
`tests/test_cpu_models.py` (observed: `Ran 7 tests … OK`).
2020

2121
## Code Structure
2222

@@ -52,10 +52,12 @@ Python; root rules apply. Local patterns:
5252

5353
## Design and Invariants
5454

55-
- `QlArch` creates the `Uc` lazily as a cached property (`qiling/arch/arch.py:34`)
56-
and exposes `regs` (`:42`), `stack_push/stack_pop` (`:52`/`:66`),
57-
`save/restore` via `UcContext` (`:108`/`:112`), `disassembler` (`:117`),
58-
and `assembler` (`:125`). Everything above arch must go through these.
55+
- `QlArch` declares `uc` as an abstract property that each concrete arch
56+
builds lazily as a `cached_property` (`qiling/arch/x86.py:27`); the base
57+
(`qiling/arch/arch.py:32-34`) exposes `regs` (`:42`),
58+
`stack_push/stack_pop` (`:52`/`:66`), `save/restore` via `UcContext`
59+
(`:108`/`:112`), `disassembler` (`:117`), and `assembler` (`:125`).
60+
Everything above arch must go through these.
5961
- `ql.uc` is a proxy to `arch.uc` (`qiling/core.py:479`); there is exactly
6062
one Unicorn instance per `Qiling` (the multi-Unicorn threading idea in
6163
`TODO.md:462-488` is a proposal only).
@@ -66,9 +68,10 @@ Python; root rules apply. Local patterns:
6668
base for the GDT manager's constructor annotation. Do not add further
6769
upward imports; see [os-baremetal.md](os-baremetal.md) for the multitask
6870
contract.
69-
- CPU models are selected by the `cputype` kwarg and validated by
70-
`select_arch`; a model belongs to exactly one enum in
71-
`qiling/arch/models.py`.
71+
- CPU models come in through the `cputype` kwarg; `select_arch` forwards
72+
the value unvalidated (`qiling/utils.py:377`) and the arch class applies
73+
it with `ctl_set_cpu_model` (`qiling/arch/arm.py:47`); a model belongs
74+
to exactly one enum in `qiling/arch/models.py`.
7275
- Endianness and thumb are constructor inputs for ARM/MIPS only
7376
(`qiling/utils.py:379-386`).
7477

ARCHITECTURE/cli.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# CLI — qltool and qltui
@@ -44,8 +44,8 @@ argparse actions mapping lowercase names to `QL_ARCH`/`QL_OS`/`QL_ENDIAN`/
4444
`--arch`, `--os`, `--endian`, `--thumb`, `--format asm|hex|bin`),
4545
`examples`, `qltui` (`qiling/cli.py:196-217`); common flags cover
4646
verbosity, `--env` (pickled dict), `--gdb`, `--qdb`, `--rr`,
47-
`--profile`, `--filter`, `--log-file`, `--log-plain`, `--root`,
48-
`--debug-stop`, `--multithread`, `--timeout`, `--coverage-file`,
47+
`--profile`, `--filter`, `--log-file`, `--log-plain`, `--no-console`,
48+
`--root`, `--debug-stop`, `--multithread`, `--timeout`, `--coverage-file`,
4949
`--coverage-format`, `--json`, `--libcache` (`:225-242`).
5050
- `handle_run`/`handle_code` return the kwargs dict; `Qiling(**ql_args)`
5151
at `qiling/cli.py:276` is the single construction point, followed by

ARCHITECTURE/core.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Core — the Qiling facade and plumbing
@@ -42,7 +42,7 @@ Python only; root toolchain and style rules apply (see
4242
patterns to follow:
4343

4444
- Circular-import avoidance with `TYPE_CHECKING` guards and string
45-
annotations (`qiling/core.py:1-33`, `qiling/log.py:15-19`).
45+
annotations (`qiling/core.py:1-33`, `qiling/log.py:19-20`).
4646
- Components are reached through read-only properties (`mem`, `arch`,
4747
`loader`, `os`, `hw`, `log`; `qiling/core.py:203-244`); setters exist only
4848
for `verbose`, `debugger`, `filter`, `debug_stop`.
@@ -70,12 +70,13 @@ patterns to follow:
7070
`QL_HOOK_BLOCK` set (`qiling/const.py:77`) stops remaining hooks
7171
(`qiling/core_hooks.py:186`, `:209`). Address hooks are keyed per address
7272
(`hook_address`, `:550`). `begin=1, end=0` means "whole address space".
73-
- **Exceptions raised inside hooks** are captured by the OS layer into
74-
`ql.internal_exception` and re-raised after `uc.emu_start` returns
73+
- **Exceptions raised inside hooks** are captured by the hook wrapper
74+
(`qiling/core_hooks.py:141-144`) into `ql._internal_exception`, which
75+
`emu_start` resets beforehand and re-raises after `uc.emu_start` returns
7576
(`qiling/core.py:763`, `:773-774`), because Unicorn cannot propagate
7677
Python exceptions through its C callbacks.
7778
- **Emulation state** is tracked in `QL_STATE` (`qiling/const.py:67`) around
78-
`emu_start` (`qiling/core.py:768-771`); `QlOs.call` refuses to move `pc`
79+
`emu_start` (`qiling/core.py:765-770`); `QlOs.call` refuses to move `pc`
7980
once stopped to work around a Unicorn bug (`qiling/os/os.py:215-221`).
8081
- **Stop guard**: when `stop=QL_STOP.*` is requested, a trap page is mapped
8182
at or above `0x9000000` (`qiling/core.py:525`) and the loader's

ARCHITECTURE/debugger.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Debugger — GDB server and Qdb
@@ -67,8 +67,10 @@ Python; root rules apply. Local patterns:
6767
(`qiling/debugger/gdb/xmlregs.py:56-137`, `qiling/debugger/gdb/gdb.py:488`).
6868
- **Qdb** installs a breakpoint hook and drives `ql.emu_start` per
6969
step; branch predictors compute the next pc for `step_over`; `rr` mode
70-
snapshots full state per step in `SnapshotManager`
71-
(`qiling/debugger/qdb/utils.py:260`) for `do_backward`.
70+
takes a full `ql.save()` before and after each stepping command and
71+
keeps only the diff as a `SnapshotManager` layer
72+
(`qiling/debugger/qdb/utils.py:260`, decorator `:275-297`) for
73+
`do_backward`.
7274
- **State access** goes through `ql.arch.regs`, `ql.mem`, and
7375
`hook_address`; the one concrete-type import is `QlProcFS`, used to
7476
serve `/proc/self/maps` to the client (`qiling/debugger/gdb/gdb.py:38`,
@@ -138,5 +140,7 @@ cd tests && python3 test_qdb.py # pass = "Ran 6 tests … OK", exit 0
138140

139141
- Qdb has no PowerPC support (`qiling/debugger/qdb/arch/`); GDB XML covers
140142
all ten arches.
141-
- Record/replay stores full state per step; memory-heavy on long runs.
143+
- Record/replay keeps one diff layer per step but performs two full
144+
`ql.save()` calls per step (`qiling/debugger/qdb/utils.py:273`); slow
145+
on long runs.
142146
- `TODO.md:628-636` lists bare `except:` blocks in `qiling/debugger/qdb/qdb.py`.

ARCHITECTURE/extensions.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Extensions — optional tooling on top of the core
@@ -17,10 +17,10 @@ roadmap milestone applies; maturity-based status.
1717

1818
## Status
1919

20-
`done` — the history coverage tracker is tested in CI (observed:
21-
`Ran 4 tests … OK`); pipes are exercised by many suites; AFL and r2 need
22-
optional extras (`fuzz`, `RE`) and are untested here; the IDA plugin needs
23-
IDA Pro and is untested in CI.
20+
`done` — the history coverage tracker has its own suite, run manually
21+
rather than by CI (observed: `Ran 4 tests … OK`); pipes are exercised by
22+
many suites; AFL and r2 need optional extras (`fuzz`, `RE`) and are
23+
untested here; the IDA plugin needs IDA Pro and is untested in CI.
2424

2525
## Code Structure
2626

@@ -54,15 +54,15 @@ file with IDA-specific conventions; treat it as its own style domain.
5454
- Everything here consumes only public APIs of [core.md](core.md) (hooks,
5555
`mem`, `arch.regs`, `save/restore`) and [os-base.md](os-base.md) (heap,
5656
stdio, `set_syscall`/`set_api`). Nothing in `qiling/os`, `qiling/loader`,
57-
or `qiling/arch` may import from here except the documented cases:
58-
`qiling/cli.py:22-23` for coverage/report, `qiling/arch/utils.py:94`
59-
lazy r2, and the multitask/mcu owners noted above.
57+
or `qiling/arch` may import from here except the exceptions listed under
58+
"Downward imports only" in the root System Design section.
6059
- `collect_coverage` activates hooks on enter and dumps on exit even on
6160
exceptions (`qiling/extensions/coverage/utils.py:48-63`); `qltool
6261
--coverage-format` lists `factory.formats` (`qiling/cli.py:240`).
6362
- The heap sanitizer replaces `ql.os.heap` with a compatible object that
64-
surrounds chunks with canaries and detects double-free/UAF; it hooks
65-
memory access to report faults (`qiling/extensions/sanitizers/heap.py:29`).
63+
surrounds chunks with canaries and detects double-free/UAF; `alloc` and
64+
`free` install memory hooks on the canaries to report faults
65+
(`qiling/extensions/sanitizers/heap.py:97-107`, `:125`).
6666
- `pipe.py` streams implement the `ql_file` surface expected by the fd
6767
table so they can be assigned to `ql.os.stdin/stdout/stderr`.
6868
- AFL: `ql_afl_fuzz` wraps `unicornafl.uc_afl_fuzz` with input placement,
@@ -98,8 +98,9 @@ file with IDA-specific conventions; treat it as its own style domain.
9898
tenda_ac15,dlink_dir815,rt_n12_b1}` pair AFL with `pipe.py` and
9999
`set_syscall` ([os-posix.md](os-posix.md)) or MCU mode
100100
([os-baremetal.md](os-baremetal.md)).
101-
- `pipe.py` is used by `tests/test_pe.py`, `test_windows_stdio.py`,
102-
`test_riscv.py`, `test_windows_cpp_x86.py`, and `test_kernel_proxy.py`.
101+
- `pipe.py` is used by `tests/test_elf.py`, `test_pe.py`,
102+
`test_windows_stdio.py`, `test_riscv.py`, `test_windows_cpp_x86.py`,
103+
`test_windows_cpp_x8664.py`, and `test_kernel_proxy.py`.
103104
- The IDA plugin offers a front end comparable to [debugger.md](debugger.md).
104105

105106
## How to Test

ARCHITECTURE/hw.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# HW — peripheral emulation for bare-metal targets
@@ -17,8 +17,8 @@ No roadmap milestone applies; maturity-based status.
1717
## Status
1818

1919
`done` — exercised by `tests/test_mcu.py` (observed: `Ran 18 tests … OK`)
20-
across GPIO, USART, EXTI, I2C, SPI, DMA, CRC, RTC, timers, and NVIC on
21-
STM32F103/F407/F411/F429, GD32VF103, and SAM3X8E firmware.
20+
across GPIO, USART, EXTI, I2C, SPI, DMA, CRC, ADC, watchdog, timers, and
21+
NVIC on STM32F103/F407/F411/F429, GD32VF103, and SAM3X8E firmware.
2222

2323
## Code Structure
2424

@@ -40,10 +40,10 @@ Python; root rules apply. Local patterns (observed, enforced by
4040
- A peripheral is a class whose inner `Type(ctypes.Structure)` lists
4141
registers in datasheet order with offset comments
4242
(`qiling/hw/char/stm32f4xx_usart.py:12-49`); `__init__(ql, label,
43-
**kwargs)` builds `self.instance` with reset values (`:52-58`).
43+
**kwargs)` builds `self.instance` with reset values (`:51-58`).
4444
- Register access overrides `read/write` decorated with
4545
`@QlPeripheral.monitor()` and falls back to `raw_read/raw_write`
46-
(`:60-70`, `qiling/hw/peripheral.py:162-172`).
46+
(`:60-70`, `qiling/hw/peripheral.py:157-173`).
4747
- Class names are looked up by `struct` string through
4848
`ql_get_module_function('qiling.hw', struct)` (`qiling/hw/hw.py:81`), so
4949
every peripheral must be exported from `qiling/hw/__init__.py`.
@@ -70,8 +70,10 @@ Python; root rules apply. Local patterns (observed, enforced by
7070
- **User hooks**: `hook_read/hook_write` with `QL_INTERCEPT` stages
7171
(`qiling/hw/peripheral.py:34-46`); `watch()` enables verbose access logs.
7272
- **Snapshots**: `QlHwManager.save/restore` (`qiling/hw/hw.py:165-171`)
73-
pickle peripheral instances; `QlPripheralHandler.__getstate__` strips
74-
the manager reference (`:24`).
73+
serialize each peripheral's ctypes register struct to bytes and back
74+
(`qiling/hw/peripheral.py:258-262`); `QlPripheralHandler.__getstate__`
75+
(`qiling/hw/hw.py:24`) strips the manager reference so the MMIO handler
76+
can be pickled with the memory map.
7577
- Peripheral fidelity is demand-driven: registers behave as observed
7678
firmware needs, not per full datasheets (root deviations).
7779

ARCHITECTURE/kernel-proxy.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Kernel proxy — hybrid syscall forwarding to a real Linux kernel
@@ -48,7 +48,7 @@ through the standard `logging` module, not `ql.log`, because it has no
4848

4949
- **Integration is only `set_syscall`**: `forward_syscall` registers a
5050
CALL hook named `ql_syscall_<name>`
51-
(`qiling/os/posix/kernel_proxy/__init__.py:136-166`, `:211`), so
51+
(`qiling/os/posix/kernel_proxy/__init__.py:136-166`, `:210`), so
5252
user ENTER/EXIT hooks still fire around it and a later user CALL hook
5353
overrides it (`tests/test_kernel_proxy.py:186-257`).
5454
- **Two syscall tables**: guest numbers come from the guest arch table and
@@ -116,9 +116,10 @@ through the standard `logging` module, not `ql.log`, because it has no
116116
cd tests && python3 test_kernel_proxy.py # Linux host; currently "Ran 21 tests … FAILED (errors=1)"
117117
```
118118

119-
- Expected after the Open Gaps fix: `Ran 21 tests … OK`. Every case builds
120-
a `Qiling` on `examples/rootfs/x8664_linux/bin/x8664_hello` and invokes
121-
the registered hook directly.
119+
- Expected after the Open Gaps fix: `Ran 21 tests … OK`. All cases except
120+
the pure unit tests `test_ipc_roundtrip` and `test_ptr_size_callable`
121+
build a `Qiling` on `examples/rootfs/x8664_linux/bin/x8664_hello` and
122+
invoke the registered hook directly.
122123
- Non-Linux hosts skip the whole class (`tests/test_kernel_proxy.py:17`).
123124
- No end-to-end test runs a guest binary that actually issues a forwarded
124125
syscall (`TODO.md:251-259` lists the intended validation).
@@ -130,7 +131,7 @@ cd tests && python3 test_kernel_proxy.py # Linux host; currently "Ran 21 tests
130131
instead, and add a message type to `ipc.py` if the wire format grows.
131132
- **Wire-format changes** must update both `ProxyClient` and
132133
`ProxyServer`, and the format comment at
133-
`qiling/os/posix/kernel_proxy/ipc.py:40-52`.
134+
`qiling/os/posix/kernel_proxy/ipc.py:40-53`.
134135
- **Security review** focuses on `_collect_buffers`/`_writeback_buffers`
135136
sizes (guest-controlled via `PtrOut(size=callable)`) and on which
136137
syscalls a harness chooses to forward; there is no allow-list.

ARCHITECTURE/loader.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# Loader — binary format loaders
@@ -43,9 +43,10 @@ Python; root rules apply. Local patterns:
4343
segments are mapped through `ql.mem.map` with an `info` label naming the
4444
image so `QlMemoryManager.get_lib_base` can find it by basename
4545
(`qiling/os/memory.py:254`).
46-
- Loaders expose `entry_point`, `exit_point`/OS exit point, `images`, and
47-
`skip_exit_check` (`qiling/loader/loader.py:27`); the ELF loader also sets
48-
`elf_entry` and `is_driver` (`qiling/loader/elf.py:121`).
46+
- The base loader owns `images` and `skip_exit_check`
47+
(`qiling/loader/loader.py:26-27`); concrete loaders set `entry_point`,
48+
and the exit point lives on `ql.os`. The ELF loader also sets
49+
`is_driver` (`qiling/loader/elf.py:121`) and `elf_entry` (`:221`).
4950

5051
## Design and Invariants
5152

ARCHITECTURE/os-baremetal.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
eatmycode_version: "1.1.0"
2+
eatmycode_version: "1.2.0"
33
---
44

55
# OS bare-metal — MCU firmware and raw blobs
@@ -18,7 +18,9 @@ applies; maturity-based status.
1818

1919
`done` — MCU covered by `tests/test_mcu.py` (observed: `Ran 18 tests … OK`,
2020
STM32F1/F4, GD32VF103, SAM3X8E firmware); BLOB by
21-
`test_blob.BlobTest.test_uboot_arm` (`OK`) and `tests/test_edl.py` (`OK`).
21+
`test_blob.BlobTest.test_uboot_arm` (`OK`). `tests/test_edl.py` is not
22+
BLOB coverage: its ELF is sniffed as Linux and runs under `QlOsLinux`
23+
([os-posix.md](os-posix.md)).
2224
`test_blob.BlobTest.test_blob_raw` errors on the pinned rootfs submodule
2325
(see Open Gaps).
2426

@@ -53,8 +55,10 @@ memory manager.
5355
installed by the MCU loader (`qiling/loader/mcu.py:140`).
5456
- **Effective PC**: run loops read `arch.effective_pc` when present to
5557
keep the thumb bit consistent (`qiling/os/mcu/mcu.py:54-58`).
56-
- **BLOB**: `QlOsBlob.run` executes `entry_point``exit_point` with the
57-
standard `emu_start` and nothing else (`qiling/os/blob/blob.py:43`).
58+
- **BLOB**: `QlOsBlob.run` resolves entry/exit overrides, creates a heap
59+
only if the profile `[CODE]` section defines `heap_address`/`heap_size`,
60+
then runs `entry_point``exit_point` with the standard `emu_start`
61+
(`qiling/os/blob/blob.py:43-60`).
5862
- `ql.hw` exists only when `ql.baremetal` (`qiling/core.py:191`,
5963
`:357`); BLOB targets needing hardware must use MCU mode.
6064

@@ -79,7 +83,8 @@ memory manager.
7983
`qiling/utils.py:419`).
8084
- Interrupt entry/exit uses [arch.md](arch.md) `QlArchCORTEX_M`; the
8185
Cortex-M arch constructs a `MultiTaskUnicorn` instead of a plain `Uc`
82-
(`qiling/arch/cortex_m.py:22`), the one upward import in the arch layer.
86+
(`qiling/arch/cortex_m.py:78`; the import at `:22` is one of the three
87+
documented upward imports in the arch layer, see [arch.md](arch.md)).
8388
- Fuzzing MCU firmware (`examples/fuzzing/stm32f429/`) combines this mode
8489
with [extensions.md](extensions.md) AFL support.
8590
- Snapshots include hardware state when `save(hw=True)`
@@ -93,8 +98,6 @@ cd tests && python3 -m unittest test_blob.BlobTest.test_uboot_arm # pass = "OK
9398

9499
- MCU (also proves [hw.md](hw.md)): `cd tests && python3 test_mcu.py`
95100
— pass = `Ran 18 tests … OK`.
96-
- Qualcomm EDL blob: `cd tests && python3 test_edl.py` — pass =
97-
`Ran 1 test … OK`.
98101
- `python3 test_blob.py` as a whole fails on a clean checkout; see Open Gaps.
99102

100103
## Review and Refactor Guide

0 commit comments

Comments
 (0)