Merge pull request #1658 from qilingframework/dev

Dev
This commit is contained in:
xwings
2026-09-05 21:35:58 +08:00
committed by GitHub
52 changed files with 2557 additions and 810 deletions
+41 -12
View File
@@ -1,44 +1,73 @@
name: PyPI 📦 Distribution
on: [push]
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: "^3.8"
python-version: "3.13"
- name: Install dependencies
run: pipx install poetry
run: python -m pip install "poetry>=2,<3" build twine
- name: Check package metadata
run: poetry check --lock
- name: Check release tag matches package version
if: startsWith(github.ref, 'refs/tags/')
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
python - <<'PY'
import os
import tomllib
from pathlib import Path
from packaging.version import Version
tag = os.environ['RELEASE_TAG'].removeprefix('v')
version = tomllib.loads(Path('pyproject.toml').read_text())['tool']['poetry']['version']
if Version(tag) != Version(version):
raise SystemExit(f'Release tag {tag!r} does not match package version {version!r}')
PY
- name: Build distribution 📦
run: poetry build -n
run: python -m build
- name: Check distribution metadata
run: python -m twine check --strict dist/*
- name: Test installed wheel
run: |
python -m pip install dist/*.whl
python -I tests/test_qltool.py InstalledQltool_Test -v
- uses: actions/upload-artifact@v4
with:
name: distributions
path: ${{ github.workspace }}/dist/*
if-no-files-found: error
publish:
needs: [build]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags')
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v4
with:
name: artifact
name: distributions
path: dist
- name: Publish distribution 📦 to test PyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.testpypi_pass }}
repository_url: https://test.pypi.org/legacy/
repository-url: https://test.pypi.org/legacy/
skip-existing: true
- name: Publish distribution 📦 to PyPI
if: ${{ success() }}
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.pypi_pass }}
Symlink
+1
View File
@@ -0,0 +1 @@
ARCHITECTURE.md
+375 -422
View File
@@ -1,427 +1,380 @@
# Qiling Framework Architecture
# Qiling Framework — Architecture
Qiling is a cross-platform, multi-architecture binary emulation framework built on top of the [Unicorn](https://www.unicorn-engine.org/) CPU emulation engine. It adds OS-level abstractions (syscalls, file systems, loaders) on top of raw CPU emulation, enabling full binary execution without native hardware.
This is the control center for agent-readable architecture docs. Cross-cutting
facts live here; each subsystem is documented once in `ARCHITECTURE/<module>.md`
(see the [Index](#index)).
## High-Level Overview
## Mission
```
┌──────────────────────────────────────────────────┐
│ User Script / qltool │
├──────────────────────────────────────────────────┤
│ Qiling Core (core.py) │
│ hooks · state snapshots · patches │
├────────────┬─────────────┬───────────────────────┤
│ OS Layer │ Loader │ Memory Manager │
│ (QlOs) │ (QlLoader) │ (QlMemoryManager) │
│ syscalls │ ELF/PE/ │ map · read · write │
│ APIs │ MachO/etc │ MMIO callbacks │
├────────────┴─────────────┴───────────────────────┤
│ Architecture Layer (QlArch) │
│ registers · disassembly · calling conventions │
├──────────────────────────────────────────────────┤
│ Unicorn Engine (CPU) │
│ instruction-level emulation │
└──────────────────────────────────────────────────┘
Qiling is an advanced binary emulation framework: it emulates and sandboxes
code in an isolated environment across multiple platforms and architectures.
Built on top of Unicorn Engine, it adds what raw CPU emulation lacks —
operating-system context (syscalls, APIs, filesystems, registries), executable
format loading, and dynamic linking (`README.md:13`).
Supported combinations are defined authoritatively in code:
- Architectures — `QL_ARCH` (`qiling/const.py:15`): 8086, x86, x86-64, ARM,
ARM64, Cortex-M, MIPS, RISC-V 32/64, PowerPC.
- Operating systems — `QL_OS` (`qiling/const.py:28`): Linux, FreeBSD, macOS,
Windows, UEFI, DOS, QNX, MCU (bare-metal), BLOB.
- Formats: ELF, PE, Mach-O, COM/MBR, Intel HEX / raw firmware. Kernel-module
emulation for Windows `.sys`, Linux `.ko`, macOS `.kext`.
Headline capabilities: fine-grained instrumentation hooks (instruction, basic
block, memory access, interrupt, syscall/API), VM state save/restore, dynamic
hot patching, cross-architecture debugging (GDB server and the built-in Qdb
with reverse debugging), and fuzzing integration (AFL++/unicornafl).
## Target Environment
- **Shape**: pure-Python library (`from qiling import Qiling`), plus the
`qltool` CLI and `qltui.py` TUI. Also shipped as a Docker image
(`Dockerfile`) and PyPI package.
- **Python**: 3.10+ (`pyproject.toml:37`). Packaging via Poetry; version
1.4.11.dev0 (`pyproject.toml:4`), status Beta, license GPL-2.0-or-later.
- **Core dependencies**: `unicorn ==2.1.3` (hard-pinned CPU emulator,
`pyproject.toml:39`), `capstone` (disassembly), `keystone-engine`
(assembly), `pefile`, `pyelftools`, `python-registry`, `gevent`
(multithread emulation), `pyyaml` (MCU profiles). Extras: `fuzz` →
unicornafl/fuzzercorn, `RE` → r2libr.
- **Hosts**: Linux, Windows, macOS. Some test suites are host-gated (Windows
PE tests need real system DLLs collected on Windows; see
[os-windows.md](ARCHITECTURE/os-windows.md)).
- **Fixtures**: `examples/rootfs/` is a git submodule
(https://github.com/qilingframework/rootfs.git) holding target binaries for
tests and examples.
## Workspace Layout
| Path | Holds |
| ---- | ----- |
| `qiling/` | The framework package (see Index for per-subsystem docs) |
| `qiling/profiles/` | Default per-OS config profiles (`linux.ql`, `windows.ql`, …) |
| `examples/` | Curated demo scripts, `fuzzing/`, `mcu/`, `shellcodes/`, `extensions/`, `scripts/` (DLL collectors), and the `rootfs/` submodule |
| `jexamples/` | Legacy example set (not covered by CI) |
| `tests/` | CI test suite — standalone `unittest` files run from `tests/` (`tests/test_onlinux.sh`, `tests/test_pe.bat`, `tests/test_macho.sh`) |
| `docs/` | Stubs and assets; real documentation lives at https://docs.qiling.io |
| `qltool` / `qiling/cli.py` | Checkout launcher / installed CLI implementation (see [cli.md](ARCHITECTURE/cli.md)) |
| `qltui.py` | Terminal UI invoked via `qltool qltui` |
| `Dockerfile` | Multi-stage Poetry wheel build on `python:3-slim` |
| `pyproject.toml` / `poetry.lock` | Packaging and dependency pins |
| `.github/workflows/` | CI (`build-ci.yml`: Windows + Ubuntu × Python 3.11/3.13, plus a Docker container job), Docker publish, PyPI publish, Gitee mirror sync |
## Boot / Entry Flow
From CLI to emulated instructions (details live in the module docs):
1. `qltool` calls `qiling.cli.run`, which parses args and builds kwargs;
`run` and `code` subcommands end in `Qiling(**ql_args)`
(`qiling/cli.py:276`) — see [cli.md](ARCHITECTURE/cli.md).
2. `Qiling.__init__` (`qiling/core.py:35`) is the composition root. Order:
guess arch/OS from the binary if not given → instantiate arch → init
struct/hook mixins → logger → profile → loader → memory manager → OS layer
→ hardware manager (bare-metal only) → `loader.run()` maps the target into
emulated memory (`qiling/core.py:154-195`). All name→class resolution is in
`qiling/utils.py` — see [core.md](ARCHITECTURE/core.md).
3. `Qiling.run()` (`qiling/core.py:561`) attaches the debugger if configured,
applies queued patches, writes the exit trap, then delegates to
`os.run()` — see [debugger.md](ARCHITECTURE/debugger.md) and the OS docs.
4. The OS layer drives `Qiling.emu_start` (`qiling/core.py:743`), the thin
wrapper over Unicorn's `uc.emu_start`. Syscalls/APIs raised by the emulated
code are dispatched back into the OS layer
([os-posix.md](ARCHITECTURE/os-posix.md),
[os-windows.md](ARCHITECTURE/os-windows.md),
[os-baremetal.md](ARCHITECTURE/os-baremetal.md)).
## Roadmap
Maturity-based — Qiling is a released project in maintenance/beta
(v1.4.11.dev0). There are no in-repo milestones; module Status is `done` when
its test suite proves it. Feature requests and the forward-looking wishlist are tracked in
GitHub issue [#333](https://github.com/qilingframework/qiling/issues/333)
(the `TODO` file is a pointer to it). Known cross-cutting gaps:
- `ChangeLog` stops at 1.4.6 (`ChangeLog:4`) while `pyproject.toml:4` says
1.4.11.dev0.
- macOS is dropped from the CI matrix and the macOS/kext job is commented out
(`.github/workflows/build-ci.yml:12`, `:84`).
- `jexamples/` is legacy and unexercised by CI.
## Packaging and Releases
`.github/workflows/pythonpublish.yml` validates metadata and the lock file,
builds a source distribution and a wheel from it, checks both with Twine,
and runs `InstalledQltool_Test` against the installed wheel outside the
checkout. Builds run on pushes and pull requests; publishing runs on tags.
Before tagging a release, set its version with `poetry version <version>`
and commit the change. The tag must match the package version under PEP 440
normalization, with an optional `v` prefix. Development versions remain
prereleases; a stable release requires a stable version in `pyproject.toml`.
Publishing uploads to TestPyPI, then PyPI, using the existing
`testpypi_pass` and `pypi_pass` API-token secrets. TestPyPI skips existing
files so a production upload can be retried after TestPyPI succeeds.
## Development Loop
Coding Discipline governs writing; Review Checks govern review. This
loop connects them and defines when work is ready to release.
```text
Frame → Write → Prove → Review → Gate
▲ findings │
└────────────────────────┘
```
## Directory Structure
```
qiling/
├── qiling/ # Core framework package
│ ├── core.py # Qiling class — main entry point and orchestrator
│ ├── core_hooks.py # Hook system (code, memory, interrupt, address hooks)
│ ├── core_hooks_types.py # Hook type definitions and dispatch
│ ├── core_struct.py # Endian-aware struct packing utilities
│ ├── const.py # Enumerations: QL_ARCH, QL_OS, QL_ENDIAN, etc.
│ ├── exception.py # Custom exception hierarchy
│ ├── utils.py # Component selection (select_arch, select_os, etc.)
│ ├── host.py # Host platform interface
│ ├── log.py # Logging configuration
│ │
│ ├── arch/ # Architecture implementations
│ │ ├── arch.py # QlArch — abstract base class
│ │ ├── x86.py # x86 / x86-64 / 8086
│ │ ├── arm.py # ARMv7 (32-bit)
│ │ ├── arm64.py # ARMv8 (64-bit)
│ │ ├── mips.py # MIPS32
│ │ ├── riscv.py # RISC-V 32-bit
│ │ ├── riscv64.py # RISC-V 64-bit
│ │ ├── ppc.py # PowerPC 32-bit
│ │ ├── cortex_m.py # ARM Cortex-M (MCU)
│ │ ├── register.py # Register management
│ │ └── models.py # CPU model definitions
│ │
│ ├── os/ # Operating system implementations
│ │ ├── os.py # QlOs — abstract base class
│ │ ├── memory.py # QlMemoryManager
│ │ ├── fcall.py # Function call interface (read params, set return)
│ │ ├── mapper.py # Syscall/API mapping
│ │ ├── path.py # Virtual filesystem path resolution
│ │ ├── filestruct.py # File descriptor abstraction
│ │ ├── thread.py # Threading primitives
│ │ ├── posix/ # POSIX shared layer (syscall handlers)
│ │ ├── linux/ # Linux-specific OS
│ │ ├── freebsd/ # FreeBSD-specific OS
│ │ ├── macos/ # macOS-specific OS
│ │ ├── qnx/ # QNX RTOS
│ │ ├── windows/ # Windows (Win32/Win64 API emulation)
│ │ ├── uefi/ # UEFI firmware services
│ │ ├── dos/ # DOS (8086 interrupts)
│ │ ├── mcu/ # Bare-metal microcontroller
│ │ └── blob/ # Raw binary blob execution
│ │
│ ├── loader/ # Binary format loaders
│ │ ├── loader.py # QlLoader — abstract base class
│ │ ├── elf.py # ELF (Linux, FreeBSD, QNX)
│ │ ├── pe.py # PE (Windows)
│ │ ├── pe_uefi.py # PE for UEFI
│ │ ├── macho.py # Mach-O (macOS)
│ │ ├── dos.py # DOS COM/EXE
│ │ ├── mcu.py # MCU firmware images
│ │ └── blob.py # Raw binary blobs
│ │
│ ├── cc/ # Calling conventions
│ │ ├── intel.py # cdecl, stdcall, ms64
│ │ ├── arm.py # aarch32, aarch64
│ │ ├── mips.py # MIPS o32
│ │ ├── riscv.py # RISC-V ABI
│ │ └── ppc.py # PowerPC ABI
│ │
│ ├── hw/ # Hardware peripheral emulation (MCU)
│ │ ├── peripheral.py # Base peripheral class
│ │ ├── hw.py # Hardware manager
│ │ ├── gpio/ # GPIO pins and interrupts
│ │ ├── timer/ # Timers, PWM, counters
│ │ ├── char/ # UART serial
│ │ ├── spi/ # SPI bus
│ │ ├── i2c/ # I2C bus
│ │ ├── net/ # Network interfaces
│ │ ├── analog/ # ADC/DAC
│ │ ├── intc/ # Interrupt controllers
│ │ ├── flash/ # Flash memory
│ │ ├── dma/ # DMA controllers
│ │ └── ... # Power, SD, misc peripherals
│ │
│ ├── debugger/ # Debugger subsystem
│ │ ├── gdb/ # GDB remote protocol server
│ │ └── qdb/ # Qiling native debugger (with reverse debugging)
│ │
│ ├── extensions/ # Optional extensions
│ │ ├── multitask.py # gevent-based multithreading
│ │ ├── trace.py # Instruction tracing
│ │ ├── coverage/ # Code coverage collection
│ │ ├── sanitizers/ # Memory sanitizers
│ │ ├── afl/ # AFL fuzzer integration
│ │ ├── r2/ # Radare2 integration
│ │ └── idaplugin/ # IDA Pro plugin
│ │
│ └── profiles/ # Default OS configuration files (.ql)
│ ├── linux.ql # Stack/heap addresses, kernel params
│ ├── windows.ql
│ ├── macos.ql
│ └── ...
│
├── qltool # CLI tool for running binaries
├── qltui.py # TUI interface
├── examples/ # Usage examples and sample scripts
├── tests/ # Test suite
└── docs/ # Documentation
```
## Core Components
### `Qiling` (core.py)
The central class. Every emulation session creates one `Qiling` instance that owns and wires together all other components:
```python
ql = Qiling(
argv=["/path/to/binary", "arg1"], # binary + arguments
rootfs="/path/to/rootfs", # virtual filesystem root
ostype=QL_OS.LINUX, # target OS (auto-detected if omitted)
archtype=QL_ARCH.X8664, # target arch (auto-detected if omitted)
)
```
`Qiling` inherits from `QlCoreHooks` (hook management) and `QlCoreStructs` (endian-aware packing). Key properties:
| Property | Type | Description |
|----------|------|-------------|
| `ql.arch` | `QlArch` | CPU architecture — registers, disassembly |
| `ql.os` | `QlOs` | Operating system — syscalls, I/O, APIs |
| `ql.loader` | `QlLoader` | Binary loader — parses and maps the executable |
| `ql.mem` | `QlMemoryManager` | Memory — map, read, write, MMIO |
| `ql.uc` | `unicorn.Uc` | Underlying Unicorn engine instance |
### Architecture Layer (`qiling/arch/`)
`QlArch` is the abstract base. Each architecture subclass configures:
- **Unicorn engine** mode and architecture constants
- **Register** access via `ql.arch.regs` (read/write by name)
- **Disassembler** (Capstone) and **assembler** (Keystone)
- **Stack operations** — push, pop, pointer-width-aware
- **Endianness** and **bit width** (16/32/64)
Supported: x86, x86-64, 8086, ARM, ARM64, MIPS, RISC-V (32/64), PowerPC, Cortex-M.
### OS Layer (`qiling/os/`)
`QlOs` is the abstract base. Each OS subclass provides:
- **Syscall/interrupt dispatch** — routes CPU interrupts to handler functions
- **I/O streams** — `stdin`, `stdout`, `stderr` (interceptable)
- **Virtual filesystem** — path mapping through `rootfs`
- **Function call interface** (`ql.os.fcall`) — read params, set return values
- **API interception** — `set_api()` for hooking library functions
**POSIX subsystem** (`os/posix/`): Shared syscall implementation for Linux, FreeBSD, macOS, and QNX. Individual syscall handlers live under `os/posix/syscall/`.
**Windows** (`os/windows/`): Emulates Win32/Win64 API by hooking DLL imports. Includes registry, thread, handle, and fiber support.
**UEFI** (`os/uefi/`): Emulates UEFI Boot Services, Runtime Services, and SMM. Uses a GUID database and protocol framework.
### Loader Layer (`qiling/loader/`)
`QlLoader` is the abstract base. Loaders parse a binary format, map segments into memory, resolve symbols, load dependencies, and set initial CPU state (PC, SP).
| Loader | Format | Used By |
|--------|--------|---------|
| `QlLoaderELF` | ELF | Linux, FreeBSD, QNX |
| `QlLoaderPE` | PE/COFF | Windows, UEFI |
| `QlLoaderMacho` | Mach-O | macOS |
| `QlLoaderDOS` | COM/EXE | DOS |
| `QlLoaderMCU` | Firmware | Cortex-M MCU |
| `QlLoaderBlob` | Raw bytes | Shellcode / blob |
### Memory Manager (`qiling/os/memory.py`)
Wraps Unicorn's memory model with higher-level operations:
- `map(addr, size, perms)` / `unmap(addr, size)` — region management
- `read(addr, size)` / `write(addr, data)` — data access
- `read_ptr(addr)` / `write_ptr(addr, val)` — pointer-width-aware access
- `read_cstring(addr)` — null-terminated string read
- MMIO callback support for memory-mapped peripherals
### Calling Conventions (`qiling/cc/`)
Each architecture has calling convention classes that abstract argument passing and return values. The `QlOs.fcall` interface uses these to provide a uniform way to read function parameters regardless of platform.
## Execution Flow
### 1. Initialization (`Qiling.__init__`)
```
Qiling(argv, rootfs)
│
├─ Detect arch/OS from binary headers (ql_guess_emu_env)
│ ELF magic → parse e_machine, OSABI
│ PE magic → parse Machine, Subsystem
│ MachO magic → parse CPU type
│
├─ Create QlArch (select_arch) → initializes Unicorn engine
├─ Create QlLoader (select_loader)
├─ Create QlMemoryManager
├─ Create QlOs (select_os)
│
└─ loader.run()
├─ Parse binary format (headers, segments, sections)
├─ Map segments into memory
├─ Load shared libraries / DLLs
├─ Setup stack, heap, TLS, auxiliary vectors
└─ Set initial PC (entry point) and SP
```
### 2. Execution (`ql.run()`)
```
ql.run(begin, end, timeout, count)
│
├─ Apply binary patches (ql.patch)
├─ Write exit trap (guard address)
│
└─ os.run()
└─ uc.emu_start(entry_point, exit_point)
│
├─ Unicorn executes instructions
│
├─ Hooks fire on:
│ ├─ Every instruction (hook_code)
│ ├─ Basic blocks (hook_block)
│ ├─ Memory access (hook_mem_read/write)
│ ├─ Interrupts (hook_intno) → syscall dispatch
│ ├─ Specific addresses (hook_address)
│ └─ Specific instructions (hook_insn)
│
└─ Stops when PC reaches exit point, timeout,
ql.emu_stop(), or unhandled exception
```
### 3. Syscall Handling
When the emulated binary issues a syscall (via `int 0x80`, `syscall`, `svc`, etc.):
```
CPU interrupt/instruction
→ Unicorn interrupt hook
→ QlOs syscall dispatcher
→ Look up handler by syscall number
→ Handler reads args via calling convention
→ Emulates syscall behavior
→ Sets return value in registers
```
## Component Selection
Components are selected dynamically at runtime based on `QL_ARCH` and `QL_OS` enums. The `qiling/utils.py` module provides:
- `select_arch(archtype)` → architecture class
- `select_os(ostype)` → OS class
- `select_loader(ostype)` → loader class
- `select_debugger(options)` → debugger class
This makes it possible to support diverse platform combinations from a unified codebase.
## Hook System
The hook system (`core_hooks.py`) wraps Unicorn's callback mechanism:
| Hook Type | Trigger |
|-----------|---------|
| `hook_code` | Every instruction (optionally within address range) |
| `hook_block` | Every basic block entry |
| `hook_address` | Specific address reached |
| `hook_intno` | CPU interrupt/exception |
| `hook_insn` | Specific instruction type (e.g., `syscall`) |
| `hook_mem_read` | Memory read |
| `hook_mem_write` | Memory write |
| `hook_mem_invalid` | Invalid memory access |
Hooks can be scoped to address ranges and return `QL_HOOK_BLOCK` to suppress further hooks in the chain.
## Key Extension Points
- **Custom syscall handlers** — replace or extend any syscall
- **API hooking** — `ql.os.set_api(name, callback)` to intercept library calls
- **Binary patching** — `ql.patch(offset, data)` for runtime patching
- **State snapshots** — `ql.save()` / `ql.restore()` for checkpointing
- **Debugger attachment** — GDB remote protocol or native QDB debugger
- **Coverage/tracing** — `extensions/coverage/` and `extensions/trace.py`
- **Fuzzing** — AFL integration via `extensions/afl/`
- **Hardware peripherals** — register custom MCU peripherals in `hw/`
## Dependencies
| Package | Role |
|---------|------|
| `unicorn` (2.1.3) | CPU emulation engine |
| `capstone` | Disassembly |
| `keystone-engine` | Assembly |
| `pyelftools` | ELF parsing |
| `pefile` | PE parsing |
| `python-registry` | Windows registry emulation |
| `gevent` | Cooperative multithreading |
| `pyyaml` | Configuration parsing |
Optional: `unicornafl` / `fuzzercorn` (fuzzing), `r2libr` (Radare2 integration).
## Supported Platforms
**Architectures:** x86, x86-64, 8086, ARM, ARM64, MIPS, RISC-V (32/64), PowerPC, Cortex-M
**Operating Systems:** Linux, FreeBSD, macOS, Windows, UEFI, DOS, QNX, MCU (bare-metal), Blob
## Improvement: Hybrid Kernel Architecture
> Detailed implementation plan and task tracking: [TODO.md](TODO.md)
### The Problem
Qiling reimplements Linux kernel behavior syscall-by-syscall in Python. This works
for simple operations (file I/O, memory management, stat) but fundamentally cannot
scale to the full kernel surface:
- **Networking**: No epoll. Sockets are proxied to host sockets with no isolation.
No real TCP state machine, no multicast, no raw/netlink sockets.
- **Multithreading**: Gevent greenlets are cooperative and single-threaded. No
preemption, no real concurrency. Futex is a gevent Event. Programs using pthreads,
mutexes, or condition variables don't behave correctly.
- **Signals**: `signal()`, `sigaction()`, `kill()` are mostly stubbed. No delivery,
no `EINTR`, no `SA_RESTART`.
- **Long tail**: capabilities, cgroups, namespaces, io_uring, seccomp, eBPF — the
kernel API surface is vast and growing.
### The Solution
A **hybrid architecture** that keeps Unicorn for CPU emulation and Qiling for
instrumentation, but offloads complex kernel subsystems to a real Linux kernel via
a **kernel proxy** helper process. Simple syscalls stay emulated in Python.
```
Syscall interrupt
→ load_syscall() [UNCHANGED — existing dispatch in posix.py]
→ check posix_syscall_hooks[CALL]
→ proxy hook registered? → forward to kernel proxy
→ no proxy hook? → existing Python handler [UNCHANGED]
```
The user explicitly chooses which missing syscalls to forward. Nothing is automatic —
by default Qiling behaves exactly as today. The integration uses the **existing
`set_syscall()` CALL hook mechanism** (`posix.py:128-143`), so `load_syscall()` and
all existing dispatch code remain completely unchanged.
```python
proxy = KernelProxy(ql)
proxy.forward_syscall("epoll_create", returns_fd=True)
proxy.forward_syscall("epoll_ctl")
proxy.forward_syscall("epoll_wait")
ql.run()
```
Under the hood, `forward_syscall()` registers a CALL hook that serializes the
arguments and sends them to a helper process (the kernel proxy) which executes
the real syscall and returns the result. For syscalls that return FDs, the result
is wrapped in a `ql_proxy_fd` object and stored in Qiling's FD table. Since the
FD table is already polymorphic (`ql_socket`, `ql_file`, `ql_pipe`), existing
handlers like `ql_syscall_read` and `ql_syscall_close` dispatch through the proxy
FD's `.read()`/`.close()` methods automatically — no changes needed.
### Phases
| Phase | Scope | Risk | Goal |
|-------|-------|------|------|
| 0 | Proof of concept | Low | User manually forwards specific syscalls — zero existing code changed |
| 1 | Networking foundation | Low-Med | Specific hooks for socket syscalls, `ql_proxy_fd`, TCP works |
| 2 | Complete networking | Medium | epoll, poll/select, network namespaces |
| 3 | Real threading | **High** | One Unicorn per thread, shared memory, real futex |
| 4 | Signals | Medium | Real signal delivery, EINTR, handler execution |
| 5 | Integration | Low | API polish, fallback, platform support, benchmarks |
Phase 0 gives users explicit control — they identify which missing syscalls to
forward and the proxy handles them. Phases 1-2 add pointer-aware forwarding for
networking with pre-built forwarders so users don't have to wire up each syscall.
Phase 3 (threading) is the highest-risk change and is deferred until networking is
stable. Each phase preserves backward compatibility — hybrid mode is opt-in, default
behavior is unchanged.
### Alternatives Considered
- **Run a real kernel in Unicorn**: Unicorn doesn't emulate hardware (interrupt
controllers, MMU page tables, timers). Would require rebuilding QEMU system mode.
- **ptrace-based execution**: Run natively, intercept syscalls. Fast, but no
cross-architecture support and limited instruction-level hooks.
- **User-Mode Linux (UML)**: Run the kernel as a userspace process. x86-only,
somewhat unmaintained, complex syscall bridge.
- **Auto-forward all unimplemented syscalls**: Forward every missing syscall
automatically. Convenient but unpredictable — hard to debug, may forward syscalls
that shouldn't be (security, state leaks). Explicit user control is safer.
The hybrid approach was chosen because it preserves Qiling's core value
(instrumentation + cross-arch emulation) while getting real kernel behavior where
it matters most — without modifying the existing dispatch path.
## Testing
Tests live in `tests/` and are organized by platform: `test_elf.py`, `test_pe.py`, `test_macho.py`, `test_dos.py`, `test_mcu.py`, etc. They use binaries from `examples/rootfs/` as test fixtures.
### The loop
**1. Frame.** Convert the request into a goal with an observable check.
Inspect the request, code, docs, and repository conventions; record the
narrowest supported assumptions. Ask one focused question only when a
required decision cannot be discovered or safely inferred and guessing
would materially change the result. Once framed, continue without an
approval pause.
**2. Write.** Make the smallest change that reaches the goal. Add no
unrequested features or abstractions, match local style, touch only
in-scope code, and remove only orphans created by the change.
**3. Prove.** Run relevant tests and retain observable evidence.
*Survey the suite before touching it.* Before adding, changing, merging,
or deleting any test, inventory the whole suite: enumerate every test
file and case name, then read in full each test whose subject, fixtures,
or assertions touch this change. Use a subagent for broad inventory when
supported. From that inventory decide the complete set of test edits at
once — what to change, what to add, what to merge, what to remove — each
backed by `file:line`, then execute only that plan. Never write a test
before the survey, and never discover existing coverage afterward.
The plan obeys four rules:
- **Reuse or extend first.** Add a case to the test that already owns
the behavior or shares its setup, fixtures, and subject. A new test
function or file is justified only when the survey found no existing
test owning the behavior, or when merging would hide which case
failed.
- **Add only what the goal needs.** A bug fix needs a reproducing
regression test; a new capability needs a test of its claimed
behavior. Nothing further.
- **Retire what this change made obsolete.** Delete tests whose behavior
no longer exists, and merge tests this change turned into duplicates,
citing the surviving test. Leave unrelated pre-existing tests alone;
record suspected redundancy under **Open Gaps / Roadmap**.
- **Never delete to reach green.** A failing test is a finding for
Write. Removal requires evidence that its behavior is gone or is still
covered elsewhere, cited by `file:line`.
Coverage of claimed behavior must not decrease. A failure returns
directly to Write, never forward to Review.
**4. Review.** Walk all seven Review Checks as separate passes. Read
whole affected files, not only the diff. Every finding needs `file:line`
evidence. Use an independent agent or isolated pass for Fit,
Dependencies, and Security when available.
**5. Gate.** Apply the Definition of Done. Any unticked criterion,
`blocker`, or unresolved `major` returns its evidence to Write. All
criteria passing means the change is ready for public or production
release. There is no separate approval or reporting phase.
### Definition of Done
**Correctness**
- The framed goal and its named check pass.
- Tests cover claimed behavior and pass; a bug fix has a regression test.
- The suite was surveyed before any test was written, changed, or
deleted; no added test duplicates coverage another test owns, and no
removal left claimed behavior uncovered.
- The owning module's **How to Test** command passes with evidence.
- The project builds and tests from a fresh clone without local-only
dependencies.
**Review**
- All seven Review Checks ran; none was skipped or assumed.
- No `blocker` or unresolved `major` remains.
- Nits were applied or consciously declined.
**Legibility and contract**
- A new maintainer can build, test, run, and understand public behavior
from the docs.
- Every changed line serves the goal; no drive-by formatting, debugging
remnants, commented-out code, secrets, tokens, or local paths remain.
- Public names, signatures, errors, and recovery are intelligible.
- Architecture docs and `file:line` references are current.
- Breaking changes, deprecations, dependencies, licenses, and attribution
are handled; commit or PR text explains why.
### Iterating without thrashing
- Every pass closes a named finding and touches only what it names.
- Nits alone do not trigger another pass.
- Re-run Prove after every fix.
- Two no-change passes force Gate re-evaluation: release if Done passes;
otherwise return the surviving evidence to Frame.
- Three passes on one finding return automatically to Frame for a new
approach.
- Never widen scope to satisfy a finding. Record out-of-scope work under
**Open Gaps / Roadmap**.
## Coding Discipline
### 1. Think Before Coding
- Understand the request, code, goal, and repository conventions first.
- Record assumptions and choose the narrowest evidence-backed reading.
- Prefer the simpler approach when it reaches the same verified goal.
- Ask only during planning and only for a required answer that cannot be
discovered or safely inferred.
### 2. Simplicity First
- Implement only what was requested.
- Do not add single-use abstractions, speculative flexibility, or checks
for impossible conditions.
- If the implementation is materially larger than the problem, simplify
it.
### 3. Surgical Changes
- Do not refactor, reformat, or clean up unrelated code.
- Match the surrounding style.
- Remove imports, variables, and functions made unused by this change;
leave pre-existing dead code alone unless requested.
- Every changed line must trace to the stated goal.
### 4. Goal-Driven Execution
Turn work into verifiable outcomes, then loop until they pass:
- Add validation → invalid inputs are rejected by a named passing test.
- Fix a bug → a regression test fails before the fix and passes after.
- Refactor → behavior tests pass before and after.
Give every plan step its own check. Strengthen vague criteria from
repository evidence before implementation.
### Project-Specific Deviations
- Emulation fidelity beats abstraction: syscall, API, and peripheral
implementations mirror the real platform's observable behavior even
when that means repetitive per-OS or per-chip code. Cross-OS
"unification" is a scope increase, not a simplification.
- Coverage is demand-driven by design (see the OS and HW module docs).
Adding an unrequested syscall, Win32 API, or peripheral register is
out of scope; record it under the owning module's **Open Gaps /
Roadmap**.
- `unicorn` is hard-pinned (`pyproject.toml:39`). Changing it, or any
behavior that depends on its version, is a project-wide event and
never an incidental part of another change.
## Review Checks
Run every check against every change before merge. Keep checks separate.
Four rules bind all checks:
- **Evidence or no finding.** Every finding cites `file:line`.
- **The repository is authoritative.** Demand only conventions visible
in the tree.
- **Read files, not only hunks.** Context can invalidate a finding or
reveal unreachable code, unused parameters, and hidden duplication.
- **Review the change, never the author.** Describe code and impact, not
how or by whom it was produced.
### 1. Style
Check indentation and local file conventions. Mixed indentation is
`major`; a consistent new file using the wrong local indent is `nit`.
Leave machine-checkable formatting to existing formatters and linters;
never demand unrelated reformatting.
### 2. Naming
Compare new names with nearby precedents before filing a finding. If the
repository is inconsistent, demand nothing. A local mismatch is `nit`;
an inconsistent public name is `major`.
### 3. Duplication
Search distinctive constants, errors, fields, and call sequences—not
only symbol names—for code performing the same job. Cite both sites and
the remedy. Cross-layer duplication is `major`; small local repetition
is `nit`. Similar code with meaningfully different branches is not
duplication.
### 4. Quality
Require followable control flow, errors handled where they occur, and
abstractions proportional to the problem. Swallowed errors,
inappropriate prints, unexplained magic values, and dead branches are
`major`. Remove unrequested configurability, one-caller wrappers, filler
comments, debugging remnants, and unrelated formatting. Missing tests
belong to Prove, not this check.
### 5. Fit
Read `ARCHITECTURE.md` and the owning module doc before the diff. Check
scope, layering, ownership, public-API growth, and performance claims. A
layering violation or unjustified public API is `major`. Architectural or
public-behavior changes must update the relevant docs in the same change.
### 6. Dependencies
Check manifests and imports, maintenance, supply-chain risk, advisories,
install-time behavior, license, transitive cost, and whether the standard
library is sufficient. An unjustified top-level dependency is `major`;
a live advisory or abandoned upstream is `blocker`. Incomplete evidence
does not pass.
### 7. Security
Check both defects and widened exposure: unsafe memory access, unchecked
sizes or offsets, integer overflow, path traversal, unsafe
deserialization, command construction, committed secrets, and unbounded
untrusted input. Trace input to impact; without a reachable path there is
no finding. A real defect is `major`; a trust-boundary break is `blocker`.
Describe the fix without publishing exploit steps.
### Project-Specific Deviations
- **Security, scope.** Qiling *emulates* untrusted binaries; guest code
doing something hostile inside the sandbox is the product working, not
a finding. Findings target the host boundary: rootfs escape via path
handling (`qiling/os/path.py`), unchecked guest-controlled sizes or
offsets reaching host allocations or `struct` unpacking, and parser
input in `qiling/loader/` reachable from an untrusted image.
- **Dependencies.** `pyproject.toml` is the only manifest. A new
top-level runtime dependency is `blocker` absent an explicit request;
optional integrations belong in an extra (`fuzz`, `RE`).
### Severity and the merge threshold
| Severity | Effect |
| -------- | ------ |
| `blocker` | Must not merge. |
| `major` | Must be resolved before merge. |
| `nit` | Apply or consciously decline. |
| `info` | Context or a question; no action implied. |
Merge only with no `blocker` and no unresolved `major`. A check that did
not run does not pass. Findings feed Write and Gate directly; they do not
create a reporting phase.
## Index
- [core.md](ARCHITECTURE/core.md) — the `Qiling` facade, hook engine, component selection, profiles, logging, exceptions
- [arch.md](ARCHITECTURE/arch.md) — CPU layer: registers, Unicorn instance, disassembler, calling conventions
- [loader.md](ARCHITECTURE/loader.md) — binary format loaders: ELF, PE, PE/UEFI, Mach-O, DOS, MCU firmware, raw blobs
- [os-base.md](ARCHITECTURE/os-base.md) — shared OS layer: `QlOs`, memory manager/heap, fcall marshalling, fs mapper, path virtualization
- [os-posix.md](ARCHITECTURE/os-posix.md) — POSIX syscall emulation: Linux, FreeBSD, macOS, QNX
- [os-windows.md](ARCHITECTURE/os-windows.md) — Windows API emulation, UEFI boot/runtime/SMM services, DOS interrupts
- [os-baremetal.md](ARCHITECTURE/os-baremetal.md) — MCU and raw-blob execution modes, cooperative multitasking
- [hw.md](ARCHITECTURE/hw.md) — MMIO peripheral emulation for bare-metal targets, board/chip definitions
- [debugger.md](ARCHITECTURE/debugger.md) — GDB remote-serial server and the built-in Qdb debugger
- [extensions.md](ARCHITECTURE/extensions.md) — AFL fuzzing, coverage/tracing, sanitizers, r2/IDA integration, pipes, reports
- [cli.md](ARCHITECTURE/cli.md) — `qltool` CLI and `qltui.py` TUI
+58
View File
@@ -0,0 +1,58 @@
# Arch — CPU architecture layer
## Goal
Own everything CPU-specific: the Unicorn `Uc` instance, register access, stack
primitives, disassembler/assembler, CPU models, and per-arch calling
conventions. This is the bottom layer — everything else reads `ql.arch`; arch
depends only on Unicorn/Capstone/Keystone. Mature released infrastructure;
maturity-based status.
## Status
`done` — all architectures exercised by the CI suite; CPU model selection
covered by `tests/test_cpu_models.py`.
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/arch/arch.py` | Abstract base `QlArch`: owns `uc`, `regs`, stack push/pop, save/restore, disassembler |
| `qiling/arch/x86.py` | `QlArchIntel` base + `QlArchA8086`/`QlArchX86`/`QlArchX8664`, GDT/MSR wiring |
| `qiling/arch/arm.py`, `arm64.py` | ARM/AArch64, thumb handling, coprocessor access |
| `qiling/arch/cortex_m.py` | Cortex-M on top of ARM: NVIC-style interrupt entry/exit for MCU mode |
| `qiling/arch/mips.py`, `riscv.py`, `riscv64.py`, `ppc.py` | Remaining architectures |
| `qiling/arch/register.py` | `QlRegisterManager` — attribute-style register read/write |
| `qiling/arch/models.py` | CPU model enums (`X86_CPU_MODEL` … `RISCV64_CPU_MODEL`) |
| `qiling/arch/msr.py`, `cpr.py`, `cpr64.py` | x86 MSRs, ARM/ARM64 coprocessor registers |
| `qiling/arch/utils.py` | `QlArchUtils`: disassembly output for verbose/trace modes |
| `qiling/cc/__init__.py` + `intel.py`, `arm.py`, `mips.py`, `ppc.py`, `riscv.py` | Calling conventions (arg/retval marshalling) consumed by `qiling/os/fcall.py` |
## Key Types and Entry Points
- `qiling/arch/arch.py:22` - `QlArch(ABC)` - cached properties `uc` (`:34`), `regs` (`:42`), `stack_push/stack_pop` (`:52`/`:66`), `save/restore` via UcContext (`:108`/`:112`), `disassembler` (`:117`).
- `qiling/arch/register.py:11` - `QlRegisterManager` - `ql.arch.regs.rax`-style access, backed by per-arch `*_const.py` tables.
- `qiling/arch/x86.py:22,53,79,111` - `QlArchIntel` / `QlArchA8086` / `QlArchX86` / `QlArchX8664`.
- `qiling/arch/cortex_m.py:67` - `QlArchCORTEX_M(QlArchARM)` - plus `QlInterruptContext` (`:25`) for exception entry/exit in MCU mode.
- `qiling/arch/models.py` - CPU model enums selected via the `cputype` kwarg (resolved in `select_arch`, `qiling/utils.py:376`).
- `qiling/cc/__init__.py:9` - `QlCC` - abstract calling convention; `QlCommonBaseCC` (`:110`); e.g. `qiling/cc/intel.py` defines `cdecl`/`stdcall`/`ms64`/`macosx64`.
## Interactions
- Instantiated first by [core.md](core.md) (`qiling/core.py:154`); `Qiling.uc` proxies `arch.uc` (`qiling/core.py:479`).
- [loader.md](loader.md) and the OS layers use `arch.regs` and stack primitives to set up entry state.
- `qiling/cc/` is consumed by `QlFunctionCall` in [os-base.md](os-base.md) for API argument marshalling.
- [debugger.md](debugger.md) reads/writes registers through this layer.
## How to Test
```sh
cd tests && python3 test_cpu_models.py # pass = unittest "OK", exit code 0
```
- Broader arch coverage comes for free from `test_shellcode.py` (5 archs) and the per-OS suites.
## Open Gaps / Roadmap
- PowerPC and RISC-V have fewer OS-level tests than x86/ARM/MIPS (no dedicated POSIX suite beyond `tests/test_riscv.py`).
- Thumb state handling has a known fixup in `Qiling.emu_start` (`qiling/core.py:743`) rather than in the arch layer itself.
+55
View File
@@ -0,0 +1,55 @@
# CLI — qltool and qltui
## Goal
Give users a no-code way to run emulations: `qltool run` executes a binary
against a rootfs, `qltool code` runs shellcode (hex/asm/bin), `qltool
examples` prints usage samples, and `qltool qltui` launches an interactive
TUI that gathers the same options. Mature released infrastructure;
maturity-based status.
## Status
`done` — covered by `tests/test_qltool.py`, which shells out to `qltool` for
run/code subcommands and coverage output. `InstalledQltool_Test` checks the
installed command, shellcode exit status, bundled profiles, and TUI import
outside the checkout.
## Code Structure
| File | Role |
| ---- | ---- |
| `qltool` | Checkout launcher for `qiling.cli.run` |
| `qiling/cli.py` | Argparse CLI; installed as `qltool`, builds kwargs and drives `Qiling` |
| `qltui.py` | questionary/pyfx/termcolor TUI; collects options, returned to qltool |
## Key Types and Entry Points
- `qiling/cli.py:189` - `run()` - argparse setup with subcommands `run`, `code`, `examples`, `qltui`; enum-mapping actions translate `--arch/--os/--endian/--verbose` strings to `QL_ARCH`/`QL_OS` enums (`qiling/cli.py:59-75`).
- `qiling/cli.py:129` - `handle_run(options)` - builds `{'argv': [file]+args, 'rootfs': ...}`.
- `qiling/cli.py:78` - `handle_code(options)` - reads hex/asm/bin shellcode, assembling asm via `qiling.arch.utils.assembler` (`qiling/cli.py:104`).
- `qiling/cli.py:276` - `ql = Qiling(**ql_args)` - the single construction point; then optional Qdb (`:279`), gdbserver (`:285`), coverage-wrapped `ql.run()` (`:306-310`), JSON report (`:312`), exit with `ql.os.exit_code` (`:321`).
## Interactions
- Thin client of [core.md](core.md): constructs `Qiling` and calls `run()`.
- Attaches [debugger.md](debugger.md) via `--gdb host:port` / `--qdb [--rr]`.
- Uses [extensions.md](extensions.md) for `--coverage-file` (drcov) and `--json` report output.
- `qltool examples` mirrors scripts documented in `examples/README.md`.
## How to Test
```sh
python3 -m pip install .
cd tests && python3 test_qltool.py # pass = unittest "OK", exit 0
```
- Packaging regression checks, from the repository root after installing
the built wheel: `python3 -I tests/test_qltool.py InstalledQltool_Test -v`.
- Manual smoke test: `./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux` — pass = prints `Hello, World!`.
## Open Gaps / Roadmap
- `qltui.py` has an installed import smoke test; its interactive flows are
not covered by automated tests.
- `qltool` predates subcommand-style config files; complex setups (fs mappers, custom hooks) still require the Python API.
+61
View File
@@ -0,0 +1,61 @@
# Core — the Qiling facade and plumbing
## Goal
Own the public API and object lifecycle of an emulation: the `Qiling` class
composes arch, loader, memory, OS, and (for bare-metal) hardware components,
and exposes hooks, memory/register access, patching, and save/restore to
users. No roadmap milestone applies — this is mature released infrastructure;
status is maturity-based per the control center.
## Status
`done` — exercised by the whole test suite; the How to Test command below
boots shellcode end-to-end through `Qiling.__init__` → `run()` → `emu_start`.
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/core.py` | `Qiling` class: constructor/composition root, `run`, `emu_start/stop`, `save/restore`, `patch`, properties |
| `qiling/core_hooks.py` | `QlCoreHooks` mixin: wraps Unicorn hooks, dispatches to `Hook` lists |
| `qiling/core_hooks_types.py` | `Hook`, `HookAddr`, `HookIntr`, `HookRet` records |
| `qiling/core_struct.py` | `QlCoreStructs` mixin: endian/bit-width-aware pack/unpack helpers |
| `qiling/utils.py` | Name→class resolution: `select_arch/loader/os/component/debugger`, binary format sniffing, profile loading |
| `qiling/const.py` | Enums: `QL_ARCH`, `QL_OS`, `QL_VERBOSE`, `QL_INTERCEPT`, `QL_STATE`, groupings `QL_OS_POSIX`/`QL_OS_BAREMETAL` |
| `qiling/exception.py` | `QlErrorBase` and ~20 subclasses (`QlErrorArch`, `QlSyscallError`, …) |
| `qiling/host.py` | `QlHost`: describes the *hosting* platform (for pass-through features) |
| `qiling/log.py` | Logger setup, colored/plain formatters, regex filtering behind `Qiling.filter` |
| `qiling/profiles/*.ql` | Default per-OS config (memory layout, kernel uid/gid/pid) merged with user overrides |
## Key Types and Entry Points
- `qiling/core.py:35` - `Qiling(QlCoreHooks, QlCoreStructs)` - the facade; `__init__` composes components in fixed order: arch (`:154`) → mixins (`:157`) → logger → profile (`:178`) → loader (`:183`) → memory (`:188`) → OS (`:189`) → hw if bare-metal (`:191`) → `loader.run()` (`:195`).
- `qiling/core.py:561` - `Qiling.run(begin, end, timeout, count)` - attaches debugger, applies patches, writes exit trap, delegates to `os.run()`.
- `qiling/core.py:743` - `Qiling.emu_start(begin, end, timeout, count)` - thin wrapper over `uc.emu_start`; manages thumb bit, `QL_STATE`, exception re-raise.
- `qiling/core.py:609` / `:658` - `save()` / `restore()` - snapshot regs/mem/fd/os per-component.
- `qiling/core_hooks.py:150` - `QlCoreHooks` - hook registration API: `hook_code` (`:400`), `hook_block` (`:422`), `hook_address` (`:550`), `hook_intno` (`:575`), `hook_mem_read/write` (`:592`/`:610`), `hook_insn` (`:646`), `hook_del` (`:686`).
- `qiling/utils.py:278` - `ql_guess_emu_env(path)` - sniffs arch/OS/endian from pathname, ELF, Mach-O, or PE headers when not given.
- `qiling/utils.py:297,376,409,323,332` - `select_loader/arch/os/component/debugger` - dynamic-import factories; core never imports concrete subclasses.
- `qiling/utils.py:419` - `profile_setup(ostype, user_config)` - YAML for MCU, else ConfigParser over `qiling/profiles/<os>.ql` + user overrides.
## Interactions
- Instantiates every other subsystem: [arch.md](arch.md), [loader.md](loader.md), [os-base.md](os-base.md) (memory + OS), [hw.md](hw.md) (bare-metal only).
- Lazily instantiates [debugger.md](debugger.md) inside `run()` via `select_debugger`.
- [extensions.md](extensions.md) and [cli.md](cli.md) consume only this public API.
- Hook dispatch protocol (`QL_HOOK_BLOCK`, `qiling/const.py:77`) is honored by the OS layers when they intercept syscalls/APIs.
## How to Test
```sh
cd tests && python3 test_shellcode.py # pass = unittest "OK", exit code 0
```
- Exercises `Qiling(code=...)` construction and `run()` across x86/x86-64/ARM/ARM64/MIPS shellcode for Linux and Windows.
## Open Gaps / Roadmap
- `ChangeLog` lags the released version (1.4.6 vs 1.4.8 in `pyproject.toml`).
- `unicorn` is hard-pinned to 2.1.3; upgrading Unicorn is a project-wide event.
- Feature wishlist lives in GitHub issue [#333](https://github.com/qilingframework/qiling/issues/333).
+56
View File
@@ -0,0 +1,56 @@
# Debugger — GDB server and Qdb
## Goal
Let users debug emulated targets: a GDB remote-serial-protocol server so any
GDB/IDA/lldb front end can attach cross-architecture, and Qdb — a built-in
interactive CLI debugger with stepping, branch prediction, and record/replay
reverse debugging. Mature released infrastructure; maturity-based status.
## Status
`done` — Qdb covered by `tests/test_qdb.py`; the GDB server by
`tests/test_debugger.py` (spawns a real client session).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/debugger/debugger.py` | Base `QlDebugger` |
| `qiling/debugger/gdb/gdb.py` | `QlGdb`: GDB remote-serial-protocol server, plus the `GdbSerialConn` transport |
| `qiling/debugger/gdb/utils.py` | `QlGdbUtils`: breakpoint table and the per-instruction `dbg_hook` that services breakpoints, stepping, and async interrupts |
| `qiling/debugger/gdb/xmlregs.py`, `gdb/xml/` | Target-description XML per arch for modern GDB clients |
| `qiling/debugger/qdb/qdb.py` | `QlQdb`: interactive Cmd-based debugger |
| `qiling/debugger/qdb/arch/` | Per-arch Qdb support (arm, intel, mips) |
| `qiling/debugger/qdb/branch_predictor/` | Predicts branch targets for step/next |
| `qiling/debugger/qdb/render/` | Register/stack/disasm view rendering |
## Key Types and Entry Points
- `qiling/debugger/debugger.py:13` - `QlDebugger` - base; `run()` starts the session.
- `qiling/debugger/gdb/gdb.py:84` - `QlGdb(QlDebugger)` - listens on ip:port, translates RSP packets to Qiling hook/mem/reg operations; `run()` (`:139`) serves the session.
- `qiling/debugger/gdb/gdb.py:817` - `GdbSerialConn` - the socket transport; `poll_interrupt()` (`:856`) is a non-blocking check for a client `\x03`, wired into `QlGdbUtils.check_interrupt` (`qiling/debugger/gdb/gdb.py:145`) so a running guest can be broken into asynchronously.
- `qiling/debugger/gdb/utils.py:16` - `QlGdbUtils` - `dbg_hook` (`:48`) runs per instruction to service breakpoints, single-step, and interrupts; `bp_insert`/`bp_remove` (`:81`/`:94`); `resume_emu` (`:107`).
- Stop replies report `SIGTRAP` (`qiling/debugger/gdb/gdb.py:49`) for both single-step (`:243`) and async-interrupt stops (`:262`).
- `qiling/debugger/qdb/qdb.py:59` - `QlQdb(Cmd, QlDebugger)` - CLI loop; `rr` mode enables record/replay reverse debugging.
- Activation: set `ql.debugger = True | "gdb" | "gdb:0.0.0.0:9999" | "qdb" | "qdb:rr"` (`qiling/core.py:437`); instantiated lazily in `Qiling.run` via `select_debugger` (`qiling/utils.py:332`).
- `qltool` flags: `--gdb` and `--qdb` (see [cli.md](cli.md)).
## Interactions
- Instantiated by [core.md](core.md) at `Qiling.run` time, not construction.
- Reads/writes state exclusively through public APIs: registers via [arch.md](arch.md), memory via [os-base.md](os-base.md), breakpoints via `hook_address` ([core.md](core.md)).
- The IDA plugin in [extensions.md](extensions.md) offers an alternative front end over the same public API.
## How to Test
```sh
cd tests && python3 test_qdb.py # pass = unittest "OK", exit 0
```
- GDB server: `cd tests && python3 test_debugger.py` — starts `QlGdb` and drives a scripted client.
## Open Gaps / Roadmap
- Qdb per-arch support covers arm/cortex-m/mips/intel; RISC-V and PPC lack Qdb arch modules (`qiling/debugger/qdb/arch/`).
- Record/replay (`qdb:rr`) stores full state per step — memory-heavy on long runs.
+61
View File
@@ -0,0 +1,61 @@
# Extensions — optional tooling on top of the core
## Goal
House everything that builds on the public `Qiling` API without being required
by it: fuzzing (AFL), code-coverage and execution-trace writers, a heap
sanitizer, radare2 and IDA Pro integration, fake stdio pipes, and JSON run
reports. Mature released infrastructure; maturity-based status.
## Status
`done` — history/coverage tracker tested in CI; AFL and r2 integrations need
optional extras (`fuzz`, `RE`); the IDA plugin needs IDA Pro (untested in CI).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/extensions/afl/` | `ql_afl_fuzz` (unicornafl bridge) + `QlFuzzer` harness base class |
| `qiling/extensions/coverage/` | `collect_coverage` context manager; writers in `formats/` (drcov, drcov_exact, ezcov, history) |
| `qiling/extensions/tracing/` | Tenet-style execution trace writers (`formats/`) |
| `qiling/extensions/trace.py` | Disassembly tracing: full trace or ring-buffer history |
| `qiling/extensions/sanitizers/heap.py` | Canary-based heap sanitizer (UAF/OOB detection) |
| `qiling/extensions/r2/r2.py` | radare2 (r2libr) integration: sections/symbols/functions/xrefs of the loaded target |
| `qiling/extensions/idaplugin/qilingida.py` | IDA Pro plugin driving Qiling emulation from IDA |
| `qiling/extensions/pipe.py` | Fake stdio streams for hijacking emulated I/O (fuzzing staple) |
| `qiling/extensions/report/report.py` | `generate_report(ql)`: JSON summary of a run |
| `qiling/extensions/multitask.py` | Cooperative-multitask Unicorn wrapper (documented in [os-baremetal.md](os-baremetal.md)) |
| `qiling/extensions/mcu/` | Board/chip definitions (documented in [hw.md](hw.md)) |
| `qiling/extensions/winsdkapi.py` | Windows API signature decorator glue |
## Key Types and Entry Points
- `qiling/extensions/afl/afl.py:21` / `:87` - `ql_afl_fuzz` / `ql_afl_fuzz_custom` - hand control to AFL++ via unicornafl; harness base `QlFuzzer` (`qiling/extensions/afl/qlfuzzer.py:14`).
- `qiling/extensions/coverage/utils.py:48` - `collect_coverage(ql, name, file)` - context manager writing e.g. drcov files (used by `qltool --coverage-file`).
- `qiling/extensions/trace.py:145` / `:180` - `enable_full_trace` / `enable_history_trace` - disassembly tracing via `hook_code`.
- `qiling/extensions/sanitizers/heap.py:16` - `QlSanitizedMemoryHeap` - drop-in replacement for `ql.os.heap` with canaries and free-list checks.
- `qiling/extensions/r2/r2.py:135` - `R2(ql)` - rzpipe-backed analysis of the loaded image.
- `qiling/extensions/pipe.py` - `SimpleInStream`/`SimpleOutStream` (`:62`/`:69`) - assigned to `ql.os.stdin`/`stdout`.
- `qiling/extensions/report/report.py:56` - `generate_report(ql)` - JSON report (used by `qltool --json`).
## Interactions
- Everything here consumes only the public API of [core.md](core.md) (hooks, mem, regs) and [os-base.md](os-base.md) (heap, stdio, syscall/API overrides).
- The heap sanitizer wraps `QlMemoryHeap` from [os-base.md](os-base.md) (demo: `examples/uefi_sanitized_heap.py` with [os-windows.md](os-windows.md) UEFI).
- Fuzzing harnesses live in `examples/fuzzing/` and pair AFL with `pipe.py` and `set_syscall` from [os-posix.md](os-posix.md).
- `qltool` wires in coverage and report generation ([cli.md](cli.md)).
## How to Test
```sh
cd tests && python3 test_history.py # pass = unittest "OK", exit 0
```
- r2 integration (needs `pip install qiling[RE]`): `cd tests && python3 test_r2.py`.
- AFL (needs `pip install qiling[fuzz]` + AFL++): run a harness from `examples/fuzzing/linux_x8664/`.
## Open Gaps / Roadmap
- `afl/` and `r2/` depend on optional extras not installed by default; the IDA plugin cannot be CI-tested (requires an IDA Pro license).
- Two overlapping trace mechanisms exist (`trace.py` and `tracing/`); no unified interface.
+53
View File
@@ -0,0 +1,53 @@
# HW — peripheral emulation for bare-metal targets
## Goal
Emulate memory-mapped peripherals (GPIO, UART/char, SPI, I2C, timers, DMA,
interrupt controllers, flash, …) so MCU firmware runs against realistic
hardware. Peripherals are instantiated from a YAML profile naming which
peripheral class sits at which MMIO base address. Only active in bare-metal
(MCU) mode. Mature released infrastructure; maturity-based status.
## Status
`done` — exercised by `tests/test_mcu.py` against STM32F1/F4 and GD32VF1
firmware images (UART echo, freertos, blink, crc, dma_clock, i2c/spi/lcd).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/hw/hw.py` | `QlHwManager`: creates peripherals from profile, maps MMIO, steps them each tick |
| `qiling/hw/peripheral.py` | Base `QlPeripheral` (+ `QlPeripheralUtils`) |
| `qiling/hw/analog/ char/ dma/ flash/ gpio/ i2c/ intc/ math/ mem/ misc/ net/ power/ sd/ spi/ timer/` | One directory per peripheral class, chip-specific implementations inside |
| `qiling/hw/const/` | Register layout constants per chip family |
| `qiling/hw/connectivity.py`, `external_device/` | External-interface plumbing (e.g. connecting a fake device to a bus) |
| `qiling/extensions/mcu/` | Board/chip definitions (stm32f4xx, gd32vf1, nxp, atmel, bes): memory maps naming peripheral class + base address, passed as `env=` to `Qiling` |
## Key Types and Entry Points
- `qiling/hw/hw.py:60` - `QlHwManager` - available as `ql.hw`; created by core only when `ql.baremetal` (`qiling/core.py:191`).
- `qiling/hw/hw.py:67` - `QlHwManager.create(label, struct, base)` - instantiates a peripheral from the profile entry and maps its MMIO region.
- `qiling/hw/hw.py:135` - `QlHwManager.step()` - advances every peripheral one tick; called from the MCU run loop.
- `qiling/hw/peripheral.py:132` - `QlPeripheral(QlPeripheralUtils)` - base class: a ctypes register struct + read/write handlers on the MMIO region.
- `qiling/extensions/mcu/stm32f4/stm32f407.py` (and siblings) - chip `env` dicts consumed at `Qiling(..., env=...)` construction.
## Interactions
- Created by [core.md](core.md) for bare-metal targets; driven by the MCU run loop in [os-baremetal.md](os-baremetal.md) (`QlHwManager.step()` between execution slices).
- Interrupt controller peripherals (`intc/`) raise exceptions delivered through `QlInterruptContext` in [arch.md](arch.md).
- MMIO regions are mapped through `QlMemoryManager` ([os-base.md](os-base.md)).
- Profile parsing (YAML) is in `profile_setup` ([core.md](core.md), `qiling/utils.py:419`).
## How to Test
```sh
cd tests && python3 test_mcu.py # pass = unittest "OK", exit 0
```
- Exercises GPIO, UART, EXTI, I2C, SPI, DMA, CRC, RTC, watchdog peripherals on real firmware from `examples/rootfs/mcu/`.
## Open Gaps / Roadmap
- Peripheral fidelity is demand-driven: registers behave as observed firmware needs, not per full datasheets.
- Chip coverage limited to families under `qiling/extensions/mcu/`; adding a chip means writing its memory map + any missing peripheral classes.
+58
View File
@@ -0,0 +1,58 @@
# Loader — binary format loaders
## Goal
Map the target binary into emulated memory and prepare execution state: parse
the format, load segments/sections and dependencies (ld.so, DLLs, dylibs),
build stack/argv/auxv or OS structures (PEB/TEB, UEFI protocols), and set
entry/exit points. One loader per format, selected by OS type. Mature released
infrastructure; maturity-based status.
## Status
`done` — every format loader is exercised by its OS test suite; the How to
Test command proves the ELF path end-to-end.
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/loader/loader.py` | Base `QlLoader` + `Image` record; abstract `run()`, save/restore |
| `qiling/loader/elf.py` | ELF for Linux/FreeBSD/QNX: segments, interpreter (ld.so), stack, auxv |
| `qiling/loader/pe.py` | Windows PE: DLL resolution + `QlPeCache`, PEB/TEB/LDR via `Process` |
| `qiling/loader/pe_uefi.py` | UEFI DXE/SMM module loading, protocol installation |
| `qiling/loader/macho.py` | Mach-O (with its own `macho_parser/` package) |
| `qiling/loader/dos.py` | DOS COM/EXE/MBR |
| `qiling/loader/mcu.py` | MCU firmware: Intel HEX (`IhexParser`), ELF, or raw bin per YAML profile |
| `qiling/loader/blob.py` | Raw blobs loaded at a fixed address (e.g. u-boot) |
## Key Types and Entry Points
- `qiling/loader/loader.py:21` - `QlLoader` - base; `Image` NamedTuple (`:15`), abstract `run()` (`:62`), `skip_exit_check` (`:27`).
- `qiling/loader/elf.py:68` - `QlLoaderELF` - loads binary + interpreter, builds stack/auxv (`AUXV` enum `:32`), sets `entry_point`/`elf_entry`.
- `qiling/loader/pe.py:817` - `QlLoaderPE(QlLoader, Process)` - `Process` (`:77`) builds PEB/TEB/LDR; `QlPeCache` (`:49`) caches parsed DLLs behind the `libcache` kwarg (wired in `select_loader`, `qiling/utils.py:300`).
- `qiling/loader/pe_uefi.py:26` - `QlLoaderPE_UEFI` - loads DXE/SMM modules and installs protocols into the UEFI context.
- `qiling/loader/macho.py:70` - `QlLoaderMACHO`.
- `qiling/loader/mcu.py:57` - `QlLoaderMCU` - with `IhexParser` (`:15`).
- OS→loader mapping: `select_loader` (`qiling/utils.py:297`) — LINUX/FREEBSD/QNX→elf, MACOS→macho, WINDOWS→pe, UEFI→pe_uefi, DOS→dos, MCU→mcu, BLOB→blob.
## Interactions
- Instantiated by [core.md](core.md) (`qiling/core.py:183`); `loader.run()` fires at the end of `Qiling.__init__` (`qiling/core.py:195`).
- Uses `ql.mem` ([os-base.md](os-base.md)) for mapping and `ql.arch` ([arch.md](arch.md)) for initial register/stack state.
- The PE loader builds Windows process structures with [os-windows.md](os-windows.md) components (heap, handles); the UEFI loader populates the context in `qiling/os/uefi/`.
- IAT entries recorded here drive address-based API hooking in [os-windows.md](os-windows.md).
- The MCU loader reads the YAML profile owned by [core.md](core.md) and memory maps defined via [hw.md](hw.md).
## How to Test
```sh
cd tests && python3 -m unittest test_elf.ELFTest.test_elf_linux_x8664 # pass = "OK", exit 0
```
- Full loader coverage: `test_elf.py`, `test_pe.py` (Windows host), `test_macho.py` (macOS host), `test_dos.py`, `test_mcu.py`, `test_blob.py`, `test_uefi.py`.
## Open Gaps / Roadmap
- PE and Mach-O loading depend on host-collected libraries (`examples/scripts/dllscollector.bat` / `dylibcollector.sh`); not runnable from a Linux checkout alone.
- `QlPeCache` cache invalidation is manual (delete cache files) — no versioning.
+61
View File
@@ -0,0 +1,61 @@
# OS bare-metal — MCU firmware and raw blobs
## Goal
Execute code with no operating system. MCU mode runs microcontroller firmware
(STM32, GD32V, NXP, …) with peripheral emulation and interrupt scheduling on
a cooperative multitasking wrapper around Unicorn; BLOB mode runs raw binaries
(e.g. u-boot) at a fixed load address with no OS services at all. Mature
released infrastructure; maturity-based status.
## Status
`done` — MCU covered by `tests/test_mcu.py` (STM32F1/F4, GD32VF1 firmware),
BLOB by `test_blob.BlobTest.test_uboot_arm` and `tests/test_edl.py`. The other
case in `tests/test_blob.py` is blocked on a missing fixture (see Open Gaps).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/os/mcu/mcu.py` | `QlOsMcu` run loop + `MCUTask` driving Unicorn and peripheral steps |
| `qiling/os/blob/blob.py` | `QlOsBlob`: minimal run loop for raw binaries |
| `qiling/extensions/multitask.py` | `UnicornTask` / `MultiTaskUnicorn`: cooperative multitasking over Unicorn |
## Key Types and Entry Points
- `qiling/os/mcu/mcu.py:41` - `QlOsMcu(QlOs)` - run loop; steps hardware between execution chunks and delivers interrupts.
- `qiling/os/mcu/mcu.py:17` - `MCUTask(UnicornTask)` - the firmware execution task.
- `qiling/extensions/multitask.py:26` / `:152` - `UnicornTask` / `MultiTaskUnicorn(Uc)` - task-switching Unicorn subclass MCU mode runs on.
- `qiling/os/blob/blob.py:14` - `QlOsBlob(QlOs)` - runs `entry_point` → `exit_point` with no OS services.
- MCU selection: `QL_OS.MCU` is the only member of `QL_OS_BAREMETAL` (`qiling/const.py:74`); `ql.baremetal` (`qiling/core.py:357`) gates hardware-manager creation.
## Interactions
- Both subclass [os-base.md](os-base.md) `QlOs`.
- MCU mode drives [hw.md](hw.md): `QlHwManager.step()` is called between execution slices; board/chip memory maps come from `qiling/extensions/mcu/` passed as `env=`.
- Interrupt entry/exit uses `QlInterruptContext` in [arch.md](arch.md) (`qiling/arch/cortex_m.py:25`).
- Firmware images are loaded by `QlLoaderMCU` / `QlLoaderBLOB` ([loader.md](loader.md)); MCU profiles are YAML (`profile_setup`, [core.md](core.md)).
- Fuzzing MCU firmware (e.g. `examples/fuzzing/stm32f429/`) combines this mode with [extensions.md](extensions.md) AFL support.
## How to Test
```sh
cd tests && python3 -m unittest test_blob.BlobTest.test_uboot_arm # pass = "OK", exit 0
```
- MCU (also proves [hw.md](hw.md)): `cd tests && python3 test_mcu.py` — pass = `Ran 18 tests … OK`.
- Qualcomm EDL loader: `cd tests && python3 test_edl.py` — pass = `Ran 1 test … OK`.
- The whole `test_blob.py` file does **not** pass from a clean checkout; see
Open Gaps.
## Open Gaps / Roadmap
- Supported chip families are those with board definitions in `qiling/extensions/mcu/` (STM32F1/F4, GD32VF1, NXP, Atmel, BES); new chips need new peripheral maps.
- BLOB mode provides no services by design — targets needing hardware must use MCU mode instead.
- `test_blob.BlobTest.test_blob_raw` (`tests/test_blob.py:85`) errors with
`FileNotFoundError` on a clean checkout: it reads
`examples/rootfs/blob/example_raw.bin` (`tests/test_blob.py:96`), but the
pinned `examples/rootfs` submodule ships only `u-boot.bin.img`. Either the
fixture must be added upstream and the submodule bumped, or the test skipped
when the fixture is absent.
+60
View File
@@ -0,0 +1,60 @@
# OS base — shared OS-layer services
## Goal
Own the OS-agnostic services every OS personality builds on: the abstract
`QlOs` class, the emulated memory manager and heap, function-call argument
marshalling (via calling conventions), filesystem virtualization (rootfs
confinement + host mapping), file descriptor objects, green threads, and
API-call statistics. Mature released infrastructure; maturity-based status.
## Status
`done` — path virtualization and struct helpers have dedicated unit tests;
everything else is exercised by every OS suite.
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/os/os.py` | Abstract `QlOs`: utils/stats/path/fs-mapper composition, `set_api`, user-defined API dict, abstract `run()` |
| `qiling/os/memory.py` | `QlMemoryManager` (map/unmap/search, `ql.mem`) and `QlMemoryHeap` |
| `qiling/os/fcall.py` | `QlFunctionCall`: read args / write retval using `qiling/cc/` conventions |
| `qiling/os/mapper.py` | `QlFsMapper`/`QlFsMappedObject`: redirect emulated paths to host files/objects |
| `qiling/os/path.py` | `QlOsPath`: rootfs confinement and virtual→host path conversion |
| `qiling/os/filestruct.py`, `disk.py` | File-descriptor and raw-disk objects |
| `qiling/os/thread.py` | `QlThread(Greenlet)` base for multithread emulation |
| `qiling/os/stats.py` | `QlOsStats`: API/syscall call statistics |
| `qiling/os/struct.py`, `utils.py`, `const.py` | ctypes-based struct helpers, OS utils, shared constants |
## Key Types and Entry Points
- `qiling/os/os.py:24` - `QlOs` - composes utils/stats/path/fs-mapper; `user_defined_api` keyed by `QL_INTERCEPT`; abstract `run()` (`:240`).
- `qiling/os/os.py:225` - `QlOs.set_api(target, handler, intercept)` - user override of an emulated API (address- or name-based).
- `qiling/os/memory.py:40` - `QlMemoryManager` - `ql.mem`; instantiated by core *before* the OS (`qiling/core.py:188`).
- `qiling/os/memory.py:678` - `QlMemoryHeap` - heap used by Windows/UEFI APIs and the sanitizers extension.
- `qiling/os/fcall.py:21` - `QlFunctionCall` - argument/return marshalling on top of `qiling/cc/`.
- `qiling/os/mapper.py:64` - `QlFsMapper` - behind `ql.add_fs_mapper` (`qiling/core.py:701`).
- `qiling/os/path.py:16` - `QlOsPath` - rootfs-confined path resolution.
- `qiling/os/thread.py:11` - `QlThread(Greenlet)` - gevent-based thread base.
## Interactions
- Instantiated by [core.md](core.md): memory at `qiling/core.py:188`, the concrete OS at `:189` (via `select_os`).
- Uses [arch.md](arch.md) calling conventions (`qiling/cc/`) inside `QlFunctionCall`.
- Subclassed by [os-posix.md](os-posix.md) (`QlOsPosix`), [os-windows.md](os-windows.md) (`QlOsWindows`, `QlOsUefi`, `QlOsDos`), and [os-baremetal.md](os-baremetal.md) (`QlOsMcu`, `QlOsBlob`).
- [loader.md](loader.md) maps segments through `QlMemoryManager`.
- The heap is wrapped by `QlSanitizedMemoryHeap` in [extensions.md](extensions.md).
## How to Test
```sh
cd tests && python3 test_pathutils.py && python3 test_struct.py # pass = both print "OK", exit 0
```
- `test_pathutils.py` proves rootfs path virtualization (incl. case-insensitive Windows semantics); `test_struct.py` proves the struct helpers.
## Open Gaps / Roadmap
- Multithread emulation (gevent) is opt-in via `multithread=True` and less battle-tested than single-threaded mode (see `tests/test_elf_multithread.py`).
- `QlOs.run()` contract is loose: each personality implements its own loop; no shared scheduler outside POSIX threads and MCU multitask.
+58
View File
@@ -0,0 +1,58 @@
# OS POSIX — Linux, FreeBSD, macOS, QNX
## Goal
Emulate POSIX operating systems by intercepting syscalls: a shared
`QlOsPosix` layer owns the per-arch syscall ABI, number→name mapping, and the
file-descriptor table; per-OS subclasses add their personality (Linux futex
and procfs, macOS mach ports, QNX message passing). Mature released
infrastructure; maturity-based status.
## Status
`done` — Linux is the flagship target with the largest suite; FreeBSD/QNX
covered by dedicated tests; macOS tests are host-gated (see Open Gaps).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/os/posix/posix.py` | `QlOsPosix`: syscall ABI per arch, dispatcher, `set_syscall`, fd table |
| `qiling/os/posix/syscall/` | Syscall implementations, one module per family (unistd, mman, socket, fcntl, signal, ioctl, …) |
| `qiling/os/posix/shm.py`, `msq.py` | SysV IPC (shared memory, message queues) |
| `qiling/os/linux/linux.py` | `QlOsLinux`: syscall hook wiring and the dynamic-ELF run loop |
| `qiling/os/linux/` | `futex.py`, `thread.py` (green threads), `procfs.py`, `map_syscall.py`/`syscall_nums.py`, `kernel_api/` (for .ko emulation) |
| `qiling/os/freebsd/freebsd.py` | `QlOsFreebsd` |
| `qiling/os/macos/macos.py` | `QlOsMacos` + mach ports/tasks/kext support |
| `qiling/os/qnx/qnx.py` | `QlOsQnx` + QNX message passing (`message.py`) |
## Key Types and Entry Points
- `qiling/os/posix/posix.py:48` - `QlOsPosix(QlOs)` - per-arch syscall ABI tables (`:69`), fd table, SysV IPC.
- `qiling/os/posix/posix.py:170` - `load_syscall()` - the dispatcher: syscall id → name (via per-OS `map_syscall`) → handler in `os/posix/syscall/*` or user override.
- `qiling/os/posix/posix.py:132` - `set_syscall(target, handler, intercept)` - user syscall hijack (by number or name, CALL/ENTER/EXIT).
- `qiling/os/posix/posix.py:100` - `__get_syscall_mapper(archtype)` - loads `qiling/os/<os>/map_syscall.py` dynamically.
- `qiling/os/linux/linux.py:26` - `QlOsLinux(QlOsPosix)` - `hook_syscall` (`:137`) registered on interrupt/insn hooks.
- `qiling/os/linux/linux.py:148` - `QlOsLinux.run()` - shellcode single-shot, gevent thread manager when `multithread=True`, or the common path: run ld.so to `elf_entry`, apply lib patches, then run to exit.
## Interactions
- Subclasses [os-base.md](os-base.md) `QlOs`; fd objects come from `qiling/os/filestruct.py`.
- Syscall entry is an arch hook: interrupt/insn hooks from [core.md](core.md) (`QlCoreHooks`) fire `hook_syscall`, using [arch.md](arch.md) registers per the syscall ABI.
- Runs binaries prepared by [loader.md](loader.md) (`QlLoaderELF`, `QlLoaderMACHO`).
- Path/fs access goes through `QlOsPath`/`QlFsMapper` in [os-base.md](os-base.md).
- Fuzzing harnesses in [extensions.md](extensions.md) commonly hijack fd 0 with `pipe.py` and syscalls via `set_syscall`.
## How to Test
```sh
cd tests && python3 test_posix.py # pass = unittest "OK", exit 0
```
- Full POSIX coverage in CI: `tests/test_onlinux.sh` (adds `test_elf.py` targets, `test_elf_multithread.py`, `test_elf_ko.py`, `test_qnx.py`, `test_android.py`).
## Open Gaps / Roadmap
- macOS emulation tests (`test_macho.py`, `test_macho_kext.py`) need a macOS host with collected dylibs; the macOS CI job is commented out in `.github/workflows/build-ci.yml`.
- `test_elf_ko.py` requires a one-time `unzip -P infected m0hamed_rootkit.ko.zip` in `examples/rootfs/x86_linux/kernel/`.
- Syscall coverage is demand-driven — unimplemented syscalls raise/log and get added as targets need them.
+59
View File
@@ -0,0 +1,59 @@
# OS Windows family — Windows, UEFI, DOS
## Goal
Emulate API-call-driven (rather than syscall-driven) environments. Windows:
Win32/NT API emulation with handles, registry, and PE process structures.
UEFI: boot/runtime/SMM services and protocol database for DXE/SMM modules.
DOS: BIOS/DOS interrupt services for COM/EXE/MBR targets. Mature released
infrastructure; maturity-based status.
## Status
`done` — Windows PE suite runs in CI on a Windows host; UEFI and DOS suites
run on Linux. Windows tests are platform-gated locally (need collected DLLs).
## Code Structure
| File | Role |
| ---- | ---- |
| `qiling/os/windows/windows.py` | `QlOsWindows`: component setup (handles, registry, clipboard, fiber), run loop |
| `qiling/os/windows/dlls/` | Win32 API implementations per DLL (kernel32/, ntdll.py, msvcrt.py, advapi32.py, …) |
| `qiling/os/windows/fncc.py`, `api.py` | Decorators declaring API signatures/calling conventions |
| `qiling/os/windows/handle.py`, `registry.py`, `clipboard.py`, `fiber.py`, `thread.py`, `structs.py` | Windows kernel-object emulation |
| `qiling/os/uefi/uefi.py` | `QlOsUefi` run loop |
| `qiling/os/uefi/bs.py`, `rt.py`, `ds.py`, `smm.py`, `protocols/` | Boot/runtime/DXE/SMM services and protocol implementations |
| `qiling/os/uefi/context.py` | UEFI execution contexts populated by the loader |
| `qiling/os/dos/dos.py` | `QlOsDos` run loop |
| `qiling/os/dos/interrupts/` | INT 10h/13h/15h/16h/21h… service handlers |
## Key Types and Entry Points
- `qiling/os/windows/windows.py:33` - `QlOsWindows(QlOs)` - `__setup_components` (`:156`) builds handle manager/registry/clipboard/fiber; `run()` (`:201`).
- `qiling/os/windows/fncc.py` - `@winsdkapi` decorator - declares an API's calling convention and typed params; implementations live in `dlls/`.
- API dispatch is address-based: IAT addresses recorded by the PE loader are hooked and marshalled through `QlFunctionCall` — user overrides via `QlOs.set_api` (`qiling/os/os.py:225`).
- `qiling/os/uefi/uefi.py:22` - `QlOsUefi(QlOs)` - executes DXE/SMM modules; services in `bs.py`/`rt.py`/`smm.py` are installed as callable tables.
- `qiling/os/dos/dos.py:33` - `QlOsDos(QlOs)` - dispatches BIOS/DOS interrupts from `interrupts/`.
## Interactions
- All three subclass [os-base.md](os-base.md) `QlOs` and allocate from `QlMemoryHeap`.
- [loader.md](loader.md): `QlLoaderPE` builds PEB/TEB and records IAT hook addresses; `QlLoaderPE_UEFI` installs protocols into `qiling/os/uefi/context.py`; `QlLoaderDOS` sets real-mode state.
- Argument marshalling uses `QlFunctionCall` + `qiling/cc/intel.py` conventions ([arch.md](arch.md)).
- The registry emulation reads hive files from the rootfs via [os-base.md](os-base.md) path services.
- `examples/uefi_sanitized_heap.py` pairs UEFI with the heap sanitizer from [extensions.md](extensions.md).
## How to Test
```sh
cd tests && python3 test_uefi.py # pass = unittest "OK", exit 0 (Linux-runnable)
```
- Windows host only: `cd tests && cmd.exe /C test_pe.bat` after running `examples/scripts/dllscollector.bat` (CI does this in `.github/workflows/build-ci.yml`).
- DOS: `cd tests && python3 test_dos.py` (uses scripted keyboard input).
## Open Gaps / Roadmap
- Windows tests require a Windows host: system DLLs/registry hives are not redistributable and must be collected locally.
- Win32 API surface is demand-driven; unimplemented APIs raise `QlErrorSyscallNotFound`-style errors and are added as samples need them.
- SMM emulation covers common protocols only (see `qiling/os/uefi/smm.py`).
Symlink
+1
View File
@@ -0,0 +1 @@
ARCHITECTURE.md
+4
View File
@@ -208,6 +208,10 @@ Qiling Framework emulates MBR
Qiling also provides a friendly tool named `qltool` to quickly emulate shellcode & executable binaries.
Installing Qiling with pip also installs the `qltool` command, including
`qltool qltui` for the terminal UI. In a source checkout, use `./qltool` as
shown below. Example binaries and rootfs files must be downloaded separately.
With qltool, easy execution can be performed:
@@ -1,4 +1,4 @@
from future import __annotations__
from __future__ import annotations
from typing import TYPE_CHECKING, List
+13 -4
View File
@@ -1,13 +1,13 @@
[tool.poetry]
urls = { homepage = "https://qiling.io" }
name = "qiling"
version = "1.4.10"
version = "1.4.11"
description = "Qiling is an advanced binary emulation framework that cross-platform-architecture"
authors = ["KaiJern Lau (xwings) <[email protected]>"]
license = "GPLv2"
license = "GPL-2.0-or-later"
readme = "README.md"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Disassemblers",
"Topic :: System :: Emulators",
@@ -24,6 +24,15 @@ keywords = [
"IoT",
]
packages = [
{ include = "qiling" },
{ include = "qltui.py" },
]
include = [{ path = "qltool", format = "sdist" }]
[tool.poetry.scripts]
qltool = "qiling.cli:run"
[tool.poetry.dependencies]
python = "^3.10"
capstone = "^5"
@@ -50,5 +59,5 @@ fuzz = ["unicornafl", "fuzzercorn"]
RE = ["r2libr"]
[build-system]
requires = ["poetry-core"]
requires = ["poetry-core>=2.0,<3.0"]
build-backend = "poetry.core.masonry.api"
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
import argparse
import os
import sys
import ast
import pickle
from pprint import pprint
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Literal, Mapping, Type, overload
from unicorn import __version__ as uc_ver
from qiling import __version__ as ql_ver
from qiling import Qiling
from qiling.arch import utils as arch_utils
from qiling.debugger.qdb import QlQdb
from qiling.const import QL_ENDIAN, QL_VERBOSE, endian_map, os_map, arch_map, verbose_map
from qiling.extensions.coverage import utils as cov_utils
from qiling.extensions import report
if TYPE_CHECKING:
from enum import Enum
@overload
def read_file(fname: AnyStr) -> str: ...
@overload
def read_file(fname: AnyStr, mode: Literal['b']) -> bytes: ...
def read_file(fname: AnyStr, mode: Literal['b', ''] = ''):
"""Read file contents.
"""
with open(fname, f'r{mode}') as f:
content = f.read()
return content
class __arg_env(argparse.Action):
def __call__(self, parser, namespace, values: str, option_string):
if os.path.exists(values):
with open(values, 'rb') as f:
env = pickle.load(f)
else:
env = ast.literal_eval(values)
setattr(namespace, self.dest, env or {})
def __make_enum_arg(enum_rmap: Mapping[str, 'Enum'], aliases: Mapping[str, str] = {}) -> Type[argparse.Action]:
class __enum_arg(argparse.Action):
def __call__(self, parser, namespace, values: str, option_string):
values = values.casefold()
if values in aliases:
values = aliases[values]
setattr(namespace, self.dest, enum_rmap[values])
return __enum_arg
__arg_endian = __make_enum_arg(endian_map)
__arg_archtype = __make_enum_arg(arch_map, {'x86_64': 'x8664', 'riscv32': 'riscv'})
__arg_ostype = __make_enum_arg(os_map, {'darwin': 'macos'})
__arg_verbose = __make_enum_arg(verbose_map)
def handle_code(options: argparse.Namespace) -> Dict[str, Any]:
if options.format == 'hex':
if options.input is not None:
print("Loading HEX from ARGV")
payload = str(options.input)
elif options.filename is not None:
print("Loading HEX from FILE")
payload = read_file(options.filename)
else:
print("Error: please specify an input source for hex")
exit(1)
code = bytes.fromhex(payload.replace(r'\x', ''))
elif options.format == 'asm':
print("Loading ASM from FILE")
if options.filename is None:
print("Error: please specify an input source for asm")
exit(1)
payload = read_file(options.filename)
# assemble the payload and turn it into opcodes bytes
assembler = arch_utils.assembler(options.arch, options.endian, options.thumb)
code, _ = assembler.asm(payload)
code = bytes(code)
elif options.format == 'bin':
print("Loading BIN from FILE")
if options.filename is None:
print("Error: please specify an input source for bin")
exit(1)
code = read_file(options.filename, 'b')
ql_args = {
'rootfs': options.rootfs,
'code': code,
'ostype': options.os,
'archtype': options.arch,
'endian': options.endian,
'thumb': options.thumb
}
return ql_args
def handle_run(options: argparse.Namespace) -> Dict[str, Any]:
effective_argv = []
# with argv
if options.filename is not None and options.run_args == []:
effective_argv = [options.filename] + options.args
# Without argv
elif options.filename is None and options.args == [] and options.run_args != []:
effective_argv = options.run_args
else:
print("ERROR: Command error!")
ql_args = {
'argv': effective_argv,
'rootfs': options.rootfs
}
return ql_args
def handle_examples(parser: argparse.ArgumentParser):
prog = parser.prog
__ql_examples = f"""Examples:
With code:
{prog} code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex
{prog} code --os linux --arch x86 --format asm -f examples/shellcodes/lin32_execve.asm
With binary file:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux
With binary file and Qdb:
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb --rr
With binary file and gdbserver:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux
With binary file and additional argv:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_args --rootfs examples/rootfs/x8664_linux --args test1 test2 test3
With binary file and various output format:
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --verbose disasm
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --filter ^open
With UEFI file:
{prog} run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --env examples/rootfs/x8664_efi/rom2_nvar.pickel
With binary file and json output:
{prog} run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows --no-console --json
"""
parser.exit(0, __ql_examples)
def run():
parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version=f'qltool for Qiling {ql_ver}, using Unicorn {uc_ver}')
commands = parser.add_subparsers(title='sub commands', description='select execution mode', dest='subcommand', required=True)
# set "run" subcommand options
run_parser = commands.add_parser('run', help='run a program')
run_parser.add_argument('-f', '--filename', default=None, metavar="FILE", help="filename")
run_parser.add_argument('--rootfs', required=True, help='emulated rootfs')
run_parser.add_argument('--args', default=[], nargs=argparse.REMAINDER, dest="args", help="args")
run_parser.add_argument('run_args', default=[], nargs=argparse.REMAINDER)
# set "code" subcommand options
code_parser = commands.add_parser('code', help='execute a shellcode')
code_parser.add_argument('-f', '--filename', metavar="FILE", help="filename")
code_parser.add_argument('-i', '--input', metavar="INPUT", dest="input", help='input hex value')
code_parser.add_argument('--arch', required=True, choices=arch_map, action=__arg_archtype)
code_parser.add_argument('--thumb', action='store_true', default=False, help='specify thumb mode for ARM')
code_parser.add_argument('--endian', choices=endian_map, default=QL_ENDIAN.EL, action=__arg_endian, help='specify endianness for bi-endian archs')
code_parser.add_argument('--os', required=True, choices=os_map, action=__arg_ostype)
code_parser.add_argument('--rootfs', default='.', help='emulated root filesystem, that is where all libraries reside')
code_parser.add_argument('--format', choices=('asm', 'hex', 'bin'), default='bin', help='input file format')
# set "examples" subcommand
expl_parser = commands.add_parser('examples', help='show examples and exit', add_help=False)
# set "qltui" subcommand
qltui_parser = commands.add_parser('qltui', help='show qiling Terminal User Interface', add_help=False)
comm_parser = run_parser
if len(sys.argv) > 1 and sys.argv[1] == 'code':
comm_parser = code_parser
# set common options
comm_parser.add_argument('-v', '--verbose', choices=verbose_map, default=QL_VERBOSE.DEFAULT, action=__arg_verbose, help='set verbosity level')
comm_parser.add_argument('--env', metavar="FILE", action=__arg_env, default={}, help="pickle file containing an environment dictionary")
comm_parser.add_argument('-g', '--gdb', nargs='?', metavar='SERVER:PORT', const='gdb', help='enable gdb server')
comm_parser.add_argument('--qdb', action='store_true', help='attach Qdb at entry point, it\'s MIPS, ARM(THUMB) supported only for now')
comm_parser.add_argument('--rr', action='store_true', help='switch on record and replay feature in qdb, only works with --qdb')
comm_parser.add_argument('--profile', help="define a customized profile")
comm_parser.add_argument('--no-console', action='store_false', dest='console', help='do not emit output to console')
comm_parser.add_argument('-e', '--filter', metavar='REGEXP', default=None, help="apply a filtering regexp on log output")
comm_parser.add_argument('--log-file', help="write log to a file")
comm_parser.add_argument('--log-plain', action='store_true', help="do not use colors in log output")
comm_parser.add_argument('--root', action='store_true', help='enable sudo required mode')
comm_parser.add_argument('--debug-stop', action='store_true', help='stop running on error; requires verbose to be set to either "debug" or "dump"')
comm_parser.add_argument('-m', '--multithread', action='store_true', help='run in multithread mode')
comm_parser.add_argument('--timeout', type=int, default=0, help='set emulation timeout')
comm_parser.add_argument('-c', '--coverage-file', default=None, help='code coverage file name')
comm_parser.add_argument('--coverage-format', default='drcov', choices=cov_utils.factory.formats, help='code coverage file format')
comm_parser.add_argument('--json', action='store_true', help='print a json report of the emulation')
comm_parser.add_argument('--libcache', action='store_true', help='enable dll caching for windows')
options = parser.parse_args()
qltui_enabled = False
ql_args = {}
if options.subcommand == 'qltui':
import qltui
options = qltui.get_data()
qltui_enabled = True
if options.subcommand == 'examples':
handle_examples(parser)
# ql file setup
elif options.subcommand == 'run':
ql_args = handle_run(options)
# ql code setup
elif options.subcommand == 'code':
ql_args = handle_code(options)
ql_args.update({
'env': options.env,
'verbose': options.verbose,
'profile': options.profile,
'console': options.console,
'filter': options.filter,
'log_devices': options.log_file and [options.log_file],
'log_plain': options.log_plain,
'multithread': options.multithread,
'libcache': options.libcache
})
ql = Qiling(**ql_args)
# attach Qdb at entry point
if options.qdb:
QlQdb(ql, rr=bool(options.rr)).run()
parser.exit(0)
# ql execute additional options
if options.gdb:
argval = options.gdb
if argval != 'gdb':
argval = f'gdb:{argval}'
ql.debugger = argval
if options.debug_stop:
if options.verbose not in (QL_VERBOSE.DEBUG, QL_VERBOSE.DUMP):
parser.error('the debug_stop option requires verbose to be set to either "debug" or "dump"')
ql.debug_stop = True
if options.root:
ql.os.root = True
if qltui_enabled:
hook_dictionary = qltui.hook(ql)
# ql run
if options.coverage_file:
with cov_utils.collect_coverage(ql, options.coverage_format, options.coverage_file):
ql.run(timeout=options.timeout)
else:
ql.run(timeout=options.timeout)
if options.json:
rep = report.generate_report(ql)
if qltui_enabled:
rep["syscalls"] = qltui.transform_syscalls(ql.os.stats.syscalls)
qltui.show_report(ql, rep, hook_dictionary)
else:
pprint(rep)
exit(ql.os.exit_code)
if __name__ == '__main__':
run()
+79 -25
View File
@@ -15,6 +15,7 @@
import os
import socket
import select
import re
import tempfile
from functools import partial
@@ -30,7 +31,7 @@ from unicorn.unicorn_const import (
)
from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS, QL_STATE
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS
from qiling.debugger import QlDebugger
from qiling.debugger.gdb.xmlregs import QlGdbFeatures
from qiling.debugger.gdb.utils import QlGdbUtils
@@ -56,6 +57,21 @@ SIGCHLD = 16
SIGCONT = 17
SIGSTOP = 18
# translate a unicorn cpu fault into the closest posix signal, shared by the
# continue ('c') and single-step ('s') handlers when emulation faults
UC_ERROR_SIGMAP = {
UC_ERR_READ_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_PROT : SIGSEGV,
UC_ERR_READ_PROT : SIGSEGV,
UC_ERR_FETCH_PROT : SIGSEGV,
UC_ERR_READ_UNALIGNED : SIGBUS,
UC_ERR_WRITE_UNALIGNED : SIGBUS,
UC_ERR_FETCH_UNALIGNED : SIGBUS,
UC_ERR_INSN_INVALID : SIGILL
}
# common replies
REPLY_ACK = b'+'
REPLY_EMPTY = b''
@@ -124,6 +140,10 @@ class QlGdb(QlDebugger):
server = GdbSerialConn(self.ip, self.port, self.ql.log)
killed = False
# let the run hook break into a free-running guest when the client sends
# an async interrupt (ctrl-c / \x03); see QlGdbUtils.dbg_hook.
self.gdb.check_interrupt = server.poll_interrupt
def __hexstr(value: int, nibbles: int = 0) -> str:
"""Encode a value into a hex string.
"""
@@ -184,6 +204,7 @@ class QlGdb(QlDebugger):
from unicorn.arm64_const import UC_ARM64_REG_X29
from unicorn.mips_const import UC_MIPS_REG_INVALID
from unicorn.ppc_const import UC_PPC_REG_31
from unicorn.riscv_const import UC_RISCV_REG_S0
arch_uc_bp = {
QL_ARCH.X86 : UC_X86_REG_EBP,
@@ -193,7 +214,9 @@ class QlGdb(QlDebugger):
QL_ARCH.MIPS : UC_MIPS_REG_INVALID, # skipped
QL_ARCH.A8086 : UC_X86_REG_EBP,
QL_ARCH.CORTEX_M : UC_ARM_REG_R11,
QL_ARCH.PPC : UC_PPC_REG_31
QL_ARCH.PPC : UC_PPC_REG_31,
QL_ARCH.RISCV : UC_RISCV_REG_S0,
QL_ARCH.RISCV64 : UC_RISCV_REG_S0
}[self.ql.arch.type]
def __get_reg_idx(ucreg: int) -> int:
@@ -226,33 +249,24 @@ class QlGdb(QlDebugger):
try:
self.gdb.resume_emu()
except UcError as err:
sigmap = {
UC_ERR_READ_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_PROT : SIGSEGV,
UC_ERR_READ_PROT : SIGSEGV,
UC_ERR_FETCH_PROT : SIGSEGV,
UC_ERR_READ_UNALIGNED : SIGBUS,
UC_ERR_WRITE_UNALIGNED : SIGBUS,
UC_ERR_FETCH_UNALIGNED : SIGBUS,
UC_ERR_INSN_INVALID : SIGILL
}
# determine signal from uc error; default to SIGTERM
reply = f'S{sigmap.get(err.errno, SIGTERM):02x}'
reply = f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
except KeyboardInterrupt:
# emulation was interrupted with ctrl+c
reply = f'S{SIGINT:02x}'
else:
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
if self.gdb.interrupted:
# emulation was stopped by an async interrupt from the client
reply = f'S{SIGINT:02x}'
elif getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
# emulation stopped because it hit a breakpoint
reply = f'S{SIGTRAP:02x}'
else:
# emulation has completed successfully
reply = f'W{self.ql.os.exit_code:02x}'
# emulation has completed successfully. note bare-metal os layers
# do not have an exit code (issue #1276)
reply = f'W{getattr(self.ql.os, "exit_code", 0):02x}'
return reply
@@ -662,10 +676,17 @@ class QlGdb(QlDebugger):
for grp in groups:
cmd, *tid = grp.split(':', maxsplit=1)
if cmd in ('c', f'C{SIGTRAP:02x}'):
# 'C sig' and 'S sig' resume or step while delivering a signal
# to the guest. we do not deliver signals, so the signal value
# is ignored and the action is carried out as a plain resume or
# step. matching only 'C05' and 'S05' here made clients that
# resume with any other pending signal (e.g. 'S0f' after a stop
# reply we sent) receive an empty reply and bail out with
# 'Invalid remote reply' (issue #1377)
if cmd[:1] in ('c', 'C'):
return handle_c('')
elif cmd in ('s', f'S{SIGTRAP:02x}'):
elif cmd[:1] in ('s', 'S'):
return handle_s('')
# FIXME: not sure how to handle multiple command
@@ -678,11 +699,23 @@ class QlGdb(QlDebugger):
"""Perform a single step.
"""
self.gdb.resume_emu(steps=1)
try:
self.gdb.resume_emu(steps=1)
except UcError as err:
# stepping faulted; report the closest posix signal
return f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
# if emulation has been stopped, signal program termination
if self.ql.emu_state is QL_STATE.STOPPED:
return f'S{SIGTERM:02x}'
except KeyboardInterrupt:
# emulation was interrupted with ctrl+c
return f'S{SIGINT:02x}'
# emu_start always leaves emu_state as STOPPED after a step, so that
# cannot tell an ordinary step apart from the guest exiting (see
# issues #1377 and #1538). instead, the guest has terminated only
# when the step carried pc all the way to the emulation exit point.
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.exit_point:
# program terminated; report its exit code
return f'W{getattr(self.ql.os, "exit_code", 0):02x}'
# otherwise, this is just single stepping
return f'S{SIGTRAP:02x}'
@@ -823,6 +856,27 @@ class GdbSerialConn:
self.client.close()
self.sock.close()
def poll_interrupt(self) -> bool:
"""Non-blocking check for an async interrupt from the client.
While the target is running the only thing gdb sends is a bare ``\\x03``
break byte (it waits for a stop reply before sending anything else), so
any readable bytes here are that interrupt or a stray protocol ack.
Returns True if a break was seen. Called from the run hook, so it must
never block.
"""
readable, _, _ = select.select([self.client], [], [], 0)
if not readable:
return False
try:
incoming = self.client.recv(self.BUFSIZE)
except (ConnectionError, OSError):
return False
return b'\x03' in incoming
def readpackets(self) -> Iterator[bytes]:
"""Iterate through incoming packets in an active connection until
it is terminated.
+52
View File
@@ -14,16 +14,31 @@ PROMPT = r'gdb>'
class QlGdbUtils:
# how often (in guest instructions) the run hook polls the client socket for
# an async interrupt. small enough to feel instant, large enough that the
# extra non-blocking socket check does not dominate the per-instruction hook.
INTR_POLL_INTERVAL = 200
def __init__(self, ql: Qiling, entry_point: int, exit_point: int):
self.ql = ql
self.exit_point = exit_point
self.swbp = set()
self.last_bp = None
self._need_flush_tb = False
# async-interrupt support: `check_interrupt` is a callable installed by the
# gdb stub that returns True when the client sent a break (ctrl-c / \x03)
# while the target was running. `interrupted` records that the last resume
# stopped for that reason (rather than a breakpoint or normal exit).
self.check_interrupt = None
self.interrupted = False
self._poll_counter = 0
def __entry_point_hook(ql: Qiling):
ql.hook_del(ep_hret)
ql.hook_code(self.dbg_hook)
self._need_flush_tb = True
ql.log.info(f'{PROMPT} stopped at entry point: {ql.arch.regs.arch_pc:#x}')
ql.stop()
@@ -32,10 +47,35 @@ class QlGdbUtils:
# that hook will be used to set up the breakpoint handling hook
ep_hret = ql.hook_address(__entry_point_hook, entry_point)
def flush_tb(self):
"""Flush Unicorn translation blocks after GDB hook or breakpoint updates."""
flush_tb = getattr(self.ql.uc, 'ctl_flush_tb', None)
if flush_tb is not None:
flush_tb()
def dbg_hook(self, ql: Qiling, address: int, size: int):
if getattr(ql.arch, 'is_thumb', False):
address |= 1
# poll for an async interrupt from the client (gdb sends a bare \x03 while
# the target is running). throttled so the socket check stays off the hot
# path. this is the only way to break into a free-running guest, since the
# stub's packet loop is blocked inside emu_start until the target stops.
if self.check_interrupt is not None:
self._poll_counter += 1
if self._poll_counter >= self.INTR_POLL_INTERVAL:
self._poll_counter = 0
if self.check_interrupt():
self.interrupted = True
self.last_bp = None
ql.log.info(f'{PROMPT} interrupted by client, stopped at {address:#x}')
ql.stop()
return
# resuming emulation after hitting a breakpoint will re-enter this hook.
# avoid an endless hooking loop by detecting and skipping this case
if address == self.last_bp:
@@ -56,6 +96,8 @@ class QlGdbUtils:
for bp in targets:
self.swbp.add(bp)
self._need_flush_tb = True
self.ql.log.info(f'{PROMPT} breakpoint added at {addr:#x}')
return True
@@ -69,6 +111,8 @@ class QlGdbUtils:
for bp in targets:
self.swbp.remove(bp)
self._need_flush_tb = True
self.ql.log.info(f'{PROMPT} breakpoint removed from {addr:#x}')
return True
@@ -80,7 +124,15 @@ class QlGdbUtils:
if getattr(self.ql.arch, 'is_thumb', False):
address |= 0b1
if self._need_flush_tb:
self.flush_tb()
self._need_flush_tb = False
op = f'stepping {steps} instructions' if steps else 'resuming'
self.ql.log.info(f'{PROMPT} {op} from {address:#x}')
# clear any pending interrupt state from a previous resume
self.interrupted = False
self._poll_counter = 0
self.ql.emu_start(address, self.exit_point, count=steps)
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!-- Register numbers are hard-coded in order to maintain backward
compatibility with older versions of tools that didn't use xml
register descriptions. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.riscv.cpu">
<reg name="zero" bitsize="32" type="int" regnum="0"/>
<reg name="ra" bitsize="32" type="code_ptr"/>
<reg name="sp" bitsize="32" type="data_ptr"/>
<reg name="gp" bitsize="32" type="data_ptr"/>
<reg name="tp" bitsize="32" type="data_ptr"/>
<reg name="t0" bitsize="32" type="int"/>
<reg name="t1" bitsize="32" type="int"/>
<reg name="t2" bitsize="32" type="int"/>
<reg name="fp" bitsize="32" type="data_ptr"/>
<reg name="s1" bitsize="32" type="int"/>
<reg name="a0" bitsize="32" type="int"/>
<reg name="a1" bitsize="32" type="int"/>
<reg name="a2" bitsize="32" type="int"/>
<reg name="a3" bitsize="32" type="int"/>
<reg name="a4" bitsize="32" type="int"/>
<reg name="a5" bitsize="32" type="int"/>
<reg name="a6" bitsize="32" type="int"/>
<reg name="a7" bitsize="32" type="int"/>
<reg name="s2" bitsize="32" type="int"/>
<reg name="s3" bitsize="32" type="int"/>
<reg name="s4" bitsize="32" type="int"/>
<reg name="s5" bitsize="32" type="int"/>
<reg name="s6" bitsize="32" type="int"/>
<reg name="s7" bitsize="32" type="int"/>
<reg name="s8" bitsize="32" type="int"/>
<reg name="s9" bitsize="32" type="int"/>
<reg name="s10" bitsize="32" type="int"/>
<reg name="s11" bitsize="32" type="int"/>
<reg name="t3" bitsize="32" type="int"/>
<reg name="t4" bitsize="32" type="int"/>
<reg name="t5" bitsize="32" type="int"/>
<reg name="t6" bitsize="32" type="int"/>
<reg name="pc" bitsize="32" type="code_ptr"/>
</feature>
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!-- Register numbers are hard-coded in order to maintain backward
compatibility with older versions of tools that didn't use xml
register descriptions. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.riscv.fpu">
<union id="riscv_double">
<field name="float" type="ieee_single"/>
<field name="double" type="ieee_double"/>
</union>
<reg name="ft0" bitsize="64" type="riscv_double" regnum="33"/>
<reg name="ft1" bitsize="64" type="riscv_double"/>
<reg name="ft2" bitsize="64" type="riscv_double"/>
<reg name="ft3" bitsize="64" type="riscv_double"/>
<reg name="ft4" bitsize="64" type="riscv_double"/>
<reg name="ft5" bitsize="64" type="riscv_double"/>
<reg name="ft6" bitsize="64" type="riscv_double"/>
<reg name="ft7" bitsize="64" type="riscv_double"/>
<reg name="fs0" bitsize="64" type="riscv_double"/>
<reg name="fs1" bitsize="64" type="riscv_double"/>
<reg name="fa0" bitsize="64" type="riscv_double"/>
<reg name="fa1" bitsize="64" type="riscv_double"/>
<reg name="fa2" bitsize="64" type="riscv_double"/>
<reg name="fa3" bitsize="64" type="riscv_double"/>
<reg name="fa4" bitsize="64" type="riscv_double"/>
<reg name="fa5" bitsize="64" type="riscv_double"/>
<reg name="fa6" bitsize="64" type="riscv_double"/>
<reg name="fa7" bitsize="64" type="riscv_double"/>
<reg name="fs2" bitsize="64" type="riscv_double"/>
<reg name="fs3" bitsize="64" type="riscv_double"/>
<reg name="fs4" bitsize="64" type="riscv_double"/>
<reg name="fs5" bitsize="64" type="riscv_double"/>
<reg name="fs6" bitsize="64" type="riscv_double"/>
<reg name="fs7" bitsize="64" type="riscv_double"/>
<reg name="fs8" bitsize="64" type="riscv_double"/>
<reg name="fs9" bitsize="64" type="riscv_double"/>
<reg name="fs10" bitsize="64" type="riscv_double"/>
<reg name="fs11" bitsize="64" type="riscv_double"/>
<reg name="ft8" bitsize="64" type="riscv_double"/>
<reg name="ft9" bitsize="64" type="riscv_double"/>
<reg name="ft10" bitsize="64" type="riscv_double"/>
<reg name="ft11" bitsize="64" type="riscv_double"/>
<reg name="fcsr" bitsize="32" type="int" regnum="68"/>
</feature>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target xmlns:xi="http://www.w3.org/2001/XInclude">
<architecture>riscv:rv32</architecture>
<osabi>GNU/Linux</osabi>
<xi:include href="riscv-core.xml"/>
<xi:include href="riscv-fpu.xml"/>
</target>
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!-- Register numbers are hard-coded in order to maintain backward
compatibility with older versions of tools that didn't use xml
register descriptions. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.riscv.cpu">
<reg name="zero" bitsize="64" type="int" regnum="0"/>
<reg name="ra" bitsize="64" type="code_ptr"/>
<reg name="sp" bitsize="64" type="data_ptr"/>
<reg name="gp" bitsize="64" type="data_ptr"/>
<reg name="tp" bitsize="64" type="data_ptr"/>
<reg name="t0" bitsize="64" type="int"/>
<reg name="t1" bitsize="64" type="int"/>
<reg name="t2" bitsize="64" type="int"/>
<reg name="fp" bitsize="64" type="data_ptr"/>
<reg name="s1" bitsize="64" type="int"/>
<reg name="a0" bitsize="64" type="int"/>
<reg name="a1" bitsize="64" type="int"/>
<reg name="a2" bitsize="64" type="int"/>
<reg name="a3" bitsize="64" type="int"/>
<reg name="a4" bitsize="64" type="int"/>
<reg name="a5" bitsize="64" type="int"/>
<reg name="a6" bitsize="64" type="int"/>
<reg name="a7" bitsize="64" type="int"/>
<reg name="s2" bitsize="64" type="int"/>
<reg name="s3" bitsize="64" type="int"/>
<reg name="s4" bitsize="64" type="int"/>
<reg name="s5" bitsize="64" type="int"/>
<reg name="s6" bitsize="64" type="int"/>
<reg name="s7" bitsize="64" type="int"/>
<reg name="s8" bitsize="64" type="int"/>
<reg name="s9" bitsize="64" type="int"/>
<reg name="s10" bitsize="64" type="int"/>
<reg name="s11" bitsize="64" type="int"/>
<reg name="t3" bitsize="64" type="int"/>
<reg name="t4" bitsize="64" type="int"/>
<reg name="t5" bitsize="64" type="int"/>
<reg name="t6" bitsize="64" type="int"/>
<reg name="pc" bitsize="64" type="code_ptr"/>
</feature>
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!-- Register numbers are hard-coded in order to maintain backward
compatibility with older versions of tools that didn't use xml
register descriptions. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.riscv.fpu">
<union id="riscv_double">
<field name="float" type="ieee_single"/>
<field name="double" type="ieee_double"/>
</union>
<reg name="ft0" bitsize="64" type="riscv_double" regnum="33"/>
<reg name="ft1" bitsize="64" type="riscv_double"/>
<reg name="ft2" bitsize="64" type="riscv_double"/>
<reg name="ft3" bitsize="64" type="riscv_double"/>
<reg name="ft4" bitsize="64" type="riscv_double"/>
<reg name="ft5" bitsize="64" type="riscv_double"/>
<reg name="ft6" bitsize="64" type="riscv_double"/>
<reg name="ft7" bitsize="64" type="riscv_double"/>
<reg name="fs0" bitsize="64" type="riscv_double"/>
<reg name="fs1" bitsize="64" type="riscv_double"/>
<reg name="fa0" bitsize="64" type="riscv_double"/>
<reg name="fa1" bitsize="64" type="riscv_double"/>
<reg name="fa2" bitsize="64" type="riscv_double"/>
<reg name="fa3" bitsize="64" type="riscv_double"/>
<reg name="fa4" bitsize="64" type="riscv_double"/>
<reg name="fa5" bitsize="64" type="riscv_double"/>
<reg name="fa6" bitsize="64" type="riscv_double"/>
<reg name="fa7" bitsize="64" type="riscv_double"/>
<reg name="fs2" bitsize="64" type="riscv_double"/>
<reg name="fs3" bitsize="64" type="riscv_double"/>
<reg name="fs4" bitsize="64" type="riscv_double"/>
<reg name="fs5" bitsize="64" type="riscv_double"/>
<reg name="fs6" bitsize="64" type="riscv_double"/>
<reg name="fs7" bitsize="64" type="riscv_double"/>
<reg name="fs8" bitsize="64" type="riscv_double"/>
<reg name="fs9" bitsize="64" type="riscv_double"/>
<reg name="fs10" bitsize="64" type="riscv_double"/>
<reg name="fs11" bitsize="64" type="riscv_double"/>
<reg name="ft8" bitsize="64" type="riscv_double"/>
<reg name="ft9" bitsize="64" type="riscv_double"/>
<reg name="ft10" bitsize="64" type="riscv_double"/>
<reg name="ft11" bitsize="64" type="riscv_double"/>
<reg name="fcsr" bitsize="32" type="int" regnum="68"/>
</feature>
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2018-2025 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target xmlns:xi="http://www.w3.org/2001/XInclude">
<architecture>riscv:rv64</architecture>
<osabi>GNU/Linux</osabi>
<xi:include href="riscv64-core.xml"/>
<xi:include href="riscv64-fpu.xml"/>
</target>
+9 -1
View File
@@ -42,6 +42,12 @@ from qiling.arch.ppc_const import (
reg_map as ppc_regs
)
from qiling.arch.riscv_const import (
reg_csr_map as riscv_regs_csr,
reg_float_map as riscv_regs_float,
reg_map as riscv_regs
)
from qiling.const import QL_ARCH, QL_OS
RegEntry = Tuple[Optional[int], int, int]
@@ -147,7 +153,9 @@ class QlGdbFeatures:
QL_ARCH.CORTEX_M: dict(**cortex_m_regs),
QL_ARCH.ARM64: dict(**arm64_regs, **arm64_regs_v, **arm64_reg_map_fp),
QL_ARCH.MIPS: dict(**mips_regs_gpr),
QL_ARCH.PPC: dict(**ppc_regs)
QL_ARCH.PPC: dict(**ppc_regs),
QL_ARCH.RISCV: dict(**riscv_regs, **riscv_regs_float, **riscv_regs_csr),
QL_ARCH.RISCV64: dict(**riscv_regs, **riscv_regs_float, **riscv_regs_csr)
}[archtype]
regsinfo = sorted(QlGdbFeatures.__walk_xml_regs(xmltree))
+1
View File
@@ -6,3 +6,4 @@
from .arch_arm import ArchARM, ArchCORTEX_M
from .arch_intel import ArchIntel, ArchX86, ArchX64
from .arch_mips import ArchMIPS
from .arch_riscv import ArchRISCV, ArchRISCV64
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
from __future__ import annotations
from .arch import Arch
class ArchRISCV(Arch):
def __init__(self) -> None:
regs = (
'zero', 'ra', 'sp', 'gp', 'tp',
't0', 't1', 't2', 's0', 's1',
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7',
's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11',
't3', 't4', 't5', 't6', 'pc'
)
aliases = {
's0': 'fp'
}
super().__init__(regs, aliases, 4, 4)
def unalias(self, name: str) -> str:
if name.startswith('x') and name[1:].isdigit():
idx = int(name[1:])
xregs = (
'zero', 'ra', 'sp', 'gp', 'tp',
't0', 't1', 't2', 's0', 's1',
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7',
's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11',
't3', 't4', 't5', 't6'
)
if idx < len(xregs):
return xregs[idx]
return super().unalias(name)
class ArchRISCV64(ArchRISCV):
def __init__(self) -> None:
super().__init__()
self._asize = 8
@@ -7,10 +7,12 @@ from .branch_predictor import BranchPredictor
from .branch_predictor_arm import BranchPredictorARM, BranchPredictorCORTEX_M
from .branch_predictor_intel import BranchPredictorX86, BranchPredictorX64
from .branch_predictor_mips import BranchPredictorMIPS
from .branch_predictor_riscv import BranchPredictorRISCV, BranchPredictorRISCV64
__all__ = [
'BranchPredictor',
'BranchPredictorARM', 'BranchPredictorCORTEX_M',
'BranchPredictorX86', 'BranchPredictorX64',
'BranchPredictorMIPS'
'BranchPredictorMIPS',
'BranchPredictorRISCV', 'BranchPredictorRISCV64'
]
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
from __future__ import annotations
from typing import Callable, Dict, List, Optional, Set
from capstone import CS_OP_IMM, CS_OP_MEM, CS_OP_REG
from .branch_predictor import BranchPredictor, Prophecy
from ..arch import ArchRISCV, ArchRISCV64
from ..misc import InvalidInsn
class BranchPredictorRISCV(BranchPredictor, ArchRISCV):
"""Branch Predictor for RISC-V 32-bit.
"""
stop = 'ebreak'
xlen = 32
supports_c_jal = True
def _unconditional_branches(self) -> Set[str]:
branches = {'j', 'jal', 'jalr', 'jr', 'ret'}
if self.supports_c_jal:
branches.add('c.jal')
return branches
def _normalize_mnemonic(self, mnemonic: str) -> str:
return (mnemonic or '').lower()
def _signed(self, val: int) -> int:
mask = (1 << self.xlen) - 1
sign = 1 << (self.xlen - 1)
val &= mask
return (val ^ sign) - sign
def _unsigned(self, val: int) -> int:
return val & ((1 << self.xlen) - 1)
def predict(self) -> Prophecy:
insn = self.disasm(self.cur_addr, True)
going = False
where = 0
if isinstance(insn, InvalidInsn):
return Prophecy(going, where)
mnemonic = self._normalize_mnemonic(insn.mnemonic)
base = mnemonic[2:] if mnemonic.startswith('c.') else mnemonic
operands: List[object] = list(insn.operands)
conditional: Dict[str, Callable[..., bool]] = {
'beq' : lambda a, b: a == b,
'bne' : lambda a, b: a != b,
'blt' : lambda a, b: a < b,
'bge' : lambda a, b: a >= b,
'bltu': lambda a, b: a < b,
'bgeu': lambda a, b: a >= b,
'beqz': lambda a: a == 0,
'bnez': lambda a: a != 0,
'bgez': lambda a: a >= 0,
'bltz': lambda a: a < 0,
'bgtz': lambda a: a > 0,
'blez': lambda a: a <= 0,
}
def __read_reg(reg: int) -> Optional[int]:
name = insn.reg_name(reg)
return name and self.read_reg(self.unalias(name))
def __parse_op(op: object) -> Optional[int]:
if getattr(op, 'type', None) == CS_OP_REG:
return __read_reg(op.reg)
if getattr(op, 'type', None) == CS_OP_IMM:
return op.imm
if getattr(op, 'type', None) == CS_OP_MEM:
mem = op.mem
base_reg = __read_reg(mem.base) or 0
index = __read_reg(mem.index) or 0
return base_reg + index * mem.scale + mem.disp
return None
def __direct_target() -> Optional[int]:
imms = [op for op in operands if getattr(op, 'type', None) == CS_OP_IMM]
if not imms:
return None
return self.cur_addr + imms[-1].imm
def __indirect_target() -> Optional[int]:
memop = next((op for op in operands if getattr(op, 'type', None) == CS_OP_MEM), None)
if memop is not None:
return __parse_op(memop)
regs = [op for op in operands if getattr(op, 'type', None) == CS_OP_REG]
imms = [op for op in operands if getattr(op, 'type', None) == CS_OP_IMM]
if regs and imms:
return (__parse_op(regs[-1]) or 0) + imms[-1].imm
if regs:
return __parse_op(regs[-1])
return None
unconditional = self._unconditional_branches()
is_unconditional = mnemonic in unconditional or base in unconditional
if mnemonic == 'c.jal' and not self.supports_c_jal:
is_unconditional = False
if is_unconditional:
going = True
if base == 'ret' and not operands:
where = self.read_reg('ra')
elif base in {'jalr', 'jr', 'ret'}:
where = __indirect_target() or 0
else:
where = __direct_target() or 0
if base in {'jalr', 'jr', 'ret'}:
where &= ~1
elif base in conditional:
predicate = conditional[base]
if base in {'beqz', 'bnez', 'bgez', 'bltz', 'bgtz', 'blez'}:
reg = __parse_op(operands[0]) if operands else None
going = reg is not None and predicate(self._signed(reg))
else:
lhs = __parse_op(operands[0]) if len(operands) > 0 else None
rhs = __parse_op(operands[1]) if len(operands) > 1 else None
if base in {'blt', 'bge'}:
lhs = None if lhs is None else self._signed(lhs)
rhs = None if rhs is None else self._signed(rhs)
if base in {'bltu', 'bgeu'}:
lhs = None if lhs is None else self._unsigned(lhs)
rhs = None if rhs is None else self._unsigned(rhs)
going = lhs is not None and rhs is not None and predicate(lhs, rhs)
if going:
where = __direct_target() or 0
return Prophecy(going, where)
class BranchPredictorRISCV64(BranchPredictorRISCV, ArchRISCV64):
"""Branch Predictor for RISC-V 64-bit.
"""
xlen = 64
supports_c_jal = False
+4 -2
View File
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, List, Tuple
from qiling.const import QL_ARCH
from .context import Context
from .arch import ArchCORTEX_M, ArchARM, ArchMIPS, ArchX86, ArchX64
from .arch import ArchCORTEX_M, ArchARM, ArchMIPS, ArchRISCV, ArchRISCV64, ArchX86, ArchX64
if TYPE_CHECKING:
@@ -26,7 +26,9 @@ def setup_command_helper(ql: Qiling):
QL_ARCH.X8664: ArchX64,
QL_ARCH.MIPS: ArchMIPS,
QL_ARCH.ARM: ArchARM,
QL_ARCH.CORTEX_M: ArchCORTEX_M
QL_ARCH.CORTEX_M: ArchCORTEX_M,
QL_ARCH.RISCV: ArchRISCV,
QL_ARCH.RISCV64: ArchRISCV64,
}
ret = type('CommandHelper', (CommandHelper, atypes[ql.arch.type]), {})
+1
View File
@@ -7,3 +7,4 @@ from .render import ContextRender
from .render_intel import ContextRenderX86, ContextRenderX64
from .render_mips import ContextRenderMIPS
from .render_arm import ContextRenderARM, ContextRenderCORTEX_M
from .render_riscv import ContextRenderRISCV, ContextRenderRISCV64
+3
View File
@@ -108,6 +108,9 @@ class Render:
elements.clear()
if elements:
yield '\t'.join(elements)
for line in __render_regs_line():
print(line)
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
from .render import ContextRender
from ..arch import ArchRISCV, ArchRISCV64
class ContextRenderRISCV(ContextRender, ArchRISCV):
"""Context renderer for RISC-V architecture.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.regs_a_row = 5
def print_mode_info(self) -> None:
pass
class ContextRenderRISCV64(ContextRenderRISCV, ArchRISCV64):
"""Context renderer for RISC-V 64-bit architecture.
"""
+12 -3
View File
@@ -16,7 +16,9 @@ from .render import (
ContextRenderX64,
ContextRenderARM,
ContextRenderCORTEX_M,
ContextRenderMIPS
ContextRenderMIPS,
ContextRenderRISCV,
ContextRenderRISCV64,
)
from .branch_predictor import (
@@ -26,6 +28,8 @@ from .branch_predictor import (
BranchPredictorARM,
BranchPredictorCORTEX_M,
BranchPredictorMIPS,
BranchPredictorRISCV,
BranchPredictorRISCV64,
)
from .const import color, QDB_MSG
@@ -111,13 +115,16 @@ def setup_branch_predictor(ql: Qiling) -> BranchPredictor:
QL_ARCH.X8664: BranchPredictorX64,
QL_ARCH.ARM: BranchPredictorARM,
QL_ARCH.CORTEX_M: BranchPredictorCORTEX_M,
QL_ARCH.MIPS: BranchPredictorMIPS
QL_ARCH.MIPS: BranchPredictorMIPS,
QL_ARCH.RISCV: BranchPredictorRISCV,
QL_ARCH.RISCV64: BranchPredictorRISCV64,
}
p = preds[ql.arch.type]
return p(ql)
def setup_context_render(ql: Qiling, predictor: BranchPredictor) -> ContextRender:
"""Setup context render according to arch.
"""
@@ -127,7 +134,9 @@ def setup_context_render(ql: Qiling, predictor: BranchPredictor) -> ContextRende
QL_ARCH.X8664: ContextRenderX64,
QL_ARCH.ARM: ContextRenderARM,
QL_ARCH.CORTEX_M: ContextRenderCORTEX_M,
QL_ARCH.MIPS: ContextRenderMIPS
QL_ARCH.MIPS: ContextRenderMIPS,
QL_ARCH.RISCV: ContextRenderRISCV,
QL_ARCH.RISCV64: ContextRenderRISCV64,
}
r = rends[ql.arch.type]
+4
View File
@@ -462,6 +462,10 @@ class Process:
self.ql.log.error(f'Error encountered while running {dll_name} DllMain, bailing')
self.ql.arch.regs.restore(regs_state)
# emu_start leaves the state at STARTED when it raises; reset it so a single
# failing DllMain does not gate every subsequently loaded DLL's entry point.
self.ql._state = QL_STATE.STOPPED
else:
fcall.cc.unwind(len(args))
+26 -18
View File
@@ -380,24 +380,32 @@ class linux_mips_socket_level(Enum):
# https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2mips_2include_2uapi_2asm_2socket_8h_source.html
# https://github.com/torvalds/linux/blob/master/arch/mips/include/uapi/asm/socket.h
class linux_mips_socket_options(Enum):
SO_DEBUG = 0x01
SO_REUSEADDR = 0x04
SO_KEEPALIVE = 0x08
SO_DONTROUTE = 0x10
SO_BINDTODEVICE = 0x19
SO_BROADCAST = 0x20
SO_LINGER = 0x80
SO_OOBINLINE = 0x00
SO_REUSEPORT = 0x00
SO_SNDBUF = 0x01
SO_RCVBUF = 0x02
SO_SNDLOWAT = 0x03
SO_RCVLOWAT = 0x04
SO_SNDTIMEO_OLD = 0x05
SO_RCVTIMEO_OLD = 0x06
SO_TIMESTAMP_OLD = 0x1d
# SO_TIMESTAMPNS_OLD = 0x23
# SO_TIMESTAMPING_OLD = 0x25
# MIPS uses its own SO_* numbering (arch/mips/include/uapi/asm/socket.h);
# the buffer/timeout/type options live in the 0x1000 range, unlike the
# generic asm-generic values.
SO_DEBUG = 0x0001
SO_REUSEADDR = 0x0004
SO_KEEPALIVE = 0x0008
SO_DONTROUTE = 0x0010
SO_BINDTODEVICE = 0x0019
SO_BROADCAST = 0x0020
SO_LINGER = 0x0080
SO_OOBINLINE = 0x0100
SO_REUSEPORT = 0x0200
SO_TYPE = 0x1008
SO_ERROR = 0x1007
SO_SNDBUF = 0x1001
SO_RCVBUF = 0x1002
SO_SNDLOWAT = 0x1003
SO_RCVLOWAT = 0x1004
SO_SNDTIMEO_OLD = 0x1005
SO_RCVTIMEO_OLD = 0x1006
SO_ACCEPTCONN = 0x1009
SO_PROTOCOL = 0x1028
SO_DOMAIN = 0x1029
SO_TIMESTAMP_OLD = 0x001d
# SO_TIMESTAMPNS_OLD = 0x0023
# SO_TIMESTAMPING_OLD = 0x0025
SO_TIMESTAMP_NEW = 0x3f
SO_TIMESTAMPNS_NEW = 0x40
SO_TIMESTAMPING_NEW = 0x41
+32
View File
@@ -130,6 +130,38 @@ def ql_syscall_clone(ql: Qiling, flags: int, child_stack: int, parent_tidptr: in
return regreturn
def ql_syscall_clone3(ql: Qiling, cl_args: int, size: int):
# clone3(struct clone_args *uargs, size_t size). Translate the struct into
# the legacy clone() argument set and delegate. Modern glibc (used by recent
# toolchains) issues clone3 from pthread_create; without this it falls through
# to the "not implemented" path and thread creation fails.
#
# struct clone_args (all fields __aligned_u64):
# 0:flags 8:pidfd 16:child_tid 24:parent_tid 32:exit_signal
# 40:stack 48:stack_size 56:tls 64:set_tid 72:set_tid_size 80:cgroup
flags = ql.mem.read_ptr(cl_args + 0, 8)
child_tid = ql.mem.read_ptr(cl_args + 16, 8)
parent_tid = ql.mem.read_ptr(cl_args + 24, 8)
exit_signal = ql.mem.read_ptr(cl_args + 32, 8)
stack = ql.mem.read_ptr(cl_args + 40, 8)
stack_size = ql.mem.read_ptr(cl_args + 48, 8)
tls = ql.mem.read_ptr(cl_args + 56, 8)
# legacy clone() takes the highest stack address; clone3 gives the base + size
child_stack = (stack + stack_size) if stack else 0
# clone3 keeps exit_signal in its own field; legacy clone packs it into the
# low CSIGNAL byte of flags
flags |= exit_signal & 0xff
# ql_syscall_clone swaps newtls<->child_tidptr for x8664 to undo that arch's
# raw-syscall register order. clone3 hands us already-logical args, so on
# x8664 we pre-swap to cancel that out.
if ql.arch.type == QL_ARCH.X8664:
return ql_syscall_clone(ql, flags, child_stack, parent_tid, child_tid, tls)
return ql_syscall_clone(ql, flags, child_stack, parent_tid, tls, child_tid)
def ql_syscall_sched_yield(ql: Qiling):
def _sched_yield(cur_thread):
gevent.sleep(0)
+18 -1
View File
@@ -926,7 +926,15 @@ def __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
d_off = 0
d_name = (result.name if isinstance(result, os.DirEntry) else result._str).encode() + b'\x00'
d_type = _type_mapping(result)
d_reclen = n + n + 2 + len(d_name) + 1
# The kernel rounds each record up to the alignment of its leading
# (pointer-sized) d_ino, so the next record's d_ino stays aligned.
# Without this, strict-alignment guests (e.g. MIPS) fault with an
# unaligned load when walking the buffer; x86 tolerates it, which is
# why it was never caught. Matches the kernel's ALIGN(reclen,
# sizeof(long)) for both getdents and getdents64.
unaligned_reclen = n + n + 2 + len(d_name) + 1
d_reclen = (unaligned_reclen + (n - 1)) & ~(n - 1)
# TODO: Dirty fix for X8664 MACOS 11.6 APFS
# For some reason MACOS return int value is 64bit
@@ -936,6 +944,8 @@ def __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
packed_d_ino = (ql.pack64(d_ino), n)
if is_64:
# getdents64 places d_type *before* d_name, so the trailing pad
# bytes are inert.
fields = (
(ql.pack64(d_ino), n),
(ql.pack64(d_off), n),
@@ -944,11 +954,18 @@ def __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
(d_name, len(d_name))
)
else:
# legacy linux_dirent stores d_type in the record's last byte
# (offset d_reclen-1), so the alignment padding goes *between*
# d_name and d_type to keep d_type there. padding without
# relocating d_type is what got the earlier blanket fix
# (PR #1419) reverted.
pad = d_reclen - unaligned_reclen
fields = (
packed_d_ino,
(ql.pack(d_off), n),
(ql.pack16(d_reclen), 2),
(d_name, len(d_name)),
(b'\x00' * pad, pad),
(d_type, 1)
)
@@ -25,6 +25,15 @@ def hook_FlsFree(ql: Qiling, address: int, params):
def hook_FlsGetValue(ql: Qiling, address: int, params):
return ql.os.fiber_manager.get(params['dwFlsIndex'])
# LPVOID FlsGetValue2(
# DWORD dwFlsIndex
# );
@winsdkapi(cc=STDCALL, params={
'dwFlsIndex' : DWORD
})
def hook_FlsGetValue2(ql: Qiling, address: int, params):
return ql.os.fiber_manager.get_noerr(params['dwFlsIndex'])
# BOOL FlsSetValue(
# DWORD dwFlsIndex
# PVOID lpFlsData
+10 -3
View File
@@ -33,7 +33,7 @@ class FiberManager:
def free(self, idx: int) -> bool:
if idx not in self.fibers:
self.last_error = ERROR_INVALID_PARAMETER
self.ql.os.last_error = ERROR_INVALID_PARAMETER
return False
fiber = self.fibers[idx]
@@ -99,7 +99,7 @@ class FiberManager:
def set(self, idx: int, data: int) -> bool:
if idx not in self.fibers:
self.last_error = ERROR_INVALID_PARAMETER
self.ql.os.last_error = ERROR_INVALID_PARAMETER
return False
self.fibers[idx].data = data
@@ -108,7 +108,14 @@ class FiberManager:
def get(self, idx: int) -> int:
if idx not in self.fibers:
self.last_error = ERROR_INVALID_PARAMETER
self.ql.os.last_error = ERROR_INVALID_PARAMETER
return 0
return self.fibers[idx].data
# equivalent to get, but does not set errors; used for FlsGetValue2
def get_noerr(self, idx: int) -> int:
if idx not in self.fibers:
return 0
return self.fibers[idx].data
+1 -315
View File
@@ -3,322 +3,8 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
import argparse
import os
import sys
import ast
import pickle
from qiling.cli import run
from pprint import pprint
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Literal, Mapping, Type, overload
from unicorn import __version__ as uc_ver
from qiling import __version__ as ql_ver
from qiling import Qiling
from qiling.arch import utils as arch_utils
from qiling.debugger.qdb import QlQdb
from qiling.const import QL_ENDIAN, QL_VERBOSE, endian_map, os_map, arch_map, verbose_map
from qiling.extensions.coverage import utils as cov_utils
from qiling.extensions import report
if TYPE_CHECKING:
from enum import Enum
@overload
def read_file(fname: AnyStr) -> str: ...
@overload
def read_file(fname: AnyStr, mode: Literal['b']) -> bytes: ...
def read_file(fname: AnyStr, mode: Literal['b', ''] = ''):
"""Read file contents.
"""
with open(fname, f'r{mode}') as f:
content = f.read()
return content
class __arg_env(argparse.Action):
def __call__(self, parser, namespace, values: str, option_string):
if os.path.exists(values):
with open(values, 'rb') as f:
env = pickle.load(f)
else:
env = ast.literal_eval(values)
setattr(namespace, self.dest, env or {})
def __make_enum_arg(enum_rmap: Mapping[str, 'Enum'], aliases: Mapping[str, str] = {}) -> Type[argparse.Action]:
class __enum_arg(argparse.Action):
def __call__(self, parser, namespace, values: str, option_string):
values = values.casefold()
if values in aliases:
values = aliases[values]
setattr(namespace, self.dest, enum_rmap[values])
return __enum_arg
__arg_endian = __make_enum_arg(endian_map)
__arg_archtype = __make_enum_arg(arch_map, {'x86_64': 'x8664', 'riscv32': 'riscv'})
__arg_ostype = __make_enum_arg(os_map, {'darwin': 'macos'})
__arg_verbose = __make_enum_arg(verbose_map)
def handle_code(options: argparse.Namespace) -> Dict[str, Any]:
if options.format == 'hex':
if options.input is not None:
print("Loading HEX from ARGV")
payload = str(options.input)
elif options.filename is not None:
print("Loading HEX from FILE")
payload = read_file(options.filename)
else:
print("Error: please specify an input source for hex")
exit(1)
code = bytes.fromhex(payload.replace(r'\x', ''))
elif options.format == 'asm':
print("Loading ASM from FILE")
if options.filename is None:
print("Error: please specify an input source for asm")
exit(1)
payload = read_file(options.filename)
# assemble the payload and turn it into opcodes bytes
assembler = arch_utils.assembler(options.arch, options.endian, options.thumb)
code, _ = assembler.asm(payload)
code = bytes(code)
elif options.format == 'bin':
print("Loading BIN from FILE")
if options.filename is None:
print("Error: please specify an input source for bin")
exit(1)
code = read_file(options.filename, 'b')
ql_args = {
'rootfs': options.rootfs,
'code': code,
'ostype': options.os,
'archtype': options.arch,
'endian': options.endian,
'thumb': options.thumb
}
return ql_args
def handle_run(options: argparse.Namespace) -> Dict[str, Any]:
effective_argv = []
# with argv
if options.filename is not None and options.run_args == []:
effective_argv = [options.filename] + options.args
# Without argv
elif options.filename is None and options.args == [] and options.run_args != []:
effective_argv = options.run_args
else:
print("ERROR: Command error!")
ql_args = {
'argv': effective_argv,
'rootfs': options.rootfs
}
return ql_args
def handle_examples(parser: argparse.ArgumentParser):
prog = os.path.basename(__file__)
__ql_examples = f"""Examples:
With code:
{prog} code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex
{prog} code --os linux --arch x86 --format asm -f examples/shellcodes/lin32_execve.asm
With binary file:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux
With binary file and Qdb:
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb --rr
With binary file and gdbserver:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux
With binary file and additional argv:
{prog} run -f examples/rootfs/x8664_linux/bin/x8664_args --rootfs examples/rootfs/x8664_linux --args test1 test2 test3
With binary file and various output format:
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --verbose disasm
{prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --filter ^open
With UEFI file:
{prog} run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --env examples/rootfs/x8664_efi/rom2_nvar.pickel
With binary file and json output:
{prog} run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows --no-console --json
"""
parser.exit(0, __ql_examples)
def run():
parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version=f'qltool for Qiling {ql_ver}, using Unicorn {uc_ver}')
commands = parser.add_subparsers(title='sub commands', description='select execution mode', dest='subcommand', required=True)
# set "run" subcommand options
run_parser = commands.add_parser('run', help='run a program')
run_parser.add_argument('-f', '--filename', default=None, metavar="FILE", help="filename")
run_parser.add_argument('--rootfs', required=True, help='emulated rootfs')
run_parser.add_argument('--args', default=[], nargs=argparse.REMAINDER, dest="args", help="args")
run_parser.add_argument('run_args', default=[], nargs=argparse.REMAINDER)
# set "code" subcommand options
code_parser = commands.add_parser('code', help='execute a shellcode')
code_parser.add_argument('-f', '--filename', metavar="FILE", help="filename")
code_parser.add_argument('-i', '--input', metavar="INPUT", dest="input", help='input hex value')
code_parser.add_argument('--arch', required=True, choices=arch_map, action=__arg_archtype)
code_parser.add_argument('--thumb', action='store_true', default=False, help='specify thumb mode for ARM')
code_parser.add_argument('--endian', choices=endian_map, default=QL_ENDIAN.EL, action=__arg_endian, help='specify endianness for bi-endian archs')
code_parser.add_argument('--os', required=True, choices=os_map, action=__arg_ostype)
code_parser.add_argument('--rootfs', default='.', help='emulated root filesystem, that is where all libraries reside')
code_parser.add_argument('--format', choices=('asm', 'hex', 'bin'), default='bin', help='input file format')
# set "examples" subcommand
expl_parser = commands.add_parser('examples', help='show examples and exit', add_help=False)
# set "qltui" subcommand
qltui_parser = commands.add_parser('qltui', help='show qiling Terminal User Interface', add_help=False)
comm_parser = run_parser
if len(sys.argv) > 1 and sys.argv[1] == 'code':
comm_parser = code_parser
# set common options
comm_parser.add_argument('-v', '--verbose', choices=verbose_map, default=QL_VERBOSE.DEFAULT, action=__arg_verbose, help='set verbosity level')
comm_parser.add_argument('--env', metavar="FILE", action=__arg_env, default={}, help="pickle file containing an environment dictionary")
comm_parser.add_argument('-g', '--gdb', nargs='?', metavar='SERVER:PORT', const='gdb', help='enable gdb server')
comm_parser.add_argument('--qdb', action='store_true', help='attach Qdb at entry point, it\'s MIPS, ARM(THUMB) supported only for now')
comm_parser.add_argument('--rr', action='store_true', help='switch on record and replay feature in qdb, only works with --qdb')
comm_parser.add_argument('--profile', help="define a customized profile")
comm_parser.add_argument('--no-console', action='store_false', dest='console', help='do not emit output to console')
comm_parser.add_argument('-e', '--filter', metavar='REGEXP', default=None, help="apply a filtering regexp on log output")
comm_parser.add_argument('--log-file', help="write log to a file")
comm_parser.add_argument('--log-plain', action='store_true', help="do not use colors in log output")
comm_parser.add_argument('--root', action='store_true', help='enable sudo required mode')
comm_parser.add_argument('--debug-stop', action='store_true', help='stop running on error; requires verbose to be set to either "debug" or "dump"')
comm_parser.add_argument('-m', '--multithread', action='store_true', help='run in multithread mode')
comm_parser.add_argument('--timeout', type=int, default=0, help='set emulation timeout')
comm_parser.add_argument('-c', '--coverage-file', default=None, help='code coverage file name')
comm_parser.add_argument('--coverage-format', default='drcov', choices=cov_utils.factory.formats, help='code coverage file format')
comm_parser.add_argument('--json', action='store_true', help='print a json report of the emulation')
comm_parser.add_argument('--libcache', action='store_true', help='enable dll caching for windows')
options = parser.parse_args()
qltui_enabled = False
ql_args = {}
if options.subcommand == 'qltui':
import qltui
options = qltui.get_data()
qltui_enabled = True
if options.subcommand == 'examples':
handle_examples(parser)
# ql file setup
elif options.subcommand == 'run':
ql_args = handle_run(options)
# ql code setup
elif options.subcommand == 'code':
ql_args = handle_code(options)
ql_args.update({
'env': options.env,
'verbose': options.verbose,
'profile': options.profile,
'console': options.console,
'filter': options.filter,
'log_devices': options.log_file and [options.log_file],
'log_plain': options.log_plain,
'multithread': options.multithread,
'libcache': options.libcache
})
ql = Qiling(**ql_args)
# attach Qdb at entry point
if options.qdb:
QlQdb(ql, rr=bool(options.rr)).run()
parser.exit(0)
# ql execute additional options
if options.gdb:
argval = options.gdb
if argval != 'gdb':
argval = f'gdb:{argval}'
ql.debugger = argval
if options.debug_stop:
if options.verbose not in (QL_VERBOSE.DEBUG, QL_VERBOSE.DUMP):
parser.error('the debug_stop option requires verbose to be set to either "debug" or "dump"')
ql.debug_stop = True
if options.root:
ql.os.root = True
if qltui_enabled:
hook_dictionary = qltui.hook(ql)
# ql run
if options.coverage_file:
with cov_utils.collect_coverage(ql, options.coverage_format, options.coverage_file):
ql.run(timeout=options.timeout)
else:
ql.run(timeout=options.timeout)
if options.json:
rep = report.generate_report(ql)
if qltui_enabled:
rep["syscalls"] = qltui.transform_syscalls(ql.os.stats.syscalls)
qltui.show_report(ql, rep, hook_dictionary)
else:
pprint(rep)
exit(ql.os.exit_code)
if __name__ == '__main__':
run()
+6
View File
@@ -0,0 +1,6 @@
x/8xw $sp
x/8i $pc
s 1
info snapshot
p
q
+6
View File
@@ -0,0 +1,6 @@
x/8xw $sp
x/8i $pc
s 1
info snapshot
p
q
+190
View File
@@ -44,6 +44,61 @@ class SimpleGdbClient:
self.__file.flush()
class ReadingGdbClient:
"""A minimal gdb remote client that can also read replies, so tests can
assert on the stop-reply packets the server sends back.
"""
def __init__(self, host: str, port: int):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.settimeout(10)
self.__sock = sock
self.__buf = b''
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_traceback):
self.__sock.close()
@staticmethod
def checksum(data: str) -> int:
return sum(ord(c) for c in data) & 0xff
def send(self, msg: str):
self.__sock.sendall(f'${msg}#{ReadingGdbClient.checksum(msg):02x}'.encode('latin'))
def send_break(self):
"""Send an async interrupt: a bare '\x03' byte, outside of packet framing.
"""
self.__sock.sendall(b'\x03')
def read_packet(self) -> str:
"""Read a single '$<data>#<checksum>' reply, skipping any '+'/'-' acks.
"""
# wait for a packet start marker
while b'$' not in self.__buf:
self.__buf += self.__sock.recv(4096)
start = self.__buf.index(b'$')
# wait until the terminating '#' and its two checksum digits arrived
while True:
end = self.__buf.find(b'#', start)
if end != -1 and len(self.__buf) >= end + 3:
break
self.__buf += self.__sock.recv(4096)
data = self.__buf[start + 1:end]
self.__buf = self.__buf[end + 3:]
return data.decode('latin')
class DebuggerTest(unittest.TestCase):
def test_gdbdebug_file_server(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.DEBUG)
@@ -168,6 +223,141 @@ class DebuggerTest(unittest.TestCase):
ql.run()
del ql
def test_gdbdebug_stepi_reports_sigtrap(self):
# regression for issues #1377 and #1538: a single-step ('s') used to be
# answered with a SIGTERM stop-reply because emu_state is always STOPPED
# after a step, which made gdb clients believe the program had died. the
# stop-reply for an ordinary step must be a SIGTRAP ('S05'), never a
# SIGTERM ('S0f').
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
ql.debugger = 'gdb:127.0.0.1:9996'
replies = []
def gdb_test_client():
# yield to allow ql to launch its gdbserver
time.sleep(1.337 * 2)
with ReadingGdbClient('127.0.0.1', 9996) as client:
client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386')
client.read_packet()
client.send('QStartNoAckMode')
client.read_packet()
# step a few instructions; every reply must be a SIGTRAP stop
for _ in range(3):
client.send('s')
replies.append(client.read_packet())
client.send('k')
thread = threading.Thread(target=gdb_test_client, daemon=True)
thread.start()
ql.run()
thread.join(timeout=30)
del ql
self.assertEqual(replies, ['S05', 'S05', 'S05'])
def test_gdbdebug_vcont_signal_actions(self):
# regression for issue #1377: a client resuming with a pending signal sends
# a 'vCont;S<sig>' or 'vCont;C<sig>' action (e.g. 'S0f' after a stop-reply we
# sent). the stub only recognized 'S05' and 'C05', replied empty to anything
# else, and the client bailed out with 'Invalid remote reply'. any signal must
# be accepted and carried out as a plain step or resume.
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
ql.debugger = 'gdb:127.0.0.1:9995'
replies = []
def gdb_test_client():
# yield to allow ql to launch its gdbserver
time.sleep(1.337 * 2)
with ReadingGdbClient('127.0.0.1', 9995) as client:
client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386')
client.read_packet()
client.send('QStartNoAckMode')
client.read_packet()
client.send('vCont?')
replies.append(client.read_packet())
# step while delivering SIGTERM; this is the packet reported in #1377
client.send('vCont;S0f:p1.1;c:p1.-1')
replies.append(client.read_packet())
# resume while delivering SIGTERM; runs the guest to completion
client.send('vCont;C0f:p1.1')
replies.append(client.read_packet())
client.send('k')
thread = threading.Thread(target=gdb_test_client, daemon=True)
thread.start()
ql.run()
thread.join(timeout=30)
del ql
self.assertEqual(replies[0], 'vCont;c;C;s;S')
# the signalled step is an ordinary step: SIGTRAP, not an empty reply
self.assertEqual(replies[1], 'S05')
# the signalled resume ran to termination and reported an exit code
self.assertTrue(replies[2].startswith('W'), f'unexpected reply to signalled resume: {replies[2]!r}')
def test_gdbdebug_async_interrupt(self):
# a free-running guest could not be interrupted at all: the stub is blocked
# inside emu_start while the target runs, so the bare '\x03' break byte a
# client sends to pause it was never read, and clients reported 'Cannot
# execute this command while the target is running'. a break must stop the
# guest and yield a SIGINT ('S02') stop-reply.
INFINITE_LOOP = bytes.fromhex('90ebfd') # nop ; jmp -3
ql = Qiling(code=INFINITE_LOOP, archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.OFF)
ql.debugger = 'gdb:127.0.0.1:9994'
replies = []
def gdb_test_client():
# yield to allow ql to launch its gdbserver
time.sleep(1.337 * 2)
with ReadingGdbClient('127.0.0.1', 9994) as client:
client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386')
client.read_packet()
client.send('QStartNoAckMode')
client.read_packet()
# let the guest free-run; it never stops on its own
client.send('c')
# give it time to spin, then break into it
time.sleep(1.337)
client.send_break()
try:
replies.append(client.read_packet())
except OSError:
# the stub ignored the break and the guest is still spinning.
# stop it from here so the assertion below reports the failure
# instead of hanging the test run forever
ql.stop()
client.send('k')
thread = threading.Thread(target=gdb_test_client, daemon=True)
thread.start()
ql.run()
thread.join(timeout=30)
del ql
self.assertEqual(replies, ['S02'])
def test_gdbdebug_shellcode_server(self):
X8664_LIN = bytes.fromhex('31c048bbd19d9691d08c97ff48f7db53545f995257545eb03b0f05')
+111
View File
@@ -404,6 +404,33 @@ class ELFTest(unittest.TestCase):
ql.run()
del ql
def test_setsockopt_mips_so_rcvbuf(self):
# MIPS uses its own SO_* numbering (SO_RCVBUF = 0x1002, not the generic
# 0x08). The MIPS socket-option table had the wrong values, so e.g.
# busybox ping's setsockopt(SOL_SOCKET, SO_RCVBUF, ...) raised
# NotImplementedError ("Could not convert emulated socket option 4098").
from qiling.const import QL_ENDIAN
from qiling.os.posix.syscall.socket import ql_syscall_socket, ql_syscall_setsockopt
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX,
endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux",
verbose=QL_VERBOSE.OFF)
AF_INET, SOCK_DGRAM = 2, 2
SOL_SOCKET, SO_RCVBUF = 0xffff, 0x1002 # MIPS values
fd = ql_syscall_socket(ql, AF_INET, SOCK_DGRAM, 0)
self.assertGreaterEqual(fd, 0)
base = 0x100000
ql.mem.map(base, 0x1000)
ql.mem.write_ptr(base, 16384, 4) # requested SO_RCVBUF size
# must map SO_RCVBUF (0x1002) to the host option and succeed, not raise
self.assertEqual(ql_syscall_setsockopt(ql, fd, SOL_SOCKET, SO_RCVBUF, base, 4), 0)
del ql
def test_elf_linux_arm_static(self):
ql = Qiling(["../examples/rootfs/arm_linux/bin/arm_hello_static"], "../examples/rootfs/arm_linux", verbose=QL_VERBOSE.DEFAULT)
all_mem = ql.mem.save()
@@ -725,6 +752,90 @@ class ELFTest(unittest.TestCase):
del ql
# Regression for getdents64 record alignment. Each linux_dirent64 record
# must be padded so the next record's leading u64 d_ino stays 8-byte
# aligned; otherwise a strict-alignment guest (e.g. MIPS) faults with an
# unaligned load while walking the buffer. x86 tolerates the unaligned read,
# which is why it was never caught.
def test_linux_getdents64_alignment(self):
from qiling.const import QL_ENDIAN
from qiling.os.posix.syscall.fcntl import ql_syscall_open
from qiling.os.posix.syscall.unistd import ql_syscall_getdents64
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX,
endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux",
verbose=QL_VERBOSE.OFF)
base = 0x100000
ql.mem.map(base, 0x4000)
path_ptr, buf_ptr = base, base + 0x100
ql.mem.write(path_ptr, b"/\x00")
fd = ql_syscall_open(ql, path_ptr, 0, 0) # O_RDONLY on a directory
self.assertGreaterEqual(fd, 0)
nbytes = ql_syscall_getdents64(ql, fd, buf_ptr, 0x2000)
self.assertGreater(nbytes, 0)
# walk the linux_dirent64 records; every record (hence its leading u64
# d_ino) must start at an 8-byte boundary. d_reclen is a u16 at offset 16.
buf = bytes(ql.mem.read(buf_ptr, nbytes))
off = 0
while off < nbytes:
self.assertEqual(off % 8, 0, f"dirent at offset {off} is not 8-byte aligned")
d_reclen = int.from_bytes(buf[off + 16:off + 18], "big")
self.assertNotEqual(d_reclen, 0)
off += d_reclen
del ql
def test_linux_getdents_alignment(self):
# the legacy getdents record was not rounded up to the alignment of its
# word-sized d_ino either, so a strict-alignment guest faulted walking
# the buffer. unlike getdents64, legacy linux_dirent keeps d_type in the
# record's last byte (offset d_reclen-1), so the padding must sit between
# d_name and d_type -- verify the records are aligned AND d_type survives.
from qiling.const import QL_ENDIAN
from qiling.os.posix.syscall.fcntl import ql_syscall_open
from qiling.os.posix.syscall.unistd import ql_syscall_getdents
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX,
endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux",
verbose=QL_VERBOSE.OFF)
base = 0x100000
ql.mem.map(base, 0x4000)
path_ptr, buf_ptr = base, base + 0x100
ql.mem.write(path_ptr, b"/\x00")
fd = ql_syscall_open(ql, path_ptr, 0, 0) # O_RDONLY on a directory
self.assertGreaterEqual(fd, 0)
nbytes = ql_syscall_getdents(ql, fd, buf_ptr, 0x2000)
self.assertGreater(nbytes, 0)
# walk the linux_dirent records; on 32-bit MIPS d_ino is 4 bytes, so each
# record must start at a 4-byte boundary. d_reclen is a u16 at offset 8
# (after the 4-byte d_ino + 4-byte d_off); d_name starts at offset 10.
buf = bytes(ql.mem.read(buf_ptr, nbytes))
off = 0
while off < nbytes:
self.assertEqual(off % 4, 0, f"dirent at offset {off} is not 4-byte aligned")
d_reclen = int.from_bytes(buf[off + 8:off + 10], "big")
self.assertNotEqual(d_reclen, 0)
name = buf[off + 10:buf.index(b"\x00", off + 10)].decode()
# legacy getdents stores d_type in the record's last byte; '.' and
# '..' must still report DT_DIR (4) after the alignment padding
if name in (".", ".."):
self.assertEqual(buf[off + d_reclen - 1], 4, f"{name}: d_type not DT_DIR")
off += d_reclen
self.assertEqual(off, nbytes) # records tile the buffer exactly
del ql
def test_memory_search(self):
ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG)
+51 -1
View File
@@ -17,9 +17,10 @@ from typing import List
sys.path.append("..")
from qiling import Qiling
from qiling.arch.models import X86_CPU_MODEL
from qiling.const import QL_VERBOSE, QL_INTERCEPT
from qiling.const import QL_VERBOSE, QL_INTERCEPT, QL_ARCH, QL_OS
from qiling.os.filestruct import ql_file
from qiling.os.stats import QlOsNullStats
import qiling.os.posix.syscall.sched as ql_sched
BASE_ROOTFS = r'../examples/rootfs'
@@ -201,6 +202,55 @@ class ELFTest(unittest.TestCase):
self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_clone3_translates_to_clone(self):
# clone3(struct clone_args *, size) must unpack the struct and delegate
# to the legacy clone() handler with translated args. Modern glibc issues
# clone3 from pthread_create, so without this thread creation fails.
# This drives ql_syscall_clone3 directly (stock unicorn, no clone3 binary
# needed) and asserts the translation.
CL = 0x100000
FLAGS, CHILD_TID, PARENT_TID = 0x00010f00, 0x222000, 0x223000
EXIT_SIGNAL, STACK, STACK_SIZE, TLS = 0x11, 0x7ff00000, 0x8000, 0x224000
def run_case(archtype: QL_ARCH, rootfs: str):
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=archtype, ostype=QL_OS.LINUX,
rootfs=rootfs, verbose=QL_VERBOSE.OFF)
ql.mem.map(CL, 0x1000)
for off, val in ((0, FLAGS), (16, CHILD_TID), (24, PARENT_TID),
(32, EXIT_SIGNAL), (40, STACK), (48, STACK_SIZE), (56, TLS)):
ql.mem.write_ptr(CL + off, val, 8)
captured = {}
def fake_clone(ql, flags, child_stack, parent_tidptr, newtls, child_tidptr):
captured.update(flags=flags, child_stack=child_stack, parent_tidptr=parent_tidptr,
newtls=newtls, child_tidptr=child_tidptr)
return 0xabc
orig = ql_sched.ql_syscall_clone
ql_sched.ql_syscall_clone = fake_clone
try:
ret = ql_sched.ql_syscall_clone3(ql, CL, 88)
finally:
ql_sched.ql_syscall_clone = orig
return captured, ret
# generic path (no x8664 register swap)
cap, ret = run_case(QL_ARCH.ARM, fr'{ARM_LINUX_ROOTFS}')
self.assertEqual(ret, 0xabc)
self.assertEqual(cap['child_stack'], STACK + STACK_SIZE) # base + size -> stack top
self.assertEqual(cap['flags'], FLAGS | EXIT_SIGNAL) # exit_signal folded in
self.assertEqual(cap['parent_tidptr'], PARENT_TID)
self.assertEqual(cap['newtls'], TLS)
self.assertEqual(cap['child_tidptr'], CHILD_TID)
# x8664: clone3 pre-swaps newtls<->child_tid so ql_syscall_clone's own
# x8664 swap cancels out to the same logical mapping
cap, _ = run_case(QL_ARCH.X8664, fr'{X64_LINUX_ROOTFS}')
self.assertEqual(cap['newtls'], CHILD_TID)
self.assertEqual(cap['child_tidptr'], TLS)
def test_tcp_elf_linux_x86(self):
logged: List[str] = []
+14
View File
@@ -53,6 +53,20 @@ class DebuggerTest(unittest.TestCase):
r'qdb_scripts/x86.qdb'
)
def test_qdb_riscv32_hello(self):
self.__test_common(
r'/bin/hello',
r'../examples/rootfs/riscv32_linux',
r'qdb_scripts/riscv32.qdb'
)
def test_qdb_riscv64_hello(self):
self.__test_common(
r'/bin/hello',
r'../examples/rootfs/riscv64_linux',
r'qdb_scripts/riscv64.qdb'
)
if __name__ == '__main__':
unittest.main()
+42
View File
@@ -7,7 +7,11 @@ import os
import subprocess
import re
import sys
import sysconfig
import tempfile
import unittest
from importlib.metadata import version
from pathlib import Path
class Qltool_Test(unittest.TestCase):
@@ -48,5 +52,43 @@ class Qltool_Test(unittest.TestCase):
self.assertTrue(all(p.search(entry) is not None for entry in log_entries))
class InstalledQltool_Test(unittest.TestCase):
"""Exercise the installed distribution outside the source checkout."""
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.cwd = directory.name
self.qltool = Path(sysconfig.get_path('scripts')) / ('qltool.exe' if os.name == 'nt' else 'qltool')
def __run(self, *args: str, expected_exit: int = 0) -> str:
result = subprocess.run(
[str(self.qltool), *args], cwd=self.cwd,
env={**os.environ, 'PYTHONPATH': ''},
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=30
)
self.assertEqual(result.returncode, expected_exit, result.stdout)
return result.stdout
def test_qltool_version_and_examples(self):
self.assertIn(f'Qiling {version("qiling")}', self.__run('--version'))
self.assertIn('qltool code --os linux', self.__run('examples'))
def test_qltool_shellcode_exit_status(self):
# Linux x86 exit(7), using the profiles shipped in the installed wheel.
self.__run('code', '--os', 'linux', '--arch', 'x86', '--format', 'hex',
'--input', 'b801000000bb07000000cd80', '--verbose', 'off',
expected_exit=7)
def test_qltui_import(self):
result = subprocess.run(
[sys.executable, '-I', '-c', 'import qltui; assert callable(qltui.get_data)'],
cwd=self.cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=30
)
self.assertEqual(result.returncode, 0, result.stdout)
if __name__ == "__main__":
unittest.main()